Repository: harttle/liquidjs (ts)

Issue:
Array sum filter
hello hello, upstream ruby implementation has added [sum filter](https://github.com/Shopify/liquid/pull/1722) a while ago, I need it implemented here for compatibility and I assume this would be of your interest? I will attach a PR.

Pull request description:
feat: Array sum filter
Implement `sum` filter from shopify liquid. Fixes #660 
Note the last two tests assert incompatibility with the ruby version, in particular coercing of `true`

Patch:
diff --git a/docs/source/_data/sidebar.yml b/docs/source/_data/sidebar.yml
index 6c5ba97b1..46cc138c4 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 000000000..2d54c3ae9
--- /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 9aece587d..ed98b861f 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<unknown> {
+  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<T> (this: FilterImpl, arr: T[]) {
   arr = toValue(arr)
   return toArray(arr).filter(x => !isNil(toValue(x)))


Test patch:
diff --git a/test/integration/filters/array.spec.ts b/test/integration/filters/array.spec.ts
index 7ca5ada23..b1d50a6d2 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' }]