154 lines
5.5 KiB
C#
154 lines
5.5 KiB
C#
using Sprache;
|
||
using Core.Entities;
|
||
using Core.Models;
|
||
|
||
namespace Core.Parsers;
|
||
|
||
/// <summary>
|
||
/// Grammar definitions for parsing event occurrence DSL using parser combinators.
|
||
/// Provides composable parsers for each grammar rule.
|
||
/// </summary>
|
||
public static class EventOccurrenceGrammar
|
||
{
|
||
// Months - all 12 months supported
|
||
private static readonly Parser<string> January = Parse.String("January").Text().Token();
|
||
private static readonly Parser<string> February = Parse.String("February").Text().Token();
|
||
private static readonly Parser<string> March = Parse.String("March").Text().Token();
|
||
private static readonly Parser<string> April = Parse.String("April").Text().Token();
|
||
private static readonly Parser<string> May = Parse.String("May").Text().Token();
|
||
private static readonly Parser<string> June = Parse.String("June").Text().Token();
|
||
private static readonly Parser<string> July = Parse.String("July").Text().Token();
|
||
private static readonly Parser<string> August = Parse.String("August").Text().Token();
|
||
private static readonly Parser<string> September = Parse.String("September").Text().Token();
|
||
private static readonly Parser<string> October = Parse.String("October").Text().Token();
|
||
private static readonly Parser<string> November = Parse.String("November").Text().Token();
|
||
private static readonly Parser<string> December = Parse.String("December").Text().Token();
|
||
|
||
/// <summary>
|
||
/// Parser for month names (January through December).
|
||
/// </summary>
|
||
public static readonly Parser<string> Month = January
|
||
.Or(February)
|
||
.Or(March)
|
||
.Or(April)
|
||
.Or(May)
|
||
.Or(June)
|
||
.Or(July)
|
||
.Or(August)
|
||
.Or(September)
|
||
.Or(October)
|
||
.Or(November)
|
||
.Or(December);
|
||
|
||
/// <summary>
|
||
/// Parser for day of month (1-31, optional semicolon).
|
||
/// </summary>
|
||
public static readonly Parser<int> DayOfMonth =
|
||
from day in Parse.Number
|
||
from semicolon in Parse.Char(';').Optional()
|
||
select int.Parse(day);
|
||
|
||
// Time parsing components
|
||
private static readonly Parser<string> Noon = Parse.String("NOON").Text().Token();
|
||
private static readonly Parser<string> Tbd = Parse.String("TBD").Text().Token();
|
||
|
||
private static readonly Parser<string> AmPm =
|
||
Parse.String("a.m.").Or(Parse.String("am")).Or(Parse.String("A.M.")).Or(Parse.String("AM"))
|
||
.Or(Parse.String("p.m.")).Or(Parse.String("pm")).Or(Parse.String("P.M.")).Or(Parse.String("PM"))
|
||
.Text().Token();
|
||
|
||
private static readonly Parser<string> TimeValue =
|
||
from hour in Parse.Number
|
||
from colon in Parse.Char(':').Optional()
|
||
from minute in Parse.Number.Optional()
|
||
from ws in Parse.WhiteSpace.Many()
|
||
from ampm in AmPm
|
||
select $"{hour}:{(minute.IsDefined ? minute.Get() : "00")} {ampm}";
|
||
|
||
/// <summary>
|
||
/// Parser for hyphen characters (en-dash, hyphen, em-dash).
|
||
/// </summary>
|
||
public static readonly Parser<char> Hyphen = Parse.Char('–').Or(Parse.Char('-')).Or(Parse.Char('—'));
|
||
|
||
/// <summary>
|
||
/// Parser for time values, including ranges and special values (NOON, TBD).
|
||
/// </summary>
|
||
public static readonly Parser<string> Time =
|
||
Noon.Or(Tbd)
|
||
.Or(
|
||
from start in TimeValue.Or(Noon)
|
||
from dash in Hyphen.Then(_ => Parse.WhiteSpace.Many()).Optional()
|
||
from end in TimeValue.Or(Noon).Optional()
|
||
select end.IsDefined
|
||
? $"{start} – {end.Get()}"
|
||
: start
|
||
);
|
||
|
||
/// <summary>
|
||
/// Parser for section headers: EventName [–-—] (MS|HS).
|
||
/// </summary>
|
||
public static readonly Parser<(string EventName, string SchoolLevel)> SectionHeader =
|
||
from eventName in Parse.AnyChar.Except(Hyphen).Many().Text().Token()
|
||
from hyphen in Hyphen.Token()
|
||
from schoolLevel in Parse.String("MS").Or(Parse.String("HS")).Text().Token()
|
||
select (eventName.Trim(), schoolLevel);
|
||
|
||
/// <summary>
|
||
/// Parser for General Schedule/Session headers.
|
||
/// </summary>
|
||
public static readonly Parser<string> GeneralSchedule =
|
||
Parse.String("General Schedule").Or(Parse.String("General Session")).Text().Token();
|
||
|
||
/// <summary>
|
||
/// Parser for comment lines (starting with #).
|
||
/// </summary>
|
||
public static readonly Parser<string> CommentLine =
|
||
from hash in Parse.Char('#')
|
||
from rest in Parse.AnyChar.Many().Text()
|
||
select rest;
|
||
|
||
/// <summary>
|
||
/// Attempts to parse a section header from the given line.
|
||
/// Returns null if not a section header.
|
||
/// </summary>
|
||
public static (string EventName, string SchoolLevel)? TryParseSectionHeader(string line)
|
||
{
|
||
try
|
||
{
|
||
var result = SectionHeader.Parse(line);
|
||
return result;
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Attempts to parse a General Schedule/Session header from the given line.
|
||
/// Returns null if not a General Schedule header.
|
||
/// </summary>
|
||
public static bool IsGeneralSchedule(string line)
|
||
{
|
||
try
|
||
{
|
||
GeneralSchedule.Parse(line);
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Attempts to parse a comment line from the given line.
|
||
/// Returns true if the line is a comment.
|
||
/// </summary>
|
||
public static bool IsCommentLine(string line)
|
||
{
|
||
return line.TrimStart().StartsWith("#", StringComparison.Ordinal);
|
||
}
|
||
}
|
||
|