188 lines
6.4 KiB
C#
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;
|
|
}
|
|
}
|