Add Sprache package and enhance event occurrence parsing with new grammar rules

This commit is contained in:
2026-01-08 08:23:31 -05:00
parent 5fdd5fadba
commit f32ce649cd
5 changed files with 373 additions and 32 deletions
+153
View File
@@ -0,0 +1,153 @@
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);
}
}