Skip to content

Commit e09657c

Browse files
Yang Junharttle
Yang Jun
authored andcommitted
fix: allow %Z for TimezoneDate, update docs accordingly #684
1 parent d48ac56 commit e09657c

File tree

10 files changed

+79
-42
lines changed

10 files changed

+79
-42
lines changed

bin/build-contributors.sh

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Run `sed` in a way that's compatible with both macOS (BSD) and Linux (GNU)
44
sedi() {
55
if [[ "$OSTYPE" == "darwin"* ]]; then
6-
sed -i '' "$@"
6+
/usr/bin/sed -i '' "$@"
77
else
88
sed -i "$@"
99
fi
@@ -16,6 +16,7 @@ sedi \
1616
-e 's/"contributorsPerLine": 7/"contributorsPerLine": 65535/g' \
1717
docs/.all-contributorsrc
1818

19+
touch docs/themes/navy/layout/partial/all-contributors.swig
1920
all-contributors --config docs/.all-contributorsrc generate
2021
sedi 's/<br \/>.*<\/td>/<\/a><\/td>/g' docs/themes/navy/layout/partial/all-contributors.swig
2122

docs/source/filters/date.md

+14-17
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ title: date
33
---
44
{% since %}v1.9.1{% endsince %}
55

6-
# Format
7-
* Converts a timestamp into another date format
8-
* LiquidJS tries to be conformant with Shopify/Liquid which is using Ruby's core [Time#strftime(string)](http://www.ruby-doc.org/core/Time.html#method-i-strftime)
9-
* Refer [format flags](https://ruby-doc.org/core/strftime_formatting_rdoc.html)
10-
* Not all options are supported though - refer [differences here](/tutorials/differences.html#Differences)
11-
* The input is firstly converted to `Date` object via [new Date()][jsDate]
12-
* Date format can be provided individually as a filter option
13-
* If not provided, then `%A, %B %-e, %Y at %-l:%M %P %z` format will be used as default format
14-
* Override this using [`dateFormat`](/api/interfaces/LiquidOptions.html#dateFormat) LiquidJS option, to set your preferred default format for all date filters
6+
Date filter is used to convert a timestamp into the specified format.
7+
8+
* LiquidJS tries to conform to Shopify/Liquid, which uses Ruby's core [Time#strftime(string)](http://www.ruby-doc.org/core/Time.html#method-i-strftime). There're differences with [Ruby's format flags](https://ruby-doc.org/core/strftime_formatting_rdoc.html):
9+
* `%Z` (since v10.11.1) works when there's a passed-in timezone name from `LiquidOption` or in-place value (see TimeZone below). If passed-in timezone is an offset number instead of string, it'll behave like `%z`. If there's none passed-in timezone, it returns [the runtime's default time zone](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#timezone).
10+
* LiquidJS provides an additional `%q` flag for date ordinals. e.g. `{{ '2023/02/02' | date: '%d%q of %b'}}` => `02nd of Feb`
11+
* Date literals are firstly converted to `Date` object via [new Date()][jsDate], that means literal values are considered in runtime's time zone by default.
12+
* The format filter argument is optional:
13+
* If not provided, it defaults to `%A, %B %-e, %Y at %-l:%M %P %z`.
14+
* The above default can be overridden by [`dateFormat`](/api/interfaces/LiquidOptions.html#dateFormat) LiquidJS option.
1515

1616
### Examples
1717
```liquid
@@ -23,16 +23,14 @@ title: date
2323
```
2424

2525
# TimeZone
26-
* By default, dates will be converted to local timezone before output
27-
* You can override that by,
28-
* setting a timezone for each individual `date` filter via the second parameter
29-
* using the [`timezoneOffset`](/api/interfaces/LiquidOptions.html#timezoneOffset) LiquidJS option
30-
* Its default value is your local timezone offset which can be obtained by `new Date().getTimezoneOffset()`
26+
* During output, LiquidJS uses local timezone which can override by:
27+
* setting a timezone in-place when calling `date` filter, or
28+
* setting the [`timezoneOffset`](/api/interfaces/LiquidOptions.html#timezoneOffset) LiquidJS option
29+
* It defaults to runtime's time one.
3130
* Offset can be set as,
3231
* minutes: `-360` means `'+06:00'` and `360` means `'-06:00'`
3332
* timeZone ID: `Asia/Colombo` or `America/New_York`
34-
* Use minutes for better performance with repeated processing of templates with many dates like, converting template for each email recipient
35-
* Refer [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for TZ database values
33+
* See [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for TZ database values
3634

3735
### Examples
3836
```liquid
@@ -41,7 +39,6 @@ title: date
4139
{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S", "Asia/Colombo" }} => 1991-01-01T04:30:00
4240
```
4341

44-
4542
# Input
4643
* `date` works on strings if they contain well-formatted dates
4744
* Note that LiquidJS is using [JavaScript Date][jsDate] to parse the input string, that means [IETF-compliant RFC 2822 timestamps](https://datatracker.ietf.org/doc/html/rfc2822#page-14) and strings in [a version of ISO8601](https://www.ecma-international.org/ecma-262/11.0/#sec-date.parse) are supported.

docs/source/tutorials/differences.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ Though we're trying to be compatible with the Ruby version, there are still some
3333
* LiquidJS-defined tags: [layout][layout], [render][render] and corresponding `block` tag.
3434
* LiquidJS-defined filters: [json][json].
3535
* Tags/filters that don't depend on Shopify platform are borrowed from [Shopify][shopify-tags].
36-
* Tags/filters that don't depend on Jekyll framework are borrowed from [Jekyll][jekyll-filters]
37-
* LiquidJS [date][date] filter supports `%q` for date ordinals like `{{ '2023/02/02' | date: '%d%q of %b'}}` => `02nd of Feb`
36+
* Tags/filters that don't depend on Jekyll framework are borrowed from [Jekyll][jekyll-filters].
37+
* Some tags/filters behave differently: [date][date] filter.
3838

3939
[date]: https://liquidjs.com/filters/date.html
4040
[layout]: ../tags/layout.html

docs/source/zh-cn/filters/date.md

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ title: date
66

77
把时间戳转换为字符串。LiquidJS 尝试跟 Shopify/Liquid 保持一致,它用的是 Ruby 核心的 [Time#strftime(string)](http://www.ruby-doc.org/core/Time.html#method-i-strftime)。此外 LiquidJS 会先通过 [new Date()][newDate] 尝试把输入转换为 Date 对象。
88

9+
但 LiquidJS 支持的格式与 [Ruby 的 flag](https://ruby-doc.org/core/strftime_formatting_rdoc.html) 有些不同:
10+
* `%Z`(自 v10.11.1 起支持)只有在传入了时区时才起作用(可以通过 `LiquidOption` 传入,也可以在创建日期时单独传入,见下文)。如果传入的时区是个数字,那么它的表现将会与 `%z` 相同。如果没有传入时区,将会返回 [运行时默认时区](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#timezone)
11+
* LiquidJS 提供额外的 `%q` 用来处理序数:`{{ '2023/02/02' | date: '%d%q of %b'}}` => `02nd of Feb`
12+
* 日期字面量会通过 [new Date()][jsDate] 转化为 `Date` 对象,这意味着字面量默认使用运行时默认时区。
13+
* 格式字参数是可选的:
14+
* 如果不传,默认为 `%A, %B %-e, %Y at %-l:%M %P %z`
15+
* 上述默认值可以通过 [`dateFormat`](/api/interfaces/LiquidOptions.html#dateFormat) 参数覆盖。
16+
917
输入
1018
```liquid
1119
{{ article.published_at | date: "%a, %b %d, %y" }}

docs/source/zh-cn/tutorials/differences.md

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ LiquidJS 一直很重视兼容于 Ruby 版本的 Liquid。Liquid 模板语言最
3434
* LiquidJS 自己定义的过滤器:[json][json]
3535
*[Shopify][shopify-tags] 借来的不依赖 Shopify 平台的标签/过滤器。
3636
*[Jekyll][jekyll-filters] 借来的不依赖 Jekyll 框架的标签/过滤器。
37+
* 有些过滤器和标签表现不同:比如 [date][date]
3738

3839
[layout]: ../tags/layout.html
3940
[render]: ../tags/render.html

src/filters/date.ts

+2-12
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,13 @@ export function date (this: FilterImpl, v: string | Date, format?: string, timez
2525
}
2626
if (!isValidDate(date)) return v
2727
if (timezoneOffset !== undefined) {
28-
date = new TimezoneDate(date, parseTimezoneOffset(date, timezoneOffset))
28+
date = new TimezoneDate(date, timezoneOffset)
2929
} else if (!(date instanceof TimezoneDate) && opts.timezoneOffset !== undefined) {
30-
date = new TimezoneDate(date, parseTimezoneOffset(date, opts.timezoneOffset))
30+
date = new TimezoneDate(date, opts.timezoneOffset)
3131
}
3232
return strftime(date, format)
3333
}
3434

3535
function isValidDate (date: any): date is Date {
3636
return (date instanceof Date || date instanceof TimezoneDate) && !isNaN(date.getTime())
3737
}
38-
39-
/**
40-
* need pass in a `date` because offset is dependent on whether DST is active
41-
*/
42-
function parseTimezoneOffset (date: Date, timeZone: string | number) {
43-
if (isNumber(timeZone)) return timeZone
44-
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }))
45-
const tzDate = new Date(date.toLocaleString('en-US', { timeZone }))
46-
return (utcDate.getTime() - tzDate.getTime()) / 6e4
47-
}

src/util/liquid-date.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface LiquidDate {
1414
getMonth(): number;
1515
getFullYear(): number;
1616
getTimezoneOffset(): number;
17+
getTimezoneName?(): string;
1718
toLocaleTimeString(): string;
1819
toLocaleDateString(): string;
1920
}

src/util/strftime.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ const padChars = {
105105
p: ' ',
106106
P: ' '
107107
}
108+
function getTimezoneOffset (d: LiquidDate, opts: FormatOptions) {
109+
const nOffset = Math.abs(d.getTimezoneOffset())
110+
const h = Math.floor(nOffset / 60)
111+
const m = nOffset % 60
112+
return (d.getTimezoneOffset() > 0 ? '-' : '+') +
113+
padStart(h, 2, '0') +
114+
(opts.flags[':'] ? ':' : '') +
115+
padStart(m, 2, '0')
116+
}
108117
const formatCodes = {
109118
a: (d: LiquidDate) => dayNamesShort[d.getDay()],
110119
A: (d: LiquidDate) => dayNames[d.getDay()],
@@ -140,14 +149,12 @@ const formatCodes = {
140149
X: (d: LiquidDate) => d.toLocaleTimeString(),
141150
y: (d: LiquidDate) => d.getFullYear().toString().slice(2, 4),
142151
Y: (d: LiquidDate) => d.getFullYear(),
143-
z: (d: LiquidDate, opts: FormatOptions) => {
144-
const nOffset = Math.abs(d.getTimezoneOffset())
145-
const h = Math.floor(nOffset / 60)
146-
const m = nOffset % 60
147-
return (d.getTimezoneOffset() > 0 ? '-' : '+') +
148-
padStart(h, 2, '0') +
149-
(opts.flags[':'] ? ':' : '') +
150-
padStart(m, 2, '0')
152+
z: getTimezoneOffset,
153+
Z: (d: LiquidDate, opts: FormatOptions) => {
154+
if (d.getTimezoneName) {
155+
return d.getTimezoneName() || getTimezoneOffset(d, opts)
156+
}
157+
return (typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : '')
151158
},
152159
't': () => '\t',
153160
'n': () => '\n',

src/util/timezone-date.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { LiquidDate } from './liquid-date'
2+
import { isString } from './underscore'
23

34
// one minute in milliseconds
45
const OneMinute = 60000
@@ -13,13 +14,15 @@ const ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/
1314
*/
1415
export class TimezoneDate implements LiquidDate {
1516
private timezoneOffset: number
17+
private timezoneName: string
1618
private date: Date
1719
private displayDate: Date
18-
constructor (init: string | number | Date | TimezoneDate, timezoneOffset: number) {
20+
constructor (init: string | number | Date | TimezoneDate, timezone: number | string) {
1921
this.date = init instanceof TimezoneDate
2022
? init.date
2123
: new Date(init)
22-
this.timezoneOffset = timezoneOffset
24+
this.timezoneOffset = isString(timezone) ? TimezoneDate.getTimezoneOffset(timezone, this.date) : timezone
25+
this.timezoneName = isString(timezone) ? timezone : ''
2326

2427
const diff = (this.date.getTimezoneOffset() - this.timezoneOffset) * OneMinute
2528
const time = this.date.getTime() + diff
@@ -69,6 +72,9 @@ export class TimezoneDate implements LiquidDate {
6972
getTimezoneOffset () {
7073
return this.timezoneOffset!
7174
}
75+
getTimezoneName () {
76+
return this.timezoneName
77+
}
7278

7379
/**
7480
* Create a Date object fixed to it's declared Timezone. Both
@@ -97,4 +103,12 @@ export class TimezoneDate implements LiquidDate {
97103
}
98104
return new Date(dateString)
99105
}
106+
private static getTimezoneOffset (timezoneName: string, date = new Date()) {
107+
const localDateString = date.toLocaleString('en-US', { timeZone: timezoneName })
108+
const utcDateString = date.toLocaleString('en-US', { timeZone: 'UTC' })
109+
110+
const localDate = new Date(localDateString)
111+
const utcDate = new Date(utcDateString)
112+
return (+utcDate - +localDate) / (60 * 1000)
113+
}
100114
}

test/integration/filters/date.spec.ts

+18
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,24 @@ describe('filters/date', function () {
108108
const html = liquid.parseAndRenderSync('{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S", "Asia/Colombo" }}')
109109
expect(html).toEqual('1991-01-01T04:30:00')
110110
})
111+
it('should use runtime default timezone when not specified', async () => {
112+
const liquid = new Liquid()
113+
const html = liquid.parseAndRenderSync('{{ "1990-12-31T23:00:00Z" | date: "%Z" }}')
114+
expect(html).toEqual(Intl.DateTimeFormat().resolvedOptions().timeZone)
115+
})
116+
it('should use in-place timezoneOffset as timezone name', async () => {
117+
const liquid = new Liquid({ preserveTimezones: true })
118+
const html = liquid.parseAndRenderSync('{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S %Z", "Asia/Colombo" }}')
119+
expect(html).toEqual('1991-01-01T04:30:00 Asia/Colombo')
120+
})
121+
it('should use options.timezoneOffset as default timezone name', function () {
122+
const opts: LiquidOptions = { timezoneOffset: 'Australia/Brisbane' }
123+
return test('{{ "1990-12-31T23:00:00.000Z" | date: "%Y-%m-%dT%H:%M:%S %Z"}}', '1991-01-01T10:00:00 Australia/Brisbane', undefined, opts)
124+
})
125+
it('should use given timezone offset number as timezone name', function () {
126+
const opts: LiquidOptions = { preserveTimezones: true }
127+
return test('{{ "1990-12-31T23:00:00+02:30" | date: "%Y-%m-%dT%H:%M:%S %:Z"}}', '1990-12-31T23:00:00 +02:30', undefined, opts)
128+
})
111129
})
112130
describe('dateFormat', function () {
113131
const optsWithoutDateFormat: LiquidOptions = { timezoneOffset: 360 } // -06:00

0 commit comments

Comments
 (0)