Refactor event occurrence parsing to use dynamic month parsers and improve input normalization

This commit replaces individual month parsers with a dynamic array of month names, enhancing maintainability. The EventOccurrenceParser has been updated to utilize this new structure, ensuring consistent parsing of month names. Additionally, input normalization has been improved by standardizing hyphen handling and ensuring that all relevant parsing methods utilize the sanitized input. This change streamlines the parsing process and enhances overall robustness.
This commit is contained in:
2026-01-08 08:46:11 -05:00
parent f32ce649cd
commit 7ddc55f672
2 changed files with 121 additions and 90 deletions
+69 -29
View File
@@ -10,35 +10,27 @@ namespace Core.Parsers;
/// </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>
/// Array of all month names in order (January through December).
/// This is the single source of truth for month names used throughout the parser.
/// </summary>
public static readonly string[] MonthNames = new[]
{
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
// Build month parsers dynamically from MonthNames array
private static readonly Parser<string>[] MonthParsers = MonthNames
.Select(month => Parse.String(month).Text().Token())
.ToArray();
/// <summary>
/// Parser for month names (January through December).
/// Built dynamically from MonthNames array.
/// </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);
public static readonly Parser<string> Month = MonthParsers
.Aggregate((current, next) => current.Or(next));
/// <summary>
/// Parser for day of month (1-31, optional semicolon).
@@ -66,9 +58,10 @@ public static class EventOccurrenceGrammar
select $"{hour}:{(minute.IsDefined ? minute.Get() : "00")} {ampm}";
/// <summary>
/// Parser for hyphen characters (en-dash, hyphen, em-dash).
/// Parser for hyphen character.
/// Note: Input is assumed to be normalized (en-dash and em-dash converted to regular hyphen) via SanitizeInput.
/// </summary>
public static readonly Parser<char> Hyphen = Parse.Char('').Or(Parse.Char('-')).Or(Parse.Char('—'));
public static readonly Parser<char> Hyphen = Parse.Char('-');
/// <summary>
/// Parser for time values, including ranges and special values (NOON, TBD).
@@ -80,12 +73,13 @@ public static class EventOccurrenceGrammar
from dash in Hyphen.Then(_ => Parse.WhiteSpace.Many()).Optional()
from end in TimeValue.Or(Noon).Optional()
select end.IsDefined
? $"{start} {end.Get()}"
? $"{start} - {end.Get()}"
: start
);
/// <summary>
/// Parser for section headers: EventName [-—] (MS|HS).
/// Parser for section headers: EventName - (MS|HS).
/// Note: Input is assumed to be normalized (hyphens normalized) via SanitizeInput.
/// </summary>
public static readonly Parser<(string EventName, string SchoolLevel)> SectionHeader =
from eventName in Parse.AnyChar.Except(Hyphen).Many().Text().Token()
@@ -149,5 +143,51 @@ public static class EventOccurrenceGrammar
{
return line.TrimStart().StartsWith("#", StringComparison.Ordinal);
}
/// <summary>
/// Attempts to parse an occurrence line from the given text.
/// Returns null if parsing fails.
/// Strategy: Find the first month name in the line, then parse from there.
/// </summary>
public static (string Name, string Month, int Day, string TimeAndLocation)? TryParseOccurrenceLine(string line)
{
// Find the first occurrence of any month name (using normalized MonthNames array)
int monthIndex = -1;
string foundMonth = string.Empty;
foreach (var month in MonthNames)
{
var index = line.IndexOf(month, StringComparison.OrdinalIgnoreCase);
if (index >= 0 && (monthIndex < 0 || index < monthIndex))
{
monthIndex = index;
foundMonth = month;
}
}
if (monthIndex < 0)
return null;
// Extract name (everything before the month)
var name = line.Substring(0, monthIndex).Trim();
// Parse from the month onwards
var restOfLine = line.Substring(monthIndex);
try
{
var monthParser = Parse.String(foundMonth).Text().Token();
var result = from month in monthParser
from day in DayOfMonth.Token()
from timeAndLocation in Parse.AnyChar.Many().Text()
select (name, month, day, timeAndLocation.Trim());
var parsed = result.Parse(restOfLine);
return parsed;
}
catch
{
return null;
}
}
}