Search code examples
c#linq

Linq query to check if net value is equal


I have a list like this:

public class Record
{
    public int Qty { get; set; }
    public string Seq { get; set; }
}

seq contains int separated by ";".

List<Record> shipments = new List<Record>
{
    new Record { Qty = 50, Seq = "1" },
    new Record { Qty = 25, Seq = "2" },
    new Record { Qty = 25, Seq = "3" },
    new Record { Qty = 100, Seq = "1;2;3" }
};

I want to check if new quantity of all sequences are equal (or net quantity 0). I am writing a linq query that should return true if above condition is met like this:

Dictionary<string, int> sequenceQuantities = shipments
        .SelectMany(shipment => shipment.Seq.Split(';'), (shipment, seq) => new { shipment, seq })
        .GroupBy(x => x.seq)
        .ToDictionary(g => g.Key, g => g.Sum(x => x.shipment.Qty));

but its only adding and not subtracting the seq once we meet it again. How do I achieve this?

Another eg:

qty = 50, seq = 1
qty = 50, seq = 2;3
qty = 25, seq = 3
qty = 75, seq = 1,2

This should also return true as net qty is 0.


Solution

  • If I understood your question correctly, you have to found out, if all items sharing the same (partial) identity have the equal sum. If we take a look at your second example:

    qty = 50, seq = 1
    qty = 50, seq = 2;3
    qty = 25, seq = 3
    qty = 75, seq = 1,2
    

    This means, we take the first entry and decrease the last one by the same amount. This brings up this sequence:

    qty =  0, seq = 1
    qty = 50, seq = 2;3
    qty = 25, seq = 3
    qty = 25, seq = 1,2
    

    Afterwards we check the third row and apply it on the second one:

    qty =  0, seq = 1
    qty = 25, seq = 2;3
    qty =  0, seq = 3
    qty = 25, seq = 1,2
    

    Cause second and fourth row also share a same id, we can also subtract both, which brings out:

    qty =  0, seq = 1
    qty =  0, seq = 2;3
    qty =  0, seq = 3
    qty =  0, seq = 1,2
    

    This should be considered as a valid shipment. If this is correct, you can't solve this problem with a simple LINQ query. All elements referencing multiple sequence identities have to share the same quantity to ensure we can correctly erase fractions of quantities.

    Here is my approach, that adds an additional shared value to each instance that was build up from an instance of multiple sequences:

    public record Record
    {
        [SetsRequiredMembers]
        public Record(int qty, string seq)
        {
            Qty = qty;
            Seq = seq;
            Value = new Value(Qty);
        }
    
        private Record(Value value)
        {
            Value = value;
        }
    
        public required int Qty { get; init; }
        public required string Seq { get; init; }
    
        public Value Value { get; init; }
    
        public IReadOnlyCollection<Record> CreateSharedInstances()
        {
            return Seq.Split(';')
                .Select(seq => new Record(Value) { Qty = Qty, Seq = seq })
                .ToList();
        }
    }
    
    public class Value
    {
        public Value(int amount)
        {
            Amount = amount;
        }
        public int Amount { get; private set; }
    
        public void Decrease(int qty)
        {
            Amount -= qty;
        }
    
        public override string ToString()
        {
            return $"Amount = {Amount}";
        }
    }
    

    To correctly apply the fractions on all records, you could try this method:

    public static class ShipmentCalculator
    {
        public static IReadOnlyList<Record> FindMisalignedShipments(IReadOnlyCollection<Record> shipments)
        {
            var individualEntries = shipments
                .SelectMany(record => record.CreateSharedInstances())
                .ToList();
    
            foreach (var group in individualEntries.GroupBy(record => record.Seq))
            {
                var entries = group.OrderBy(record => record.Value.Amount).ToList();
    
                foreach (var entry in entries.SkipLast(1))
                {
                    var amount = entry.Value.Amount;
                    entries.ForEach(e => e.Value.Decrease(amount));
                }
            }
    
            return shipments
                .Where(record => record.Value.Amount != 0)
                .ToList();
        }
    }
    

    If we apply this on your two given correct examples, it returns no misaligned entries:

            var shipments = new[]
            {
                new Record(50, "1"),
                new Record(25, "2"),
                new Record(25, "3"),
                new Record(100, "1;2;3"),
            };
    
            var misalignedEntries = ShipmentCalculator.FindMisalignedShipments(shipments);
            Console.WriteLine($"Misaligned items: {misalignedEntries.Count}");
    
            foreach (var item in misalignedEntries)
            {
                Console.WriteLine($"{item.Seq} has still amount of {item.Value.Amount}");
            }
    
            var shipments2 = new[]
            {
                new Record(50, "1"),
                new Record(50, "2;3"),
                new Record(25, "3"),
                new Record(75, "1;2"),
            };
    
            misalignedEntries = ShipmentCalculator.FindMisalignedShipments(shipments2);
            Console.WriteLine($"Misaligned items: {misalignedEntries.Count}");
    
            foreach (var item in misalignedEntries)
            {
                Console.WriteLine($"{item.Seq} has still amount of {item.Value.Amount}");
            }
    

    And the whole code is also available as fiddle.