Skip to content

Commit

Permalink
fix: Introduce a timeZone module allowing custom time zone data
Browse files Browse the repository at this point in the history
Load different data in different modules and reuse the custom module, which requests timezone-support with no data included.
  • Loading branch information
prantlf committed Nov 17, 2018
1 parent 1f79603 commit b778a61
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 134 deletions.
27 changes: 9 additions & 18 deletions build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ const configFactory = require('./rollup.config')
const { promisify } = util

const promisifyReadDir = promisify(fs.readdir)
const promisifyReadFile = promisify(fs.readFile)
const promisifyWriteFile = promisify(fs.writeFile)

const formatName = n => n.replace(/\.js/, '').replace('-', '_')

Expand All @@ -17,20 +15,6 @@ async function build(option) {
await bundle.write(option.output)
}

async function addLimitedTimeZonePluginVersions() {
const originalFile = path.join(__dirname, '../plugin/timeZone.js')
const originalContent = await promisifyReadFile(originalFile, { encoding: 'utf-8' })
const limitedVersions = ['1900-2050', '2012-2022']
for (let i = 0; i < limitedVersions.length; ++i) { // eslint-disable-line no-plusplus
const limitedVersion = limitedVersions[i]
const limitedFile = path.join(__dirname, `../plugin/timeZone-${limitedVersion}.js`)
const limitedContent = originalContent.replace('require("timezone-support")',
`require("timezone-support/dist/index-${limitedVersion}")`)
// eslint-disable-next-line no-await-in-loop
await promisifyWriteFile(limitedFile, limitedContent)
}
}

(async () => {
try {
const locales = await promisifyReadDir(path.join(__dirname, '../src/locale'))
Expand All @@ -51,12 +35,19 @@ async function addLimitedTimeZonePluginVersions() {
}))
})

const timeZoneVariants = ['custom', '1900-2050', '2012-2022']
timeZoneVariants.forEach((moduleName) => {
build(configFactory({
input: `./src/plugin/timeZone/${moduleName}`,
fileName: `./plugin/timeZone-${moduleName}.js`,
name: 'dayjs_plugin_timeZone'
}))
})

build(configFactory({
input: './src/index.js',
fileName: './dayjs.min.js'
}))

addLimitedTimeZonePluginVersions()
} catch (e) {
console.error(e) // eslint-disable-line no-console
}
Expand Down
9 changes: 7 additions & 2 deletions build/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ module.exports = (config) => {
input: {
input,
external: [
'dayjs-ext', 'fast-plural-rules', 'timezone-support'
'dayjs-ext', 'fast-plural-rules', 'timezone-support/dist/lookup-convert',
'timezone-support/dist/data', 'timezone-support/dist/data-1900-2050',
'timezone-support/dist/data-2012-2022'
],
plugins: [
babel({
Expand All @@ -23,7 +25,10 @@ module.exports = (config) => {
globals: {
'dayjs-ext': 'dayjs',
'fast-plural-rules': 'fastPluralRules',
'timezone-support': 'timezone-support'
'timezone-support/dist/lookup-convert': 'timezone-support',
'timezone-support/dist/data': 'timezone-data',
'timezone-support/dist/data-1900-2050': 'timezone-data-1900-2050',
'timezone-support/dist/data-2012-2022': 'timezone-data-2012-2022'
},
sourcemap: true
}
Expand Down
26 changes: 26 additions & 0 deletions docs/en/Plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,32 @@ Day.js uses an embedded `Date` object. This object supports only local time zone
* The time zone parameter in the constructor is meant only for converting the parsed input string correctly to UTC. The embedded `Date` object will be initialised with UTC and offer the local time zone representation as usual. The original time zone offset will not be remembered. It is usually not important, because dates should be rendered consistently in user's time zone; not in various time zones, which their string sources referred to.
* The time zone parameter in the `format` method will extract the date parts (year, month, ...) from the embedded `Date` object in UTC and convert them to the specified time zone, before producing the output string.

#### Package Size

The plugin includes all available time zone data in the main module `dayjs-ext/plugin/timeZone`. If you can afford limit the support for recent years only, you can reduce the size of your package, if you bundle `dayjs-ext` with other sources using rollup or webpack:

```txt
Full IANA TZ data: 923 KB minified, 33.3 KB gzipped
Data for 1900-2050: 200 KB minified, 23.3 KB gzipped
Data for 2012-2022: 27 KB minified, 6.5 KB gzipped
```

Modules with limited time zone data are exposed as `dayjs-ext/plugin/timeZone-1900-2050` and `dayjs-ext/plugin/timeZone-2012-2022`. A custom module with different time zone data can be used via `dayjs-ext/plugin/timeZone-custom`:

```js
import dayjs from 'dayjs-ext'
import timeZonePlugin from 'dayjs-ext/plugin/timeZone-custom'

import { populateTimeZones } from 'timezone-support/dist/lookup-convert'
import timeZoneData from './data-1970-2025'

populateTimeZones(timeZoneData)

dayjs.extend(timeZonePlugin)
```

When `dayjs-ext` is loaded in the browser as described below, custom data can be loaded by using the [interface of `timezone-support` for limited data loading](https://github.com/prantlf/timezone-support/blob/master/docs/usage.md#limit-the-loaded-time-zone-data).

#### Installation

This plugin has a dependency on the [`timezone-support`](https://www.npmjs.com/package/timezone-support) NPM module. If you are going to use it on a web page directly, add its script to your section of `<script>`s too, along with the `Day.js`, for example:
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
"plugin/relativeTime.js.map",
"plugin/timeZone.js",
"plugin/timeZone.js.map",
"plugin/timeZone-custom.js",
"plugin/timeZone-custom.js.map",
"plugin/timeZone-1900-2050.js",
"plugin/timeZone-1900-2050.js.map",
"plugin/timeZone-2012-2022.js",
Expand Down Expand Up @@ -156,7 +158,7 @@
},
"dependencies": {
"fast-plural-rules": "^0.0.1",
"timezone-support": "^1.6.1"
"timezone-support": "^1.7.0"
},
"devDependencies": {
"@babel/cli": "^7.1.5",
Expand Down
6 changes: 6 additions & 0 deletions src/plugin/timeZone/1900-2050.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { populateTimeZones } from 'timezone-support/dist/lookup-convert'
import timeZoneData from 'timezone-support/dist/data-1900-2050'

populateTimeZones(timeZoneData)

export { default } from './custom'
6 changes: 6 additions & 0 deletions src/plugin/timeZone/2012-2022.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { populateTimeZones } from 'timezone-support/dist/lookup-convert'
import timeZoneData from 'timezone-support/dist/data-2012-2022'

populateTimeZones(timeZoneData)

export { default } from './custom'
112 changes: 112 additions & 0 deletions src/plugin/timeZone/custom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { findTimeZone, setTimeZone, getZonedTime } from 'timezone-support/dist/lookup-convert'
import { FORMAT_DEFAULT } from '../../constant'

function updateTime(instance, {
year, month, day, dayOfWeek, hours, minutes, epoch, zone
}, convertTimeZone) {
const date = instance.$d
// Update the Date object with the epoch time from the date
// after applying the specified time zone on it.
date.setTime(epoch)
if (convertTimeZone) {
// If the date was converted to a specified time zone for formatting
// purposes, replace date parts from the zoned time object.
instance.$Y = year
instance.$M = month - 1
instance.$D = day
instance.$W = dayOfWeek
instance.$H = hours
instance.$m = minutes
const { abbreviation, offset } = zone
instance.$z = abbreviation
instance.$o = offset
} else {
// If the time zone was applied to a zone-less input string only
// to convert it to UTC, update date parts in the local time zone.
instance.$Y = date.getFullYear()
instance.$M = date.getMonth()
instance.$D = date.getDate()
instance.$W = date.getDay()
instance.$H = date.getHours()
instance.$m = date.getMinutes()
}
}

function padToTwoDigits(number) {
return number > 9 ? number : `0${number}`
}

function formatTimeZoneOffset(offset, separator) {
let sign
if (offset <= 0) {
offset = -offset
sign = '+'
} else {
sign = '-'
}
const hours = padToTwoDigits(Math.floor(offset / 60))
const minutes = padToTwoDigits(offset % 60)
return sign + hours + separator + minutes
}

function formatTimeZoneTokens(instance, format) {
const str = format || FORMAT_DEFAULT
return str.replace(/z|ZZ|Z/g, (match) => {
switch (match) {
case 'z':
return `[${instance.$z}]`
case 'Z':
return formatTimeZoneOffset(instance.$o, ':')
default: // 'ZZ'
return formatTimeZoneOffset(instance.$o, '')
}
})
}

export default (o, C) => {
const proto = C.prototype
const oldParse = proto.parse
const oldFormat = proto.format
proto.parse = function (cfg) {
oldParse.call(this, cfg)
const { timeZone: timeZoneName, convertTimeZone } = cfg
if (timeZoneName) {
const date = this.$d
try {
const timeZone = findTimeZone(timeZoneName)
const zonedTime = convertTimeZone
// Temporary object created for formatting purposes receives
// a date in UTC and needs to convert it to the specified TZ.
? getZonedTime(date, timeZone)
// Input string without time zone passed to the constructor
// needs to get the specified TZ assigned without conversion.
: setTimeZone(date, timeZone, { useUTC: false })
updateTime(this, zonedTime, convertTimeZone)
} catch (error) {
date.setTime(Number.NaN)
this.init(cfg)
}
}
}
proto.format = function (format, options = {}) {
if (typeof format === 'object' && !(format instanceof String)) {
options = format
format = undefined
}
const { timeZone } = options
let date
if (timeZone) {
// Run the format on a temporary instance, which will use
// the date converted to the specified time zone.
date = new C({
date: this.$d.valueOf(), locale: this.$L, timeZone, convertTimeZone: true
})
// Replace tokens supported by this plugin; the rest will
// be replaced by the original method.
format = formatTimeZoneTokens(date, format)
} else {
date = this
}
return oldFormat.call(date, format)
}
}
114 changes: 4 additions & 110 deletions src/plugin/timeZone/index.js
Original file line number Diff line number Diff line change
@@ -1,112 +1,6 @@
import { findTimeZone, setTimeZone, getZonedTime } from 'timezone-support'
import { FORMAT_DEFAULT } from '../../constant'
import { populateTimeZones } from 'timezone-support/dist/lookup-convert'
import timeZoneData from 'timezone-support/dist/data'

function updateTime(instance, {
year, month, day, dayOfWeek, hours, minutes, epoch, zone
}, convertTimeZone) {
const date = instance.$d
// Update the Date object with the epoch time from the date
// after applying the specified time zone on it.
date.setTime(epoch)
if (convertTimeZone) {
// If the date was converted to a specified time zone for formatting
// purposes, replace date parts from the zoned time object.
instance.$Y = year
instance.$M = month - 1
instance.$D = day
instance.$W = dayOfWeek
instance.$H = hours
instance.$m = minutes
const { abbreviation, offset } = zone
instance.$z = abbreviation
instance.$o = offset
} else {
// If the time zone was applied to a zone-less input string only
// to convert it to UTC, update date parts in the local time zone.
instance.$Y = date.getFullYear()
instance.$M = date.getMonth()
instance.$D = date.getDate()
instance.$W = date.getDay()
instance.$H = date.getHours()
instance.$m = date.getMinutes()
}
}
populateTimeZones(timeZoneData)

function padToTwoDigits(number) {
return number > 9 ? number : `0${number}`
}

function formatTimeZoneOffset(offset, separator) {
let sign
if (offset <= 0) {
offset = -offset
sign = '+'
} else {
sign = '-'
}
const hours = padToTwoDigits(Math.floor(offset / 60))
const minutes = padToTwoDigits(offset % 60)
return sign + hours + separator + minutes
}

function formatTimeZoneTokens(instance, format) {
const str = format || FORMAT_DEFAULT
return str.replace(/z|ZZ|Z/g, (match) => {
switch (match) {
case 'z':
return `[${instance.$z}]`
case 'Z':
return formatTimeZoneOffset(instance.$o, ':')
default: // 'ZZ'
return formatTimeZoneOffset(instance.$o, '')
}
})
}

export default (o, C) => {
const proto = C.prototype
const oldParse = proto.parse
const oldFormat = proto.format
proto.parse = function (cfg) {
oldParse.call(this, cfg)
const { timeZone: timeZoneName, convertTimeZone } = cfg
if (timeZoneName) {
const date = this.$d
try {
const timeZone = findTimeZone(timeZoneName)
const zonedTime = convertTimeZone
// Temporary object created for formatting purposes receives
// a date in UTC and needs to convert it to the specified TZ.
? getZonedTime(date, timeZone)
// Input string without time zone passed to the constructor
// needs to get the specified TZ assigned without conversion.
: setTimeZone(date, timeZone, { useUTC: false })
updateTime(this, zonedTime, convertTimeZone)
} catch (error) {
date.setTime(Number.NaN)
this.init(cfg)
}
}
}
proto.format = function (format, options = {}) {
if (typeof format === 'object' && !(format instanceof String)) {
options = format
format = undefined
}
const { timeZone } = options
let date
if (timeZone) {
// Run the format on a temporary instance, which will use
// the date converted to the specified time zone.
date = new C({
date: this.$d.valueOf(), locale: this.$L, timeZone, convertTimeZone: true
})
// Replace tokens supported by this plugin; the rest will
// be replaced by the original method.
format = formatTimeZoneTokens(date, format)
} else {
date = this
}
return oldFormat.call(date, format)
}
}
export { default } from './custom'

0 comments on commit b778a61

Please sign in to comment.