This repository has been archived on 2023-02-02. You can view files and clone it, but cannot push or open issues or pull requests.
mfgames-nitride-cil/src/MfGames.Nitride.Temporal/CreateDateIndexes.cs

188 lines
6.4 KiB
C#

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;
/// <summary>
/// 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.
/// </summary>
[WithProperties]
public partial class CreateDateIndexes : OperationBase, IResolvingOperation
{
private readonly IValidator<CreateDateIndexes> validator;
public CreateDateIndexes(
IValidator<CreateDateIndexes> validator,
TimeService timeService)
{
this.validator = validator;
this.TimeService = timeService;
}
/// <summary>
/// Gets or sets the callback used to create a new index.
/// </summary>
public Func<DateIndex, Entity>? CreateIndex { get; set; } = null!;
/// <summary>
/// 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.
/// </summary>
public List<string> Formats { get; set; } = null!;
/// <summary>
/// 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.
/// </summary>
public int LessThanEqualCollapse { get; set; }
public TimeService TimeService { get; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(
IEnumerable<Entity> 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<Dictionary<string, List<Entity>>> entries = new();
for (int i = 0; i < this.Formats.Count; i++)
{
entries.Add(new Dictionary<string, List<Entity>>());
}
// 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<Instant>(
(
entity,
instant) => this.GroupOnFormats(instant, entries, entity))
.ToList();
// Going in reverse order (most precise to less precise), we create the various indexes.
Dictionary<string, List<Entity>> indexes = new();
List<Entity> seen = new();
for (int i = 0; i < this.Formats.Count; i++)
{
foreach (KeyValuePair<string, List<Entity>> 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<Entity>? 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<Entity>? childIndexes =
indexes.TryGetValue(pair.Key, out List<Entity>? list)
? list
: new List<Entity>();
// 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<Instant>())
.ToString(this.Formats[i + 1]);
if (!indexes.ContainsKey(nextKey))
{
indexes[nextKey] = new List<Entity>();
}
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<Dictionary<string, List<Entity>>> 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<Entity>();
}
grouped[i][formatted]
.Add(entity);
}
return entity;
}
}