diff --git a/PlotLine/Services/CharacterAgeDisplayService.cs b/PlotLine/Services/CharacterAgeDisplayService.cs new file mode 100644 index 0000000..34b5de6 --- /dev/null +++ b/PlotLine/Services/CharacterAgeDisplayService.cs @@ -0,0 +1,109 @@ +using PlotLine.Models; + +namespace PlotLine.Services; + +public sealed class CharacterAgeDisplayResult +{ + public bool HasExactAge { get; init; } + public string? Label { get; init; } + public string? DisplayText { get; init; } +} + +public static class CharacterAgeDisplayService +{ + public static CharacterAgeDisplayResult GetAgeForScene(SceneCharacter character, string? sceneTimeModeName, DateTime? sceneStartDateTime) + { + var effectiveDate = ResolveEffectiveSceneDate(sceneTimeModeName, sceneStartDateTime); + if (character.BirthDate.HasValue && effectiveDate.HasValue) + { + return new CharacterAgeDisplayResult + { + HasExactAge = true, + Label = "Age in scene", + DisplayText = FormatExactAge(character.BirthDate.Value.Date, effectiveDate.Value.Date) + }; + } + + if (character.AgeAtSeriesStart.HasValue) + { + return new CharacterAgeDisplayResult + { + Label = "Approximate age", + DisplayText = $"About {character.AgeAtSeriesStart.Value}" + }; + } + + return new CharacterAgeDisplayResult(); + } + + public static CharacterAgeDisplayResult GetAgeForScene(Character character, string? sceneTimeModeName, DateTime? sceneStartDateTime) + { + var effectiveDate = ResolveEffectiveSceneDate(sceneTimeModeName, sceneStartDateTime); + if (character.BirthDate.HasValue && effectiveDate.HasValue) + { + return new CharacterAgeDisplayResult + { + HasExactAge = true, + Label = "Age in scene", + DisplayText = FormatExactAge(character.BirthDate.Value.Date, effectiveDate.Value.Date) + }; + } + + if (character.AgeAtSeriesStart.HasValue) + { + return new CharacterAgeDisplayResult + { + Label = "Approximate age", + DisplayText = $"About {character.AgeAtSeriesStart.Value}" + }; + } + + return new CharacterAgeDisplayResult(); + } + + public static DateTime? ResolveEffectiveSceneDate(string? timeModeName, DateTime? startDateTime) + { + if (!startDateTime.HasValue) + { + return null; + } + + return string.Equals(timeModeName, "Exact Date", StringComparison.OrdinalIgnoreCase) + || string.Equals(timeModeName, "Exact DateTime", StringComparison.OrdinalIgnoreCase) + || string.Equals(timeModeName, "Exact Date and Time", StringComparison.OrdinalIgnoreCase) + ? startDateTime.Value.Date + : null; + } + + public static string FormatExactAge(DateTime birthDate, DateTime sceneDate) + { + var years = sceneDate.Year - birthDate.Year; + if (sceneDate < birthDate.AddYears(years)) + { + years--; + } + + if (years < 0) + { + years = 0; + } + + var lastBirthday = birthDate.AddYears(years); + var months = ((sceneDate.Year - lastBirthday.Year) * 12) + sceneDate.Month - lastBirthday.Month; + if (sceneDate.Day < lastBirthday.Day) + { + months--; + } + + if (months < 0) + { + months = 0; + } + + return months == 0 + ? $"{years} year{Pluralise(years)}" + : $"{years} year{Pluralise(years)}, {months} month{Pluralise(months)}"; + } + + private static string Pluralise(int value) => value == 1 ? string.Empty : "s"; +} diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index c39444c..43a0d0d 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -1001,6 +1001,7 @@ public sealed class SceneService( SceneNumber = existing.Count + 1, RevisionStatusID = ChapterService.DefaultRevisionStatusId(lookupData), TimeModeID = lookupData.TimeModes.FirstOrDefault(x => x.TimeModeName == "Unknown")?.TimeModeID ?? lookupData.TimeModes.First().TimeModeID, + TimeModeName = lookupData.TimeModes.FirstOrDefault(x => x.TimeModeName == "Unknown")?.TimeModeName ?? lookupData.TimeModes.First().TimeModeName, TimeConfidenceID = lookupData.TimeConfidences.FirstOrDefault(x => x.TimeConfidenceName == "Unknown")?.TimeConfidenceID ?? lookupData.TimeConfidences.First().TimeConfidenceID, Chapter = context.Value.Chapter, Book = context.Value.Book, @@ -1031,6 +1032,7 @@ public sealed class SceneService( SceneTitle = scene.SceneTitle, Summary = scene.Summary, TimeModeID = scene.TimeModeID, + TimeModeName = scene.TimeModeName, StartDateTime = scene.StartDateTime, EndDateTime = scene.EndDateTime, DurationAmount = scene.DurationAmount, @@ -5614,20 +5616,9 @@ public sealed class CharacterService( public string DisplayAge(Character character, DateTime? sceneStartDateTime = null) { - if (character.BirthDate.HasValue && sceneStartDateTime.HasValue) - { - var age = sceneStartDateTime.Value.Year - character.BirthDate.Value.Year; - if (sceneStartDateTime.Value.Date < character.BirthDate.Value.Date.AddYears(age)) - { - age--; - } + var age = CharacterAgeDisplayService.GetAgeForScene(character, sceneStartDateTime.HasValue ? "Exact DateTime" : null, sceneStartDateTime); - return age.ToString(); - } - - return character.AgeAtSeriesStart.HasValue - ? $"About {character.AgeAtSeriesStart.Value}" - : "Unknown"; + return age.DisplayText ?? "Unknown"; } private static IReadOnlyList ToRelationshipTypeSelectList(IEnumerable relationshipTypes) diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index f87d852..b5a316f 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -126,6 +126,8 @@ public sealed class SceneEditViewModel [Display(Name = "Time mode")] public int TimeModeID { get; set; } + public string TimeModeName { get; set; } = string.Empty; + [Display(Name = "Start date/time")] public DateTime? StartDateTime { get; set; } diff --git a/PlotLine/Views/Scenes/_SceneInspector.cshtml b/PlotLine/Views/Scenes/_SceneInspector.cshtml index 0e2700d..d921f93 100644 --- a/PlotLine/Views/Scenes/_SceneInspector.cshtml +++ b/PlotLine/Views/Scenes/_SceneInspector.cshtml @@ -354,10 +354,15 @@ @foreach (var character in Model.SceneCharacters) { var editId = $"scene-character-edit-{character.SceneCharacterID}"; + var ageDisplay = CharacterAgeDisplayService.GetAgeForScene(character, Model.TimeModeName, Model.StartDateTime);
@character.CharacterName + @if (!string.IsNullOrWhiteSpace(ageDisplay.DisplayText)) + { + @ageDisplay.Label: @ageDisplay.DisplayText + } @((character.RoleInSceneTypeName ?? "Role not set")) • @((character.PresenceTypeName ?? "Presence not set")) Location: @(character.LocationName ?? "Not set") Entry: @(character.EntryLocationName ?? "Not set") diff --git a/PlotLine/Views/_ViewImports.cshtml b/PlotLine/Views/_ViewImports.cshtml index ed3292a..ae6b7b1 100644 --- a/PlotLine/Views/_ViewImports.cshtml +++ b/PlotLine/Views/_ViewImports.cshtml @@ -1,5 +1,6 @@ @using PlotLine @using PlotLine.Models +@using PlotLine.Services @using PlotLine.ViewModels @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, PlotLine