-1
//Items.cs
public class Items
{
    public int ItemNumber { get; set; }
    public DateTime ItemDate { get; set; }
    public List<ItemsLine> ItemsLines { get; set; } = new List<ItemsLine>();

    /// MergeItem appends the items from the sourceItem to the current item
    /// </summary>
    /// <param name="sourceItem">Item to merge from</param>
    public void MergeItem( Items sourceItem)
    {
        //IEnumerable<ItemsLine> items = new List<ItemsLine>();
        //IEnumerable<Items> items = ItemsLines.Union<item>
        
        throw new NotImplementedException();
    }
}

//ItemLine.cs
public class ItemsLine
{
    public int ItemLineId { get; set; }
    public int Quantity { get; set; }
    public double Cost { get; set; }

    // public double Total { get { return this.Cost * this.Quantity; } }

    public ItemsLine(int itemLineId, int quantity, double cost)
    {
        this.ItemLineId = itemLineId;
        this.Quantity = quantity;
        this.Cost = cost;
    }

    public ItemsLine() { }
}

//Program.cs
private static void MergeItem()
{
    var item1 = new Items();

    item1.ItemsLines.Add(new ItemsLine()
    {
        ItemLineId = 1,
        Cost = 10.33,
        Quantity = 4,
    });

    var item2 = new Items();

    item2.ItemsLines.Add(new ItemsLine()
    {
        ItemLineId = 2,
        Cost = 5.22,
        Quantity = 1,
    });

    item2.ItemsLines.Add(new ItemsLine()
    {
        ItemLineId = 3,
        Cost = 6.27,
        Quantity = 3,
    });

    item1.MergeItem(item2);
    Console.WriteLine("Grand total after merge = {0}", item1.ItemsLines.Sum(i => i.Cost * i.Quantity));
}

The rules for merging items are:

  • The ItemsLine instances in the .ItemsLines of the Item passed into Merge, are added to the this.ItemsLines, or used to update existing items
  • ItemsLine has an ItemLineID that is unique in the list
  • If the incoming ItemsLine.ItmeLineID already exists in the list the Quantity of the incoming item should be added to the existing item and existing Cost should be adjusted to the average of the existing and the incoming costs
  • If the incoming ItemsLine.ItemLineID doesn't exist in the list the ItemsLine should be added

I tried using below code but that didn't work

  • IEnumerable<ItemsLine> items = new List<ItemsLine>();

  • IEnumerable<Items> items = ItemsLines.Union<item>()

To merge do i need to use Concat?, so you can combine two collections that implement IEnumerable or the Linq join query to achieve mergeritem?

dbc
  • 80,875
  • 15
  • 141
  • 235
h d
  • 53
  • 5
  • What do you want to do with duplicates? If this itemslines has an item ID 1 with quantity 10, and the other item's itemslines you're merging in has an item ID 1 with quantity 20, do you want: one ID1 quantity 30 (merge add), one ID1 quantity 20 (merge replace), one ID1 quantity 10 (merge ignore), or two items with ID1 and quantities 10 and 20 (don't merge items) – Caius Jard Jul 19 '20 at 06:22
  • Thanks @Caius basically I just want to merge with item1 with item 2 – h d Jul 19 '20 at 06:25
  • All you've done there is restated what you said in the question which doesn't help us understand what you mean by "merge". I will be more specific: You must read and fully understand my comment and answer my question with exactly one of the following phrases: "merge add", "merge replace", "merge ignore", "don't merge items" – Caius Jard Jul 19 '20 at 06:33
  • its actually merge add – h d Jul 19 '20 at 06:36
  • Cool, so we're adding the quantities etc. How do we cope with two items of the same ID with different costs? Do we take the higher price, lower price or average them? If averaging is it a simple x+y/2 or is there something complex such as the number of items skewing the average? Edit all this information into your question then nominate it for reopening. At the moment @rene has closed it as a duplicate of AddRange but AddRange isn't what you want because that is the "don't merge" scenario – Caius Jard Jul 19 '20 at 06:45
  • @CaiusJard I can reopen now if you want? Or do you want to wait for the edit? I'll leave a link in SOCVR for this question as well, if you need re-open assistance while I'm out, do ask there for a [reopen-pls](https://socvr.org/faq#how-and-why-do-i-need-to-format-my-cv-pls-and-other-requests). – rene Jul 19 '20 at 06:59
  • @hdave I've edited the requirements you gave in the comments, into the question - this is like how you should write questions - everything you do as a software engineer will be to a spec, and other engineers work in the same way. Everything you want another software engineer to do should be an exact specification, otherwise you'll just be left with assumptions, incorrect solutions and wasted time. Please review the changes to the question and adjust them accordingly. Your questions on SO are living things that should be adjusted in response to feedback via the comments – Caius Jard Jul 19 '20 at 08:17
  • @rene I'm happy to leave it 'til the question is edited to be right (still an open query on what to do about different costs); it's a good learning exercise on how SO works, and how questions are not "I'll just dump any old effort on SO and make sorting my work out into someone else's problem" – Caius Jard Jul 19 '20 at 08:19
  • @hdave you'll need to make some amendments to the question. If you don't then it will likely remain closed and won't be able to have answers posted. If it doesn't reopen then I offer these as pointers to solving the problem: *You'll need to enumerate the incoming list*. *For each `incomingItem` in the incoming list, use `var x = ItemsLines.FirstOrDefault(i => i.ItemLineId == incomingItem.ItemLineID);`. `x` will be null if the item is new or an item if the id exists*. *If new, add the item*. *If existing, copy the properties across according to the addition/averaging etc rules you want* – Caius Jard Jul 19 '20 at 08:25
  • It sounds as though you want to do a [full outer join](https://www.w3schools.com/sql/sql_join_full.asp) of the `ItemsLines` lists, matching on `ItemLineId` and using the projection logic as described in your question. So I grabbed an implementation of full outer join from [this answer](https://stackoverflow.com/a/43669055/3744182) to [LINQ - Full Outer Join](https://stackoverflow.com/q/5489987/3744182) by [NetMage](https://stackoverflow.com/users/2557128/netmage) and it worked perfectly, see https://dotnetfiddle.net/AuDSPI. Does that answer your question? – dbc Jul 19 '20 at 19:06
  • 1
    That's quite a bit more complex than I was thinking of making it, but my bigger concern is that it answers the question in a comment - be careful not to teach new users that they can throw any old crap question up and we'll answer it anyway, in the comments if the question is closed. Hdave has engaged with us sufficiently to seem to want his question answered so I don't see any harm in waiting until the question is modified that tiny bit more to resolve the Cost query then it can be reopened and answered properly. I appreciate the need to balance "be nice" with "follow standards" but consider – Caius Jard Jul 20 '20 at 06:08
  • that writing a good, explicit and well defined question and engaging with our feedback is an OP's way of being nice to us - it's a two way street – Caius Jard Jul 20 '20 at 06:09
  • @CaiusJard - The question came up for me in the "Reopen" queue. At this point the question seems mostly clear enough and it's definitely not a dup of the currently linked question, but one option would be to leave it closed and link it to the "Full Outer Join" question instead, if that answers OP's question sufficiently. Which is why I asked OP whether it did. – dbc Jul 20 '20 at 14:03

2 Answers2

0

You need an implementation of MergeItem that merges the local item with the incoming:

public void MergeItem(Item incoming)
{
    if(this.ItemNumber != incoming.ItemNumber)
      throw new BlahException($"Item with number {this.ItemNumber} cannot be merged with different item having number {incoming.ItemNumber}");

    this.ItemDate = DateTime.UtcNow;

    foreach(var il in incoming.ItemLines){

      var existing = this.ItemLines.SingleOrDefault(t => t.ItemLineId == il.ItemLineId);

      if(existing == null) //not known, add 
        this.ItemLines.Add(il);
      else{ //merge
        existing.Quantity += il.Quantity;
        existing.Cost = (existing.Cost + il.Cost) / 2.0;
      }
    }
}

Please don't name your classes in the plural form, it's bad practice because it makes it hard to name your variables in the plural if they are a collection of your things. A List<Items> should be called Itemss (multiple Items), which is a bit awkward. If you create classes that are a collection of something that don't really have an accepted name that people understand to include multiples of something (like a Library has List<Book> Books, an Order has List<Product> Products etc) then name your class with a Collection suffix or similar that implies it is a group of things, but be careful to make it so that the class's purpose is really nothing more than "to be a collection of the things", like Microsoft did with DataTable (has a DataColumnCollection called Columns, and a DataRowCollection called Rows - these things are literally just collections of those things)

Caius Jard
  • 47,616
  • 4
  • 34
  • 62
0

You ask how to merge two lists using a "Linq join query". Conceptually, your proposed algorithm corresponds to a full outer join on your two ItemsLine lists, matching on the ItemLineId, preserving unmatched items from both lists as-is and merging matched items using the logic described in your question:

Types of join taken from this answer https://stackoverflow.com/a/28598795/3744182 to https://stackoverflow.com/questions/38549/what-is-the-difference-between-inner-join-and-outer-join by https://stackoverflow.com/users/3326275/pratik

(Diagram from this answer by Pratik.)

Unfortunately, there is no full outer join built into LINQ currently. Fortunately there are several working imlementations from LINQ - Full Outer Join. I copied Ext.FullOuterJoin<TLeft, TRight, TKey, TResult>() from this answer by NetMage, and was able to merge your Items lists as follows:

public class Items
{
    public int ItemNumber { get; set; }
    public DateTime ItemDate { get; set; }
    public List<ItemsLine> ItemsLines { get; set; } = new List<ItemsLine>();

    /// MergeItem appends the items from the sourceItem to the current item
    /// </summary>
    /// <param name="sourceItem">Item to merge from</param>
    public void MergeItem( Items sourceItem)
    {
        var mergedItems = ItemsLines.FullOuterJoin(sourceItem.ItemsLines, i => i.ItemLineId, i => i.ItemLineId, (i1, i2) =>
                                                   {
                                                       if (i1 == default)
                                                           return i2;
                                                       else if (i2 == default)
                                                           return i1;
                                                       //It's not clear from your question whether the average cost should be weighted by the quantities.
                                                       //i1.Cost = (i1.Cost + i2.Cost) / 2.0,
                                                       i1.Cost = (i1.Quantity * i1.Cost + i2.Quantity * i2.Cost) / (i1.Quantity + i2.Quantity);
                                                       i1.Quantity = i1.Quantity + i2.Quantity;
                                                       return i1;
                                                   })
            .ToList();
        ItemsLines.Clear();
        ItemsLines.AddRange(mergedItems);
    }
}

Working demo fiddle here.

dbc
  • 80,875
  • 15
  • 141
  • 235