diff --git a/docs/source/_data/sidebar.yml b/docs/source/_data/sidebar.yml index 6c5ba97b16..46cc138c44 100644 --- a/docs/source/_data/sidebar.yml +++ b/docs/source/_data/sidebar.yml @@ -70,6 +70,7 @@ filters: strip: strip.html strip_html: strip_html.html strip_newlines: strip_newlines.html + sum: sum.html times: times.html truncate: truncate.html truncatewords: truncatewords.html diff --git a/docs/source/filters/sum.md b/docs/source/filters/sum.md new file mode 100644 index 0000000000..2d54c3ae9e --- /dev/null +++ b/docs/source/filters/sum.md @@ -0,0 +1,22 @@ +--- +title: sum +--- + +{% since %}v10.10.0{% endsince %} + +Computes the sum of all the numbers in an array. +An optional argument specifies which property of the array's items to sum up. + +In this example, assume the object `cart.products` contains an array of all products in the cart of a website. +Assume each cart product has a `qty` property that gives the count of that product instance in the cart. +Using the `sum` filter we can calculate the total number of products in the cart. + +Input +```liquid +The cart has {{ order.products | sum: "qty" }} products. +``` + +Output +```text +The cart has 7 products. +``` diff --git a/src/filters/array.ts b/src/filters/array.ts index 9aece587d0..ed98b861f9 100644 --- a/src/filters/array.ts +++ b/src/filters/array.ts @@ -43,6 +43,15 @@ export function * map (this: FilterImpl, arr: Scope[], property: string): Iterab return results } +export function * sum (this: FilterImpl, arr: Scope[], property?: string): IterableIterator { + let sum = 0 + for (const item of toArray(toValue(arr))) { + const data = Number(property ? yield this.context._getFromScope(item, stringify(property), false) : item) + sum += Number.isNaN(data) ? 0 : data + } + return sum +} + export function compact (this: FilterImpl, arr: T[]) { arr = toValue(arr) return toArray(arr).filter(x => !isNil(toValue(x))) diff --git a/test/integration/filters/array.spec.ts b/test/integration/filters/array.spec.ts index 7ca5ada23b..b1d50a6d24 100644 --- a/test/integration/filters/array.spec.ts +++ b/test/integration/filters/array.spec.ts @@ -74,6 +74,44 @@ describe('filters/array', function () { return test(tpl, { arr: [a, b, c] }, 'Alice Bob Carol') }) }) + describe('sum', () => { + it('should support sum with no args', function () { + const ages = [21, null, -4, '4.5', 13.25, undefined, 0] + return test('{{ages | sum}}', { ages }, '34.75') + }) + it('should support sum with property', function () { + const ages = [21, null, -4, '4.5', 13.25, undefined, 0].map(x => ({ age: x })) + return test('{{ages | sum: "age"}}', { ages }, '34.75') + }) + it('should support sum with nested property', function () { + const ages = [21, null, -4, '4.5', 13.25, undefined, 0].map(x => ({ age: { first: x } })) + return test('{{ages | sum: "age.first"}}', { ages }, '34.75') + }) + it('should support non-array input', function () { + const age = 21.5 + return test('{{age | sum}}', { age }, '21.5') + }) + it('should coerce missing property to zero', function () { + const ages = [{ qty: 1 }, { qty: 2, cnt: 3 }, { cnt: 4 }] + return test('{{ages | sum}} {{ages | sum: "cnt"}} {{ages | sum: "other"}}', { ages }, '0 7 0') + }) + it('should coerce indexable non-map values to zero', function () { + const input = [1, 'foo', { quantity: 3 }] + return test('{{input | sum}}', { input }, '1') + }) + it('should coerce unindexable values to zero', function () { + const input = [1, null, { quantity: 2 }] + return test('{{input | sum}}', { input }, '1') + }) + it('should coerce true to 1', function () { + const input = [1, true, null, { quantity: 2 }] + return test('{{input | sum}}', { input }, '2') + }) + it('should not support nested arrays', function () { + const ages = [1, [2, [3, 4]]] + return test('{{ages | sum}}', { ages }, '1') + }) + }) describe('compact', () => { it('should compact array', function () { const posts = [{ category: 'foo' }, { category: 'bar' }, { foo: 'bar' }]