0

I've got two classes like this:

class SomeObject_source
{
    public string Model;
    public string Color;
    public string Size;
}

class SomeObject_target
{
    public string Model;
    public string Color;
    public string Size;
    public double Quantity;
    public double Nett;
}

Then we are getting an instance of the SomeObject_source with the following data structure:

    SomeObject_source someObject_source = new SomeObject_source
    {
        Model = "Model_1|Model_2|Model_3|Model_4|Model_5",
        Color = "Black|Black|White|Blue|Red",
        Size = "S^1^0.5|M^2^1.0|L^1^0.6|S^3^1.5|S^1^0.6"
    };

The Size string has the following pattern: size^quantity^nett. But sometimes there is an object with a simple data like:

    SomeObject_source someObject_source = new SomeObject_source
    {
        Model = "Model_1",
        Color = "Black",
        Size = "S"
    };

I'm trying to figure out how do an elegant single LINQ query to split the following data into a List of SomeObject_target, so that the result would be equal to:

    List<SomeObject_target> someObject_Target_List = new List<SomeObject_target>
    {
        new SomeObject_target
        {
            Model = "Model_1",
            Color = "Black",
            Size = "S",
            Quantity = 1,
            Nett = 0.5
        },
        new SomeObject_target
        {
            Model = "Model_2",
            Color = "Black",
            Size = "M",
            Quantity = 2,
            Nett = 1
        },
        new SomeObject_target
        {
            Model = "Model_3",
            Color = "White",
            Size = "L",
            Quantity = 1,
            Nett = 0.6
        },
        new SomeObject_target
        {
            Model = "Model_4",
            Color = "Blue",
            Size = "S",
            Quantity = 3,
            Nett = 1.5
        },
        new SomeObject_target
        {
            Model = "Model_5",
            Color = "Red",
            Size = "S",
            Quantity = 1,
            Nett = 0.6
        },
    };

For now i'm doing as follow:

    char Delimeter_main = '|';
    char Delimeter_inner = '^';
    List<SomeObject_target> someObject_Target_List_ = new List<SomeObject_target>();
    for (int ind = 0; ind < someObject_source.Model.Split(Delimeter_main).Count(); ind++)
    {
        string Model = someObject_source.Model.Split(Delimeter_main)[ind];
        string Color = someObject_source.Color.Split(Delimeter_main)[ind];
        string Size_unparsed = someObject_source.Size.Split(Delimeter_main)[ind];
        if (Size_unparsed.Contains(Delimeter_inner))
        {
            string size = Size_unparsed.Split(Delimeter_inner)[0];
            double quantity = double.TryParse(Size_unparsed.Split(Delimeter_inner)[1], out double _quantity) ? _quantity : 1;
            double nett = double.TryParse(Size_unparsed.Split(Delimeter_inner)[2], out double _nett) ? _nett : 1;
            someObject_Target_List_.Add(new SomeObject_target
            {
                Model = Model,
                Color = Color,
                Size = size,
                Quantity = quantity,
                Nett = nett
            });
        }
        else
        {
            someObject_Target_List_.Add(new SomeObject_target
            {
                Model = Model,
                Color = Color,
                Size = Size_unparsed,
                Quantity = 1,
                Nett = 1
            });
        }
    }

But this obviously looks weird as well as the data architecture overall. Is there any LINQ query to accomplish that in single elegant query?

kevintw
  • 58
  • 5
  • 1
    "But this obviously looks weird as well as the data architecture overall." Huuum? Looks pretty clear to me. Don´t expect linq to be any neater or whatever. – MakePeaceGreatAgain Jan 25 '21 at 15:21
  • 3
    I'd change this to do the splitting of the Model, Color, and Size strings once before the loop. And then split the individual size values once. – juharr Jan 25 '21 at 15:24
  • 1
    Also a purely Linq solution for this would likely look even "weirder". – juharr Jan 25 '21 at 15:28
  • Also the if statement should be `if (Size_unparsed.Contains(Delimeter_inner))` to check for the `^` delimiter and not the `|` delimiter. And you'd want `Size = size` instead of `Size = Size_unparsed` – juharr Jan 25 '21 at 15:33
  • Is this arcane source solid enought for the number of elment in Model, Color, and Size to match ? And perhaps a Zip can tie the 3 first split into a nicer object – Drag and Drop Jan 25 '21 at 15:33
  • juharr, yes, you are right, corrected the question. @DragandDrop, yes, the model is solid, split by '|' has always the same count on all the fields – kevintw Jan 25 '21 at 15:36
  • using https://stackoverflow.com/a/10297160/14900199, you can zip the 3 collection from `someObject_source.Model.Split(Delimeter_main)`, `someObject_source.Color .Split(Delimeter_main)`, `someObject_source.Size.Split(Delimeter_main)`, into `SomeObject_target` you can just add a constructor that do the last split on `"^"` or do it in the zip. – Drag and Drop Jan 25 '21 at 15:42
  • I think you forgot what you learned in OO-programming course. You are programming in strings instead of correct types. Shouldn't you have a ModelType, a ColorType and a SizeType? Shouldn't SizeType have properties Size / Quantity / Nett, of the proper type (double?). Wouldn't it be better if you have functions to convert from strings to ModelType / SizeType and back? This way you are sure that every `SomeObjectSource` has correct values. Your conversions from `SomeObjectSource` to `SomeObjectTarget` will be piece of cake. – Harald Coppoolse Jan 26 '21 at 07:33

2 Answers2

1

Normal enumerable.Zip

var result = someObject_source.Model.Split(Delimeter_main)
    .Zip(someObject_source.Color.Split(Delimeter_main), (x, y) => new { x, y })
    .Zip(someObject_source.Size.Split(Delimeter_main), (zip1, z) =>
    {
        var secondSplit = z.Split(Delimeter_inner);
        return new SomeObject_target
        {
            Model = zip1.x,
            Color = zip1.y,
            Size = secondSplit[0],
            Quantity = double.TryParse(secondSplit[1], out double _quantity) ? _quantity : 1,
            Nett = double.TryParse(secondSplit[2], out double _nett) ? _nett : 1,
        };
    });

Extention Method Zip on 3 collection

A little more readable, without the annonymous object wraper for the first Zip result.

var results = someObject_source.Model.Split(Delimeter_main)
    .ZipThree(
        someObject_source.Color.Split(Delimeter_main),
        someObject_source.Size.Split(Delimeter_main), 
        (x, y, z) => {
            var secondSplit = z.Split(Delimeter_inner);
            return new SomeObject_target
            {
                Model = x,
                Color = y,
                Size = secondSplit[0],
                Quantity = double.TryParse(secondSplit[1], out double _quantity) ? _quantity : 1,
                Nett = double.TryParse(secondSplit[2], out double _nett) ? _nett : 1,
            };                    
        }
    );
Drag and Drop
  • 2,672
  • 3
  • 25
  • 37
  • this seems a lot more complicated than just using a loop. – Hogan Jan 25 '21 at 15:54
  • @Hogan, yes I will use the loop with minor change over that. Because it will be easier to debug and change. But it's more for how to LinQ it. – Drag and Drop Jan 25 '21 at 15:57
  • You can obviously inline `size`, `quantity` and `nett` – Charlieface Jan 25 '21 at 16:10
  • @Charlieface, I copy pasted from author. and did bother but yes. But As i didn't wrote the `Size_unparsed.Contains(Delimeter_inner)` else block. I let it there cause it's either to get the way to code in this block – Drag and Drop Jan 25 '21 at 16:17
  • Won't this cause an error in the second example where `secondSplit[1]` doesn't exist and will throw an exception? – NetMage Jan 26 '21 at 17:14
  • @NetMage, yes did not implement that if case. But it's a simple copy past from op code. Like I said "_ But As i didn't wrote the `Size_unparsed.Contains(Delimeter_inner)`_" – Drag and Drop Jan 27 '21 at 11:19
1

Using this Pivot extension method that pivots IEnumerable<IEnumerable<T>> (note: this isn't particularly efficient, but a better/faster method is quite a bit longer):

public static class IEnumerableExt {
    // Pivot IEnumerable<IEnumerable<T>> by grouping matching positions of each sub-IEnumerable<T>
    // itemGroups - source data
    public static IEnumerable<IEnumerable<T>> Pivot<T>(this IEnumerable<IEnumerable<T>> itemGroups) =>
        itemGroups.Select(g => g.Select((item, i) => (item, i)))
                  .SelectMany(g => g)
                  .GroupBy(ii => ii.i, si => si.item);
}

You can process someObject_source as follows:

char Delimeter_main = '|';
char Delimeter_inner = '^';

var someObject_Target_List =
    new[] {
        someObject_source.Model.Split(Delimeter_main),
        someObject_source.Color.Split(Delimeter_main),
        someObject_source.Size.Split(Delimeter_main)
    } // Create IEnumerable<string>[] (== IEnumerable<IEnumerable<string>>)
    .Pivot()
    .Select(t => t.ToList()) // project to IEnumerable<List<string>> to access members
    .Select(t => (model: t[0], color: t[1], rest: t[2].Split(Delimeter_inner))) // project to IEnumerable<ValueTuple> to access members
    .Select(t => new SomeObject_target {
        Model = t.model,
        Color = t.color,
        Size = t.rest[0],
        Quantity = t.rest.Length > 1 ? double.Parse(t.rest[1]) : default(double),
        Nett = t.rest.Length > 2 ? double.Parse(t.rest[2]) : default(double),
    })
    .ToList();
NetMage
  • 26,163
  • 3
  • 34
  • 55