mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-16 04:40:39 -07:00
The %s handler read LiquidDate.getTime(), which returns the displayDate deliberately shifted by the display timezone offset for wall-clock getters. With a timezone argument or timezoneOffset option set, %s produced an epoch shifted by (server offset - display offset) instead of the true Unix timestamp. Expose the unshifted time as LiquidDate.dateValue() and use it for %s. Also switch Math.round to Math.floor so fractional seconds truncate toward the epoch like Ruby strftime. Fixes #931
164 lines
5.4 KiB
TypeScript
164 lines
5.4 KiB
TypeScript
import { getDateTimeFormat } from './intl'
|
|
import { isString } from './underscore'
|
|
|
|
// one minute in milliseconds
|
|
const OneMinute = 60000
|
|
/**
|
|
* Need support both ISO8601 and RFC2822 as in major browsers & NodeJS
|
|
* RFC2822: https://datatracker.ietf.org/doc/html/rfc2822#section-3.3
|
|
*/
|
|
const TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):?(\d{2}))$/
|
|
const monthNames = [
|
|
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
|
|
'September', 'October', 'November', 'December'
|
|
]
|
|
const monthNamesShort = monthNames.map(name => name.slice(0, 3))
|
|
const dayNames = [
|
|
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
|
|
]
|
|
const dayNamesShort = dayNames.map(name => name.slice(0, 3))
|
|
|
|
/**
|
|
* A date implementation with timezone info, just like Ruby date
|
|
*
|
|
* Implementation:
|
|
* - create a Date offset by it's timezone difference, avoiding overriding a bunch of methods
|
|
* - rewrite getTimezoneOffset() to trick strftime
|
|
*/
|
|
export class LiquidDate {
|
|
private timezoneOffset: number
|
|
private timezoneName: string
|
|
private date: Date
|
|
private displayDate: Date
|
|
private DateTimeFormat = getDateTimeFormat()
|
|
public timezoneFixed: boolean
|
|
constructor (
|
|
init: string | number | Date,
|
|
private locale: string,
|
|
timezone?: number | string
|
|
) {
|
|
this.date = new Date(init)
|
|
this.timezoneFixed = timezone !== undefined
|
|
if (timezone === undefined) {
|
|
timezone = this.date.getTimezoneOffset()
|
|
}
|
|
this.timezoneOffset = isString(timezone) ? LiquidDate.getTimezoneOffset(timezone, this.date) : timezone
|
|
this.timezoneName = isString(timezone) ? timezone : ''
|
|
|
|
const diff = (this.date.getTimezoneOffset() - this.timezoneOffset) * OneMinute
|
|
const time = this.date.getTime() + diff
|
|
this.displayDate = new Date(time)
|
|
}
|
|
|
|
getTime () {
|
|
return this.displayDate.getTime()
|
|
}
|
|
/**
|
|
* The underlying UTC timestamp in milliseconds, unaffected by the display
|
|
* timezone. Use this (not `getTime()`) for timezone-invariant values like
|
|
* `%s`: `getTime()` reads `displayDate`, which is deliberately shifted by
|
|
* the display timezone offset so wall-clock getters can delegate to Date.
|
|
*/
|
|
dateValue () {
|
|
return this.date.getTime()
|
|
}
|
|
getMilliseconds () {
|
|
return this.displayDate.getMilliseconds()
|
|
}
|
|
getSeconds () {
|
|
return this.displayDate.getSeconds()
|
|
}
|
|
getMinutes () {
|
|
return this.displayDate.getMinutes()
|
|
}
|
|
getHours () {
|
|
return this.displayDate.getHours()
|
|
}
|
|
getDay () {
|
|
return this.displayDate.getDay()
|
|
}
|
|
getDate () {
|
|
return this.displayDate.getDate()
|
|
}
|
|
getMonth () {
|
|
return this.displayDate.getMonth()
|
|
}
|
|
getFullYear () {
|
|
return this.displayDate.getFullYear()
|
|
}
|
|
toLocaleString (locale?: string, init?: any) {
|
|
if (init?.timeZone) {
|
|
return this.date.toLocaleString(locale, init)
|
|
}
|
|
return this.displayDate.toLocaleString(locale, init)
|
|
}
|
|
toLocaleTimeString (locale?: string) {
|
|
return this.displayDate.toLocaleTimeString(locale)
|
|
}
|
|
toLocaleDateString (locale?: string) {
|
|
return this.displayDate.toLocaleDateString(locale)
|
|
}
|
|
getTimezoneOffset () {
|
|
return this.timezoneOffset!
|
|
}
|
|
getTimeZoneName () {
|
|
if (this.timezoneFixed) return this.timezoneName
|
|
if (!this.DateTimeFormat) return
|
|
return this.DateTimeFormat().resolvedOptions().timeZone
|
|
}
|
|
getLongMonthName () {
|
|
return this.format({ month: 'long' }) ?? monthNames[this.getMonth()]
|
|
}
|
|
getShortMonthName () {
|
|
return this.format({ month: 'short' }) ?? monthNamesShort[this.getMonth()]
|
|
}
|
|
getLongWeekdayName () {
|
|
return this.format({ weekday: 'long' }) ?? dayNames[this.displayDate.getDay()]
|
|
}
|
|
getShortWeekdayName () {
|
|
return this.format({ weekday: 'short' }) ?? dayNamesShort[this.displayDate.getDay()]
|
|
}
|
|
valid () {
|
|
return !isNaN(this.getTime())
|
|
}
|
|
private format (options: Intl.DateTimeFormatOptions) {
|
|
return this.DateTimeFormat && this.DateTimeFormat(this.locale, options).format(this.displayDate)
|
|
}
|
|
|
|
/**
|
|
* Create a Date object fixed to it's declared Timezone. Both
|
|
* - 2021-08-06T02:29:00.000Z and
|
|
* - 2021-08-06T02:29:00.000+08:00
|
|
* will always be displayed as
|
|
* - 2021-08-06 02:29:00
|
|
* regardless timezoneOffset in JavaScript realm
|
|
*
|
|
* The implementation hack:
|
|
* Instead of calling `.getMonth()`/`.getUTCMonth()` respect to `preserveTimezones`,
|
|
* we create a different Date to trick strftime, it's both simpler and more performant.
|
|
* Given that a template is expected to be parsed fewer times than rendered.
|
|
*/
|
|
static createDateFixedToTimezone (dateString: string, locale: string): LiquidDate {
|
|
const m = dateString.match(TIMEZONE_PATTERN)
|
|
// representing a UTC timestamp
|
|
if (m && m[1] === 'Z') {
|
|
return new LiquidDate(+new Date(dateString), locale, 0)
|
|
}
|
|
// has a timezone specified
|
|
if (m && m[2] && m[3] && m[4]) {
|
|
const [, , sign, hours, minutes] = m
|
|
const offset = (sign === '+' ? -1 : 1) * (parseInt(hours, 10) * 60 + parseInt(minutes, 10))
|
|
return new LiquidDate(+new Date(dateString), locale, offset)
|
|
}
|
|
return new LiquidDate(dateString, locale)
|
|
}
|
|
private static getTimezoneOffset (timezoneName: string, date: Date) {
|
|
const localDateString = date.toLocaleString('en-US', { timeZone: timezoneName })
|
|
const utcDateString = date.toLocaleString('en-US', { timeZone: 'UTC' })
|
|
|
|
const localDate = new Date(localDateString)
|
|
const utcDate = new Date(utcDateString)
|
|
return (+utcDate - +localDate) / (60 * 1000)
|
|
}
|
|
}
|