using System; using System.Collections.Generic; using System.Linq; using System.Threading; using FluentValidation; using MfGames.Gallium; using MfGames.Nitride.Generators; using NodaTime; namespace MfGames.Nitride.Temporal; /// /// Constructs indexes for any arbitrary formatting of date time to allow for /// nested structures. This takes a list /// of DateTime formats, ordered from most specific to least specific and then /// organizes the results. /// [WithProperties] public partial class CreateDateIndexes : OperationBase, IResolvingOperation { private readonly IValidator validator; public CreateDateIndexes( IValidator validator, TimeService timeService) { this.validator = validator; this.TimeService = timeService; } /// /// Gets or sets the callback used to create a new index. /// public Func? CreateIndex { get; set; } = null!; /// /// Gets or sets the ordered list of DateTime formats, such as "yyyy/MM", going /// from most specific to least /// specific. Indexes will be created for every applicable entry at all the levels. /// public List Formats { get; set; } = null!; /// /// Gets or sets the threshold where entries will be "collapsed" and emitted a /// higher level. For example, with a /// threshold of 10, if there are 10 or less entities, then they will also be /// emitted at a higher-level index. /// public int LessThanEqualCollapse { get; set; } public TimeService TimeService { get; } /// public override IEnumerable Run( IEnumerable input, CancellationToken cancellationToken = default) { // Validate our input. this.validator.ValidateAndThrow(this); // Go through all the inputs that have an Instant, get the DateTime, and then use that to categories each entity // into all the categories they match. We make the assumption that the entity "belongs" into the most precise // category they fit in. The `entries` variable is a list with the same indexes as the `this.Formats` property. List>> entries = new(); for (int i = 0; i < this.Formats.Count; i++) { entries.Add(new Dictionary>()); } // Go through the inputs and group each one. We also use `ToList` to force the enumeration to completely // resolve and we can get everything we need. We will append the created indexes to the end of this list. var output = input.SelectEntity( ( entity, instant) => this.GroupOnFormats(instant, entries, entity)) .ToList(); // Going in reverse order (most precise to less precise), we create the various indexes. Dictionary> indexes = new(); List seen = new(); for (int i = 0; i < this.Formats.Count; i++) { foreach (KeyValuePair> pair in entries[i]) { // Ignore blank entries. This should not be possible, but we're being paranoid. if (pair.Value.Count == 0) { continue; } // Get all the entities at this level and split them into ones we've seen (at a lower level) and which // ones are new (these always go on the index). var seenEntities = pair.Value.Where(a => seen.Contains(a)) .ToList(); var newEntities = pair.Value.Where(a => !seen.Contains(a)) .ToList(); seen.AddRange(newEntities); // The new entities are going to always be added, but if the new + seen is <= the threshold, we'll be // including both of them. List? childEntities = (newEntities.Count + seenEntities.Count) <= this.LessThanEqualCollapse ? pair.Value : newEntities; // Figure out which child indexes need to be included. If there isn't a list, then return an empty one. List? childIndexes = indexes.TryGetValue(pair.Key, out List? list) ? list : new List(); // Create the index then add it to the output list. var index = new DateIndex( pair.Key, this.Formats[i], childEntities, childIndexes); Entity? indexEntity = this.CreateIndex!(index); output.Add(indexEntity); // Also add the index into the next level up. We don't do this if we are in the last format (-1) plus // the zero-based index (-1). if (i > this.Formats.Count - 2) { continue; } Entity? first = pair.Value[0]; string? nextKey = this.TimeService .ToDateTime(first.Get()) .ToString(this.Formats[i + 1]); if (!indexes.ContainsKey(nextKey)) { indexes[nextKey] = new List(); } indexes[nextKey] .Add(indexEntity); } } // We are done processing. return output; } public CreateDateIndexes WithFormats(params string[] formats) { this.Formats = formats.ToList(); return this; } private Entity GroupOnFormats( Instant instant, List>> grouped, Entity entity) { var dateTime = this.TimeService.ToDateTime(instant); for (int i = 0; i < this.Formats.Count; i++) { string? formatted = dateTime.ToString(this.Formats[i]); if (!grouped[i] .ContainsKey(formatted)) { grouped[i][formatted] = new List(); } grouped[i][formatted] .Add(entity); } return entity; } }