Enhance event occurrence parsing with school level filtering

This commit introduces a new SchoolLevel enum and updates the EventOccurrenceParser to filter event occurrences based on the specified school level (Middle School or High School). The EventOccurrenceParseResult and EventOccurrenceParserResult classes have been updated to track skipped section headers and counts for both school levels. Additionally, the EventOccurrenceParserService has been modified to read the school level from configuration, and the UI has been updated to allow users to select the school level for event imports. This enhancement improves the accuracy of event parsing and provides better user feedback on skipped occurrences.
This commit is contained in:
2026-01-09 09:39:00 -05:00
parent ea1a4a04ad
commit 8183c0200d
7 changed files with 270 additions and 27 deletions
+17 -1
View File
@@ -32,10 +32,26 @@ public class EventOccurrenceParseResult
/// <summary>
/// List of high school (HS) section headers that were encountered but skipped
/// because they don't match any event definition in the system.
/// because they don't match the chapter's school level setting.
/// </summary>
public List<string> SkippedHSSectionHeaders { get; set; } = new();
/// <summary>
/// List of middle school (MS) section headers that were encountered but skipped
/// because they don't match the chapter's school level setting.
/// </summary>
public List<string> SkippedMSSectionHeaders { get; set; } = new();
/// <summary>
/// Number of Middle School (MS) event occurrences that were skipped due to school level filtering.
/// </summary>
public int SkippedMSEventCount { get; set; }
/// <summary>
/// Number of High School (HS) event occurrences that were skipped due to school level filtering.
/// </summary>
public int SkippedHSEventCount { get; set; }
/// <summary>
/// Total number of event occurrences successfully parsed.
/// </summary>
+18
View File
@@ -0,0 +1,18 @@
namespace Core.Models;
/// <summary>
/// School level for filtering event occurrences.
/// </summary>
public enum SchoolLevel
{
/// <summary>
/// Middle School level
/// </summary>
MiddleSchool,
/// <summary>
/// High School level
/// </summary>
HighSchool
}
+142 -22
View File
@@ -3,6 +3,7 @@ using Core.Entities;
using Core.Models;
using EventOccurrenceParsers = Core.Parsers.EventOccurrence;
using Core.Utility;
using SchoolLevel = Core.Models.SchoolLevel;
namespace Core.Parsers;
@@ -14,17 +15,22 @@ public class EventOccurrenceParserResult
public IDictionary<EventDefinition, List<Entities.EventOccurrence>> Occurrences { get; set; } = new Dictionary<EventDefinition, List<Entities.EventOccurrence>>();
public List<ParsingIssue> Issues { get; set; } = new();
public List<string> SkippedHSSectionHeaders { get; set; } = new();
public List<string> SkippedMSSectionHeaders { get; set; } = new();
public int SkippedMSEventCount { get; set; }
public int SkippedHSEventCount { get; set; }
}
public class EventOccurrenceParser
{
private FileSystemInfo _txtFile;
private ICollection<EventDefinition> _events;
private SchoolLevel? _schoolLevel;
public EventOccurrenceParser(FileSystemInfo txtFile, ICollection<EventDefinition> events)
public EventOccurrenceParser(FileSystemInfo txtFile, ICollection<EventDefinition> events, SchoolLevel? schoolLevel = null)
{
_events = events;
_txtFile = txtFile;
_schoolLevel = schoolLevel;
}
public EventOccurrenceParserResult Parse()
@@ -35,6 +41,7 @@ public class EventOccurrenceParser
EventDefinition? currentEventDefinition = null;
bool inContinuationMode = false;
bool inHSSection = false;
bool inMSSection = false;
var lines = File.ReadLines(_txtFile.FullName);
foreach (var (line, index) in lines.Select((line, index) => (line, index + 1)))
@@ -73,22 +80,71 @@ public class EventOccurrenceParser
// 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))
// Determine if we should skip this event based on chapter's school level setting
bool shouldSkip = false;
if (!string.IsNullOrWhiteSpace(schoolLevel))
{
if (_schoolLevel.HasValue)
{
// School level is set - filter based on it
if (_schoolLevel.Value == SchoolLevel.MiddleSchool &&
schoolLevel.Equals("HS", StringComparison.OrdinalIgnoreCase))
{
shouldSkip = true;
result.SkippedHSSectionHeaders.Add(normalizedLine);
inHSSection = true;
inMSSection = false;
}
else if (_schoolLevel.Value == SchoolLevel.HighSchool &&
schoolLevel.Equals("MS", StringComparison.OrdinalIgnoreCase))
{
shouldSkip = true;
result.SkippedMSSectionHeaders.Add(normalizedLine);
inMSSection = true;
inHSSection = false;
}
}
else
{
// No school level set - backward compatibility: skip HS events
if (schoolLevel.Equals("HS", StringComparison.OrdinalIgnoreCase))
{
shouldSkip = true;
result.SkippedHSSectionHeaders.Add(normalizedLine);
inHSSection = true;
inMSSection = false;
}
}
}
if (shouldSkip)
{
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
// Reset section flags for events we're processing
if (schoolLevel.Equals("MS", StringComparison.OrdinalIgnoreCase))
{
inMSSection = true;
inHSSection = false;
}
else if (schoolLevel.Equals("HS", StringComparison.OrdinalIgnoreCase))
{
inHSSection = true;
inMSSection = false;
}
else
{
inHSSection = false;
inMSSection = false;
}
// 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
// For unmatched headers, create issue
var bestRatio = EventOccurrenceParsers.SectionHeaderMatcher.GetBestMatchRatio(eventNamePart, _events);
issues.Add(new ParsingIssue
{
@@ -100,7 +156,6 @@ public class EventOccurrenceParser
continue;
}
currentEventDefinition = evt;
inHSSection = false; // Reset HS section flag for MS events
continue;
}
@@ -109,7 +164,8 @@ public class EventOccurrenceParser
{
// General schedule breaks continuation mode
inContinuationMode = false;
inHSSection = false; // Reset HS section flag
inHSSection = false; // Reset section flags
inMSSection = false;
currentEventDefinition = EventDefinition.GeneralSchedule;
continue;
}
@@ -120,21 +176,68 @@ public class EventOccurrenceParser
// 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))
// Determine if we should skip this event based on chapter's school level setting
bool shouldSkip = false;
if (_schoolLevel.HasValue)
{
// School level is set - filter based on it
if (_schoolLevel.Value == SchoolLevel.MiddleSchool &&
normalizedLine.Contains("HS", StringComparison.OrdinalIgnoreCase))
{
shouldSkip = true;
result.SkippedHSSectionHeaders.Add(normalizedLine);
inHSSection = true;
inMSSection = false;
}
else if (_schoolLevel.Value == SchoolLevel.HighSchool &&
normalizedLine.Contains("MS", StringComparison.OrdinalIgnoreCase))
{
shouldSkip = true;
result.SkippedMSSectionHeaders.Add(normalizedLine);
inMSSection = true;
inHSSection = false;
}
}
else
{
// No school level set - backward compatibility: skip HS events
if (normalizedLine.Contains("HS", StringComparison.OrdinalIgnoreCase))
{
shouldSkip = true;
result.SkippedHSSectionHeaders.Add(normalizedLine);
inHSSection = true;
inMSSection = false;
}
}
if (shouldSkip)
{
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
// Reset section flags for events we're processing
if (normalizedLine.Contains("MS", StringComparison.OrdinalIgnoreCase))
{
inMSSection = true;
inHSSection = false;
}
else if (normalizedLine.Contains("HS", StringComparison.OrdinalIgnoreCase))
{
inHSSection = true;
inMSSection = false;
}
else
{
inHSSection = false;
inMSSection = false;
}
// 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
// For unmatched headers, create issue
var bestRatio = EventOccurrenceParsers.SectionHeaderMatcher.GetBestMatchRatio(normalizedLine, _events);
issues.Add(new ParsingIssue
{
@@ -146,7 +249,6 @@ public class EventOccurrenceParser
continue;
}
currentEventDefinition = evt;
inHSSection = false; // Reset HS section flag for MS events
continue;
}
@@ -180,10 +282,28 @@ public class EventOccurrenceParser
// Occurrence lines break continuation mode
inContinuationMode = false;
// Skip occurrences under HS sections (they won't match any event definition)
if (inHSSection)
// Skip occurrences under sections that don't match the school level setting
if (_schoolLevel.HasValue)
{
continue;
if (_schoolLevel.Value == SchoolLevel.MiddleSchool && inHSSection)
{
result.SkippedHSEventCount++;
continue;
}
if (_schoolLevel.Value == SchoolLevel.HighSchool && inMSSection)
{
result.SkippedMSEventCount++;
continue;
}
}
else
{
// If no school level is set, skip HS sections (backward compatibility)
if (inHSSection)
{
result.SkippedHSEventCount++;
continue;
}
}
var (occurrenceName, month, dayOfMonthStr, timeAndLocation) = occurrenceLine.Value;
+34 -4
View File
@@ -4,6 +4,7 @@ using Core.Entities;
using Core.Models;
using Core.Parsers;
using Microsoft.Extensions.Configuration;
using SchoolLevel = Core.Models.SchoolLevel;
namespace Core.Services;
@@ -13,9 +14,11 @@ namespace Core.Services;
/// </summary>
public class EventOccurrenceParserService : IEventOccurrenceParserService
{
private readonly IConfiguration? _configuration;
public EventOccurrenceParserService(IConfiguration? configuration = null)
{
// Configuration parameter kept for backward compatibility but not used
_configuration = configuration;
}
/// <inheritdoc/>
@@ -38,8 +41,22 @@ public class EventOccurrenceParserService : IEventOccurrenceParserService
File.WriteAllText(tempFile, text, Encoding.UTF8);
var fileInfo = new FileInfo(tempFile);
// Use the existing EventOccurrenceParser
var parser = new EventOccurrenceParser(fileInfo, events);
// Read SchoolLevel from configuration
SchoolLevel? schoolLevel = null;
if (_configuration != null)
{
var schoolLevelStr = _configuration.GetSection("ChapterSettings:SchoolLevel").Get<string>();
if (!string.IsNullOrWhiteSpace(schoolLevelStr))
{
if (Enum.TryParse<SchoolLevel>(schoolLevelStr, ignoreCase: true, out var parsed))
{
schoolLevel = parsed;
}
}
}
// Use the existing EventOccurrenceParser with school level
var parser = new EventOccurrenceParser(fileInfo, events, schoolLevel);
var parserResult = parser.Parse();
// Copy occurrences from parser result
@@ -92,8 +109,21 @@ public class EventOccurrenceParserService : IEventOccurrenceParserService
// Copy parsing issues from parser result
result.Issues.AddRange(parserResult.Issues);
// Copy skipped HS section headers from parser result
// Copy skipped section headers from parser result
result.SkippedHSSectionHeaders.AddRange(parserResult.SkippedHSSectionHeaders);
result.SkippedMSSectionHeaders.AddRange(parserResult.SkippedMSSectionHeaders);
result.SkippedMSEventCount = parserResult.SkippedMSEventCount;
result.SkippedHSEventCount = parserResult.SkippedHSEventCount;
// Add informational messages about skipped events
if (parserResult.SkippedMSEventCount > 0)
{
result.Warnings.Add($"Skipped {parserResult.SkippedMSEventCount} Middle School (MS) event occurrence(s) based on school level setting");
}
if (parserResult.SkippedHSEventCount > 0)
{
result.Warnings.Add($"Skipped {parserResult.SkippedHSEventCount} High School (HS) event occurrence(s) based on school level setting");
}
// Validate locations and add warnings for problematic ones
ValidateLocations(result);
@@ -125,9 +125,50 @@
{
<MudAlert Severity="Severity.Success" Dense="true">
Successfully parsed @_parseResult.TotalParsed occurrence(s) from @_parseResult.Occurrences.Count event definition(s)
@if (_parseResult.SkippedMSEventCount > 0 || _parseResult.SkippedHSEventCount > 0)
{
<text> (Skipped @(_parseResult.SkippedMSEventCount + _parseResult.SkippedHSEventCount) event occurrence(s) from other school level)</text>
}
</MudAlert>
}
@* Skipped Section Headers *@
@if ((_parseResult.SkippedMSSectionHeaders.Any() || _parseResult.SkippedHSSectionHeaders.Any()) && _parseResult.IsSuccess)
{
<MudExpansionPanels Elevation="0" Class="mt-2">
@if (_parseResult.SkippedMSSectionHeaders.Any())
{
<MudExpansionPanel Text="@($"Skipped MS Section Headers ({_parseResult.SkippedMSSectionHeaders.Count})")"
Icon="@Icons.Material.Filled.Info"
iconcolor="Color.Info">
<MudList T="string">
@foreach (var header in _parseResult.SkippedMSSectionHeaders)
{
<MudListItem T="string">
<MudText>@header</MudText>
</MudListItem>
}
</MudList>
</MudExpansionPanel>
}
@if (_parseResult.SkippedHSSectionHeaders.Any())
{
<MudExpansionPanel Text="@($"Skipped HS Section Headers ({_parseResult.SkippedHSSectionHeaders.Count})")"
Icon="@Icons.Material.Filled.Info"
iconcolor="Color.Info">
<MudList T="string">
@foreach (var header in _parseResult.SkippedHSSectionHeaders)
{
<MudListItem T="string">
<MudText>@header</MudText>
</MudListItem>
}
</MudList>
</MudExpansionPanel>
}
</MudExpansionPanels>
}
@* Locations Summary *@
@if (_parseResult.IsSuccess && _parseResult.Occurrences.Any())
{
@@ -4,6 +4,7 @@
@using WebApp.Models
@using WebApp.Components.Shared.Components
@using System.Text.Json
@using Core.Models
@inject IWebHostEnvironment Environment
@inject IConfiguration Configuration
@@ -43,6 +44,16 @@
MaxLength="4"
Required="true" />
</MudItem>
<MudItem xs="12" md="6">
<MudSelect T="SchoolLevel?" @bind-Value="_settings.SchoolLevel"
Label="School Level"
Variant="Variant.Outlined"
HelperText="Filter event occurrences by school level (leave empty to import both MS and HS)">
<MudSelectItem T="SchoolLevel?" Value="null">Both (MS and HS)</MudSelectItem>
<MudSelectItem T="SchoolLevel?" Value="@SchoolLevel.MiddleSchool">Middle School (MS)</MudSelectItem>
<MudSelectItem T="SchoolLevel?" Value="@SchoolLevel.HighSchool">High School (HS)</MudSelectItem>
</MudSelect>
</MudItem>
</MudGrid>
</MudPaper>
+7
View File
@@ -1,3 +1,5 @@
using Core.Models;
namespace WebApp.Models;
/// <summary>
@@ -34,4 +36,9 @@ public class ChapterSettings
/// Competition year (e.g., "2026")
/// </summary>
public string CompetitionYear { get; set; } = "2026";
/// <summary>
/// School level for the chapter (null = import both MS and HS events)
/// </summary>
public SchoolLevel? SchoolLevel { get; set; }
}