using Sprache;
using Core.Entities;
using Core.Models;
namespace Core.Parsers;
///
/// Grammar definitions for parsing event occurrence DSL using parser combinators.
/// Provides composable parsers for each grammar rule.
///
public static class EventOccurrenceGrammar
{
///
/// Array of all month names in order (January through December).
/// This is the single source of truth for month names used throughout the parser.
///
public static readonly string[] MonthNames =
[
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
// Build month parsers dynamically from MonthNames array
private static readonly Parser[] MonthParsers = MonthNames
.Select(month => Parse.String(month).Text().Token())
.ToArray();
///
/// Parser for month names (January through December).
/// Built dynamically from MonthNames array.
///
public static readonly Parser Month = MonthParsers
.Aggregate((current, next) => current.Or(next));
///
/// Parser for day of month (1-31, optional semicolon).
///
public static readonly Parser DayOfMonth =
from day in Parse.Number
from semicolon in Parse.Char(';').Optional()
select int.Parse(day);
// Time parsing components
private static readonly Parser Noon = Parse.String("NOON").Text().Token();
private static readonly Parser Tbd = Parse.String("TBD").Text().Token();
private static readonly Parser 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 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}";
///
/// Parser for hyphen character.
/// Note: Input is assumed to be normalized (en-dash and em-dash converted to regular hyphen) via SanitizeInput.
///
public static readonly Parser Hyphen = Parse.Char('-');
///
/// Parser for time values, including ranges and special values (NOON, TBD).
///
public static readonly Parser 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
);
///
/// Parser for section headers: EventName - (MS|HS).
/// Note: Input is assumed to be normalized (hyphens normalized) via SanitizeInput.
///
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);
///
/// Parser for General Schedule/Session headers.
///
public static readonly Parser GeneralSchedule =
Parse.String("General Schedule").Or(Parse.String("General Session")).Text().Token();
///
/// Parser for comment lines (starting with #).
///
public static readonly Parser CommentLine =
from hash in Parse.Char('#')
from rest in Parse.AnyChar.Many().Text()
select rest;
///
/// Attempts to parse a section header from the given line.
/// Returns null if not a section header.
///
public static (string EventName, string SchoolLevel)? TryParseSectionHeader(string line)
{
try
{
var result = SectionHeader.Parse(line);
return result;
}
catch (Sprache.ParseException)
{
// Expected - line is not a section header, return null
return null;
}
catch
{
// Unexpected exception, return null to continue parsing
return null;
}
}
///
/// Attempts to parse a General Schedule/Session header from the given line.
/// Returns null if not a General Schedule header.
///
public static bool IsGeneralSchedule(string line)
{
try
{
GeneralSchedule.Parse(line);
return true;
}
catch (Sprache.ParseException)
{
// Expected - line is not a General Schedule header
return false;
}
catch
{
// Unexpected exception, return false to continue parsing
return false;
}
}
///
/// Attempts to parse a comment line from the given line.
/// Returns true if the line is a comment.
///
public static bool IsCommentLine(string line)
{
return line.TrimStart().StartsWith("#", StringComparison.Ordinal);
}
///
/// 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.
///
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 (Sprache.ParseException)
{
// Expected - line is not a valid occurrence line, return null
return null;
}
catch
{
// Unexpected exception, return null to continue parsing
return null;
}
}
}