namespace OpenNest.Data.MaterialLedger; /// /// Takes item codes already on other open nests out of ledger stock rows, so two nests are /// not planned on the same sheet before ML holds exist for either. Pure: rows are rebuilt, /// never changed. /// public static class LedgerCodeFilter { /// /// without : every copy of a code /// leaves the row's and /// , its quantity drops by the copies removed, and /// a row left with none is dropped. Row ids and order are kept. /// counts the sheets and drops taken out. /// public static IReadOnlyList Exclude(IEnumerable rows, IEnumerable codesInUse, out int removed) { ArgumentNullException.ThrowIfNull(rows); ArgumentNullException.ThrowIfNull(codesInUse); var exclude = codesInUse as IReadOnlySet ?? codesInUse.ToHashSet(StringComparer.Ordinal); return Rebuild(rows, exclude.Contains, out removed); } /// /// with only of each code taken out, in /// row and ledger order: a bundle whose one code repeats per sheet loses as many sheets /// as were taken, not the whole bundle. Otherwise as /// . /// public static IReadOnlyList ExcludeCopies(IEnumerable rows, IReadOnlyDictionary copies, out int removed) { ArgumentNullException.ThrowIfNull(rows); ArgumentNullException.ThrowIfNull(copies); var left = new Dictionary(copies, StringComparer.Ordinal); return Rebuild(rows, code => { if (!left.TryGetValue(code, out var count) || count <= 0) return false; left[code] = count - 1; return true; }, out removed); } // takeOut is asked once per copy, in row order and the ledger order within a row. private static IReadOnlyList Rebuild(IEnumerable rows, Func takeOut, out int removed) { removed = 0; var kept = new List(); foreach (var row in rows) { if (row == null) continue; var codes = row.ItemCodes.Where(code => !takeOut(code)).ToList(); var dropped = row.ItemCodes.Count - codes.Count; if (dropped == 0) { kept.Add(row); continue; } removed += dropped; if (codes.Count == 0) continue; var left = codes.ToHashSet(StringComparer.Ordinal); var itemLocations = row.ItemLocations .Where(pair => left.Contains(pair.Key) || !row.ItemCodes.Contains(pair.Key)) .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal); // A location stays listed while an item left in the row is there, or when the row // never said which item it held. var locations = row.Locations .Where(location => itemLocations.ContainsValue(location) || !row.ItemLocations.Values.Contains(location, StringComparer.Ordinal)) .ToList(); kept.Add(row with { Quantity = codes.Count, ItemCodes = codes, Locations = locations, ItemLocations = itemLocations, }); } return kept; } /// /// with taken out of its rows and /// the sheets removed counted in . /// public static LedgerStockMapping Exclude(LedgerStockMapping mapping, IEnumerable codesInUse) { ArgumentNullException.ThrowIfNull(mapping); var rows = Exclude(mapping.Rows, codesInUse, out var removed); return mapping with { Rows = rows, OnOtherNests = mapping.OnOtherNests + removed }; } }