Files
SafelyYouCodingChallenge/dotnet/SafelyYou/Devices.cs
T

100 lines
3.6 KiB
C#

using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using SafelyYou.Config;
namespace SafelyYou;
public readonly record struct DeviceId(string Value);
public sealed class KnownDevices
{
private readonly HashSet<DeviceId> _knownDevices;
public KnownDevices(HashSet<DeviceId> knownDevices) => _knownDevices = knownDevices;
public bool Contains(DeviceId deviceId) => _knownDevices.Contains(deviceId);
}
public readonly record struct HeartBeatRange(DateTimeOffset Start, DateTimeOffset? End);
public sealed class DeviceHeartBeatHistory
{
private readonly HeartBeatHistoryConfig _config;
public enum AddResult { Added, OutOfSequence }
public DeviceHeartBeatHistory(HeartBeatHistoryConfig config) => _config = config;
private readonly PriorityQueue<HeartBeatRange, DateTimeOffset> _histories =
// lowest priority is first, but this allows the highest date to be dequeued first
new PriorityQueue<HeartBeatRange, DateTimeOffset>(new InverseDateTimeComparer());
private sealed class InverseDateTimeComparer : IComparer<DateTimeOffset>
{
public int Compare(DateTimeOffset x, DateTimeOffset y) => -(x.CompareTo(y));
}
/// <summary>Returns Added, needed to be used as an expression</summary>
private static AddResult Replace(PriorityQueue<HeartBeatRange, DateTimeOffset> histories, HeartBeatRange updated)
{
histories.DequeueEnqueue(updated, updated.Start);
return AddResult.Added;
}
/// <summary>Returns Added, needed to be used as an expression</summary>
private static AddResult EnqueueNew(PriorityQueue<HeartBeatRange, DateTimeOffset> histories, DateTimeOffset sentAt)
{
histories.Enqueue(new HeartBeatRange(sentAt, null), sentAt);
return AddResult.Added;
}
public AddResult Add(DateTimeOffset sentAt)
{
// check the latest first, assuming things are in order
// if it's "in the past" and doesn't make sense, return an error
// histories that are adjacent are combined, if there's more than a one minute gap (what about a margin of error?)
// then create a new entry. The margin is coming from the config. I've set it to 1 second.
if (!_histories.TryPeek(out var last, out _))
return EnqueueNew(_histories, sentAt);
// last registered heartbeat is End if set, otherwise Start
return (sentAt, last.Start, last.End) switch
{
var (sent, start, _) when sent < start => AddResult.OutOfSequence,
(var sent, _, { } end) when sent < end => AddResult.OutOfSequence,
// no End yet: extend if within Window of Start
(var sent, var start, null) when sent <= start + _config.Window => Replace(_histories, last with { End = sentAt }),
// End set: extend if within Window of End
(var sent, _, { } end) when sent <= end + _config.Window => Replace(_histories, last with { End = sentAt }),
// gap larger than Window: start a new range
_ => EnqueueNew(_histories, sentAt),
};
}
}
public sealed class HeartBeatHistories
{
private readonly HeartBeatHistoryConfig _config;
private readonly ConcurrentDictionary<DeviceId, DeviceHeartBeatHistory> _histories = new();
public HeartBeatHistories(HeartBeatHistoryConfig config) => _config = config;
public DeviceHeartBeatHistory.AddResult Add(DeviceId deviceId, DateTimeOffset sentAt)
{
if (!_histories.TryGetValue(deviceId, out var history))
{
history = new(_config);
_histories[deviceId] = history;
}
return history.Add(sentAt);
}
}