Apply structural chronology during core import
This commit is contained in:
parent
846016b7be
commit
296a4afb99
@ -114,7 +114,10 @@ var tests = new (string Name, Action Test)[]
|
|||||||
("Story Intelligence temporal resolves following weekday", StoryIntelligenceTemporalResolvesFollowingWeekday),
|
("Story Intelligence temporal resolves following weekday", StoryIntelligenceTemporalResolvesFollowingWeekday),
|
||||||
("Story Intelligence temporal resolves evening clock time", StoryIntelligenceTemporalResolvesEveningClockTime),
|
("Story Intelligence temporal resolves evening clock time", StoryIntelligenceTemporalResolvesEveningClockTime),
|
||||||
("Story Intelligence temporal rejects unanchored clock time", StoryIntelligenceTemporalRejectsUnanchoredClockTime),
|
("Story Intelligence temporal rejects unanchored clock time", StoryIntelligenceTemporalRejectsUnanchoredClockTime),
|
||||||
|
("Story Intelligence temporal preserves yearless calendar date clues", StoryIntelligenceTemporalPreservesYearlessCalendarDateClues),
|
||||||
|
("Story Intelligence temporal resolves yearless calendar dates from context", StoryIntelligenceTemporalResolvesYearlessCalendarDatesFromContext),
|
||||||
("Story Intelligence temporal preserves vague few days", StoryIntelligenceTemporalPreservesVagueFewDays),
|
("Story Intelligence temporal preserves vague few days", StoryIntelligenceTemporalPreservesVagueFewDays),
|
||||||
|
("Core import applies structural chronology to canonical scenes", CoreImportAppliesStructuralChronologyToCanonicalScenes),
|
||||||
("Story Intelligence temporal importer preserves manual chronology", StoryIntelligenceTemporalImporterPreservesManualChronology),
|
("Story Intelligence temporal importer preserves manual chronology", StoryIntelligenceTemporalImporterPreservesManualChronology),
|
||||||
("Story Intelligence temporal materialisation saves canonical chronology", StoryIntelligenceTemporalMaterialisationSavesCanonicalChronology),
|
("Story Intelligence temporal materialisation saves canonical chronology", StoryIntelligenceTemporalMaterialisationSavesCanonicalChronology),
|
||||||
("Story Intelligence temporal malformed values are discarded", StoryIntelligenceTemporalMalformedValuesAreDiscarded),
|
("Story Intelligence temporal malformed values are discarded", StoryIntelligenceTemporalMalformedValuesAreDiscarded),
|
||||||
@ -7211,6 +7214,26 @@ static void StoryIntelligenceTemporalRejectsUnanchoredClockTime()
|
|||||||
Assert(result.TimeModeName == "Relative", "Unanchored clock mention should remain relative.");
|
Assert(result.TimeModeName == "Relative", "Unanchored clock mention should remain relative.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void StoryIntelligenceTemporalPreservesYearlessCalendarDateClues()
|
||||||
|
{
|
||||||
|
var context = new StoryIntelligenceTemporalContext(null, null, null);
|
||||||
|
var result = StoryIntelligenceTemporalResolver.Resolve(SceneWithTemporal(absoluteDate: "28th December", partOfDay: "morning", evidence: "28th December", confidence: 0.8m), context);
|
||||||
|
|
||||||
|
Assert(result.StartDateTime is null, "Yearless calendar dates should not silently use the current calendar year.");
|
||||||
|
Assert(result.TimeModeName == "Relative", "Yearless calendar dates without context should remain temporal clues.");
|
||||||
|
Assert(result.RelativeTimeText == "28th December", "Yearless calendar date evidence should be preserved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void StoryIntelligenceTemporalResolvesYearlessCalendarDatesFromContext()
|
||||||
|
{
|
||||||
|
var context = new StoryIntelligenceTemporalContext(new DateTime(1982, 12, 24), new DateTime(1982, 12, 24), null);
|
||||||
|
var result = StoryIntelligenceTemporalResolver.Resolve(SceneWithTemporal(absoluteDate: "28th December", partOfDay: "morning", evidence: "28th December", confidence: 0.8m), context);
|
||||||
|
|
||||||
|
Assert(result.StartDateTime == new DateTime(1982, 12, 28), "Yearless calendar dates should use the reliable rolling/chapter context year.");
|
||||||
|
Assert(result.TimeModeName == "Exact Date", "Context-resolved calendar dates should become exact dates.");
|
||||||
|
Assert(result.RelativeTimeText == "28 Dec 1982, morning", "Resolved yearless dates should retain the part-of-day label.");
|
||||||
|
}
|
||||||
|
|
||||||
static void StoryIntelligenceTemporalPreservesVagueFewDays()
|
static void StoryIntelligenceTemporalPreservesVagueFewDays()
|
||||||
{
|
{
|
||||||
var context = new StoryIntelligenceTemporalContext(new DateTime(1983, 10, 4), new DateTime(1983, 10, 4), null);
|
var context = new StoryIntelligenceTemporalContext(new DateTime(1983, 10, 4), new DateTime(1983, 10, 4), null);
|
||||||
@ -7221,6 +7244,19 @@ static void StoryIntelligenceTemporalPreservesVagueFewDays()
|
|||||||
Assert(result.RelativeTimeText == "a few days later", "Vague wording should be preserved.");
|
Assert(result.RelativeTimeText == "a few days later", "Vague wording should be preserved.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void CoreImportAppliesStructuralChronologyToCanonicalScenes()
|
||||||
|
{
|
||||||
|
var root = PlotLineRoot();
|
||||||
|
var onboarding = File.ReadAllText(Path.Combine(root, "Services/OnboardingService.cs"));
|
||||||
|
var resolver = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceTemporalResolver.cs"));
|
||||||
|
|
||||||
|
Assert(onboarding.Contains("await ApplyStructuralChronologyAsync(structuralScenes, result)", StringComparison.Ordinal), "Fresh core import should apply chronology after source text has canonical SceneIDs.");
|
||||||
|
Assert(onboarding.Contains("await ApplyStructuralChronologyAsync(recoveredScenes, existingBuild)", StringComparison.Ordinal), "Recovered core import should apply chronology when missing scene sources are repaired.");
|
||||||
|
Assert(onboarding.Contains("ExtractStructuralTemporalEvidence(structuralScene.SourceText)", StringComparison.Ordinal), "Core import chronology should use SceneManuscriptSources-equivalent structural source text.");
|
||||||
|
Assert(onboarding.Contains("HasExistingChronology(canonicalScene)", StringComparison.Ordinal), "Core import chronology should preserve existing author-entered chronology.");
|
||||||
|
Assert(resolver.Contains("HasExplicitYear(clean)", StringComparison.Ordinal), "Temporal resolver should avoid current-year date parsing for yearless manuscript dates.");
|
||||||
|
}
|
||||||
|
|
||||||
static void StoryIntelligenceTemporalImporterPreservesManualChronology()
|
static void StoryIntelligenceTemporalImporterPreservesManualChronology()
|
||||||
{
|
{
|
||||||
var repository = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Data/StoryIntelligenceResultRepository.cs"));
|
var repository = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Data/StoryIntelligenceResultRepository.cs"));
|
||||||
|
|||||||
@ -47,6 +47,7 @@ public sealed class OnboardingService(
|
|||||||
ICharacterRepository characters,
|
ICharacterRepository characters,
|
||||||
IChapterRepository chapters,
|
IChapterRepository chapters,
|
||||||
ISceneRepository scenes,
|
ISceneRepository scenes,
|
||||||
|
ILookupRepository lookups,
|
||||||
IOnboardingBuildRepository builds,
|
IOnboardingBuildRepository builds,
|
||||||
IManuscriptStructureAnalysisService structureAnalysis,
|
IManuscriptStructureAnalysisService structureAnalysis,
|
||||||
IManuscriptTextStatisticsService manuscriptTextStatistics,
|
IManuscriptTextStatisticsService manuscriptTextStatistics,
|
||||||
@ -350,6 +351,7 @@ public sealed class OnboardingService(
|
|||||||
await SaveStructuralChapterSourcesAsync(preview, existingBuild, recoveredScenes);
|
await SaveStructuralChapterSourcesAsync(preview, existingBuild, recoveredScenes);
|
||||||
await SaveSceneManuscriptSourcesAsync(preview, recoveredScenes, existingBuild);
|
await SaveSceneManuscriptSourcesAsync(preview, recoveredScenes, existingBuild);
|
||||||
await coreImportCharacterMapping.MapBookAsync(preview.ProjectID, preview.BookID);
|
await coreImportCharacterMapping.MapBookAsync(preview.ProjectID, preview.BookID);
|
||||||
|
await ApplyStructuralChronologyAsync(recoveredScenes, existingBuild);
|
||||||
await ApplyStructuralPovAsync(preview, recoveredScenes, existingBuild);
|
await ApplyStructuralPovAsync(preview, recoveredScenes, existingBuild);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -437,6 +439,7 @@ public sealed class OnboardingService(
|
|||||||
await SaveStructuralChapterSourcesAsync(preview, result, structuralScenes);
|
await SaveStructuralChapterSourcesAsync(preview, result, structuralScenes);
|
||||||
await SaveSceneManuscriptSourcesAsync(preview, structuralScenes, result);
|
await SaveSceneManuscriptSourcesAsync(preview, structuralScenes, result);
|
||||||
await coreImportCharacterMapping.MapBookAsync(preview.ProjectID, preview.BookID);
|
await coreImportCharacterMapping.MapBookAsync(preview.ProjectID, preview.BookID);
|
||||||
|
await ApplyStructuralChronologyAsync(structuralScenes, result);
|
||||||
await ApplyStructuralPovAsync(preview, structuralScenes, result);
|
await ApplyStructuralPovAsync(preview, structuralScenes, result);
|
||||||
var canonicalSceneSources = await builds.ListSceneManuscriptSourcesByBookAsync(preview.BookID);
|
var canonicalSceneSources = await builds.ListSceneManuscriptSourcesByBookAsync(preview.BookID);
|
||||||
var locationReview = await coreImportBasicLocations.EnsurePreparedAsync(
|
var locationReview = await coreImportBasicLocations.EnsurePreparedAsync(
|
||||||
@ -547,6 +550,174 @@ public sealed class OnboardingService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ApplyStructuralChronologyAsync(
|
||||||
|
IReadOnlyList<ManuscriptStructureSceneBoundary> structuralScenes,
|
||||||
|
OnboardingManuscriptBuildResult result)
|
||||||
|
{
|
||||||
|
if (structuralScenes.Count == 0 || result.SceneMappings.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lookupData = await lookups.GetAllAsync();
|
||||||
|
var timeModeIds = lookupData.TimeModes.ToDictionary(item => item.TimeModeName, item => item.TimeModeID, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var timeConfidenceIds = lookupData.TimeConfidences.ToDictionary(item => item.TimeConfidenceName, item => item.TimeConfidenceID, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var sceneMap = result.SceneMappings.ToDictionary(mapping => mapping.TemporarySceneKey, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var chapterScenes in structuralScenes
|
||||||
|
.GroupBy(scene => scene.TemporaryChapterKey)
|
||||||
|
.OrderBy(group => group.Min(scene => scene.WordStartPosition)))
|
||||||
|
{
|
||||||
|
var temporalContext = StoryIntelligenceTemporalResolver.CreateChapterContext(
|
||||||
|
chapterScenes.FirstOrDefault()?.SourceText,
|
||||||
|
chapterScenes.FirstOrDefault()?.StructuralSummary,
|
||||||
|
chapterScenes.FirstOrDefault()?.StructuralSetting);
|
||||||
|
|
||||||
|
foreach (var structuralScene in chapterScenes.OrderBy(scene => scene.SceneNumberWithinChapter))
|
||||||
|
{
|
||||||
|
if (!sceneMap.TryGetValue(structuralScene.TemporarySceneKey, out var mappedScene))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var temporalScene = BuildStructuralTemporalScene(structuralScene);
|
||||||
|
if (temporalScene is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolved = StoryIntelligenceTemporalResolver.Resolve(temporalScene, temporalContext);
|
||||||
|
temporalContext = resolved.NextContext;
|
||||||
|
if (string.Equals(resolved.TimeModeName, "Unknown", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!resolved.StartDateTime.HasValue && string.IsNullOrWhiteSpace(resolved.RelativeTimeText)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canonicalScene = await scenes.GetAsync(mappedScene.SceneID);
|
||||||
|
if (canonicalScene is null || HasExistingChronology(canonicalScene))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timeModeIds.TryGetValue(resolved.TimeModeName, out var timeModeId)
|
||||||
|
|| !timeConfidenceIds.TryGetValue(resolved.TimeConfidenceName, out var timeConfidenceId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
canonicalScene.TimeModeID = timeModeId;
|
||||||
|
canonicalScene.TimeModeName = resolved.TimeModeName;
|
||||||
|
canonicalScene.StartDateTime = resolved.StartDateTime;
|
||||||
|
canonicalScene.EndDateTime = resolved.EndDateTime;
|
||||||
|
canonicalScene.DurationAmount = resolved.DurationAmount;
|
||||||
|
canonicalScene.DurationUnitID = resolved.DurationUnitID;
|
||||||
|
canonicalScene.RelativeTimeText = resolved.RelativeTimeText;
|
||||||
|
canonicalScene.TimeConfidenceID = timeConfidenceId;
|
||||||
|
canonicalScene.TimeConfidenceName = resolved.TimeConfidenceName;
|
||||||
|
await scenes.SaveAsync(canonicalScene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SceneIntelligenceScene? BuildStructuralTemporalScene(ManuscriptStructureSceneBoundary structuralScene)
|
||||||
|
{
|
||||||
|
var evidence = ExtractStructuralTemporalEvidence(structuralScene.SourceText)
|
||||||
|
?? ExtractStructuralTemporalEvidence(structuralScene.StructuralSetting)
|
||||||
|
?? ExtractStructuralTemporalEvidence(structuralScene.StructuralSummary);
|
||||||
|
var partOfDay = FirstTemporalPartOfDay(structuralScene.StructuralSetting)
|
||||||
|
?? FirstTemporalPartOfDay(structuralScene.SourceText)
|
||||||
|
?? FirstTemporalPartOfDay(structuralScene.StructuralSummary);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(evidence) && string.IsNullOrWhiteSpace(partOfDay))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SceneIntelligenceScene
|
||||||
|
{
|
||||||
|
Setting = new SceneIntelligenceSetting
|
||||||
|
{
|
||||||
|
DateOrTimeReference = evidence,
|
||||||
|
TimeOfDay = partOfDay,
|
||||||
|
Confidence = string.IsNullOrWhiteSpace(evidence) ? 0.65m : 0.9m
|
||||||
|
},
|
||||||
|
TemporalAnalysis = new SceneIntelligenceTemporalAnalysis
|
||||||
|
{
|
||||||
|
AbsoluteDate = evidence,
|
||||||
|
PartOfDay = partOfDay,
|
||||||
|
Evidence = evidence ?? partOfDay,
|
||||||
|
Confidence = string.IsNullOrWhiteSpace(evidence) ? 0.65m : 0.9m
|
||||||
|
},
|
||||||
|
TimelineClues = string.IsNullOrWhiteSpace(evidence)
|
||||||
|
? []
|
||||||
|
: [new SceneIntelligenceTimelineClue { Clue = evidence, AbsoluteDate = evidence, Evidence = evidence, Confidence = 0.9m }]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractStructuralTemporalEvidence(string? value)
|
||||||
|
{
|
||||||
|
var text = Clean(value);
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var explicitDate = Regex.Match(
|
||||||
|
text,
|
||||||
|
@"\b(?:\d{1,2}(?:st|nd|rd|th)?\s+(?:Jan|January|Feb|February|Mar|March|Apr|April|May|Jun|June|Jul|July|Aug|August|Sep|Sept|September|Oct|October|Nov|November|Dec|December)|(?:Jan|January|Feb|February|Mar|March|Apr|April|May|Jun|June|Jul|July|Aug|August|Sep|Sept|September|Oct|October|Nov|November|Dec|December)\s+\d{1,2}(?:st|nd|rd|th)?),?\s+\d{4}\b",
|
||||||
|
RegexOptions.IgnoreCase);
|
||||||
|
if (explicitDate.Success)
|
||||||
|
{
|
||||||
|
return explicitDate.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var yearlessDate = Regex.Match(
|
||||||
|
text,
|
||||||
|
@"\b(?:\d{1,2}(?:st|nd|rd|th)?\s+(?:Jan|January|Feb|February|Mar|March|Apr|April|May|Jun|June|Jul|July|Aug|August|Sep|Sept|September|Oct|October|Nov|November|Dec|December)|(?:Jan|January|Feb|February|Mar|March|Apr|April|May|Jun|June|Jul|July|Aug|August|Sep|Sept|September|Oct|October|Nov|November|Dec|December)\s+\d{1,2}(?:st|nd|rd|th)?)\b",
|
||||||
|
RegexOptions.IgnoreCase);
|
||||||
|
if (yearlessDate.Success)
|
||||||
|
{
|
||||||
|
return yearlessDate.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relative = Regex.Match(
|
||||||
|
text,
|
||||||
|
@"\b(?:the\s+)?(?:same|following|next)\s+(?:morning|afternoon|evening|night|day|week|month|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)\b|\b(?:\d+|one|two|three|four|five|six|seven|eight|nine|ten)\s+days?\s+later\b|\bafter midnight\b|\bbefore dawn\b",
|
||||||
|
RegexOptions.IgnoreCase);
|
||||||
|
return relative.Success ? relative.Value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FirstTemporalPartOfDay(string? value)
|
||||||
|
{
|
||||||
|
var text = Clean(value);
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var part in new[] { "morning", "afternoon", "evening", "night", "dawn", "dusk", "midday", "midnight" })
|
||||||
|
{
|
||||||
|
if (text.Contains(part, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasExistingChronology(Scene scene)
|
||||||
|
=> scene.StartDateTime.HasValue
|
||||||
|
|| scene.EndDateTime.HasValue
|
||||||
|
|| scene.DurationAmount.HasValue
|
||||||
|
|| scene.DurationUnitID.HasValue
|
||||||
|
|| !string.IsNullOrWhiteSpace(scene.RelativeTimeText)
|
||||||
|
|| (!string.IsNullOrWhiteSpace(scene.TimeModeName)
|
||||||
|
&& !string.Equals(scene.TimeModeName, "Unknown", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (!string.IsNullOrWhiteSpace(scene.TimeConfidenceName)
|
||||||
|
&& !string.Equals(scene.TimeConfidenceName, "Unknown", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
private async Task ApplyStructuralPovAsync(
|
private async Task ApplyStructuralPovAsync(
|
||||||
ManuscriptScanPreview preview,
|
ManuscriptScanPreview preview,
|
||||||
IReadOnlyList<ManuscriptStructureSceneBoundary> structuralScenes,
|
IReadOnlyList<ManuscriptStructureSceneBoundary> structuralScenes,
|
||||||
|
|||||||
@ -96,7 +96,8 @@ public static class StoryIntelligenceTemporalResolver
|
|||||||
}
|
}
|
||||||
|
|
||||||
var confidence = NormaliseConfidence(temporal?.Confidence);
|
var confidence = NormaliseConfidence(temporal?.Confidence);
|
||||||
var absoluteDate = ParseDate(temporal?.AbsoluteDate) ?? ParseDate(fallbackText);
|
var contextDate = context.LastDate ?? context.ChapterAnchorDate;
|
||||||
|
var absoluteDate = ParseDate(temporal?.AbsoluteDate, contextDate) ?? ParseDate(fallbackText, contextDate);
|
||||||
var baseDate = absoluteDate ?? context.LastDate ?? context.ChapterAnchorDate;
|
var baseDate = absoluteDate ?? context.LastDate ?? context.ChapterAnchorDate;
|
||||||
var resolvedDate = absoluteDate;
|
var resolvedDate = absoluteDate;
|
||||||
var isDateDeterministic = absoluteDate.HasValue;
|
var isDateDeterministic = absoluteDate.HasValue;
|
||||||
@ -259,7 +260,7 @@ public static class StoryIntelligenceTemporalResolver
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DateTime? ParseDate(string? value)
|
private static DateTime? ParseDate(string? value, DateTime? contextDate = null)
|
||||||
{
|
{
|
||||||
var clean = StripOrdinalSuffixes(Clean(value));
|
var clean = StripOrdinalSuffixes(Clean(value));
|
||||||
if (string.IsNullOrWhiteSpace(clean))
|
if (string.IsNullOrWhiteSpace(clean))
|
||||||
@ -267,6 +268,8 @@ public static class StoryIntelligenceTemporalResolver
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasExplicitYear = HasExplicitYear(clean);
|
||||||
|
|
||||||
foreach (var culture in DateCultures)
|
foreach (var culture in DateCultures)
|
||||||
{
|
{
|
||||||
if (DateTime.TryParseExact(clean, DateFormats, culture, DateTimeStyles.AllowWhiteSpaces, out var exact))
|
if (DateTime.TryParseExact(clean, DateFormats, culture, DateTimeStyles.AllowWhiteSpaces, out var exact))
|
||||||
@ -274,7 +277,7 @@ public static class StoryIntelligenceTemporalResolver
|
|||||||
return exact.Date;
|
return exact.Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DateTime.TryParse(clean, culture, DateTimeStyles.AllowWhiteSpaces, out var parsed) && parsed.Year > 1000)
|
if (hasExplicitYear && DateTime.TryParse(clean, culture, DateTimeStyles.AllowWhiteSpaces, out var parsed) && parsed.Year > 1000)
|
||||||
{
|
{
|
||||||
return parsed.Date;
|
return parsed.Date;
|
||||||
}
|
}
|
||||||
@ -289,9 +292,51 @@ public static class StoryIntelligenceTemporalResolver
|
|||||||
return ParseDate(embeddedDate.Value);
|
return ParseDate(embeddedDate.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contextDate.HasValue)
|
||||||
|
{
|
||||||
|
var monthDay = System.Text.RegularExpressions.Regex.Match(
|
||||||
|
clean,
|
||||||
|
@"\b(?<day>\d{1,2})\s+(?<month>Jan|January|Feb|February|Mar|March|Apr|April|May|Jun|June|Jul|July|Aug|August|Sep|Sept|September|Oct|October|Nov|November|Dec|December)\b",
|
||||||
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||||
|
if (!monthDay.Success)
|
||||||
|
{
|
||||||
|
monthDay = System.Text.RegularExpressions.Regex.Match(
|
||||||
|
clean,
|
||||||
|
@"\b(?<month>Jan|January|Feb|February|Mar|March|Apr|April|May|Jun|June|Jul|July|Aug|August|Sep|Sept|September|Oct|October|Nov|November|Dec|December)\s+(?<day>\d{1,2})\b",
|
||||||
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monthDay.Success
|
||||||
|
&& int.TryParse(monthDay.Groups["day"].Value, out var day)
|
||||||
|
&& TryMonthNumber(monthDay.Groups["month"].Value, out var month)
|
||||||
|
&& day >= 1
|
||||||
|
&& day <= DateTime.DaysInMonth(contextDate.Value.Year, month))
|
||||||
|
{
|
||||||
|
return new DateTime(contextDate.Value.Year, month, day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasExplicitYear(string value)
|
||||||
|
=> System.Text.RegularExpressions.Regex.IsMatch(value, @"\b(?:1[5-9]\d{2}|20\d{2}|21\d{2})\b");
|
||||||
|
|
||||||
|
private static bool TryMonthNumber(string value, out int month)
|
||||||
|
{
|
||||||
|
month = 0;
|
||||||
|
foreach (var culture in DateCultures)
|
||||||
|
{
|
||||||
|
if (DateTime.TryParseExact(value, ["MMM", "MMMM"], culture, DateTimeStyles.AllowWhiteSpaces, out var parsed))
|
||||||
|
{
|
||||||
|
month = parsed.Month;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static TimeSpan? ParseTime(string? value, string? partOfDay)
|
private static TimeSpan? ParseTime(string? value, string? partOfDay)
|
||||||
{
|
{
|
||||||
var clean = Clean(value)?.Replace('’', '\'');
|
var clean = Clean(value)?.Replace('’', '\'');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user