Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

locales/tz #608

Closed
wants to merge 20 commits into from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ index.d.ts

#dev
demo.js

\.vscode/launch\.json
42 changes: 42 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"name": "tz",
"request": "launch",
"args": [
// "/test/locale/intl.test.js",
"/test/plugin/timezones.test.js",
"--runInBand",
"--watch",
// "--icu-data-dir=./node_modules/full-icu"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
},
{
"type": "node",
"name": "intl",
"request": "launch",
"args": [
"/test/locale/intl.test.js",
// "/test/plugin/timezones.test.js",
"--runInBand",
"--watch",
// "--icu-data-dir=./node_modules/full-icu"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
]
}
6 changes: 6 additions & 0 deletions docs/en/API-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,9 @@ plugin [`MinMax`](./Plugin.md#minmax)
`.calendar` to display calendar time

plugin [`Calendar`](./Plugin.md#calendar)

### Timezones

`prototype.tz` `.tz` `.tz.guess` `.zoneAbbr` `.zoneName` `.utc` to convert to and form timezones

plugin [`Timezones`](./Plugin.md#timezones)
12 changes: 11 additions & 1 deletion docs/en/I18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,26 @@ Day.js has great support for internationalization.

But none of them will be included in your build unless you use that.

By default, Day.js comes with English (United States) locale.
By default, Day.js comes with generic English locale.

You can load multiple locales and switch between them easily.

[List of supported locales](../../src/locale)

You are super welcome to add a locale by opening a pull request :+1:

There's also support for browser's/node's Intl api which will kick in if locale is set but no files are imported, unless it's the default en locale, which can be switched to en-US for Intl to take effect. If Intl is used, then requested format will not respect order of items, instead following locale's order. If Intl is unsupported then a polyfill will be loaded synchronously. Nodejs doesn't have to support intl localizations, by default it loads system [ICU](https://nodejs.org/api/intl.html), Intl feature detection doesn't check if ICU is available.

## API

#### Relying on Intl api

- Passing options, locale supports all intl locale options, and format supports moment arguments first, and intl for second. Format's order is ignored.

```js
dayjs().locale('ja-JP-u-ca-japanese').format('DD', { weekday: 'long'})
```

#### Changing locale globally

- Returns locale string
Expand Down
19 changes: 19 additions & 0 deletions docs/en/Plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,25 @@ dayjs().calendar(null, {
})
```

### Timezones

- Timezones adds `instance.tz` `constructor.tz` `.utc`. `constructor.tz(date, timezone)` makes date with label of that timezone available under timeZone property on instance (like moment) accounting for daylight savings changes. `.tz` converts date to another timezone, and utc converts to utc timezone. Is currently compatible with format, but not add/subtract and similar methods. Arguments that require timezone name, take IANA name which is difference to time zone names that try to convey offset such as Eastern standard time. If Intl is unsupported then a polyfill will be loaded synchronously. Nodejs doesn't have to support intl localizations, by default it loads system [ICU](https://nodejs.org/api/intl.html), Intl feature detection doesn't check if ICU is available.
`prototype.tz` `.tz` `.tz.guess` `.zoneAbbr` `.zoneName`
```javascript
import timezones from 'dayjs/plugin/timezones'

dayjs.extend(timezones)
//checking Almaty's time
dayjs(new Date()).tz("Asia/Almaty")
//specifying current time in your tz to be used only with the almaty tz attached
const almaty = dayjs.tz(new Date(), "Asia/Almaty")
almaty.timeZone //returns "Asia/Almaty"
almaty.tz.guess() //your local IANA
almaty.zoneAbbr() //+6 GMT
almaty.zoneName() //something like eastern standard time
almaty.utc() //will subtract 6 hours if there's no DST for utc time with almaty's tz
```

## Customize

You could build your own Day.js plugin to meet different needs.
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
"babel-jest": "^22.4.3",
"babel-plugin-external-helpers": "^6.22.0",
"cross-env": "^5.1.6",
"date-fns": "^2.0.0-alpha.27",
"date-fns-tz": "^1.0.7",
"eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.10.0",
Expand All @@ -86,6 +88,7 @@
"karma-sauce-launcher": "^1.1.0",
"mockdate": "^2.0.2",
"moment": "^2.22.0",
"moment-timezone": "^0.5.25",
"ncp": "^2.0.0",
"pre-commit": "^1.2.2",
"prettier": "^1.16.1",
Expand Down
153 changes: 141 additions & 12 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as C from './constant'
import U from './utils'
import en from './locale/en'
import polyfillLoader from './plugin/timezones/polyfillLoader'

let L = 'en' // global locale
const Ls = {} // global loaded locale
Expand All @@ -18,6 +19,9 @@ const parseLocale = (preset, object, isLocal) => {
if (object) {
Ls[preset] = object
l = preset
} else if (!Ls[preset]) {
l = preset // using intl api
Ls[preset] = { name: preset }
}
} else {
const { name } = preset
Expand All @@ -32,13 +36,26 @@ const dayjs = (date, c, pl) => {
if (isDayjs(date)) {
return date.clone()
}
// eslint-disable-next-line no-nested-ternary
const cfg = c ? (typeof c === 'string' ? { format: c, pl } : c) : {}
let cfg
if (c) {
if (typeof c === 'string') {
cfg = { format: c, pl }
} else {
cfg = c
}
} else {
cfg = {}
}
cfg.date = date
return new Dayjs(cfg) // eslint-disable-line no-use-before-define
}

const wrapper = (date, instance) => dayjs(date, { locale: instance.$L, utc: instance.$u })
const wrapper = (date, instance) => dayjs(date, {
locale: instance.$L,
utc: instance.$u,
timeZone: instance.timeZone,
tzOffsetModifier: instance.$tzOffsetModifier
})

const Utils = U // for plugin use
Utils.l = parseLocale
Expand Down Expand Up @@ -72,6 +89,9 @@ class Dayjs {

parse(cfg) {
this.$d = parseDate(cfg)
this.timeZone = cfg.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone
// how much offset changed after tz change
this.$tzOffsetModifier = cfg.tzOffsetModifier ? cfg.tzOffsetModifier : 0
this.init()
}

Expand Down Expand Up @@ -271,23 +291,127 @@ class Dayjs {
return this.add(number * -1, string)
}

format(formatStr) {
format(formatStr, options = {}) {
if (!this.isValid()) return C.INVALID_DATE_STRING

const str = formatStr || C.FORMAT_DEFAULT
const zoneStr = Utils.z(this)
const locale = this.$locale()
const { $H, $m, $M } = this
const { name } = locale
const isIntl = Object.keys(locale).length === 1
const str = formatStr || (!isIntl && C.FORMAT_DEFAULT) || ''
if (isIntl) {
polyfillLoader()
}
const zoneStr = Utils.z(this)
const {
weekdays, months, meridiem
} = locale
$H, $m, $M, $W, $ms
} = this
const getShort = (arr, index, full, length) => (
(arr && (arr[index] || arr(this, str))) || full[index].substr(0, length)
)
const get$H = num => (
Utils.s($H % 12 || 12, num, '0')
)

if (isIntl) { // using intl api from browser
const a = { // value abreviations
n: 'numeric',
t: '2-digit',
s: 'short',
l: 'long',
na: 'narrow'
}

const c = field => // create object for options
value => ({ [field]: value })

const h12 = c('hour12')
const wd = c('weekday')
const y = c('year')
const m = c('month')
const d = c('day')
const h = c('hour')
const mi = c('minute')
const s = c('second')
const matches = {
YY: y(a.t),
YYYY: y(a.n),
M: m(a.n),
MM: m(a.t),
MMM: m(a.s),
MMMM: m(a.l),
D: d(a.n),
DD: d(a.t),
d: wd(a.s),
dd: wd(a.s),
ddd: wd(a.s),
dddd: wd(a.l),
H: [h(a.n), h12(false)],
HH: [h(a.t), h12(false)],
h: [h(a.n), h12(true)],
hh: [h(a.t), h12(true)],
a: { a: true }, // td
A: { A: true }, // td
m: mi(a.n),
mm: mi(a.t),
s: s(a.n),
ss: s(a.t),
SSS: { SSS: true }, // td
Z: { Z: zoneStr },
ZZ: { ZZ: zoneStr.replace(':', '') }
}
const keys = {} // used to determine which options are passed
const opt = (str.match(C.REGEX_FORMAT) || []).reduce((ab, x) => {
if (Array.isArray(x)) { // h and H
const opts = x.reduce((az, xz) => Object.assign(az, matches[xz]), ab)
Object.assign(keys, opts)
return opts
}
Object.assign(keys, { [x]: true })
return Object.assign(ab, matches[x])
}, {})
Object.assign(opt, options)
let dateToUse = this.$d
if (options.timeZoneName) {
dateToUse = this.add(this.$tzOffsetModifier, 'minute').toDate()
Object.assign(opt, { timeZone: this.timeZone })
}
const stringified = Intl.DateTimeFormat(name, opt).formatToParts(dateToUse).map((x) => {
switch (x.type) {
case 'weekday':
if (keys.d) {
return String($W)
} else if (opt.dd) {
return x.value.substring(0, 2)
}
break
case 'dayPeriod':
if (keys.a) {
return x.value.toLocaleLowerCase(name)
} else if (keys.A) {
return x.value.toLocaleUpperCase(name)
}
break
case 'second':
if (keys.SSS) {
return Utils.s($ms, 3, '0')
}
break
default:
return x.value
}
return x.value
}).join('')
if (opt.Z) {
return stringified + opt.Z
} else if (opt.ZZ) {
return stringified + opt.ZZ
}
return stringified
}
// using locale files
const {
weekdays, months, meridiem
} = locale

const meridiemFunc = meridiem || ((hour, minute, isLowercase) => {
const m = (hour < 12 ? 'AM' : 'PM')
return isLowercase ? m.toLowerCase() : m
Expand Down Expand Up @@ -319,14 +443,13 @@ class Dayjs {
SSS: Utils.s(this.$ms, 3, '0'),
Z: zoneStr // 'ZZ' logic below
}

return str.replace(C.REGEX_FORMAT, (match, $1) => $1 || matches[match] || zoneStr.replace(':', '')) // 'ZZ'
}

utcOffset() {
// Because a bug at FF24, we're rounding the timezone offset around 15 minutes
// https://github.com/moment/moment/pull/1871
return -Math.round(this.$d.getTimezoneOffset() / 15) * 15
return -Math.round((this.$d.getTimezoneOffset() + this.$tzOffsetModifier) / 15) * 15
}

diff(input, units, float) {
Expand Down Expand Up @@ -354,6 +477,12 @@ class Dayjs {
return this.endOf(C.M).$D
}

utc() {
// const converted = this.tz('Etc/UTC')
const a = this.tz('Etc/UTC')
return a
}

$locale() { // get locale object
return Ls[this.$L]
}
Expand Down
Loading