Search code examples
c#loopsasp.net-corefor-loopnested-loops

Looping through a collection and combine elements


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?


Solution

  • 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