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 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.
combineAttributes
Helper FunctionThe 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:
newCombinations
.currentCombinations
, iterate over each value in newAttribute
....combination
) and adding a new key-value pair, where the key is attributeName
and the value is the current value
from newAttribute
.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}}
]
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:
[{}]
with the 6 sizes, producing 6 combinations.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.
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" ];
}
}
}
cost_adjusted
cost_adjusted
has a starting value:
cost_adjusted
doesn’t exist, it’s set to cost_base
."cost_adjusted": [9.99]
is in attributes
, every combination starts with "cost_adjusted": 9.99
.{"label": "S", "cost_adjust": 0.00}
is a struct, but 9.99
is not.)cost_adjust
key?cost_adjust
value to variation["cost_adjusted"]
.In the example:
cost_adjust
.cost_adjust
) are either not structs or lack cost_adjust
, so they’re skipped.Variation: Small Red Long-Sleeve Shirt
"cost_adjusted": 9.99
cost_adjust = 0.00
cost_adjust = 0.00
cost_adjust = 0.00
9.99 + 0.00 + 0.00 + 0.00 = 9.99
Variation: 2XL Black Short-Sleeve Shirt
"cost_adjusted": 9.99
cost_adjust = 5.00
cost_adjust = 0.00
cost_adjust = -3.00
9.99 + 5.00 + 0.00 + (-3.00) = 11.99
The adjusted cost reflects the base cost plus the sum of all applicable adjustments.
Running the example code
allItemVariations = getAllVariations( itemAttributeOptions );
writeDump( allItemVariations );
writeOutput( "Total variations: #allItemVariations.len()#" );
cost_adjusted
.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
}
]
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
.cost_adjust
(like "meta") gracefully, including them in combinations without affecting the cost.cost_adjusted
from attributes
: If it’s meant to be computed, not preset, remove it from the input struct to avoid confusion.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
Leave a Comment