Enhance event occurrence parsing with new location patterns and improved issue handling
This commit updates the LocationParsingConfiguration to include additional location patterns such as "Exhibit Hall *", "Mtg. Room *", and "Online". The EventOccurrenceParser has been enhanced to better handle parsing issues, including skipping comment and continuation lines, and cleaning up location text. New methods for analyzing location parsing failures and categorizing issues have been added to improve reporting. Additionally, the UI has been updated to support larger input sizes for event occurrence text, ensuring a smoother user experience during data import.
This commit is contained in:
@@ -21,9 +21,17 @@ public class LocationParsingConfiguration
|
||||
{
|
||||
"Room *",
|
||||
"Hall *",
|
||||
"Exhibit Hall *",
|
||||
"Conference Room *",
|
||||
"Building *",
|
||||
"Auditorium *"
|
||||
"Auditorium *",
|
||||
"Mtg. Room *",
|
||||
"Meeting Room *",
|
||||
"Banquet Room *",
|
||||
"Banquet Hall *",
|
||||
"Online",
|
||||
"Virtual",
|
||||
"TBD"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,10 +42,10 @@ public class EventOccurrenceParser
|
||||
// Matches: time1 (optional dash time2/NOON), then location
|
||||
// The time group captures the full time range (including " - NOON" if present)
|
||||
// Pattern breakdown:
|
||||
// - First time: (?:NOON|\d{1,2}:?\d{0,2}\s?(?:[AaPp]\.?[Mm]\.?)) - matches NOON or time with AM/PM
|
||||
// - Optional range: (?:\s*[–-]\s*(?:NOON|\d{1,2}:?\d{0,2}\s?(?:[AaPp]\.?[Mm]\.?))) - matches dash followed by NOON or time
|
||||
// - Location: \s+.+ - whitespace followed by rest of string
|
||||
private readonly Regex _timeLocationRegex = new(@"(?<Time>(?:NOON|\d{1,2}:?\d{0,2}\s?(?:[AaPp]\.?[Mm]\.?))(?:\s*[–-]\s*(?:NOON|\d{1,2}:?\d{0,2}\s?(?:[AaPp]\.?[Mm]\.?)))?)(?<Location>\s+.+)?");
|
||||
// - First time: (?:NOON|\d{1,2}:?\d{0,2}\s*(?:[AaPp]\.?[Mm]\.?)) - matches NOON or time with AM/PM (more flexible whitespace)
|
||||
// - Optional range: (?:\s*[–-]\s*(?:NOON|\d{1,2}:?\d{0,2}\s*(?:[AaPp]\.?[Mm]\.?))) - matches dash followed by NOON or time
|
||||
// - Location: (?:\s+(?<Location>.+))? - optional whitespace followed by location (capture group with explicit name)
|
||||
private readonly Regex _timeLocationRegex = new(@"(?<Time>(?:NOON|\d{1,2}:?\d{0,2}\s*(?:[AaPp]\.?[Mm]\.?))(?:\s*[–-]\s*(?:NOON|\d{1,2}:?\d{0,2}\s*(?:[AaPp]\.?[Mm]\.?)))?)(?:\s+(?<Location>.+))?");
|
||||
|
||||
public EventOccurrenceParserResult Parse()
|
||||
{
|
||||
@@ -61,6 +61,10 @@ public class EventOccurrenceParser
|
||||
|
||||
// Skip empty lines
|
||||
if (string.IsNullOrWhiteSpace(trimmedLine))
|
||||
continue;
|
||||
|
||||
// Skip comment lines (starting with "#")
|
||||
if (trimmedLine.StartsWith("#", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var match = _re.Match(trimmedLine);
|
||||
@@ -94,6 +98,16 @@ public class EventOccurrenceParser
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip continuation lines (lines that look like they're continuing from previous line)
|
||||
// These are typically lines that:
|
||||
// - Start with lowercase or special characters (not event names)
|
||||
// - Are parenthetical notes like "(Semifinalists only)"
|
||||
// - Are informational text like "Schedule Posted on..."
|
||||
if (IsContinuationLine(trimmedLine))
|
||||
{
|
||||
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(trimmedLine))
|
||||
@@ -223,6 +237,38 @@ public class EventOccurrenceParser
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a line is a continuation/wrapped line that should be skipped.
|
||||
/// </summary>
|
||||
private bool IsContinuationLine(string line)
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
|
||||
// Skip parenthetical notes
|
||||
if (trimmed.StartsWith("(", StringComparison.Ordinal) && trimmed.EndsWith(")", StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
// Skip lines that are clearly continuation text (start with lowercase, common continuation words)
|
||||
if (trimmed.Length > 0 && char.IsLower(trimmed[0]))
|
||||
{
|
||||
// Check if it starts with common continuation words
|
||||
var continuationPrefixes = new[] { "be ", "the ", "and ", "or ", "to ", "a ", "an ", "will ", "may ", "can " };
|
||||
foreach (var prefix in continuationPrefixes)
|
||||
{
|
||||
if (trimmed.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip informational lines that don't contain dates/times
|
||||
if (trimmed.Contains("Schedule Posted", StringComparison.OrdinalIgnoreCase) ||
|
||||
trimmed.Contains("Note:", StringComparison.OrdinalIgnoreCase) ||
|
||||
trimmed.Contains("*Note:", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string SanitizeInput(string input)
|
||||
{
|
||||
|
||||
@@ -286,30 +332,57 @@ public class EventOccurrenceParser
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(locationPart))
|
||||
{
|
||||
// Try to match location using configurable patterns
|
||||
if (_locationConfig != null && _locationConfig.LocationPatterns.Any())
|
||||
{
|
||||
location = MatchLocationPattern(locationPart, _locationConfig.LocationPatterns);
|
||||
locationParseSuccess = !string.IsNullOrEmpty(location);
|
||||
}
|
||||
// Clean up location part - remove any remaining time components (e.g., "– 12:15 p.m. Exhibit Hall C" -> "Exhibit Hall C")
|
||||
locationPart = CleanLocationText(locationPart);
|
||||
|
||||
// If no pattern matched, fall back to using the location part as-is
|
||||
if (!locationParseSuccess)
|
||||
if (!string.IsNullOrWhiteSpace(locationPart))
|
||||
{
|
||||
location = locationPart;
|
||||
// Only add issue if we have patterns configured but none matched
|
||||
// Try to match location using configurable patterns
|
||||
if (_locationConfig != null && _locationConfig.LocationPatterns.Any())
|
||||
{
|
||||
issues.Add(new ParsingIssue
|
||||
location = MatchLocationPattern(locationPart, _locationConfig.LocationPatterns);
|
||||
locationParseSuccess = !string.IsNullOrEmpty(location);
|
||||
|
||||
// If pattern matching failed but location part looks valid, try matching against cleaned version
|
||||
if (!locationParseSuccess && !string.IsNullOrWhiteSpace(locationPart))
|
||||
{
|
||||
LineNumber = lineNumber,
|
||||
LineContent = lineContent,
|
||||
IssueType = ParsingIssueType.LocationParseFailure,
|
||||
Message = $"Location '{locationPart}' does not match any configured pattern"
|
||||
});
|
||||
// Some locations might not match because of extra whitespace or formatting
|
||||
// Try matching the location even if initial match failed
|
||||
var cleanedForMatching = locationPart.Trim();
|
||||
location = MatchLocationPattern(cleanedForMatching, _locationConfig.LocationPatterns);
|
||||
locationParseSuccess = !string.IsNullOrEmpty(location);
|
||||
if (locationParseSuccess)
|
||||
{
|
||||
location = cleanedForMatching; // Use the cleaned version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no pattern matched but we have a location, use it anyway but mark as not matching pattern
|
||||
// This allows parsing to continue while still tracking that the location didn't match a pattern
|
||||
if (!locationParseSuccess && !string.IsNullOrWhiteSpace(locationPart))
|
||||
{
|
||||
location = locationPart;
|
||||
// Only add issue if we have patterns configured but none matched
|
||||
// This helps identify locations that might need new patterns added
|
||||
if (_locationConfig != null && _locationConfig.LocationPatterns.Any())
|
||||
{
|
||||
issues.Add(new ParsingIssue
|
||||
{
|
||||
LineNumber = lineNumber,
|
||||
LineContent = lineContent,
|
||||
IssueType = ParsingIssueType.LocationParseFailure,
|
||||
Message = $"Location '{locationPart}' does not match any configured pattern"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No location part found, which is valid (some events might not have locations)
|
||||
locationParseSuccess = true; // Consider it a success since no location is needed
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -320,24 +393,79 @@ public class EventOccurrenceParser
|
||||
return (time, location, locationParseSuccess || string.IsNullOrWhiteSpace(location));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans location text by removing any remaining time components.
|
||||
/// Handles cases like "– 12:15 p.m. Exhibit Hall C" -> "Exhibit Hall C"
|
||||
/// </summary>
|
||||
private string CleanLocationText(string locationText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(locationText))
|
||||
return string.Empty;
|
||||
|
||||
// Remove leading dashes and whitespace
|
||||
locationText = locationText.TrimStart('–', '-', ' ', '\t');
|
||||
|
||||
// Try to match and remove time patterns at the start
|
||||
// Pattern 1: Dash, whitespace, time (e.g., "– 12:15 p.m. " or "– NOON ")
|
||||
var dashTimePattern = new Regex(@"^[–-]\s+(?:NOON|\d{1,2}:?\d{0,2}\s*[AaPp]\.?[Mm]\.?)\s+", RegexOptions.IgnoreCase);
|
||||
locationText = dashTimePattern.Replace(locationText, "").Trim();
|
||||
|
||||
// Pattern 2: Time without dash at start (e.g., "12:15 p.m. " or "NOON ")
|
||||
var timePatternAtStart = new Regex(@"^(?:NOON|\d{1,2}:?\d{0,2}\s*[AaPp]\.?[Mm]\.?)\s+", RegexOptions.IgnoreCase);
|
||||
locationText = timePatternAtStart.Replace(locationText, "").Trim();
|
||||
|
||||
// Pattern 3: Any remaining dash-time combinations (more flexible)
|
||||
var remainingDashTime = new Regex(@"^[–-]\s*(?:NOON|\d{1,2}:?\d{0,2}\s*[AaPp]\.?[Mm]\.?)\s*", RegexOptions.IgnoreCase);
|
||||
locationText = remainingDashTime.Replace(locationText, "").Trim();
|
||||
|
||||
// Pattern 4: Remove any standalone time at the start (handles cases where dash was already removed)
|
||||
var standaloneTime = new Regex(@"^(?:NOON|\d{1,2}:?\d{0,2}\s*[AaPp]\.?[Mm]\.?)$", RegexOptions.IgnoreCase);
|
||||
if (standaloneTime.IsMatch(locationText))
|
||||
return string.Empty; // If only time remains, there's no location
|
||||
|
||||
return locationText.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Matches location text against configured patterns and returns the matched location.
|
||||
/// </summary>
|
||||
private string MatchLocationPattern(string locationText, List<string> patterns)
|
||||
{
|
||||
// Normalize location text for matching (trim and handle variations)
|
||||
var normalizedLocation = locationText.Trim();
|
||||
|
||||
// If location is empty after normalization, return empty
|
||||
if (string.IsNullOrWhiteSpace(normalizedLocation))
|
||||
return string.Empty;
|
||||
|
||||
foreach (var pattern in patterns)
|
||||
{
|
||||
if (!pattern.Contains('*'))
|
||||
var normalizedPattern = pattern.Trim();
|
||||
|
||||
// Skip empty patterns
|
||||
if (string.IsNullOrWhiteSpace(normalizedPattern))
|
||||
continue;
|
||||
|
||||
// Handle exact matches (patterns without wildcards like "Online", "Virtual", "TBD")
|
||||
if (!normalizedPattern.Contains('*'))
|
||||
{
|
||||
if (string.Equals(normalizedPattern, normalizedLocation, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return normalizedLocation;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert pattern to regex: escape special chars, replace * with .*
|
||||
var escapedPattern = Regex.Escape(pattern);
|
||||
escapedPattern = escapedPattern.Replace(@"\*", ".*");
|
||||
// This handles patterns like "Exhibit Hall *", "Room *", "Mtg. Room *", etc.
|
||||
var escapedPattern = Regex.Escape(normalizedPattern);
|
||||
escapedPattern = escapedPattern.Replace(@"\*", ".*?");
|
||||
|
||||
// Use case-insensitive matching
|
||||
var regex = new Regex($"^{escapedPattern}$", RegexOptions.IgnoreCase);
|
||||
if (regex.IsMatch(locationText))
|
||||
if (regex.IsMatch(normalizedLocation))
|
||||
{
|
||||
return locationText; // Return the full matched location
|
||||
return normalizedLocation; // Return the full matched location
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,6 +477,14 @@ public class EventOccurrenceParser
|
||||
int hour = 0;
|
||||
int minute = 0;
|
||||
|
||||
// Handle TBD (To Be Determined) times gracefully
|
||||
if (string.Equals(time.Trim(), "TBD", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Use a placeholder time (midnight) for TBD - the occurrence will still be created
|
||||
// but with a time that indicates it's TBD
|
||||
return new TimeOnly(0, 0, 0);
|
||||
}
|
||||
|
||||
// get the part of the time before a timespan
|
||||
if (time.Contains(" - "))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user