3. Collections

A collection is an object that contains iterable elements. In lodash, collections can be arrays, objects, and strings. Lodash has a rich set of functions to work with collections.

In this chapter, we use the following JSON array as the sample data fruits for some code samples.

Listing 3.1 Sample data fruits
[
  {
    "name": "apple",
    "price": 0.99,
    "onSale": true
  },
  {
    "name": "orange",
    "price": 1.99,
    "onSale": false
  },
  {
    "name": "passion fruit",
    "price": 4.99,
    "onSale": false
  }
]

3.1 Each

_.each(collection, [iteratee=_.identity]) and _.eachRight(collection, [iteratee=_.identity]) iterate over elements in the collection and invoke the iteratee function. The difference is that _.eachRight iterates from right to left. _.forEach is an alias of _.each, while _.forEachRight is an alias of _.eachRight.

Listing 3.2 Iterate collections
const each = require('lodash/each');

describe('each', () => {
  it('should support basic iteration', () => {
    let sum = 0;
    each([1, 2, 3], val => sum += val);
    expect(sum).toEqual(6);
  });
});

3.2 Every and some

_.every(collection, [predicate=_.identity]) checks if all elements in the collection match the given predicate.

Listing 3.3 Check if all elements match certain condition
const every = require('lodash/every');

describe('every', () => {
  it('should support arrays with functions', () => {
    let result = every([1, 2, 3, 4], n => n % 2 === 0);
    expect(result).toBe(false);
  });

  it('should support arrays with property value', () => {
    const fruits = [
      {
        name: 'apple',
        price: 1.99,
        onSale: true
      },
      {
        name: 'orange',
        price: 0.99,
        onSale: true
      }
    ];
    let result = every(fruits, ['onSale', true]);
    expect(result).toBe(true);
  });

  it('should support objects', () => {
    const obj = {
      a: 1,
      b: 2,
      c: 3
    };
    let result = every(obj, n => n % 2 === 0);
    expect(result).toBe(false);
  });

  it('should support strings', () => {
    let result = every('aaaa', c => c === 'a');
    expect(result).toBe(true);
  });
});

_.some(collection, [predicate=_.identity]) is the opposite of _.every which checks if any element in the collection matches the given predicate. _.some doesn’t need to iterate the entire collection and the iteration exits as soon as a matching element is found.

Listing 3.4 Check if any element matches certain condition
const some = require('lodash/some');

describe('some', () => {
  it('should support arrays', () => {
    let result = some([1, 2, 3, 4], n => n % 2 === 0);
    expect(result).toBe(true);
  });

  it('should support strings', () => {
    let result = some('hello', c => c === 'x');
    expect(result).toBe(false);
  });
});

3.3 Filter and reject

_.filter(collection, [predicate=_.identity]) filters a collection by returning elements matching the given predicate. _.reject(collection, [predicate=_.identity]) is the opposite of _.filter that returns elements not matching the given predicate. When _.filter is used to filter objects, only values of matching properties are returned. If you want to keep the original object structure, use _.pick or _.omit instead. When _.filter is used on strings, matching characters are returned in an array.

Listing 3.5 Filter a collection
const filter = require('lodash/filter');

describe('filter', () => {
  it('should support arrays', () => {
    let result = filter(['a', 'b', 'c'], c => c > 'b');
    expect(result).toEqual(['c']);
  });

  it('should support objects', () => {
    const obj = {
      a: 1,
      b: 2,
      c: 3,
    };
    let result = filter(obj, n => n > 1);
    expect(result).toEqual([2, 3]);
  });

  it('should support strings', () => {
    let result = filter('hello', c => c !== 'l');
    expect(result).toEqual(['h', 'e', 'o']);
  });
});

Listing 3.6 shows the examples of _.reject.

Listing 3.6 Reject elements in a collection
const reject = require('lodash/reject');

describe('reject', () => {
  it('should support arrays', () => {
    let result = reject(['a', 'b', 'c'], c => c > 'b');
    expect(result).toEqual(['a', 'b']);
  });
});

3.4 Size

_.size(collection) gets the size of a collection. For arrays, the size is the array’s length, same as the array’s property length. For objects, the size is the number of own enumerable properties, i.e. the length of the array returned by _.keys. For strings, the size is the string’s length.

Listing 3.7 Get the size of a collection
const size = require('lodash/size');

describe('size', () => {
  it('should support arrays', () => {
    expect(size([1, 2])).toEqual(2);
  });

  it('should support objects', () => {
    expect(size({
      a: 1,
      b: 2,
      c: 3,
    })).toEqual(3);
  });

  it('should support strings', () => {
    expect(size('hello')).toEqual(5);
  });
});

3.5 Includes

_.includes(collection, value, [fromIndex=0]) checks if a collection contains the given value. An optional index can be provided as the starting position to search. If the collection is an object, values of this object’s properties, i.e. the result of _.values, are searched instead. _.includes uses the SameAsZero algorithm to check equality.

Listing 3.8 Check if a collection contains the given value
const includes = require('lodash/includes');

describe('includes', () => {
  it('should support arrays', () => {
    expect(includes(['a', 'b', 'c'], 'a')).toBe(true);
  });

  it('should support arrays with index', () => {
    expect(includes(['a', 'b', 'c'], 'a', 1)).toBe(false);
  });

  it('should support objects', () => {
    expect(includes({
      a: 1,
      b: 2,
      c: 3
    }, 1)).toBe(true);
  });

  it('should support strings', () => {
    expect(includes('hello', 'h')).toBe(true);
  });
});

3.6 Sample

_.sample(collection) gets a single random element from a collection. _.sampleSize(collection, [n=1]) gets n random elements with unique keys from a collection.

Listing 3.9 Get random elements from a collection
_.sample(['a', 'b', 'c']);
// -> 'a'

_.sample({
  a: 1,
  b: 2,
  c: 3
});
// -> 1

_.sample('hello');
// -> 'h'

_.sampleSize('hello', 2)
// -> ['h', 'l']

3.7 Shuffle

_.shuffle(collection) shuffles a collection by generating a random permutation. Lodash uses the Fisher-Yates shuffle algorithm to shuffle the collection. For objects, the return value of _.shuffle is a random permutation of the property values.

Listing 3.10 Shuffle a collection
_.shuffle(['a', 'b', 'c']);
// -> ['b', 'c', 'a']

_.shuffle({
  a: 1,
  b: 2,
  c: 3
});
// -> [1, 2, 3]

_.shuffle('hello');
// -> ['l', 'l', 'o', 'h', 'e']

3.8 Partition

_.partition(collection, [predicate=_.identity]) splits a collection into two groups based on the result of invoking the predicate on each element. The first group contains elements for which the predicate returns a truthy value, while the second group contains elements for which the predicate returns a falsy value.

Listing 3.11 Split a collection into two groups
const partition = require('lodash/partition');
const fruits = require('../data/fruits.json');

describe('partition', () => {
  it('should support arrays', () => {
    let result = partition(['a', 'b', 'c'], char => char > 'a');
    expect(result.length).toBe(2);
    expect(result[0]).toEqual(['b', 'c']);
    expect(result[1]).toEqual(['a']);
  });

  it('should support predicate syntax', () => {
    let result = partition(fruits, 'onSale');
    expect(result.length).toBe(2);
    expect(result[0].length).toBe(1);
    expect(result[1].length).toBe(2);
  });

  it('should support strings', () => {
    let result = partition('hello', char => char > 'l');
    expect(result.length).toBe(2);
    expect(result[0]).toEqual(['o']);
    expect(result[1]).toEqual(['h', 'e', 'l', 'l']);
  });
});

3.9 Count by

_.countBy(collection, [iteratee=_.identity]) applies a function to each element in the collection and counts the number of occurrences of each result. The counting result is returned as an object with the applied result as the keys and the count as the corresponding values.

Listing 3.12 Count the number of occurrences
const countBy = require('lodash/countBy');

describe('countBy', () => {
  it('should support arrays', () => {
    expect(countBy([1, 2, 3], n => n > 1)).toEqual({
      true: 2,
      false: 1,
    });
  });

  it('should support objects', () => {
    expect(countBy({
      a: 1,
      b: 1,
      c: 2,
    }, val => val / 2)).toEqual({
      1: 1,
      0.5: 2,
    });
  });

  it('should support strings', () => {
    expect(countBy('hello', char => char === 'l')).toEqual({
      true: 2,
      false: 3,
    });
  });
});

3.10 Group by and key by

_.groupBy(collection, [iteratee=_.identity]) applies a function to each element in the collection and groups the elements by the result. Elements that have the same result will be in the same group. The grouping result is returned as an object. The keys in the object are the applied results, while the values are arrays of elements which generate the corresponding result.

Listing 3.13 Group elements
const groupBy = require('lodash/groupBy');

describe('groupBy', () => {
  it('should support arrays', () => {
    expect(groupBy([1, 2, 3], n => n > 1)).toEqual({
      true: [2, 3],
      false: [1],
    });
  });

  it('should support objects', () => {
    expect(groupBy({
      a: 1,
      b: 1,
      c: 2,
    }, val => val / 2)).toEqual({
      1: [2],
      0.5: [1, 1],
    });
  });

  it('should support strings', () => {
    expect(groupBy('hello', char => char === 'l')).toEqual({
      true: ['l', 'l'],
      false: ['h', 'e', 'o'],
    });
  });
});

The difference between _.countBy and _.groupBy is that _.countBy only returns the number of grouped elements.

_.keyBy(collection, [iteratee=_.identity])’s behavior is similar to _.groupBy, but _.keyBy only keeps the last element for each key.

Listing 3.14 Get the last element of grouping
const keyBy = require('lodash/keyBy');

describe('keyBy', () => {
  it('should support arrays', () => {
    expect(keyBy([1, 2, 3], n => n > 1)).toEqual({
      true: 3,
      false: 1,
    });
  });

  it('should support objects', () => {
    expect(keyBy({
      a: 1,
      b: 1,
      c: 2,
    }, val => val / 2)).toEqual({
      1: 2,
      0.5: 1,
    });
  });

  it('should support strings', () => {
    expect(keyBy('hello', char => char === 'l')).toEqual({
      true: 'l',
      false: 'o',
    });
  });
});

3.11 invokeMap

_.invokeMap(collection, path, [args]) invokes a method on each element in the collection and returns the results in an array. The method to invoke is specified by the path, can be the function’s name or the function itself. Additional arguments can also be provided for the method invocation. In Listing 3.14, when the function is invoked, this references the current element.

Listing 3.15 Invoke a method on each element in the collection
const invokeMap = require('lodash/invokeMap');

describe('invokeMap', () => {
  it('should support method names', () => {
    expect(invokeMap(['a', 'b', 'c'], 'toUpperCase')).toEqual(['A', 'B', 'C']\
);
  });

  it('should support extra arguments', () => {
    expect(invokeMap([['a', 'b'], ['c', 'd']], 'join', ''))
      .toEqual(['ab', 'cd']);
  });

  it('should support functions', () => {
    expect(invokeMap([{a: 1}, {a: 2}], function(toAdd) {
      return this.a + toAdd;
    }, 3)).toEqual([4, 5]);
  });
});

3.12 Map and reduce

Map and reduce are common operations when processing collections. Map transforms a collection into another collection by applying an operation to each element in the collection. Reduce transforms a collection into a single value by accumulating results of applying an operation to each element. The result of the last operation is used as the input of the current operation.

3.12.1 Map

_.map(collection, [iteratee=_.identity]) is the generic map function. We can use the different iteratee syntax.

Listing 3.16 Generic map operation
const map = require('lodash/map');

describe('map', () => {
  it('should support arrays', () => {
    expect(map([1, 2, 3], n => n * 2)).toEqual([2, 4, 6]);
  });

  it('should support iteratee syntax', () => {
    const users = [
      {
        name: 'Alex',
      },
      {
        name: 'Bob',
      }
    ];
    expect(map(users, 'name')).toEqual(['Alex', 'Bob']);
    expect(map(users, {name: 'Alex'})).toEqual([true, false]);
  });
});

3.12.2 Reduce

_.reduce(collection, [iteratee=_.identity], [accumulator]) has similar arguments list with _.map, except that it accepts an optional value as the initial input of the first reduce operation. If the initial value is not provided, the first element in the collection is used instead. The provided iteratee function will be invoked with four arguments, accumulator, value, index/key and collection. accumulator is the current reduced value, while value is the current element in the collection. The returned result of the iteratee function invocation is passed as the accumulator value of the next invocation.

Listing 3.17 Use _.reduce to sum the values in an array
const reduce = require('lodash/reduce');

describe('reduce', () => {
  it('should support no initial value', () => {
    let result = reduce([1, 2, 3],
      (accumulator, value) => accumulator + value);
    expect(result).toEqual(6);
  });

  it('should support initial value', () => {
    let result = reduce([1, 2, 3],
      (accumulator, value) => accumulator + value, 100);
    expect(result).toEqual(106);
  });
});

_.reduceRight(collection, [iteratee=_.identity], [accumulator] is similar to_.reduce) except _.reduceRight iterates all the elements from right to left.

Listing 3.18 Reduce elements from right to left
const reduceRight = require('lodash/reduceRight');
const reduce = require('lodash/reduce');

describe('reduceRight', () => {
  it('should support strings', () => {
    let result = reduceRight('hello',
      (accumulator, value) => accumulator.toUpperCase() + value);
    expect(result).toEqual('OLLEh');

    result = reduce('hello',
      (accumulator, value) => accumulator.toUpperCase() + value);
    expect(result).toEqual('HELLo');
  });
});

Search is a very common task in programming. Search is performed on iterable collections with given conditions. The return result is the first element in the collection matching the condition, or undefined if no matching element is found.

3.13.1 find

_.find(collection, [predicate=_.identity], [fromIndex=0]) is the generic function to search in collections. When invoking _.find, the collection itself and the search condition should be provided. We can also provide an optional starting index for the search. _.find supports the same predicate syntax. If a function is provided as the predicate, the function is invoked for each element in the array until the function returns a truthy value. The function is invoked with three arguments: the currently iterated element, index or key of the element and the collection itself.

Listing 3.19 Find
const find = require('lodash/find');
const fruits = require('../data/fruits.json');

describe('find', () => {
  it('should support function predicates', () => {
    let result = find(fruits, fruit => fruit.price <= 2);
    expect(result).toBeDefined();
    expect(result.name).toEqual('apple');
  });

  it('should support property predicates', () => {
    let result = find(fruits, 'onSale');
    expect(result).toBeDefined();
    expect(result.name).toEqual('apple');

    result = find(fruits, ['name', 'orange']);
    expect(result).toBeDefined();
    expect(result.name).toEqual('orange');
  });

  it('should support object predicates', () => {
    let result = find(fruits, {
      name: 'passion fruit',
      onSale: false,
    });
    expect(result).toBeDefined();
    expect(result.name).toEqual('passion fruit');
  });
});

3.13.2 findLast

_.findLast(collection, [predicate=_.identity], [fromIndex=collection.length-1]) is similar to _.find, but _.findLast iterates over all elements of the collection in reverse order. For arrays, it searches from the last element. For strings, it searches from the last character. For objects, it searches from the last element of the array of property names returned by _.keys.

Listing 3.20 Find in reverse order
const findLast = require('lodash/findLast');

describe('findLast', () => {
  it('should support strings', () => {
    expect(findLast('hello', char => char < 'f')).toEqual('e');
  });
});

3.14 Sort

_.sortBy(collection, [iteratee=_.identity]) sorts a collection in ascending order with results after applying the iteratee function to each element in the collection. The sort is stable, which means it preserves original order for elements with equality. We can use multiple iteratees as sort conditions. If multiple elements in the collection have the same value for the first property name, those elements are sorted using the second property name, and so on.

Listing 3.21 Sort a collection
const sortBy = require('lodash/sortBy');

describe('sortBy', () => {
  it('should support simple sort', () => {
    expect(sortBy([3, 2, 1])).toEqual([1, 2, 3]);
  });

  it('should support function predicates', () => {
    let result = sortBy([-3, 2, 1], val => Math.abs(val));
    expect(result).toEqual([1, 2, -3]);
  });

  it('should support multiple conditions', () => {
    const users = [
      {
        name: 'David',
        age: 28,
      },
      {
        name: 'Alex',
        age: 30,
      },
      {
        name: 'Bob',
        age: 28,
      }
    ];
    let result = sortBy(users, 'age', 'name');
    expect(result[0].name).toEqual('Bob');
    expect(result[1].name).toEqual('David');
    expect(result[2].name).toEqual('Alex');
  });
});

3.15 flatMap

_.flatMap(collection, [iteratee=_.identity]) invokes an iteratee function to each element in a collection. The result of each iteratee function invocation is an array. All result arrays are concatenated and flattened into a single array as the final result.

_.flatMapDeep(collection, [iteratee=_.identity]) is similar to _.flatMap except that _.flatMapDeep recursively flattens the result array until it’s completely flattened.

_.flatMapDepth(collection, [iteratee=_.identity], [depth=1]) is similar to _.flatMapDeep except that it only flattens the result at the given times. The default value of depth is 1, so _.flatMapDepth(array, iteratee) is the same as _.flatMap(array, iteratee).

Listing 3.22 Example of _.flatMap and _.flatMapDeep
const flatMap = require('lodash/flatMap');
const flatMapDeep = require('lodash/flatMapDeep');

describe('flatMap', () => {
  it('should support basic operation', () => {
    const map = value => [value + 1, value - 1];
    let result = flatMap([1, 2], map);
    expect(result).toEqual([2, 0, 3, 1]);
  });

  it('should support recursion', () => {
    const map = value => [[value + 1], [value - 1]];
    let result = flatMap([1, 2], map);
    expect(result).toEqual([[2], [0], [3], [1]]);

    result = flatMapDeep([1, 2], map);
    expect(result).toEqual([2, 0, 3, 1]);
  });
});