using System.Text.RegularExpressions; namespace Finch.Numbers; public class Numbers : INumbers { protected IFinchNumberStoreDbProvider Db { get; set; } protected IFinchOptions Options { get; set; } const string DEFAULT_COUNTER = "Default"; Regex templateRegex = new("{(date|number|year|random):?([a-zA-Z0-9-_]*)}", RegexOptions.IgnoreCase); Regex containsYearRegex = new("{(date|year|date:([^}]*y+[^}]*))}", RegexOptions.IgnoreCase); Random random = new(); public Numbers(IFinchNumberStoreDbProvider db, IFinchOptions options) { Db = db; Options = options; } /// public Task Next(string alias, DateTimeOffset? date = null, bool store = true) { return Next(alias, date.HasValue ? DateOnly.FromDateTime(date.Value.Date) : null, store); } /// public async Task Next(string alias, DateOnly? date = null, bool store = true) { Number number = await Get(alias); string key = GetCounterKey(number); // get a matching counter NumberCounter counter = number.Counters.FirstOrDefault(x => x.Key == key) ?? new() { Key = key }; // calculate new value bool hasValue = counter.Count != 0; long oldValue = hasValue ? counter.Count : number.StartNumber; long newValue = oldValue + (hasValue ? number.Step : 0); // increment the value and store it in database if (store) { counter.Count = newValue; if (!hasValue) { number.Counters.Add(counter); } await Db.Update(number); } // compiles the template and returns the rendered result return Compile(number.Template, newValue, number.MinLength, date); } /// public async Task Set(string alias, long value, DateTimeOffset? date = null) { Number number = await Get(alias); string key = GetCounterKey(number); // get a matching counter NumberCounter counter = number.Counters.FirstOrDefault(x => x.Key == key) ?? new() { Key = key }; bool isNew = counter.Count == 0; // set new value counter.Count = value; if (isNew) { number.Counters.Add(counter); } await Db.Update(number); // compiles the template and returns the rendered result return Compile(number.Template, value, number.MinLength, date, number.PostProcess); } /// public async Task Reset(string alias) { string id = Id(alias); Number number = await Db.Load(id); if (number == null) { return; } string key = GetCounterKey(number); NumberCounter counter = number.Counters.FirstOrDefault(x => x.Key == key); if (counter != null) { number.Counters.Remove(counter); await Db.Update(number); } } /// public async Task ResetAll(string alias) { string id = Id(alias); Number number = await Db.Load(id); if (number == null) { return; } number.Counters = new(); await Db.Update(number); } /// public string Compile(string template, long value, int minLength, DateTimeOffset? date = null, Func postProcess = null) { return Compile(template, value, minLength, date.HasValue ? DateOnly.FromDateTime(date.Value.Date) : null, postProcess); } /// public string Compile(string template, long value, int minLength, DateOnly? date = null, Func postProcess = null) { string output = template; MatchCollection matches = templateRegex.Matches(output); DateOnly usedDate = date ?? DateOnly.FromDateTime(DateTime.Now); foreach (Match match in matches) { if (!match.Success) { continue; } string original = match.Value; string type = match.Groups[1].Value; string options = match.Groups[2].Value; string result = string.Empty; if (type == "year") { result = usedDate.Year.ToString(); } else if (type == "date") { result = usedDate.ToString(options.IsNullOrWhiteSpace() ? "dd-MM-yyyy" : options); } else if (type == "number") { result = value.ToString("D" + (minLength > 0 && minLength <= 16 ? minLength : 1)); } else if (type == "random") { int length = 5; if (!options.IsNullOrWhiteSpace() && int.TryParse(options, out int oLength) && oLength > 0 && oLength <= 10) { length = oLength; } result = random.Next(0, int.MaxValue).ToString("D10").Substring(0, length); } output = output.ReplaceFirst(original, result); } if (postProcess != null) { output = postProcess(output); } return output; } /// /// Create a new number implementation /// protected async Task Get(string alias) { NumberOptionsItem options = Options.For().GetValueOrDefault(alias); if (options == null) { throw new KeyNotFoundException($"Could not find number options (finch:numbers:) for alias {alias}"); } string id = Id(alias); Number number = await Db.Load(id); bool exists = number != null; if (!exists) { number = new() { Id = id, Template = options.Template, StartNumber = options.StartNumber, MinLength = options.MinLength, Step = options.Step, ResetPerYear = options.ResetPerYear }; await Db.Create(number); } else { bool changed = false; if (!number.Template.Equals(options.Template)) { number.Template = options.Template; changed = true; } if (!number.StartNumber.Equals(options.StartNumber)) { number.StartNumber = options.StartNumber; changed = true; } if (!number.MinLength.Equals(options.MinLength)) { number.MinLength = options.MinLength; changed = true; } if (!number.Step.Equals(options.Step)) { number.Step = options.Step; changed = true; } if (!number.ResetPerYear.Equals(options.ResetPerYear)) { number.ResetPerYear = options.ResetPerYear; changed = true; } if (changed) { await Db.Update(number); } } return number; } /// /// Generate number ID /// protected string Id(string alias) { return "numbers." + alias; } /// /// Get the key for the current counter /// protected virtual string GetCounterKey(Number number) { if (!number.ResetPerYear || !containsYearRegex.IsMatch(number.Template)) { return DEFAULT_COUNTER; } return DateTimeOffset.Now.Year.ToString(); } } public interface INumbers { /// /// Get the next available number for the specified template /// Task Next(string alias, DateTimeOffset? date = null, bool store = true); /// /// Get the next available number for the specified template /// Task Next(string alias, DateOnly? date = null, bool store = true); /// /// Set a number to a value /// Task Set(string alias, long value, DateTimeOffset? date = null); /// /// Resets the current counter for the specified template /// Task Reset(string alias); /// /// Resets all counters for the specified template /// Task ResetAll(string alias); /// /// Renders the final output from the template and the value /// string Compile(string template, long value, int minLength, DateTimeOffset? date = null, Func postProcess = null); /// /// Renders the final output from the template and the value /// string Compile(string template, long value, int minLength, DateOnly? date = null, Func postProcess = null); }