using System.Text.RegularExpressions; using Core.Entities; using Core.Models; using EventOccurrenceParsers = Core.Parsers.EventOccurrence; using Core.Utility; namespace Core.Parsers; /// /// Result of parsing event occurrence file, containing both occurrences and parsing issues. /// public class EventOccurrenceParserResult { public IDictionary> Occurrences { get; set; } = new Dictionary>(); public List Issues { get; set; } = new(); public List SkippedHSSectionHeaders { get; set; } = new(); } public class EventOccurrenceParser { private FileSystemInfo _txtFile; private ICollection _events; public EventOccurrenceParser(FileSystemInfo txtFile, ICollection events) { _events = events; _txtFile = txtFile; } public EventOccurrenceParserResult Parse() { var result = new EventOccurrenceParserResult(); var occurrences = result.Occurrences; var issues = result.Issues; EventDefinition? currentEventDefinition = null; bool inContinuationMode = false; bool inHSSection = false; var lines = File.ReadLines(_txtFile.FullName); foreach (var (line, index) in lines.Select((line, index) => (line, index + 1))) { // Normalize input: trim and normalize hyphens (en-dash, em-dash -> regular hyphen) // This allows the grammar parser to assume normalized input var normalizedLine = TextUtil.SanitizeInput(line.Trim()); // Skip empty lines if (EventOccurrenceParsers.LineClassifier.IsEmptyLine(normalizedLine)) { // Empty lines break continuation mode inContinuationMode = false; continue; } // Skip comment lines (starting with "#") - use grammar parser if (EventOccurrenceParsers.LineClassifier.IsCommentLine(normalizedLine)) { // Comment lines break continuation mode inContinuationMode = false; continue; } // Try to parse occurrence line using grammar parser var occurrenceLine = EventOccurrenceGrammar.TryParseOccurrenceLine(normalizedLine); if (!occurrenceLine.HasValue) { // Not an occurrence line, try other line types // Try to parse section header using grammar parser var sectionHeader = EventOccurrenceGrammar.TryParseSectionHeader(normalizedLine); if (sectionHeader.HasValue) { var (eventNamePart, schoolLevel) = sectionHeader.Value; // Section headers break continuation mode inContinuationMode = false; // Check if this is an HS event - if so, skip gracefully regardless of whether it matches // This prevents HS events from being incorrectly associated with MS events (e.g., // "Biotechnology Design – HS" matching "Biotechnology" MS event) if (schoolLevel.Equals("HS", StringComparison.OrdinalIgnoreCase)) { result.SkippedHSSectionHeaders.Add(normalizedLine); currentEventDefinition = null; // Skip subsequent occurrences inHSSection = true; // Mark that we're in an HS section continue; // No issue created } // For MS events, use fuzzy matching to find the best matching event definition var evt = EventOccurrenceParsers.SectionHeaderMatcher.MatchEventDefinition(eventNamePart, _events); if (evt == null) { // For unmatched MS headers, create issue var bestRatio = EventOccurrenceParsers.SectionHeaderMatcher.GetBestMatchRatio(eventNamePart, _events); issues.Add(new ParsingIssue { LineNumber = index, LineContent = normalizedLine, IssueType = ParsingIssueType.UnmatchedLine, Message = $"Section header '{eventNamePart} - {schoolLevel}' found but no matching event definition (best match ratio: {bestRatio})" }); continue; } currentEventDefinition = evt; inHSSection = false; // Reset HS section flag for MS events continue; } // Check for General Schedule/Session using grammar parser if (EventOccurrenceParsers.SectionHeaderMatcher.IsGeneralSchedule(normalizedLine)) { // General schedule breaks continuation mode inContinuationMode = false; inHSSection = false; // Reset HS section flag currentEventDefinition = EventDefinition.GeneralSchedule; continue; } // Also check for simple "MS" or "HS" in line (backward compatibility) if (EventOccurrenceParsers.SectionHeaderMatcher.HasSchoolLevel(normalizedLine)) { // Section headers break continuation mode inContinuationMode = false; // Check if this is an HS event - if so, skip gracefully regardless of whether it matches // This prevents HS events from being incorrectly associated with MS events if (normalizedLine.Contains("HS", StringComparison.OrdinalIgnoreCase)) { result.SkippedHSSectionHeaders.Add(normalizedLine); currentEventDefinition = null; // Skip subsequent occurrences inHSSection = true; // Mark that we're in an HS section continue; // No issue created } // For MS events, use fuzzy matching to find the best matching event definition var evt = EventOccurrenceParsers.SectionHeaderMatcher.MatchEventDefinition(normalizedLine, _events); if (evt == null) { // For unmatched MS headers, create issue var bestRatio = EventOccurrenceParsers.SectionHeaderMatcher.GetBestMatchRatio(normalizedLine, _events); issues.Add(new ParsingIssue { LineNumber = index, LineContent = normalizedLine, IssueType = ParsingIssueType.UnmatchedLine, Message = $"Section header with 'MS' or 'HS' found but no matching event definition (best match ratio: {bestRatio})" }); continue; } currentEventDefinition = evt; inHSSection = false; // Reset HS section flag for MS events continue; } // Check if line starts with "*" to enter continuation mode if (normalizedLine.TrimStart().StartsWith("*", StringComparison.Ordinal)) { inContinuationMode = true; } // Skip continuation lines (in continuation mode OR line starts with "*" or is parenthetical) if (inContinuationMode || EventOccurrenceParsers.LineClassifier.IsContinuationLine(normalizedLine)) { continue; } // "Voting Delegates" section header is no longer used - occurrences are categorized by name pattern // Track as unmatched line if it's not empty if (!string.IsNullOrWhiteSpace(normalizedLine)) { issues.Add(new ParsingIssue { LineNumber = index, LineContent = normalizedLine, IssueType = ParsingIssueType.UnmatchedLine, Message = "Line does not match expected format (Name Month Day Time/Location)" }); } continue; } // Occurrence lines break continuation mode inContinuationMode = false; // Skip occurrences under HS sections (they won't match any event definition) if (inHSSection) { continue; } var (occurrenceName, month, dayOfMonthStr, timeAndLocation) = occurrenceLine.Value; // Remove weekday suffix from occurrence name if present occurrenceName = Regex.Replace(occurrenceName, @"(?Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday),\s?$", "").Trim(); // Determine event definition based on occurrence name pattern or current section EventDefinition? eventDefinition = EventOccurrenceParsers.EventDefinitionResolver.Resolve(occurrenceName, currentEventDefinition); // Track issue if we can't determine the event definition if (eventDefinition == null) { issues.Add(new ParsingIssue { LineNumber = index, LineContent = normalizedLine, IssueType = ParsingIssueType.MissingEventDefinition, Message = $"Cannot determine event definition for occurrence: {occurrenceName}" }); continue; } // timeAndLocation is already normalized (hyphens normalized) since normalizedLine was sanitized // Parse time and location - extract time using regex, then use everything after time as location EventOccurrenceParsers.TimeLocationParser.Parse(timeAndLocation, out string time, out string location); // Parse date DateOnly? startDate = null; try { startDate = TextUtil.ParseDate(month, dayOfMonthStr.ToString(), DateTime.Now.Year); } catch (Exception ex) { issues.Add(new ParsingIssue { LineNumber = index, LineContent = normalizedLine, IssueType = ParsingIssueType.DateParseFailure, Message = $"Failed to parse date: {ex.Message}" }); continue; } // Parse time TimeOnly? startTime = null; try { startTime = EventOccurrenceParsers.TimeParser.Parse(time); } catch (Exception ex) { issues.Add(new ParsingIssue { LineNumber = index, LineContent = normalizedLine, IssueType = ParsingIssueType.TimeParseFailure, Message = $"Failed to parse time '{time}': {ex.Message}" }); continue; } if (startDate == null || startTime == null) continue; var t = new DateTime(startDate.Value, startTime.Value); var eventOccurrence = new Core.Entities.EventOccurrence { Name = occurrenceName, StartTime = t, Time = $"{time}", Date = $"{month} {dayOfMonthStr}", Location = location }; if (!occurrences.ContainsKey(eventDefinition)) occurrences.Add(eventDefinition, []); occurrences[eventDefinition].Add(eventOccurrence); // Reset HS section flag when we successfully parse an occurrence (means we're in a valid section) inHSSection = false; } return result; } }