Skip to content

Commit

Permalink
Introduce an UTC mode
Browse files Browse the repository at this point in the history
Additions:

* Constructor accepts a boolean flag to enable the UTC mode: { utc: true }.
* Method utc() returns a clone of the instance in the UTC mode.
* Method local() returns a clone of the instance in the non-UTC (local) mode.
* Method isUTC() checks, if the instance is in the UITC mode.
* Method utcOffset() returns the time zone offset to UTC consistently with Date.prototype.getTimezoneOffset()

Differences to the "normal" (local) dayjs mode:

* Assume UTC as the time zone of the string passed to the constructor - for example, "2018-10-28" will be parsed as "2018-10-28T00:00:00Z". *But only if the time zone is omitted.* If you end the string tith a TZ offset - "2018-10-28T00:00:00+02:00", it will be retained and the whole date will be converted to UTC, when iniitalizing the datejs object.
* Methods returning the date parts like year(), month() etc. return UTC parts using Date.propertotype.getUTC* methods.
* Methods manipulating the date - set() - work with the UTC values of the date parts, as the getters above.
* The format() method always formats the time zone as "+00:00".
* The utcOffset() method always returns zero.
  • Loading branch information
prantlf committed Sep 9, 2018
1 parent b14bdd1 commit 16581d7
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 34 deletions.
91 changes: 91 additions & 0 deletions docs/en/API-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ The `Dayjs` object is immutable, that is, all API operations that change the `Da
- [Is Same `.isSame(compared: Dayjs)`](#is-same-issamecompared-dayjs)
- [Is After `.isAfter(compared: Dayjs)`](#is-after-isaftercompared-dayjs)
- [Is a Dayjs `.isDayjs()`](#is-a-dayjs-isdayjscompared-any)
- [UTC Mode](#utc-mode)
- [Date-Only Mode`](#date-only-mode)
- [Is UTC `.isUTC()`](#is-utc-isutc)
- [To UTC `.utc() | dayjs(original: Dayjs, { utc: true })`](#to-utc-utcdayjsoriginal-dayjs-utc-true)
- [To Local `.local() | dayjs(original: Dayjs, { utc: false })`](#to-local-localdayjsoriginal-dayjs-utc-true)
- [UTC Offset `.utcOffset()`](#utc-offset-utcoffset)
- [Plugin APIs](#plugin-apis)
- [RelativeTime](#relativetime)
- [IsLeapYear](#isleapyear)
Expand Down Expand Up @@ -403,6 +409,91 @@ dayjs.isDayjs(dayjs()); // true
dayjs.isDayjs(new Date()); // false
```

## UTC

`Day.js` instances can be initialized using a string with any time zone offset. However, the native JavaScript `Date` object used internally works only in the local time zone with the support of accessing the object value in UTC too. If no time zone is specified, the local time zone is assumed. `Day.js` follows the same principle.

`Day.js` instances can be initialized in the *UTC mode*, which makes getters, setters and formatting methods use the UTC properties of the `Date` object. It can be useful to:

* Initialize and use `Day.js` instances always in UTC, without specifying the time zone explicitly.
* Initialize and use `Day.js` instances in a *date-only mode*.

### Date-only Mode

Sometimes only the day is important; not the time. If a `Day.js` instance or a `Date` object itself is initialized with the date part only, the constructor assumes a full UTC date and automatically adds the time part:

```js
const date = dayjs('2018-09-07')
new Date('2018-09-07')
// Both assume an input "2018-09-07T00:00:00Z".
// Both initialize the date to "2018-09-07 02:00:00 +02:00" in Central Europe.
const day = date.date() // Returns 7, OK.
const day = date.hour() // Returns 2, but well, we do not use the time part.
```

Because the input is assumed in UTC, when converting to the local time zone, which is used by default in both `Day.js` instances and `Date` objects, a time zone conversion will be performed. It can change the value of the day, if the time zone offset from UTC is negative, which may be a problem:

```js
const date = dayjs('2018-09-07')
new Date('2018-09-07')
// Both assume an input "2018-09-07T00:00:00Z".
// Both initialize the date to "2018-09-06 18:00:00 -06:00" in North-East America.
const day = date.date() // Returns 6, a bad surprise.
const day = date.hour() // Returns 18, but well, we do not use the time part.
```

Switching the `Day.js` instance to the UTC mode will not change the initialization or value of the internal `Date` object, so that any date computations and comparisons with other `Day.js` instances will be correct. However, getters, setters and formatting methods will use date parts in UTC, making at appear, that no conversion to the local time zone took place.

```js
const date = dayjs('2018-09-07', { utc: true })
const date = dayjs('2018-09-07').utc()
// Both assume an input "2018-09-07T00:00:00Z".
// Both initialize the date to "2018-09-06 18:00:00 -06:00" in North-East America.
// Both remember to use UTC date parts on the interface of Day.js.
const day = date.date() // Returns 7, OK.
const day = date.hour() // Returns 0, OK; well, we do not use the time part.
```

### Is UTC `.isUTC()`

Returns a `boolean` indicating whether the `Dayjs` instance works in the UTC mode or not.

```js
dayjs().isUTC(); // Returns false
dayjs().utc().isUTC(); // Returns true
```

### To UTC `.utc() | dayjs(original: Dayjs, { utc: true })`

Returns a cloned `Dayjs` instance in the UTC mode. The UTC mode will be retained, when cloning the `Day.js` instance further, unless `utc: false` is specified.

```js
dayjs().utc();
dayjs('2019-01-25', { utc: true });
dayjs().utc().clone(); // continues in the UTC mode
dayjs(dayjs('2019-01-25', { utc: true })); // continues in the UTC mode
```

### To Local `.local() | dayjs(original: Dayjs, { utc: false })`

Returns a cloned `Dayjs` instance in the local time zone mode. The local time zone mode will be retained, when cloning the `Day.js` instance further, unless `utc: true` is specified. It is also the default mode of constructing a new `Day.js` instance.

```js
dayjs('2019-01-25', { utc: true }).local();
dayjs('2019-01-25 15:43', { utc: false }); // default, not necessary
dayjs('2019-01-25', { utc: true }).local().clone(); // continues in the local mode
dayjs(dayjs().utc().local()); // continues in the local mode
```

### UTC Offset `.utcOffset()`

Returns an offset of the `Dayjs`'s instance to UTC in minutes. It is a negative offset like `Date.prototype.getTimezoneOffset` returns it. If it is *added* to a zoned time, a time in UTC will be obtained.

```js
const date = dayjs('2019-01-25 15:43');
const offset = date.utcOffset() // Returns -60 in Central Europe
```

## Plugin APIs

### RelativeTime
Expand Down
10 changes: 9 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ declare function dayjs (config?: dayjs.ConfigType, option?: dayjs.OptionType): d
declare namespace dayjs {
export type ConfigType = string | number | Date | Dayjs

export type OptionType = { locale: string }
export type OptionType = { locale: string, utc: boolean }

export type UnitType = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | 'date'

Expand Down Expand Up @@ -83,6 +83,14 @@ declare namespace dayjs {
isLeapYear(): boolean

locale(arg1: any, arg2?: any): Dayjs

utc(): Dayjs

local(): Dayjs

isUTC(): boolean

utcOffset(): number
}

export type PluginFunc = (option: ConfigType, d1: Dayjs, d2: Dayjs) => void
Expand Down
126 changes: 93 additions & 33 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ const dayjs = (date, c) => {
return new Dayjs(cfg) // eslint-disable-line no-use-before-define
}

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

const Utils = U // for plugin use
Utils.parseLocale = parseLocale
Utils.isDayjs = isDayjs
Utils.wrapper = wrapper

const parseDate = (date) => {
const parseDate = (cfg) => {
const { date } = cfg
let reg
if (date === null) return new Date(NaN) // Treat null as an invalid date
if (Utils.isUndefined(date)) return new Date()
Expand All @@ -53,10 +54,16 @@ const parseDate = (date) => {
&& (/.*[^Z]$/i.test(date)) // looking for a better way
&& (reg = date.match(C.REGEX_PARSE))) {
// 2018-08-08 or 20180808
return new Date(
reg[1], reg[2] - 1, reg[3] || 1,
reg[5] || 0, reg[6] || 0, reg[7] || 0, reg[8] || 0
)
const year = reg[1]
const month = reg[2] - 1
const day = reg[3] || 1
const hours = reg[5] || 0
const minutes = reg[6] || 0
const seconds = reg[7] || 0
const milliseconds = reg[8] || 0
return cfg && cfg.utc
? new Date(Date.UTC(year, month, day, hours, minutes, seconds, milliseconds))
: new Date(year, month, day, hours, minutes, seconds, milliseconds)
}
return new Date(date) // timestamp
}
Expand All @@ -67,19 +74,33 @@ class Dayjs {
}

parse(cfg) {
this.$d = parseDate(cfg.date)
this.$d = parseDate(cfg)
this.init(cfg)
}

init(cfg) {
this.$y = this.$d.getFullYear()
this.$M = this.$d.getMonth()
this.$D = this.$d.getDate()
this.$W = this.$d.getDay()
this.$H = this.$d.getHours()
this.$m = this.$d.getMinutes()
this.$s = this.$d.getSeconds()
this.$ms = this.$d.getMilliseconds()
const utc = cfg && !!cfg.utc
const date = this.$d
this.$u = utc
if (utc) {
this.$y = date.getUTCFullYear()
this.$M = date.getUTCMonth()
this.$D = date.getUTCDate()
this.$W = date.getUTCDay()
this.$H = date.getUTCHours()
this.$m = date.getUTCMinutes()
this.$s = date.getUTCSeconds()
this.$ms = date.getUTCMilliseconds()
} else {
this.$y = date.getFullYear()
this.$M = date.getMonth()
this.$D = date.getDate()
this.$W = date.getDay()
this.$H = date.getHours()
this.$m = date.getMinutes()
this.$s = date.getSeconds()
this.$ms = date.getMilliseconds()
}
this.$L = this.$L || parseLocale(cfg.locale, null, true) || L
}

Expand Down Expand Up @@ -152,14 +173,20 @@ class Dayjs {
startOf(units, startOf) { // startOf -> endOf
const isStartOf = !Utils.isUndefined(startOf) ? startOf : true
const unit = Utils.prettyUnit(units)
const setMethods = this.$u
? ['setUTCHours', 'setUTCMinutes', 'setUTCSeconds', 'setUTCMilliseconds']
: ['setHours', 'setMinutes', 'setSeconds', 'setMilliseconds']
const instanceFactory = (d, m) => {
const ins = wrapper(new Date(this.$y, m, d), this)
const date = this.$u
? new Date(Date.UTC(this.$y, m, d))
: new Date(this.$y, m, d)
const ins = wrapper(date, this)
return isStartOf ? ins : ins.endOf(C.D)
}
const instanceFactorySet = (method, slice) => {
const instanceFactorySet = (slice) => {
const argumentStart = [0, 0, 0, 0]
const argumentEnd = [23, 59, 59, 999]
return wrapper(this.toDate()[method].apply( // eslint-disable-line prefer-spread
return wrapper(this.toDate()[setMethods[slice]].apply( // eslint-disable-line prefer-spread
this.toDate(),
isStartOf ? argumentStart.slice(slice) : argumentEnd.slice(slice)
), this)
Expand All @@ -176,13 +203,13 @@ class Dayjs {
instanceFactory(this.$D + (6 - this.$W), this.$M)
case C.D:
case C.DATE:
return instanceFactorySet('setHours', 0)
return instanceFactorySet(0)
case C.H:
return instanceFactorySet('setMinutes', 1)
return instanceFactorySet(1)
case C.MIN:
return instanceFactorySet('setSeconds', 2)
return instanceFactorySet(2)
case C.S:
return instanceFactorySet('setMilliseconds', 3)
return instanceFactorySet(3)
default:
return this.clone()
}
Expand All @@ -194,30 +221,49 @@ class Dayjs {

$set(units, int) { // private set
const unit = Utils.prettyUnit(units)
const setMethods = this.$u
? {
year: 'setUTCFullYear',
month: 'setUTCMonth',
date: 'setUTCDate',
hours: 'setUTCHours',
minutes: 'setUTCMinutes',
seconds: 'setUTCSeconds',
milliseconds: 'setUTCMilliseconds'
}
: {
year: 'setFullYear',
month: 'setMonth',
date: 'setDate',
hours: 'setHours',
minutes: 'setMinutes',
seconds: 'setSeconds',
milliseconds: 'setMilliseconds'
}
switch (unit) {
case C.D:
this.$d.setDate(this.$D + (int - this.$W))
this.$d[setMethods.date](this.$D + (int - this.$W))
break
case C.DATE:
this.$d.setDate(int)
this.$d[setMethods.date](int)
break
case C.M:
this.$d.setMonth(int)
this.$d[setMethods.month](int)
break
case C.Y:
this.$d.setFullYear(int)
this.$d[setMethods.year](int)
break
case C.H:
this.$d.setHours(int)
this.$d[setMethods.hours](int)
break
case C.MIN:
this.$d.setMinutes(int)
this.$d[setMethods.minutes](int)
break
case C.S:
this.$d.setSeconds(int)
this.$d[setMethods.seconds](int)
break
case C.MS:
this.$d.setMilliseconds(int)
this.$d[setMethods.milliseconds](int)
break
default:
break
Expand All @@ -226,7 +272,6 @@ class Dayjs {
return this
}


set(string, int) {
return this.clone().$set(string, int)
}
Expand Down Expand Up @@ -272,10 +317,9 @@ class Dayjs {
return this.add(number * -1, string)
}


format(formatStr) {
const str = formatStr || C.FORMAT_DEFAULT
const zoneStr = Utils.padZoneStr(this.$d.getTimezoneOffset())
const zoneStr = this.$u ? '+00:00' : Utils.padZoneStr(this.$d.getTimezoneOffset())
const locale = this.$locale()
const {
weekdays, months
Expand Down Expand Up @@ -435,6 +479,22 @@ class Dayjs {
toString() {
return this.$d.toUTCString()
}

isUTC() {
return this.$u
}

utc() {
return dayjs(this.$d.valueOf(), { locale: this.$L, utc: true })
}

local() {
return dayjs(this.$d.valueOf(), { locale: this.$L, utc: false })
}

utcOffset() {
return this.$u ? 0 : this.$d.getTimezoneOffset()
}
}

dayjs.extend = (plugin, option) => {
Expand Down
5 changes: 5 additions & 0 deletions test/display.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ it('Format Escaping characters', () => {
expect(dayjs().format(string)).toBe(moment().format(string))
})

it('Formats an UTC instance to UTC time zone', () => {
const instance = dayjs('2018-09-06T19:34:28Z', { utc: true })
expect(instance.format()).toEqual('2018-09-06T19:34:28+00:00')
})

describe('Difference', () => {
it('empty -> default milliseconds', () => {
const dateString = '20110101'
Expand Down
Loading

0 comments on commit 16581d7

Please sign in to comment.