JavaScript: Array.reduce()

Array.reduce()

reduce()
This method executes a given "reducer" callback function on each element of array in order with a twist: Each return value is an input for the next element. It's accumulative.
  • The return value from an element operation will be passed to the next element as an argument.
    • An initial value can be given to be used as the previous value for the first element.
  • So the return value of each element along the way represent an accumulative value.
  • Meanwhile, the previous value doesn't need to be in the array anymore since its value has been represented in the accumulative result. Therefore, "reduced".
  • The final result is a single value.
reduce(...)
Syntax
// Callback function
reduce(reducerFn)
reduce(reducerFn, initialValue)

// Inline callback function
reduce(function (prev, val) {...})
reduce(function (prev, val, index) {...})
reduce(function (prev, val, index, array) {...})
reduce(function (prev, val, index, array) {...}, initialValue)

// Arrow function
reduce((prev, val)  => {...})
reduce((prev, val, index)  => {...})
reduce((prev, val, index, array)  => {...})
reduce((prev, val, index, array)  => {...}, initialValue)
Parameters (2):
  • reducerFn: The reducer function
  • initialValue (optional):
    • If specified: It will be used as the prev for the first element.
    • If not specified, the first element run will be skipped, and the first element value is used as the prev for the second element run.
Return value:
The final accumulative value
Original array:
Not changed (unless manipulated by accessing the array during callback)
Reducer function
function reducerFn(prev, val, index, array) {
    ...
    return newValue;
}
// "newValue" is the "prev" for the next element.
Parameters (4):
  • prev: The return value of the previous element run, aka the accumulative value. For the first element, will use the initialValue if supplied.
  • val: The current element's value.
  • index: The index of current element.
  • array: The array being worked on.
Return values:
The accumulative value up to current element.
Example: sum
const nums = [1,2,3,4,5];
const sum = nums.reduce((prev, val) => prev + val);

sum;  // 15

Edge cases

Array with 1 element value and an initial value
Reducer function can run
[2].reduce((prev, val) => prev + val*val, 0);        // 4
[,,2,,].reduce((prev, val) => prev + val*val, 0);    // 4
Array with 1 element value and no initial value
Reducer function can't run. That value will be taken as the reduce return value.
[2].reduce((prev, val) => prev + val*val);        // 2
[,,2,,].reduce((prev, val) => prev + val*val);    // 2
Array with 0 element value and an initial value
Reducer function can't run. That value will be taken as the reduce return value.
[].reduce((prev, val) => prev + val*val, 0);        // 0
[,,,,].reduce((prev, val) => prev + val*val, 0);    // 0
Array with 0 element value and no initial value
Will give TypeError
[].reduce((prev, val) => prev + val*val);        
// Uncaught TypeError: Reduce of empty array with no initial value

Importance of Initial Value

Simple operation
When val is simply operated directly to the prev, perhaps specifying initialValue is not important.
Not so simple operation
However when val is "processed" prior to be operated to the prev, not giving the initialValue means such process for the first element is skipped, since without an initial value, first element run is skipped and the element value is used directly as the second run's prev
Impact
  • First element callback
    • With initialValue: run
      • prev = initialValue
      • returning a return value
    • Without initialValue: skipped
  • Second element callback
    • With initialValue, since first element is operated and giving return value, this return value is used as the prev
    • Without initialValue, since first element is skipped and therefore no return value, the original element value is used as the prev
Example: sums of squares
Unfortunately when elements is processed prior to operated to the previous accumulative result. without initial value, this process is skipped for the first element.
Say, the reducer is to sum up squares of each element of an array. The squaring operation happens in each callback. Without initial value, such process never happen to the first element because the process is skipped entirely. the non-squared value will be fed to the round.
Reducer
function sumSquare(prev, val, index) {
    counter ++;
    const sum = prev + val*val;
    console.log(`Run ${counter}: index = ${index}, prev = ${prev}, val = ${val}, sum = ${sum}`);
    return sum;
};
No initial value
const nums = [2,3,4,5];
let counter = 0;
const squaresNoInit = nums.reduce(sumSquare);

In log:
Run 1: index = 1, prev = 2, val = 3, sum = 11
Run 2: index = 2, prev = 11, val = 4, sum = 27
Run 3: index = 3, prev = 27, val = 5, sum = 52
With initial value
const nums = [2,3,4,5];
let counter = 0;
const squaresWithInit = nums.reduce(sumSquare, 0);

In log:
Run 1: index = 0, prev = 0, val = 2, sum = 4
Run 2: index = 1, prev = 4, val = 3, sum = 13
Run 3: index = 2, prev = 13, val = 4, sum = 29
Run 4: index = 3, prev = 29, val = 5, sum = 54
Results
nums;             // (4) [2, 3, 4, 5]
squaresNoInit;    // 52
squaresWithInit;  // 54

Behaviour during array mutation

Behaviour of reduce()
  • Only original length of array is processed
    • If new elements added to the end of the array, they are ignored.
  • The value of each element are taken right at the beginning of individual reducer function call
    • If element value is changed before the reducer reaches it, new value is used. For example, when value is changed by direct manipulation, or the elements are shifted.
  • Empty element is skipped. Either by popping, shifting, or deleting.
Function: space-separated concatenator
function concat(prev, val, idx, arr) {
    counter ++;
    const text = prev + " " + val;
    console.log(`Run ${counter}: index = ${idx}, array = ${arr}, text = ${text}`);
    return text;
}
reduce() with no mutation
const strings = ["AA", "BB", "CC"];
let counter = 0;
const noMutation = strings.reduce(concat, '');

Log:
Run 1: index = 0, array = AA,BB,CC, text =  AA
Run 2: index = 1, array = AA,BB,CC, text =  AA BB
Run 3: index = 2, array = AA,BB,CC, text =  AA BB CC

noMutation;   // ' AA BB CC'
strings;      // (3) ['AA', 'BB', 'CC']
Mutation 1: New elements pushed to the end -> Ignored
const strings = ["AA", "BB", "CC"];
let counter = 0;
const newElements = strings.reduce((prev, val, idx, arr) => {
    arr.push(val.toLowerCase());
    return concat(prev, val, idx, arr);
}, '');

Log:
Run 1: index = 0, array = AA,BB,CC, text =  AA
Run 2: index = 1, array = AA,BB,CC, text =  AA BB
Run 3: index = 2, array = AA,BB,CC, text =  AA BB CC

newElements;  // ' AA BB CC'
strings;      // (6) ['AA', 'BB', 'CC', 'aa', 'bb', 'cc']
Mutation 2: Element value got changed prior to callback -> used
const strings = ["AA", "BB", "CC"];
let counter = 0;
const newValueue = strings.reduce((prev, val, idx, arr) => {
    arr.splice(idx+1, 0, val.toLowerCase());
    return concat(prev, val, idx, arr);
}, '');

Log:
Run 1: index = 0, array = AA,BB,CC, text =  AA
Run 2: index = 1, array = AA,BB,CC, text =  AA aa
Run 3: index = 2, array = AA,BB,CC, text =  AA aa aa

newValueue;     // ' AA aa aa'
strings;      // (6) ['AA', 'aa', 'aa', 'aa', 'BB', 'CC']
Mutation 3: Element popped (array shortened)
const strings = ["AA", "BB", "CC"];
let counter = 0;
const popped = strings.reduce((prev, val, idx, arr) => {
    arr.pop();
    return concat(prev, val, idx, arr);
}, '');

Log:
Run 1: index = 0, array = AA,BB, text =  AA
Run 2: index = 1, array = AA, text =  AA BB

popped;       // ' AA BB'
strings;      // ['AA']
Mutation 4: Element deleted (leaving empty cell)
const strings = ["AA", "BB", "CC"];
let counter = 0;
const deleted = strings.reduce((prev, val, idx, arr) => {
    delete arr[idx+1];
    return concat(prev, val, idx, arr);
}, '');

Log:
Run 1: index = 0, array = AA,,CC, text =  AA
Run 2: index = 2, array = AA,,CC, text =  AA CC

deleted;      // ' AA CC'
strings;      // (3) ['AA', empty, 'CC']

Object Array

Sum of values in object array
Must supply initialValue
Because the value is processed before summed up to the previous value: val.x
const objArray = [{x: 1}, {x: 2}, {x: 3}];
const initialValue = 0;

const sum = objArray.reduce((prev, val) => prev + val.x, initialValue);
sum;    // 6
Without initialValue
const objArray = [{x: 1}, {x: 2}, {x: 3}];

const sum = objArray.reduce((prev, val) => prev + val.x);
sum;    // '[object Object]23'
What happens?
Without initial value, the first run is between element 1 value as the prev, and the element 2 value as the val. Element 1's value is [object Object]. it is the val.x that is meaningful.
const objArray = [{x: 1}, {x: 2}, {x: 3}];
let counter = 0;

const sum = objArray.reduce((prev, val) => {
    counter ++;
    let tempSum = prev + val.x;
    console.log(`Run ${counter}: prev = ${prev}, val.x = ${val.x}, sum = ${tempSum}`);
    return tempSum;
});

Log:
Run 1: prev = [object Object], val.x = 2, sum = [object Object]2
Run 2: prev = [object Object]2, val.x = 3, sum = [object Object]23

sum;    // '[object Object]23'
Now let's see when initialValue is given.
Then the first run is between that initial value as prev, and the val will go inside the equation and processed into val.x is coded.
const objArray = [{x: 1}, {x: 2}, {x: 3}];
let counter = 0;
let initialValue = 0;

const sum = objArray.reduce((prev, val) => {
    counter ++;
    let tempSum = prev + val.x;
    console.log(`Run ${counter}: prev = ${prev}, val.x = ${val.x}, sum = ${tempSum}`);
    return tempSum;
}, initialValue);

Log:
Run 1: prev = 0, val.x = 1, sum = 1
Run 2: prev = 1, val.x = 2, sum = 3
Run 3: prev = 3, val.x = 3, sum = 6

sum;    // '[object Object]23'

Flattening Array of Array

By concatenating each array of array elements.
Basic array concatenation
[1, 2].concat([3, 4])       // (4) [1, 2, 3, 4]
Flattening array
arrayOfArray  = [[1, 2], [3, 4], [5, 6]]; 

Make it into:
flatArray     = [1, 2, 3, 4, 5, 6];
Use reduce()
let arrayOfArray  = [[1, 2], [3, 4], [5, 6]]; 
let flatArray = arrayOfArray.reduce((prev,val) => prev.concat(val));

flatArray;  //  (6) [1, 2, 3, 4, 5, 6]

Counting instances of values in an object

in operator
This operator is for checking whether a property name exist in an object. It returns boolean.
"propName" in obj;    // true or false
Example
const pet = {name: "Kitty", type: "cat", age: 2};
"name" in pet;          // true
"color" in pet;         // false
Array reduce() with in operator for counting instances of values
How?
  • Reduce is initialized with an empty object.
  • When a new element value is encountered, initialize property with the element's value as the property name and 1 is the property value.
  • When an element value has already been listed, add 1 to the property's value.
  • The result is, an object, with element values as the properties, and number represent how many times such value appears in the array.
const students = ["Abe", "Ben", "Chris", "Dodo", "Kelly", "Ben", "Dodo", "Dodo"];

const nameTags = students.reduce((nameSummary, name) => {
    if (name in nameSummary) {
        nameSummary[name] += 1;
    } else {
        nameSummary[name] = 1;
    }
    console.log(nameSummary);
    return nameSummary;
}, {})

Log:
{Abe: 1}
{Abe: 1, Ben: 1}
{Abe: 1, Ben: 1, Chris: 1}
{Abe: 1, Ben: 1, Chris: 1, Dodo: 1}
{Abe: 1, Ben: 1, Chris: 1, Dodo: 1, Kelly: 1}
{Abe: 1, Ben: 2, Chris: 1, Dodo: 1, Kelly: 1}
{Abe: 1, Ben: 2, Chris: 1, Dodo: 2, Kelly: 1}
{Abe: 1, Ben: 2, Chris: 1, Dodo: 3, Kelly: 1}

nameTags;   // {Abe: 1, Ben: 2, Chris: 1, Dodo: 3, Kelly: 1}

Grouping objects by property

Given an array of objects, classify based on chosen property
Example: students data
const classA = [
    { name: 'Alice',    age: 21,    activity: 'swimming'    },
    { name: 'Max',      age: 20,    activity: 'piano'       },
    { name: 'Jane',     age: 20,    activity: 'ballet'      },
    { name: 'Kitty',    age: 19,    activity: 'swimming'    },
    { name: 'Dodo',     age: 21,    activity: 'ballet'      }
];
Goal: classify by any chosen property
For example: age
ageGroup = {
    19: [
        { name: 'Kitty',    age: 19,    activity: 'swimming'    }
    ],
    20: [
        { name: 'Max',      age: 20,    activity: 'piano'       },
        { name: 'Jane',     age: 20,    activity: 'ballet'      }
    ],
    21: [
        { name: 'Alice',    age: 21,    activity: 'swimming'    },
        { name: 'Dodo',     age: 21,    activity: 'ballet'      }
    ]
}
How
  • First, we need a function that takes the collection and the chosen property as arguments.
  • Inside it, use reducer to create an object of the classification.
function classify(collection, property) {
    return collection.reduce(
        (cluster, element) => {
            let key = element[property];
            if (!cluster[key]) {
                cluster[key] = [];
            }
            cluster[key].push(element);
            return cluster;
        },
        {}
    );
}
Results
Age group
classify(classA, "age");

Result:
{19: Array(1), 20: Array(2), 21: Array(2)}

Expanded:
{
    19: [
        {name: 'Kitty', age: 19, activity: 'swimming'}
    ],
    20: [
        {name: 'Max', age: 20, activity: 'piano'},
        {name: 'Jane', age: 20, activity: 'ballet'}
    ],
    21: [
        {name: 'Alice', age: 21, activity: 'swimming'},
        {name: 'Dodo', age: 21, activity: 'ballet'}
    ]
}
Activity group
classify(classA, "activity");

Result:
{swimming: Array(2), piano: Array(1), ballet: Array(2)}

Expanded:
{
    ballet: [
        {name: 'Jane', age: 20, activity: 'ballet'},
        {name: 'Dodo', age: 21, activity: 'ballet'}
    ],
    piano: [
        {name: 'Max', age: 20, activity: 'piano'}
    ],
    swimming: [
        {name: 'Alice', age: 21, activity: 'swimming'},
        {name: 'Kitty', age: 19, activity: 'swimming'}
    ]
}

Gathering elements using spread operator (and initial value)

Given an array of objects, with property containing array
const plants = [{
    location    : "patio",
    type        : "outdoor",
    collection  : ["tomatoes", "cucumbers"]
}, {
    location    : "bedroom",
    type        : "indoor",
    collection  : ["african violet", "asparagus fern"]
}, {
    location    : "livingroom",
    type        : "indoor",
    collection  : ["coleus", "mini rose"]
}]
Make a list of all collections combined into an array. Initialized with a collection.
let newPlants = ["sunflowers", "beans"];

const allPlants = plants.reduce(
    (gathered, element) => [...gathered, ...element.collection],
    newPlants
)
Result
allPlants;  // (8) ['sunflowers', 'beans', 'tomatoes', 'cucumbers', 'african violet', 'asparagus fern', 'coleus', 'mini rose']

Remove duplication in an array

Use: includes()
array.includes(element)     // boolean
Example
const collection = ['sunflowers', 'beans', 'tomatoes', 'cucumbers', 'african violet', 'asparagus fern', 'coleus', 'mini rose', 'sunflowers', 'asparagus fern', 'beans', 'tomatoes', 'mini rose', 'sunflowers', 'asparagus fern', 'asparagus fern'];

const plantTypes = collection.reduce(
    (cluster, element) => {
        if (!cluster.includes(element)) {
            cluster.push(element);
        }
        return cluster;
    }, []
);

plantTypes;     // (8) ['sunflowers', 'beans', 'tomatoes', 'cucumbers', 'african violet', 'asparagus fern', 'coleus', 'mini rose']

Replace .filter().map with .reduce()

.filter().map()
Chained filter() and map() traverses the array twice.
Example: square all positive numbers in a number array
const numbers = [3, 8, 0, -3, , -6, 9];
const positiveSquare = numbers.filter(el => el > 0).map(el => el*el);
.reduce()
reduce() only traverses the array once. Could be a big time and space saving for large complicated data.
const numbers = [3, 8, 0, -3, , -6, 9];
const positiveSquare = numbers.reduce(
    (arr, el) => {
        if (el > 0) {
            arr.push(el*el);
        };
        return arr;
    },
    []
);

Stock Game

Best Time to Buy and Sell Stock
Given: array of stock price. Find max profit from buying at any time and then selling at any time. Selling must happen after buying. If there's no profit, return 0 (in some game rule, return -1).
Rule in leetcode
My leetcode submission
    const maxProfit = function (prices) {   
        let priceLowest = prices[0];
        let profitMax = 0;
        let profitToday;
    
        for (let i = 1; i < prices.length; i++) {
            
            if (prices[i-1] < priceLowest) {
                priceLowest = prices[i-1];
            }
            profitToday = prices[i] - priceLowest;
            
            if (profitToday > profitMax) {
                profitMax = profitToday;
            }
        }
        return profitMax;
    };

Wesbos Javascript30: 04 Array Cardio Practice

Task: 4. How many years did all the inventors live all together?
Given
const inventors = [
    { first: 'Albert', last: 'Einstein', year: 1879, passed: 1955 },
    { first: 'Isaac', last: 'Newton', year: 1643, passed: 1727 },
    { first: 'Galileo', last: 'Galilei', year: 1564, passed: 1642 },
    { first: 'Marie', last: 'Curie', year: 1867, passed: 1934 },
    { first: 'Johannes', last: 'Kepler', year: 1571, passed: 1630 },
    { first: 'Nicolaus', last: 'Copernicus', year: 1473, passed: 1543 },
    { first: 'Max', last: 'Planck', year: 1858, passed: 1947 },
    { first: 'Katherine', last: 'Blodgett', year: 1898, passed: 1979 },
    { first: 'Ada', last: 'Lovelace', year: 1815, passed: 1852 },
    { first: 'Sarah E.', last: 'Goode', year: 1855, passed: 1905 },
    { first: 'Lise', last: 'Meitner', year: 1878, passed: 1968 },
    { first: 'Hanna', last: 'Hammarström', year: 1829, passed: 1909 }
];
Solution
const invSum = inventors.reduce((total, inv) => total + (inv.passed - inv.year), 0);
Result
invSum;     // 861
8. Sum up the instances of each of each vehicle
Given
const data = ['car', 'car', 'truck', 'truck', 'bike', 'walk', 'car', 'van', 'bike', 'walk', 'car', 'van', 'car', 'truck' ];
Solution
const vehicle = data.reduce((summary, val) => {
    if (!(val in summary)) {
        summary[val] = 0;
    } 
    summary[val] += 1;
    return summary;
}, {})
Result
vehicle;     // {car: 5, truck: 3, bike: 2, walk: 2, van: 2}

References