Repository: harttle/liquidjs
Pull request #692 (2024-05-09 15:31:51)

feat: date filters from Jekyll, #443

coveralls: ## Pull Request Test Coverage Report for [Build 9019660616](https://coveralls.io/builds/67400547)


### Details

* **28** of **28**   **(100.0%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-0.03%**) to **99.207%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/67400547/badge)](https://coveralls.io/builds/67400547) |
| :-- | --: |
| Change from base [Build 8969691416](https://coveralls.io/builds/67319999): |  -0.03% |
| Covered Lines: | 2341 |
| Relevant Lines: | 2352 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/docs/source/_data/sidebar.yml b/docs/source/_data/sidebar.yml
index ac0631b11..b44bef785 100644
--- a/docs/source/_data/sidebar.yml
+++ b/docs/source/_data/sidebar.yml
@@ -36,6 +36,10 @@ filters:
   compact: compact.html
   concat: concat.html
   date: date.html
+  date_to_long_string: date_to_long_string.html
+  date_to_rfc822: date_to_rfc822.html
+  date_to_string: date_to_string.html
+  date_to_xmlschema: date_to_xmlschema.html
   default: default.html
   divided_by: divided_by.html
   downcase: downcase.html
diff --git a/docs/source/filters/date_to_long_string.md b/docs/source/filters/date_to_long_string.md
new file mode 100644
index 000000000..a300797d7
--- /dev/null
+++ b/docs/source/filters/date_to_long_string.md
@@ -0,0 +1,30 @@
+---
+title: date_to_long_string
+---
+{% since %}v10.13.0{% endsince %}
+
+Convert a date to long format. Same with Jekyll `date_to_long_string` filter.
+
+Input
+```liquid
+{{ site.time | date_to_long_string }}
+```
+
+Output
+```text
+07 November 2008
+```
+
+Input
+```liquid
+{{ site.time | date_to_long_string: "ordinal" }}
+```
+
+Output
+```text
+7th November 2008
+```
+
+Note that JavaScript `Date` has not timezone information, see [date][date] filter for details.
+
+[date]: ./date.html
diff --git a/docs/source/filters/date_to_rfc822.md b/docs/source/filters/date_to_rfc822.md
new file mode 100644
index 000000000..afdd98cd6
--- /dev/null
+++ b/docs/source/filters/date_to_rfc822.md
@@ -0,0 +1,20 @@
+---
+title: date_to_rfc822
+---
+{% since %}v10.13.0{% endsince %}
+
+Convert a Date into the RFC-822 format used for RSS feeds, same as Jekyll filter `date_to_rfc822`.
+
+Input
+```liquid
+{{ site.time | date_to_rfc822 }}
+```
+
+Output
+```text
+Mon, 07 Nov 2008 13:07:54 -0800
+```
+
+Note that JavaScript `Date` has not timezone information, see [date][date] filter for details.
+
+[date]: ./date.html
diff --git a/docs/source/filters/date_to_string.md b/docs/source/filters/date_to_string.md
new file mode 100644
index 000000000..0534b8fe0
--- /dev/null
+++ b/docs/source/filters/date_to_string.md
@@ -0,0 +1,30 @@
+---
+title: date_to_string
+---
+{% since %}v10.13.0{% endsince %}
+
+Convert a date to short format. Same with Jekyll `date_to_string` filter.
+
+Input
+```liquid
+{{ site.time | date_to_string }}
+```
+
+Output
+```text
+07 Nov 2008
+```
+
+Input
+```liquid
+{{ site.time | date_to_string: "ordinal", "US" }}
+```
+
+Output
+```text
+Nov 7th, 2008
+```
+
+Note that JavaScript `Date` has not timezone information, see [date][date] filter for details.
+
+[date]: ./date.html
diff --git a/docs/source/filters/date_to_xmlschema.md b/docs/source/filters/date_to_xmlschema.md
new file mode 100644
index 000000000..dd2e870eb
--- /dev/null
+++ b/docs/source/filters/date_to_xmlschema.md
@@ -0,0 +1,20 @@
+---
+title: date_to_xmlschema
+---
+{% since %}v10.13.0{% endsince %}
+
+Convert a Date into XML Schema (ISO 8601) format, same as Jekyll filter `date_to_xmlschema`.
+
+Input
+```liquid
+{{ site.time | date_to_xmlschema }}
+```
+
+Output
+```text
+2008-11-07T13:07:54-08:00
+```
+
+Note that JavaScript `Date` has not timezone information, see [date][date] filter for details.
+
+[date]: ./date.html
diff --git a/docs/source/filters/overview.md b/docs/source/filters/overview.md
index a5e11d3b8..ded8bdb55 100644
--- a/docs/source/filters/overview.md
+++ b/docs/source/filters/overview.md
@@ -13,7 +13,7 @@ Math | plus, minus, modulo, times, floor, ceil, round, divided_by, abs, at_least
 String | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, strip_newlines, split, replace, replace_first, replace_last,remove, remove_first, remove_last, truncate, truncatewords, normalize_whitespace
 HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br
 Array | slice, map, sort, sort_natural, uniq, where, where_exp, group_by, group_by_exp, find, find_exp, first, last, join, reverse, concat, compact, size, push, pop, shift, unshift
-Date | date
+Date | date, date_to_xmlschema, date_to_rfc822, date_to_string, date_to_long_string
 Misc | default, json, jsonify, inspect, raw, to_integer
 
 [shopify/liquid]: https://github.com/Shopify/liquid
diff --git a/docs/source/zh-cn/filters/date_to_long_string.md b/docs/source/zh-cn/filters/date_to_long_string.md
new file mode 100644
index 000000000..0cb54dc74
--- /dev/null
+++ b/docs/source/zh-cn/filters/date_to_long_string.md
@@ -0,0 +1,31 @@
+---
+title: date_to_long_string
+---
+{% since %}v10.13.0{% endsince %}
+
+把日期转换为长格式（只支持 US/UK 两种），与 Jekyll 的 `date_to_long_string` 过滤器一样。
+
+输入
+```liquid
+{{ site.time | date_to_long_string }}
+```
+
+输出
+```text
+07 November 2008
+```
+
+输入
+```liquid
+{{ site.time | date_to_long_string: "ordinal" }}
+```
+
+输出
+```text
+7th November 2008
+```
+
+
+注意 JavaScript `Date` 没有时区信息，详情请参考 [date][date] 过滤器。
+
+[date]: ./date.html
diff --git a/docs/source/zh-cn/filters/date_to_rfc822.md b/docs/source/zh-cn/filters/date_to_rfc822.md
new file mode 100644
index 000000000..fd704397e
--- /dev/null
+++ b/docs/source/zh-cn/filters/date_to_rfc822.md
@@ -0,0 +1,20 @@
+---
+title: date_to_rfc822
+---
+{% since %}v10.13.0{% endsince %}
+
+把日期转换为 RFC-822 格式用于 RSS feed，与 Jekyll 的 `date_to_rfc822` 过滤器一样。
+
+输入
+```liquid
+{{ site.time | date_to_rfc822 }}
+```
+
+输入
+```text
+Mon, 07 Nov 2008 13:07:54 -0800
+```
+
+注意 JavaScript `Date` 没有时区信息，详情请参考 [date][date] 过滤器。
+
+[date]: ./date.html
diff --git a/docs/source/zh-cn/filters/date_to_string.md b/docs/source/zh-cn/filters/date_to_string.md
new file mode 100644
index 000000000..569454bf4
--- /dev/null
+++ b/docs/source/zh-cn/filters/date_to_string.md
@@ -0,0 +1,30 @@
+---
+title: date_to_string
+---
+{% since %}v10.13.0{% endsince %}
+
+把日期转换为短格式（只支持 US/UK 两种），与 Jekyll 的 `date_to_string` 过滤器一样。
+
+输入
+```liquid
+{{ site.time | date_to_string }}
+```
+
+输出
+```text
+07 Nov 2008
+```
+
+输入
+```liquid
+{{ site.time | date_to_string: "ordinal", "US" }}
+```
+
+输出
+```text
+Nov 7th, 2008
+```
+
+注意 JavaScript `Date` 没有时区信息，详情请参考 [date][date] 过滤器。
+
+[date]: ./date.html
diff --git a/docs/source/zh-cn/filters/date_to_xmlschema.md b/docs/source/zh-cn/filters/date_to_xmlschema.md
new file mode 100644
index 000000000..e1051cdc3
--- /dev/null
+++ b/docs/source/zh-cn/filters/date_to_xmlschema.md
@@ -0,0 +1,20 @@
+---
+title: date_to_xmlschema
+---
+{% since %}v10.13.0{% endsince %}
+
+把日期转换为 XML Schema (ISO 8601) 格式，与 Jekyll 的 `date_to_xmlschema` 过滤器一样。
+
+输入
+```liquid
+{{ site.time | date_to_xmlschema }}
+```
+
+输出
+```text
+2008-11-07T13:07:54-08:00
+```
+
+注意 JavaScript `Date` 没有时区信息，详情请参考 [date][date] 过滤器。
+
+[date]: ./date.html
diff --git a/docs/source/zh-cn/filters/overview.md b/docs/source/zh-cn/filters/overview.md
index ce13a9e59..c4bbfc712 100644
--- a/docs/source/zh-cn/filters/overview.md
+++ b/docs/source/zh-cn/filters/overview.md
@@ -13,7 +13,7 @@ LiquidJS 共支持 40+ 个过滤器，可以分为如下几类：
 字符串 | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, strip_newlines, split, replace, replace_first, replace_last, remove, remove_first, remove_last, truncate, truncatewords, normalize_whitespace
 HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br
 数组 | slice, map, sort, sort_natural, uniq, where, where_exp, group_by, group_by_exp, find, find_exp, first, last, join, reverse, concat, compact, size, push, pop, shift, unshift
-日期 | date
+日期 | date, date_to_xmlschema, date_to_rfc822, date_to_string, date_to_long_string
 其他 | default, json, jsonify, inspect, raw, to_integer
 
 [shopify/liquid]: https://github.com/Shopify/liquid
diff --git a/src/filters/date.ts b/src/filters/date.ts
index 31ce2c890..eede5fa91 100644
--- a/src/filters/date.ts
+++ b/src/filters/date.ts
@@ -1,13 +1,46 @@
 import { toValue, stringify, isString, isNumber, TimezoneDate, LiquidDate, strftime, isNil } from '../util'
 import { FilterImpl } from '../template'
+import { LiquidOptions } from '../liquid-options'
 
 export function date (this: FilterImpl, v: string | Date, format?: string, timezoneOffset?: number | string) {
-  const opts = this.context.opts
+  const date = parseDate(v, this.context.opts, timezoneOffset)
+  if (!date) return v
+  format = toValue(format)
+  format = isNil(format) ? this.context.opts.dateFormat : stringify(format)
+  return strftime(date, format)
+}
+
+export function date_to_xmlschema (this: FilterImpl, v: string | Date) {
+  return date.call(this, v, '%Y-%m-%dT%H:%M:%S%:z')
+}
+
+export function date_to_rfc822 (this: FilterImpl, v: string | Date) {
+  return date.call(this, v, '%a, %d %b %Y %H:%M:%S %z')
+}
+
+export function date_to_string (this: FilterImpl, v: string | Date, type?: string, style?: string) {
+  return stringify_date.call(this, v, '%b', type, style)
+}
+
+export function date_to_long_string (this: FilterImpl, v: string | Date, type?: string, style?: string) {
+  return stringify_date.call(this, v, '%B', type, style)
+}
+
+function stringify_date (this: FilterImpl, v: string | Date, month_type: string, type?: string, style?: string) {
+  const date = parseDate(v, this.context.opts)
+  if (!date) return v
+  if (type === 'ordinal') {
+    const d = date.getDate()
+    return style === 'US'
+      ? strftime(date, `${month_type} ${d}%q, %Y`)
+      : strftime(date, `${d}%q ${month_type} %Y`)
+  }
+  return strftime(date, `%d ${month_type} %Y`)
+}
+
+function parseDate (v: string | Date, opts: LiquidOptions, timezoneOffset?: number | string): LiquidDate | undefined {
   let date: LiquidDate
   v = toValue(v)
-  format = toValue(format)
-  if (isNil(format)) format = opts.dateFormat
-  else format = stringify(format)
   if (v === 'now' || v === 'today') {
     date = new Date()
   } else if (isNumber(v)) {
@@ -23,13 +56,13 @@ export function date (this: FilterImpl, v: string | Date, format?: string, timez
   } else {
     date = v
   }
-  if (!isValidDate(date)) return v
+  if (!isValidDate(date)) return
   if (timezoneOffset !== undefined) {
     date = new TimezoneDate(date, timezoneOffset)
   } else if (!(date instanceof TimezoneDate) && opts.timezoneOffset !== undefined) {
     date = new TimezoneDate(date, opts.timezoneOffset)
   }
-  return strftime(date, format)
+  return date
 }
 
 function isValidDate (date: any): date is Date {
diff --git a/src/util/strftime.ts b/src/util/strftime.ts
index 335379d67..b6bf2652f 100644
--- a/src/util/strftime.ts
+++ b/src/util/strftime.ts
@@ -45,31 +45,16 @@ function isLeapYear (d: LiquidDate) {
   const year = d.getFullYear()
   return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year)))
 }
-function getSuffix (d: LiquidDate) {
+function ordinal (d: LiquidDate) {
   const date = d.getDate()
+  if ([11, 12, 13].includes(date)) return 'th'
 
-  let suffix = 'th'
-
-  switch (date) {
-    case 11:
-    case 12:
-    case 13:
-      break
-    default:
-      switch (date % 10) {
-        case 1:
-          suffix = 'st'
-          break
-        case 2:
-          suffix = 'nd'
-          break
-        case 3:
-          suffix = 'rd'
-          break
-      }
+  switch (date % 10) {
+    case 1: return 'st'
+    case 2: return 'nd'
+    case 3: return 'rd'
+    default: return 'th'
   }
-
-  return suffix
 }
 function century (d: LiquidDate) {
   return parseInt(d.getFullYear().toString().substring(0, 2), 10)
@@ -138,7 +123,7 @@ const formatCodes = {
   },
   p: (d: LiquidDate) => (d.getHours() < 12 ? 'AM' : 'PM'),
   P: (d: LiquidDate) => (d.getHours() < 12 ? 'am' : 'pm'),
-  q: (d: LiquidDate) => getSuffix(d),
+  q: (d: LiquidDate) => ordinal(d),
   s: (d: LiquidDate) => Math.round(d.getTime() / 1000),
   S: (d: LiquidDate) => d.getSeconds(),
   u: (d: LiquidDate) => d.getDay() || 7,


Test patch:
diff --git a/test/integration/filters/date.spec.ts b/test/integration/filters/date.spec.ts
index d944285e1..f2772402f 100644
--- a/test/integration/filters/date.spec.ts
+++ b/test/integration/filters/date.spec.ts
@@ -146,3 +146,54 @@ describe('filters/date', function () {
     })
   })
 })
+describe('filters/date_to_xmlschema', function () {
+  const liquid = new Liquid()
+  it('should support literal date', function () {
+    const output = liquid.parseAndRenderSync('{{ "1990-10-15T23:00:00" | date_to_xmlschema }}')
+    expect(output).toMatch(/^1990-10-15T23:00:00[+-]\d\d:\d\d$/)
+  })
+  it('should timezoned date', function () {
+    const liquid = new Liquid({ preserveTimezones: true })
+    const output = liquid.parseAndRenderSync('{{ "2008-11-07T13:07:54-08:00" | date_to_xmlschema }}')
+    expect(output).toEqual('2008-11-07T13:07:54-08:00')
+  })
+})
+describe('filters/date_to_rfc822', function () {
+  const liquid = new Liquid()
+  it('should support literal date', function () {
+    const output = liquid.parseAndRenderSync('{{ "1990-10-15T23:00:00" | date_to_rfc822 }}')
+    expect(output).toMatch(/^Mon, 15 Oct 1990 23:00:00 [+-]\d{4}$/)
+  })
+  it('should timezoned date', function () {
+    const liquid = new Liquid({ preserveTimezones: true })
+    const output = liquid.parseAndRenderSync('{{ "2008-11-07T13:07:54-08:00" | date_to_rfc822 }}')
+    expect(output).toEqual('Fri, 07 Nov 2008 13:07:54 -0800')
+  })
+})
+describe('filters/date_to_string', function () {
+  const liquid = new Liquid({ preserveTimezones: true })
+  it('should default to non-ordinal, UK', function () {
+    const output = liquid.parseAndRenderSync('{{ "2008-11-07T13:07:54-08:00" | date_to_string }}')
+    expect(output).toEqual('07 Nov 2008')
+  })
+  it('should support ordinal, US', function () {
+    const output = liquid.parseAndRenderSync('{{ "2008-11-07T13:07:54-08:00" | date_to_string: "ordinal", "US" }}')
+    expect(output).toEqual('Nov 7th, 2008')
+  })
+})
+
+describe('filters/date_to_long_string', function () {
+  const liquid = new Liquid({ preserveTimezones: true })
+  it('should default to non-ordinal, UK', function () {
+    const output = liquid.parseAndRenderSync('{{ "2008-11-07T13:07:54-08:00" | date_to_long_string }}')
+    expect(output).toEqual('07 November 2008')
+  })
+  it('should support ordinal, US', function () {
+    const output = liquid.parseAndRenderSync('{{ "2008-11-07T13:07:54-08:00" | date_to_long_string: "ordinal", "US" }}')
+    expect(output).toEqual('November 7th, 2008')
+  })
+  it('should support ordinal, UK', function () {
+    const output = liquid.parseAndRenderSync('{{ "2008-11-07T13:07:54-08:00" | date_to_long_string: "ordinal" }}')
+    expect(output).toEqual('7th November 2008')
+  })
+})