Home > front end >  Looping through a collection and combine elements
Looping through a collection and combine elements

Time:06-15

I am developing an inventory management system and I want to add a product with variants. I can add a product with three variants(color, size, material) and the options for each as below:

color - Black, Blue, Grey size - S,M,L,XL material - Cotton, Wool

If specify only 2 variants(e.g. color and size) my code is generating all the options correctly but if I add a 3rd variant then its not giving me the expected output.

Suppose I have a product called Jean my expected output would be as below:

  • Jean-Black/S/Cotton
  • Jean-Black/S/Wool
  • Jean-Black/M/Cotton
  • Jean-Black/M/Wool
  • Jean-Black/L/Cotton
  • Jean-Black/L/Wool
  • Jean-Black/XL/Cotton
  • Jean-Black/XL/Wool

===========================================

  • Jean-Blue/S/Cotton
  • Jean-Blue/S/Wool
  • Jean-Blue/M/Cotton
  • Jean-Blue/M/Wool
  • Jean-Blue/L/Cotton
  • Jean-Blue/L/Wool
  • Jean-Blue/XL/Cotton
  • Jean-Blue/XL/Wool

===========================================

  • Jean-Grey/S/Cotton
  • Jean-Grey/S/Wool
  • Jean-Grey/M/Cotton
  • Jean-Grey/M/Wool
  • Jean-Grey/L/Cotton
  • Jean-Grey/L/Wool
  • Jean-Grey/XL/Cotton
  • Jean-Grey/XL/Wool

My model is as below:

public class CreateModel : PageModel
{
    [Required]
    [BindProperty]
    public string? Name { get; set; }

    [BindProperty]
    public List<ProductVariantModel> Variants { get; set; }
    
}

Create product page

ProductVariantModel

public class ProductVariantModel
{
    public string? Name { get; set; }

    public string? Options { get; set; }
}

I'm creating the combinations as below:

List<ProductVariantOption> productOptions = new();
        try
        {
            int variantsTotal = model.Variants.Count;

            for (int a = 0; a < variantsTotal; a  )
            {
                string[] options = model.Variants[a].Options.Split(',');
                for (int i = 0; i < options.Length; i  )
                {
                    string? option = $"{model.Name}-{options[i]}";
                    if (variantsTotal > 1)
                    {
                        int index = a   1;
                        if (index < variantsTotal)
                        {
                            var levelBelowOptions = model.Variants[index].Options.Split(',');
                            var ops = GetOptions(option, levelBelowOptions);
                            productOptions.AddRange(ops);
                        }
                    }
                }
                a  = 1;
            }
        }

GetOptions method

private List<ProductVariantOption> GetOptions(string option, string[] options)
    {
        List<ProductVariantOption> variantOptions = new();

        for (int i = 0; i < options.Length; i  )
        {
            string sku = $"{option}/{options[i]}";
            string opt = $"{option}/{options[i]}";
            variantOptions.Add(new ProductVariantOption(opt, sku));
        }

        return variantOptions;
    }

ProductVariantOption

public class ProductVariantOption
{
    public string Name { get; private set; }

    public string SKU { get; private set; }

    public Guid ProductVariantId { get; private set; }

    public ProductVariant ProductVariant { get; private set; }

    public ProductVariantOption(string name, string sku)
    {
        Guard.AgainstNullOrEmpty(name, nameof(name));

        Name = name;
        SKU = sku;
    }


}

Where am I getting it wrong?

CodePudding user response:

I'm not sure why you have the name and SKU fields identical, but using your model, you need to initialize a list of the first level product variants, and then use that list as product variants so far and add the next variant to every product variant in the list.

I just used a List<string> to store the name so far, and added to it each variant's options:

var productVariantsName = model.Variants[0].Options.Split(',')
                          .Select(o => $"{model.Name}-{o}")
                          .ToList();

foreach (var variant in model.Variants.Skip(1)) {
    var pvNameSoFar = productVariantsName;
    productVariantsName = new();
    foreach (var pvName in pvNameSoFar) {
        foreach (var option in variant.Options.Split(','))
            productVariantsName.Add($"{pvName}/{option}");
    }
}
var productOptions = productVariantsName
                        .Select(pvName => new ProductVariantOption(pvName, pvName))
                        .ToList();

You can also do this with LINQ using the CartesianProduct LINQ Method from the answer (I use a slightly different lambda version).

With that defined, you can do:

var productOptions = model.Variants
                        .Select(v => v.Options.Split(','))
                        .CartesianProduct()
                        .Select(vs => $"{model.Name}-{vs.Join("/")}")
                        .Select(pvName => new ProductVariantOption(pvName, pvName))
                        .ToList();

PS: This uses the obvious definition for the Join string extension method.

For completeness, here are the extensions used:

public static class IEnumerableExt {
    public static string Join(this IEnumerable<string> ss, string sep) => String.Join(sep, ss);

    public static IEnumerable<T> AsSingleton<T>(this T item) => new[] { item };

    // ref: https://stackoverflow.com/a/3098381/2557128
    public static IEnumerable<IEnumerable<T>> CartesianProduct<T>(this IEnumerable<IEnumerable<T>> sequences) =>
        sequences.Aggregate(Enumerable.Empty<T>().AsSingleton(),
                            (accumulator, sequence) => accumulator.SelectMany(_ => sequence,
                                                                              (accseq, item) => accseq.Append(item)));
}

CodePudding user response:

If you generalize your problem, you can describe it as follows:

  1. For every potential variable of the model starting with a single variant (base model name), generate every possible combination of models so far with this variable
  2. Having generated those combinations, map each generated combination into ProductVariantOption.

So you might want to generate cross products of all lists of variables. This could be achieved with an .Aggregate which does .SelectMany inside (note that I have simplified the definition of the final output, but you can construct it as you want inside the .BuildModel method:

    private static ProductVariantOption BuildModel(string[] modelParts) {
        if (modelParts.Length == 1) {
            return new ProductVariantOption {
                Name = modelParts.Single()
            };  
        }
        
        var baseName = modelParts.First();
        var variantParts = string.Join('/', modelParts.Skip(1));
        return new ProductVariantOption {
            Name = $"{baseName}-{variantParts}"
        }; 
    }
    
    public static IList<ProductVariantOption> GetVariants(CreateModel model) {
        // Prepare all possible variables from the model in advance
        var allVariables = model.Variants.Select(v => v.Options.Split(",")).ToArray();
        
        var initialParts = new List<string[]> { new[] { model.Name } };
        // Generate cross product for every subsequent variant with initial list as a seed
        // Every iteration of aggregate produces all possible combination of models with the new variant
        var allModels = allVariables.Aggregate(initialParts, (variantsSoFar, variableValues) =>
            variantsSoFar
                .SelectMany(variant => variableValues.Select(variableValue => variant.Append(variableValue).ToArray()))
                .ToList()
        );
            
        // Map all lists of model parts into ProductVariantOption
        return allModels.Select(BuildModel).ToList();
    }

This approach has the benefit of being able to handle any amount of potential variables including cases where there are no variables (in this case only a single variant is produced - In your example it would be just "Jean")

Complete running example: https://dotnetfiddle.net/XvkPZQ

  • Related