namespace OpenNest.Data.MaterialLedger; /// /// Gives the plates Auto Nest committed on ledger stock their item codes. Runs only after the /// proposal was validated, kept and committed, so a rejected or cancelled one never changes /// codes. Codes are hand-off data for MaterialLedger holds, not inventory accounting. /// public static class LedgerStockAssignment { // ClampRemovalRecovery solves a sheet with clamps removed as "{stock id}::removed:{ids}". private const string VariantSeparator = "::"; /// The nest's plates; codes are dealt out in this order. /// Each plate the commit attached, with the engine stock id it was solved on. /// The ledger rows offered to the run. /// /// Each row's codes are a queue: a plate takes the next codes of /// its row (a bundle repeats its one code). A plate whose row runs out keeps the rest of its /// sheets uncoded. A committed plate on hand-typed stock has no codes. /// public static void Assign(IList nestPlates, IEnumerable<(Plate Plate, string StockId)> committed, IEnumerable rows) { ArgumentNullException.ThrowIfNull(nestPlates); ArgumentNullException.ThrowIfNull(committed); ArgumentNullException.ThrowIfNull(rows); var queues = new Dictionary>(StringComparer.Ordinal); foreach (var row in rows) queues.TryAdd(row.Id, new Queue(row.ItemCodes)); foreach (var (plate, stockId) in committed.OrderBy(c => IndexOf(nestPlates, c.Plate))) { var codes = new List(); if (stockId != null && queues.TryGetValue(BaseStockId(stockId), out var queue)) { while (codes.Count < plate.Quantity && queue.Count > 0) codes.Add(queue.Dequeue()); } plate.StockItemCodes = codes; } } /// The offered stock id a solved sheet came from, without a clamp-removal suffix. public static string BaseStockId(string stockId) { ArgumentNullException.ThrowIfNull(stockId); var separator = stockId.IndexOf(VariantSeparator, StringComparison.Ordinal); return separator < 0 ? stockId : stockId[..separator]; } /// /// "Stock: DROP-1182 (B3)" for display, with runs of one code counted ("SHEET-456 (A1) x3") /// and uncoded sheets noted; null when no sheet is coded. Locations are display-only. /// public static string? Describe(Plate plate, IReadOnlyDictionary? locations) { ArgumentNullException.ThrowIfNull(plate); var codes = plate.StockItemCodes; if (codes.Count == 0) return null; var parts = new List(); for (var i = 0; i < codes.Count;) { var run = 1; while (i + run < codes.Count && codes[i + run] == codes[i]) run++; var text = locations != null && locations.TryGetValue(codes[i], out var location) ? $"{codes[i]} ({location})" : codes[i]; parts.Add(run > 1 ? $"{text} x{run}" : text); i += run; } var uncoded = plate.Quantity - codes.Count; if (uncoded > 0) parts.Add($"{uncoded} uncoded"); return "Stock: " + string.Join(", ", parts); } private static int IndexOf(IList plates, Plate plate) { var index = plates.IndexOf(plate); return index < 0 ? int.MaxValue : index; } }