Product Variations With Cost in CFML

Product Variations With Cost in CFML

I've frequently encountered situations—typically related to e-commerce—where I need to generate every variation of an item with multiple potential attributes. Let's dive into a function designed to generate all possible variations based on attributes for an item—think of a shirt with varying sizes, colors, and sleeve types—and calculate an adjusted cost for each combination based on attribute-specific adjustments. We'll break it down step by step to see how it works, using the provided example to illustrate its functionality. I'm confident it can be enhanced, extended, and tailored, but for now, let’s begin with this version that covers the essentials.

The Code at a Glance

The core of this solution is the getAllVariations function, which takes a struct of attributes and returns an array of all possible combinations, each with a calculated cost_adjusted value. Here's the full code we'll be exploring:

function getAllVariations( required struct attributes ) {
    // Helper function to combine arrays of objects
    private function combineAttributes( required array currentCombinations, required array newAttribute, required string attributeName ) {
        var newCombinations = [];
        
        // For each existing combination
        for ( var combination in currentCombinations ) {
            // For each value of the new attribute
            for ( var value in newAttribute ) {
                // Create a new struct with the existing combination plus the new attribute
                
                // ACF 11-2018 & Lucee 5 & 6 Compatible
                var newCombination = structCopy( combination );
                newCombination[ arguments.attributeName ] = value;
                arrayAppend( newCombinations, newCombination );
                
                // ACF 2021+ Compatible
                // newCombinations.append( { ...combination, "#attributeName#": value } );
            }
        }
        
        return newCombinations;
    }
    // Start with an array containing an empty struct
    var combinations = [{}];
    
    // Iterate through each attribute type
    for ( var attributeName in attributes ) {
        combinations = combineAttributes( combinations, attributes[attributeName], attributeName );
    }
    
    // Set final price
    for( var variation in combinations ){
        variation[ "cost_adjusted" ] = !variation.keyExists( "cost_adjusted" ) ? variation[ "cost_base" ] : variation[ "cost_adjusted" ];
        
        for ( var key in variation ) {
            // If option has "cost_adjust" key, add it to the cost_final
            if ( isStruct( variation[ key ] ) && variation[ key ].keyExists( "cost_adjust" ) ) {
                variation[ "cost_adjusted" ] += variation[ key ][ "cost_adjust" ];
            }
        }
    }
    
    return combinations;
}
// Example usage:
itemAttributeOptions = {
    "size": [
        {"label": "S", "cost_adjust": 0.00},
        {"label": "M", "cost_adjust": 0.00},
        {"label": "L", "cost_adjust": 0.00},
        {"label": "XL", "cost_adjust": 0.00},
        {"label": "2XL", "cost_adjust": 5.00},
        {"label": "3XL", "cost_adjust": 5.00}
    ],
    "color": [
        {"label": "Red", "cost_adjust": 0.00},
        {"label": "Blue", "cost_adjust": 0.00},
        {"label": "Green", "cost_adjust": 0.00},
        {"label": "Black", "cost_adjust": 0.00}
    ],
    "sleeve": [
        {"label": "Long Sleeve", "cost_adjust": 0.00},
        {"label": "Short Sleeve", "cost_adjust": -3.00}
    ],
    "cost_base": [9.99],
    "cost_adjusted": [9.99],
    "meta": [{"material": "cotton-blend"}]
};
allItemVariations = getAllVariations( itemAttributeOptions );
writeDump( allItemVariations );
writeOutput( "Total variations: #allItemVariations.len()#" );

I've added an optional line of code that can replace the three lines above it if you're using CF2021 or later, as that’s when the spread operator was introduced. Let’s unpack this code and see what it does.

Step 1: Generating Variations

The combineAttributes Helper Function

The getAllVariations function relies on a nested helper function, combineAttributes, to build combinations incrementally. Here’s how it works:

  • Inputs:

    • currentCombinations: An array of structs representing the combinations generated so far.
    • newAttribute: An array of values (often structs) for the new attribute to add.
    • attributeName: The name of the attribute (e.g., "size", "color").
  • Process:

    • Initialize an empty array newCombinations.
    • For each struct in currentCombinations, iterate over each value in newAttribute.
    • Create a new struct by copying the existing combination (using the spread operator ...combination) and adding a new key-value pair, where the key is attributeName and the value is the current value from newAttribute.
    • Append this new struct to newCombinations.
  • Output: An array of all new combinations.

For example, if currentCombinations is [{}] (a single empty struct) and newAttribute is the "size" array from the example, the function produces:

[
    {"size": {"label": "S", "cost_adjust": 0.00}},
    {"size": {"label": "M", "cost_adjust": 0.00}},
    {"size": {"label": "L", "cost_adjust": 0.00}},
    {"size": {"label": "XL", "cost_adjust": 0.00}},
    {"size": {"label": "2XL", "cost_adjust": 5.00}},
    {"size": {"label": "3XL", "cost_adjust": 5.00}}
]

Building Combinations in getAllVariations

The main function starts with combinations as an array containing a single empty struct: [{}]. It then iterates over each key in the attributes struct, calling combineAttributes to incorporate that attribute’s values:

  • First iteration ("size"): Combines [{}] with the 6 sizes, producing 6 combinations.
  • Second iteration ("color"): Takes those 6 combinations and combines them with 4 colors, producing 6 * 4 = 24 combinations.
  • Third iteration ("sleeve"): Takes the 24 combinations and combines them with 2 sleeve types, producing 24 * 2 = 48 combinations.
  • Subsequent iterations: Adds "cost_base" (1 value), "cost_adjusted" (1 value), and "meta" (1 value). Since these attributes have single-element arrays, they don’t increase the number of combinations; they just add fixed values to each existing combination.

After all iterations, each combination includes keys for "size", "color", "sleeve", "cost_base", "cost_adjusted", and "meta". For instance, one combination might look like:

{
    "size": {"label": "S", "cost_adjust": 0.00},
    "color": {"label": "Red", "cost_adjust": 0.00},
    "sleeve": {"label": "Long Sleeve", "cost_adjust": 0.00},
    "cost_base": 9.99,
    "cost_adjusted": 9.99,
    "meta": {"material": "cotton-blend"}
}

The total number of variations is the product of the lengths of the multi-option attributes: 6 sizes * 4 colors * 2 sleeves = 48.

Step 2: Calculating Adjusted Costs

After generating the combinations, the function adjusts the cost_adjusted value for each variation:

for( variation in combinations ){
    variation[ "cost_adjusted" ] = !variation.keyExists( "cost_adjusted" ) ? variation[ "cost_base" ] : variation[ "cost_adjusted" ];
    
    for ( var key in variation ) {
        if ( isStruct( variation[ key ] ) && variation[ key ].keyExists( "cost_adjust" ) ) {
            variation[ "cost_adjusted" ] += variation[ key ][ "cost_adjust" ];
        }
    }
}

Initializing cost_adjusted

  • The first line ensures cost_adjusted has a starting value:
    • If cost_adjusted doesn’t exist, it’s set to cost_base.
    • If it exists (as it does in the example, with a value of 9.99), it keeps that value.
  • In the example, since "cost_adjusted": [9.99] is in attributes, every combination starts with "cost_adjusted": 9.99.

Applying Cost Adjustments

  • The nested loop iterates over each key in the variation.
  • For each key, it checks:
    • Is the value a struct? (e.g., {"label": "S", "cost_adjust": 0.00} is a struct, but 9.99 is not.)
    • Does the struct have a cost_adjust key?
  • If both conditions are true, it adds the cost_adjust value to variation["cost_adjusted"].

In the example:

  • "size", "color", and "sleeve" are structs that may have cost_adjust.
  • "cost_base" (9.99), "cost_adjusted" (9.99), and "meta" (a struct without cost_adjust) are either not structs or lack cost_adjust, so they’re skipped.

Example Calculations

  1. Variation: Small Red Long-Sleeve Shirt

    • Initial: "cost_adjusted": 9.99
    • "size": cost_adjust = 0.00
    • "color": cost_adjust = 0.00
    • "sleeve": cost_adjust = 0.00
    • Final: 9.99 + 0.00 + 0.00 + 0.00 = 9.99
  2. Variation: 2XL Black Short-Sleeve Shirt

    • Initial: "cost_adjusted": 9.99
    • "size": cost_adjust = 5.00
    • "color": cost_adjust = 0.00
    • "sleeve": cost_adjust = -3.00
    • Final: 9.99 + 5.00 + 0.00 + (-3.00) = 11.99

The adjusted cost reflects the base cost plus the sum of all applicable adjustments.

Example Output

Running the example code

allItemVariations = getAllVariations( itemAttributeOptions );
writeDump( allItemVariations );
writeOutput( "Total variations: #allItemVariations.len()#" );
  • Produces 48 variations, each a struct with all attributes and an updated cost_adjusted.
  • The writeOutput confirms: "Total variations: 48".

Here is a small subset of the results for reference:

[
    {
        "meta": {
            "material": "cotton-blend"
        },
        "color": {
            "cost_adjust": 0,
            "label": "Black"
        },
        "size": {
            "cost_adjust": 0,
            "label": "XL"
        },
        "sleeve": {
            "cost_adjust": 0,
            "label": "Long Sleeve"
        },
        "cost_adjusted": 9.99,
        "cost_base": 9.99
    },
    {
        "meta": {
            "material": "cotton-blend"
        },
        "color": {
            "cost_adjust": 0,
            "label": "Black"
        },
        "size": {
            "cost_adjust": 0,
            "label": "XL"
        },
        "sleeve": {
            "cost_adjust": -3,
            "label": "Short Sleeve"
        },
        "cost_adjusted": 6.99,
        "cost_base": 9.99
    },
    {
        "meta": {
            "material": "cotton-blend"
        },
        "color": {
            "cost_adjust": 0,
            "label": "Black"
        },
        "size": {
            "cost_adjust": 5,
            "label": "2XL"
        },
        "sleeve": {
            "cost_adjust": 0,
            "label": "Long Sleeve"
        },
        "cost_adjusted": 14.99,
        "cost_base": 9.99
    },
    {
        "meta": {
            "material": "cotton-blend"
        },
        "color": {
            "cost_adjust": 0,
            "label": "Black"
        },
        "size": {
            "cost_adjust": 5,
            "label": "2XL"
        },
        "sleeve": {
            "cost_adjust": -3,
            "label": "Short Sleeve"
        },
        "cost_adjusted": 11.99,
        "cost_base": 9.99
    },
    {
        "meta": {
            "material": "cotton-blend"
        },
        "color": {
            "cost_adjust": 0,
            "label": "Black"
        },
        "size": {
            "cost_adjust": 5,
            "label": "3XL"
        },
        "sleeve": {
            "cost_adjust": -3,
            "label": "Short Sleeve"
        },
        "cost_adjusted": 11.99,
        "cost_base": 9.99
    }
]

Click here to run it for yourself @ TryCF.com

Observations

  1. Single-Value Attributes: "cost_base", "cost_adjusted", and "meta" have arrays with one element, so they’re added to every combination without increasing the total count.
  2. Redundancy: Including "cost_adjusted" in attributes is redundant here, as it’s overwritten in the loop. The code could exclude it from attributes, relying on the cost_base fallback, but it works as is since its initial value matches cost_base.
  3. Flexibility: The function handles attributes without cost_adjust (like "meta") gracefully, including them in combinations without affecting the cost.

Improvements

  • Exclude cost_adjusted from attributes: If it’s meant to be computed, not preset, remove it from the input struct to avoid confusion.
  • Validation: Add checks for malformed attributes (e.g., non-array values).
  • Performance: For large attribute sets, consider optimizing the combination generation, though 48 variations is manageable.

The Wrap Up

The getAllVariations function is a robust tool for generating all possible item variations and calculating their adjusted costs. It’s perfect for e-commerce scenarios where products have multiple options, each potentially affecting the price. By breaking it down, we’ve seen how it systematically builds combinations and applies cost adjustments, making it both powerful and adaptable. Try it with your own attribute sets and see how it scales!

This was also posted @ ColdFusion.Rocks

Michael Rigsby

Senior Coldfusion Developer

Michael Rigsby has been a Cold Fusion developer for over 25 years, dedicating the majority of his career to helping small and medium-sized businesses establish and enhance their online presence and services with a deep passion for innovation and modern web development.

Leave a Comment