Repository: iamkun/dayjs (JavaScript)

Issue:
Unix timestamp is incorrect?
`if (new Date().getTimezoneOffset() === 0)` then `dayjs.unix(1550251173).startOf('day').unix()` returns `1550188800`.

`if (new Date().getTimezoneOffset() === 300)` then `dayjs.unix(1550251173).startOf('day').unix()` returns `1550206800`.

A Unix timestamp is by definition a count of seconds from "00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC)" (http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_16). Thus, the correct result should _always_ be `1550188800`.


Hints:
Because in `dayjs.unix(1550251173)`, dayjs parses and displays in local time. The same in momentjs.
Yes, that should parse into local time. But the final conversion _back_ to a Unix timestamp should generate a number at UTC. 
Yes, and that will be of course different. Cause `startOf('day')` is different.
What do you not understand? The number that results from `dayjsInstance.unix()` should be the number of seconds from 00:00:00 Thursday, 1 January 1970 _at the UTC timezone_.
I'm closing this until I can figure out where the actual bug lies. I'm not really sure what is happening here.

Patch:
diff --git a/src/index.js b/src/index.js
--- a/src/index.js
+++ b/src/index.js
@@ -38,20 +38,25 @@ const dayjs = (date, c, pl) => {
   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
+Utils.l = parseLocale
+Utils.i = isDayjs
+Utils.w = wrapper
 
-const parseDate = (date) => {
+const parseDate = (cfg) => {
+  const { date, utc } = cfg
   if (date === null) return new Date(NaN) // null is invalid
-  if (Utils.isUndefined(date)) return new Date() // today
-  if (date instanceof Date) return date
+  if (Utils.u(date)) return new Date() // today
+  if (date instanceof Date) return new Date(date)
   if (typeof date === 'string' && !/Z$/i.test(date)) {
     const d = date.match(C.REGEX_PARSE)
     if (d) {
+      if (utc) {
+        return new Date(Date.UTC(d[1], d[2] - 1, d[3]
+          || 1, d[4] || 0, d[5] || 0, d[6] || 0, d[7] || 0))
+      }
       return new Date(d[1], d[2] - 1, d[3] || 1, d[4] || 0, d[5] || 0, d[6] || 0, d[7] || 0)
     }
   }
@@ -66,7 +71,7 @@ class Dayjs {
   }
 
   parse(cfg) {
-    this.$d = parseDate(cfg.date)
+    this.$d = parseDate(cfg)
     this.init()
   }
 
@@ -146,21 +151,23 @@ class Dayjs {
   }
 
   startOf(units, startOf) { // startOf -> endOf
-    const isStartOf = !Utils.isUndefined(startOf) ? startOf : true
-    const unit = Utils.prettyUnit(units)
+    const isStartOf = !Utils.u(startOf) ? startOf : true
+    const unit = Utils.p(units)
     const instanceFactory = (d, m) => {
-      const ins = Utils.wrapper(new Date(this.$y, m, d), this)
+      const ins = Utils.w(this.$u ?
+        Date.UTC(this.$y, m, d) : new Date(this.$y, m, d), this)
       return isStartOf ? ins : ins.endOf(C.D)
     }
     const instanceFactorySet = (method, slice) => {
       const argumentStart = [0, 0, 0, 0]
       const argumentEnd = [23, 59, 59, 999]
-      return Utils.wrapper(this.toDate()[method].apply( // eslint-disable-line prefer-spread
+      return Utils.w(this.toDate()[method].apply( // eslint-disable-line prefer-spread
         this.toDate(),
         (isStartOf ? argumentStart : argumentEnd).slice(slice)
       ), this)
     }
     const { $W, $M, $D } = this
+    const utcPad = `set${this.$u ? 'UTC' : ''}`
     switch (unit) {
       case C.Y:
         return isStartOf ? instanceFactory(1, 0) :
@@ -175,13 +182,13 @@ class Dayjs {
       }
       case C.D:
       case C.DATE:
-        return instanceFactorySet('setHours', 0)
+        return instanceFactorySet(`${utcPad}Hours`, 0)
       case C.H:
-        return instanceFactorySet('setMinutes', 1)
+        return instanceFactorySet(`${utcPad}Minutes`, 1)
       case C.MIN:
-        return instanceFactorySet('setSeconds', 2)
+        return instanceFactorySet(`${utcPad}Seconds`, 2)
       case C.S:
-        return instanceFactorySet('setMilliseconds', 3)
+        return instanceFactorySet(`${utcPad}Milliseconds`, 3)
       default:
         return this.clone()
     }
@@ -192,16 +199,17 @@ class Dayjs {
   }
 
   $set(units, int) { // private set
-    const unit = Utils.prettyUnit(units)
+    const unit = Utils.p(units)
+    const utcPad = `set${this.$u ? 'UTC' : ''}`
     const name = {
-      [C.D]: 'setDate',
-      [C.DATE]: 'setDate',
-      [C.M]: 'setMonth',
-      [C.Y]: 'setFullYear',
-      [C.H]: 'setHours',
-      [C.MIN]: 'setMinutes',
-      [C.S]: 'setSeconds',
-      [C.MS]: 'setMilliseconds'
+      [C.D]: `${utcPad}Date`,
+      [C.DATE]: `${utcPad}Date`,
+      [C.M]: `${utcPad}Month`,
+      [C.Y]: `${utcPad}FullYear`,
+      [C.H]: `${utcPad}Hours`,
+      [C.MIN]: `${utcPad}Minutes`,
+      [C.S]: `${utcPad}Seconds`,
+      [C.MS]: `${utcPad}Milliseconds`
     }[unit]
     const arg = unit === C.D ? this.$D + (int - this.$W) : int
 
@@ -217,7 +225,7 @@ class Dayjs {
 
   add(number, units) {
     number = Number(number) // eslint-disable-line no-param-reassign
-    const unit = Utils.prettyUnit(units)
+    const unit = Utils.p(units)
     const instanceFactory = (u, n) => {
       const date = this.set(C.DATE, 1).set(u, n + number)
       return date.set(C.DATE, Math.min(this.$D, date.daysInMonth()))
@@ -225,7 +233,7 @@ class Dayjs {
     const instanceFactorySet = (n) => {
       const date = new Date(this.$d)
       date.setDate(date.getDate() + (n * number))
-      return Utils.wrapper(date, this)
+      return Utils.w(date, this)
     }
     if (unit === C.M) {
       return instanceFactory(C.M, this.$M)
@@ -246,7 +254,7 @@ class Dayjs {
     }[unit] || 1 // ms
 
     const nextTimeStamp = this.valueOf() + (number * step)
-    return Utils.wrapper(nextTimeStamp, this)
+    return Utils.w(nextTimeStamp, this)
   }
 
   subtract(number, string) {
@@ -257,7 +265,7 @@ class Dayjs {
     if (!this.isValid()) return C.INVALID_DATE_STRING
 
     const str = formatStr || C.FORMAT_DEFAULT
-    const zoneStr = Utils.padZoneStr(this.$d.getTimezoneOffset())
+    const zoneStr = Utils.z(this)
     const locale = this.$locale()
     const {
       weekdays, months
@@ -266,34 +274,34 @@ class Dayjs {
       (arr && arr[index]) || full[index].substr(0, length)
     )
     const get$H = num => (
-      Utils.padStart(this.$H % 12 || 12, num, '0')
+      Utils.s(this.$H % 12 || 12, num, '0')
     )
 
     const matches = {
       YY: String(this.$y).slice(-2),
       YYYY: String(this.$y),
       M: String(this.$M + 1),
-      MM: Utils.padStart(this.$M + 1, 2, '0'),
+      MM: Utils.s(this.$M + 1, 2, '0'),
       MMM: getShort(locale.monthsShort, this.$M, months, 3),
       MMMM: months[this.$M],
       D: String(this.$D),
-      DD: Utils.padStart(this.$D, 2, '0'),
+      DD: Utils.s(this.$D, 2, '0'),
       d: String(this.$W),
       dd: getShort(locale.weekdaysMin, this.$W, weekdays, 2),
       ddd: getShort(locale.weekdaysShort, this.$W, weekdays, 3),
       dddd: weekdays[this.$W],
       H: String(this.$H),
-      HH: Utils.padStart(this.$H, 2, '0'),
+      HH: Utils.s(this.$H, 2, '0'),
       h: get$H(1),
       hh: get$H(2),
       a: this.$H < 12 ? 'am' : 'pm',
       A: this.$H < 12 ? 'AM' : 'PM',
       m: String(this.$m),
-      mm: Utils.padStart(this.$m, 2, '0'),
+      mm: Utils.s(this.$m, 2, '0'),
       s: String(this.$s),
-      ss: Utils.padStart(this.$s, 2, '0'),
-      SSS: Utils.padStart(this.$ms, 3, '0'),
-      Z: zoneStr
+      ss: Utils.s(this.$s, 2, '0'),
+      SSS: Utils.s(this.$ms, 3, '0'),
+      Z: zoneStr // 'ZZ' logic below
     }
 
     return str.replace(C.REGEX_FORMAT, (match) => {
@@ -309,11 +317,11 @@ class Dayjs {
   }
 
   diff(input, units, float) {
-    const unit = Utils.prettyUnit(units)
+    const unit = Utils.p(units)
     const that = dayjs(input)
     const zoneDelta = (that.utcOffset() - this.utcOffset()) * C.MILLISECONDS_A_MINUTE
     const diff = this - that
-    let result = Utils.monthDiff(this, that)
+    let result = Utils.m(this, that)
 
     result = {
       [C.Y]: result / 12,
@@ -326,7 +334,7 @@ class Dayjs {
       [C.S]: diff / C.MILLISECONDS_A_SECOND
     }[unit] || diff // milliseconds
 
-    return float ? result : Utils.absFloor(result)
+    return float ? result : Utils.a(result)
   }
 
   daysInMonth() {
@@ -344,7 +352,7 @@ class Dayjs {
   }
 
   clone() {
-    return Utils.wrapper(this.toDate(), this)
+    return Utils.w(this.toDate(), this)
   }
 
   toDate() {
diff --git a/src/plugin/advancedFormat/index.js b/src/plugin/advancedFormat/index.js
--- a/src/plugin/advancedFormat/index.js
+++ b/src/plugin/advancedFormat/index.js
@@ -22,7 +22,7 @@ export default (o, c, d) => { // locale needed later
         }
         case 'k':
         case 'kk':
-          return utils.padStart(String(this.$H === 0 ? 24 : this.$H), match === 'k' ? 1 : 2, '0')
+          return utils.s(String(this.$H === 0 ? 24 : this.$H), match === 'k' ? 1 : 2, '0')
         case 'X':
           return Math.floor(this.$d.getTime() / 1000)
         default: // 'x'
diff --git a/src/plugin/buddhistEra/index.js b/src/plugin/buddhistEra/index.js
--- a/src/plugin/buddhistEra/index.js
+++ b/src/plugin/buddhistEra/index.js
@@ -6,12 +6,11 @@ export default (o, c) => { // locale needed later
   // extend en locale here
   proto.format = function (formatStr) {
     const yearBias = 543
-    const { padStart } = this.$utils()
     const str = formatStr || FORMAT_DEFAULT
     const result = str.replace(/BBBB|BB/g, (match) => {
       const year = String(this.$y + yearBias)
       const args = match === 'BB' ? [year.slice(-2), 2] : [year, 4]
-      return padStart(...args, '0')
+      return this.$utils().s(...args, '0')
     })
     return oldFormat.bind(this)(result)
   }
diff --git a/src/plugin/customParseFormat/index.js b/src/plugin/customParseFormat/index.js
--- a/src/plugin/customParseFormat/index.js
+++ b/src/plugin/customParseFormat/index.js
@@ -135,7 +135,7 @@ function makeParser(format) {
   }
 }
 
-const parseFormattedInput = (input, format) => {
+const parseFormattedInput = (input, format, utc) => {
   try {
     const parser = makeParser(format)
     const {
@@ -149,10 +149,17 @@ const parseFormattedInput = (input, format) => {
       ) + (zone.offset * 60 * 1000))
     }
     const now = new Date()
-    return new Date(
-      year || now.getFullYear(), month > 0 ? month - 1 : now.getMonth(), day || now.getDate(),
-      hours || 0, minutes || 0, seconds || 0, milliseconds || 0
-    )
+    const y = year || now.getFullYear()
+    const M = month > 0 ? month - 1 : now.getMonth()
+    const d = day || now.getDate()
+    const h = hours || 0
+    const m = minutes || 0
+    const s = seconds || 0
+    const ms = milliseconds || 0
+    if (utc) {
+      return new Date(Date.UTC(y, M, d, h, m, s, ms))
+    }
+    return new Date(y, M, d, h, m, s, ms)
   } catch (e) {
     return new Date('') // Invalid Date
   }
@@ -163,10 +170,16 @@ export default (o, C, d) => {
   const proto = C.prototype
   const oldParse = proto.parse
   proto.parse = function (cfg) {
-    const { date: input, format, pl } = cfg
+    const {
+      date,
+      format,
+      pl,
+      utc
+    } = cfg
+    this.$u = utc
     if (format) {
       locale = pl ? d.Ls[pl] : this.$locale()
-      this.$d = parseFormattedInput(input, format)
+      this.$d = parseFormattedInput(date, format, utc)
       this.init(cfg)
     } else {
       oldParse.call(this, cfg)
diff --git a/src/plugin/utc/index.js b/src/plugin/utc/index.js
new file mode 100644
--- /dev/null
+++ b/src/plugin/utc/index.js
@@ -0,0 +1,59 @@
+export default (option, Dayjs, dayjs) => {
+  const proto = Dayjs.prototype
+  dayjs.utc = function (date, format) {
+    const cfg = { date, utc: true, format }
+    return new Dayjs(cfg) // eslint-disable-line no-use-before-define
+  }
+
+  proto.utc = function () {
+    return dayjs(this.toDate(), { locale: this.$L, utc: true })
+  }
+
+  proto.local = function () {
+    return dayjs(this.toDate(), { locale: this.$L, utc: false })
+  }
+
+  const oldParse = proto.parse
+  proto.parse = function (cfg) {
+    if (cfg.utc) {
+      this.$u = true
+    }
+    oldParse.call(this, cfg)
+  }
+
+  const oldInit = proto.init
+  proto.init = function () {
+    if (this.$u) {
+      const { $d } = this
+      this.$y = $d.getUTCFullYear()
+      this.$M = $d.getUTCMonth()
+      this.$D = $d.getUTCDate()
+      this.$W = $d.getUTCDay()
+      this.$H = $d.getUTCHours()
+      this.$m = $d.getUTCMinutes()
+      this.$s = $d.getUTCSeconds()
+      this.$ms = $d.getUTCMilliseconds()
+    } else {
+      oldInit.call(this)
+    }
+  }
+
+  const oldUtcOffset = proto.utcOffset
+  proto.utcOffset = function () {
+    if (this.$u) {
+      return 0
+    }
+    return oldUtcOffset.call(this)
+  }
+
+  const oldFormat = proto.format
+  const UTC_FORMAT_DEFAULT = 'YYYY-MM-DDTHH:mm:ss[Z]'
+  proto.format = function (formatStr) {
+    const str = formatStr || (this.$u ? UTC_FORMAT_DEFAULT : '')
+    return oldFormat.call(this, str)
+  }
+
+  proto.isUTC = function () {
+    return !!this.$u
+  }
+}
diff --git a/src/utils.js b/src/utils.js
--- a/src/utils.js
+++ b/src/utils.js
@@ -6,7 +6,8 @@ const padStart = (string, length, pad) => {
   return `${Array((length + 1) - s.length).join(pad)}${string}`
 }
 
-const padZoneStr = (negMinuts) => {
+const padZoneStr = (instance) => {
+  const negMinuts = -instance.utcOffset()
   const minutes = Math.abs(negMinuts)
   const hourOffset = Math.floor(minutes / 60)
   const minuteOffset = minutes % 60
@@ -16,9 +17,9 @@ const padZoneStr = (negMinuts) => {
 const monthDiff = (a, b) => {
   // function from moment.js in order to keep the same result
   const wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month())
-  const anchor = a.clone().add(wholeMonthDiff, 'months')
+  const anchor = a.clone().add(wholeMonthDiff, C.M)
   const c = b - anchor < 0
-  const anchor2 = a.clone().add(wholeMonthDiff + (c ? -1 : 1), 'months')
+  const anchor2 = a.clone().add(wholeMonthDiff + (c ? -1 : 1), C.M)
   return Number(-(wholeMonthDiff + ((b - anchor) / (c ? (anchor - anchor2) :
     (anchor2 - anchor)))) || 0)
 }
@@ -42,10 +43,10 @@ const prettyUnit = (u) => {
 const isUndefined = s => s === undefined
 
 export default {
-  padStart,
-  padZoneStr,
-  monthDiff,
-  absFloor,
-  prettyUnit,
-  isUndefined
+  s: padStart,
+  z: padZoneStr,
+  m: monthDiff,
+  a: absFloor,
+  p: prettyUnit,
+  u: isUndefined
 }
diff --git a/types/index.d.ts b/types/index.d.ts
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -4,7 +4,7 @@ declare function dayjs (date?: dayjs.ConfigType, option?: dayjs.OptionType, loca
 declare namespace dayjs {
   export type ConfigType = string | number | Date | Dayjs
 
-  export type OptionType = { locale?: string, format?: string } | string
+  export type OptionType = { locale?: string, format?: string, utc?: boolean } | string
 
   type UnitTypeShort = 'd' | 'M' | 'y' | 'h' | 'm' | 's' | 'ms'
   export type UnitType = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'month' | 'quarter' | 'year' | 'date' | UnitTypeShort;
@@ -77,6 +77,8 @@ declare namespace dayjs {
 
     toString(): string
 
+    utcOffset(): number
+
     isBefore(date: ConfigType, unit?: OpUnitType): boolean
 
     isSame(date: ConfigType, unit?: OpUnitType): boolean
diff --git a/types/plugin/utc.d.ts b/types/plugin/utc.d.ts
new file mode 100644
--- /dev/null
+++ b/types/plugin/utc.d.ts
@@ -0,0 +1,17 @@
+import { PluginFunc, ConfigType } from 'dayjs'
+
+declare const plugin: PluginFunc
+export = plugin
+
+declare module 'dayjs' {
+  interface Dayjs {
+    
+    utc(): Dayjs
+    
+    local(): Dayjs
+
+    isUTC(): boolean
+  }
+
+  export function utc(config?: ConfigType, format?: string): Dayjs
+}


Test patch:
diff --git a/test/plugin.test.js b/test/plugin.test.js
--- a/test/plugin.test.js
+++ b/test/plugin.test.js
@@ -30,5 +30,6 @@ it('Plugin extend dayjs', () => {
 })
 
 it('Plugin use core utils', () => {
-  expect(dayjs().$utils().isUndefined).toBeInstanceOf(Function)
+  // u => isUndefined
+  expect(dayjs().$utils().u).toBeInstanceOf(Function)
 })
diff --git a/test/plugin/utc.test.js b/test/plugin/utc.test.js
new file mode 100644
--- /dev/null
+++ b/test/plugin/utc.test.js
@@ -0,0 +1,216 @@
+import MockDate from 'mockdate'
+import moment from 'moment'
+import dayjs from '../../src'
+import utc from '../../src/plugin/utc'
+import customParseFormat from '../../src/plugin/customParseFormat'
+
+dayjs.extend(utc)
+
+beforeEach(() => {
+  MockDate.set(new Date())
+})
+
+afterEach(() => {
+  MockDate.reset()
+})
+
+describe('UTC Get', () => {
+  it('UTC Year', () => {
+    expect(dayjs().utc().year()).toBe(moment().utc().year())
+  })
+
+  it('UTC Month', () => {
+    expect(dayjs().utc().month()).toBe(moment().utc().month())
+  })
+
+  it('UTC Day of Week', () => {
+    expect(dayjs().utc().day()).toBe(moment().utc().day())
+  })
+
+  it('UTC Date', () => {
+    expect(dayjs().utc().date()).toBe(moment().utc().date())
+  })
+
+  it('UTC Hour', () => {
+    expect(dayjs().utc().hour()).toBe(moment().utc().hour())
+  })
+
+  it('UTC Minute', () => {
+    expect(dayjs().utc().minute()).toBe(moment().utc().minute())
+  })
+
+  it('UTC Second', () => {
+    expect(dayjs().utc().second()).toBe(moment().utc().second())
+  })
+
+  it('UTC Millisecond', () => {
+    expect(dayjs().utc().millisecond()).toBe(moment().utc().millisecond())
+  })
+})
+
+describe('Parse UTC ', () => {
+  it('Parse Now', () => {
+    expect(dayjs.utc().format()).toBe(moment.utc().format())
+    expect(dayjs().utc().format()).toBe(moment().utc().format())
+  })
+
+  it('Parse date string without timezome', () => {
+    const d = '2018-09-06'
+    const d2 = '2018-09'
+    expect(dayjs.utc(d).format()).toEqual(moment.utc(d).format())
+    expect(dayjs.utc(d).format()).toEqual('2018-09-06T00:00:00Z')
+    expect(dayjs.utc(d2).format()).toEqual(moment.utc(d2).format())
+    expect(dayjs.utc(d2).format()).toEqual('2018-09-01T00:00:00Z')
+    expect(dayjs(d).utc().format()).toEqual(moment(d).utc().format())
+  })
+
+  it('creating with utc with timezone', () => {
+    const d = '2011-02-02T03:04:05+00:00'
+    expect(dayjs.utc(d).format()).toEqual(moment.utc(d).format())
+    const d2 = '2012-01-02T08:20:00+09:00'
+    expect(dayjs.utc(d2).format()).toEqual(moment.utc(d2).format())
+  })
+
+  it('Parse date string without timezome', () => {
+    const d = '2017-04-22 19:50:16'
+    expect(dayjs.utc(d).format()).toEqual('2017-04-22T19:50:16Z')
+    expect(dayjs.utc(d).format()).toEqual(moment.utc(d).format())
+    expect(dayjs(d).utc().format()).toBe(moment(d).utc().format())
+    const d2 = '2012-01-02T08:20:00'
+    expect(dayjs.utc(d2).format()).toEqual(moment.utc(d2).format())
+  })
+
+  it('Parse date string set utc in config', () => {
+    const d = '2018-09-06T19:34:28Z'
+    expect(dayjs(d, { utc: true }).format()).toEqual('2018-09-06T19:34:28Z')
+    expect(dayjs(d, { utc: true }).format()).toEqual(moment(d).utc().format())
+    expect(dayjs(d).utc().format()).toEqual('2018-09-06T19:34:28Z')
+    expect(dayjs(d).utc().format()).toEqual(moment(d).utc().format())
+    expect(dayjs.utc(d).format()).toEqual('2018-09-06T19:34:28Z')
+    expect(dayjs.utc(d).format()).toEqual(moment.utc(d).format())
+  })
+})
+
+it('Clone retains the UTC mode', () => {
+  const instance = dayjs('2018-09-06').utc()
+  const another = instance.clone()
+  expect(another.$u).toBeTruthy()
+})
+
+it('UTC mode format tokens', () => {
+  const d = '2018-09-06T19:34:28.657Z'
+  const instance = dayjs(d).utc()
+  const format = 'HH-hh-mm-ss-SSS-Z-ZZ'
+  expect(instance.format(format)).toBe('19-07-34-28-657-+00:00-+0000')
+  expect(instance.format(format)).toBe(moment.utc(d).format(format))
+})
+
+describe('local', () => {
+  it('Returns a new instance', () => {
+    const instance = dayjs.utc('2018-09-06T19:34:28.657Z')
+    const local = instance.local()
+    expect(local).not.toBe(instance)
+  })
+
+  it('UTC to local', () => {
+    const d = '2018-09-06'
+    expect(dayjs.utc(d).local().format()).toEqual(moment.utc(d).local().format())
+  })
+})
+
+it('StartOf EndOf Year ... in UTC mode', () => {
+  const testArr = ['year', 'month', 'day', 'date', 'week', 'hour', 'minute', 'second']
+  testArr.forEach((d) => {
+    expect(dayjs().utc().startOf(d).format()).toBe(moment().utc().startOf(d).format())
+    expect(dayjs().utc().endOf(d).format()).toBe(moment().utc().endOf(d).format())
+  })
+})
+
+describe('UTC Set', () => {
+  it('Set UTC Day', () => {
+    expect(dayjs().utc().set('date', 30).valueOf()).toBe(moment().utc().set('date', 30).valueOf())
+  })
+
+  it('Set UTC Day of Week', () => {
+    expect(dayjs().utc().set('day', 0).valueOf()).toBe(moment().utc().set('day', 0).valueOf())
+  })
+
+  it('Set UTC Month', () => {
+    expect(dayjs().utc().set('month', 11).valueOf()).toBe(moment().utc().set('month', 11).valueOf())
+  })
+
+  it('Set UTC Year', () => {
+    expect(dayjs().utc().set('year', 2008).valueOf()).toBe(moment().utc().set('year', 2008).valueOf())
+  })
+
+  it('Set UTC Hour', () => {
+    expect(dayjs().utc().set('hour', 6).valueOf()).toBe(moment().utc().set('hour', 6).valueOf())
+  })
+
+  it('Set UTC Minute', () => {
+    expect(dayjs().utc().set('minute', 59).valueOf()).toBe(moment().utc().set('minute', 59).valueOf())
+  })
+
+  it('Set UTC Second', () => {
+    expect(dayjs().utc().set('second', 59).valueOf()).toBe(moment().utc().set('second', 59).valueOf())
+  })
+
+  it('Set UTC Millisecond', () => {
+    expect(dayjs().utc().set('millisecond', 999).valueOf()).toBe(moment().utc().set('millisecond', 999).valueOf())
+  })
+})
+
+it('isUTC', () => {
+  expect(dayjs().isUTC()).toBe(false)
+  expect(dayjs().utc().isUTC()).toBe(true)
+  expect(dayjs.utc().isUTC()).toBe(true)
+})
+
+describe('UTC and local', () => {
+  const localDay = dayjs(Date.UTC(2011, 1, 2, 3, 4, 5, 6))
+  const utcDay = localDay.utc()
+
+  it('utc', () => {
+    expect(utcDay.date()).toBe(2)
+    expect(utcDay.day()).toBe(3)
+    expect(utcDay.hour()).toBe(3)
+  })
+
+  const localAnainDay = utcDay.local()
+  it('local', () => {
+    if (localAnainDay.utcOffset() < -180) {
+      expect(localAnainDay.date()).toBe(1)
+      expect(localAnainDay.day()).toBe(2)
+    } else {
+      expect(localAnainDay.date()).toBe(2)
+      expect(localAnainDay.day()).toBe(3)
+    }
+  })
+
+  const offset = Math.floor(localAnainDay.utcOffset() / 60)
+  const expected = (24 + 3 + offset) % 24
+  it('utcOffset', () => {
+    expect(localAnainDay.hour()).toBe(expected)
+    expect(dayjs().utc().utcOffset()).toBe(0)
+  })
+})
+
+
+describe('UTC with customParseFormat', () => {
+  it('Custom Parse Format', () => {
+    dayjs.extend(customParseFormat)
+    const instant = dayjs.utc('2011-02-02 03:04:05', 'YYYY-MM-DD HH:mm:ss')
+    const momentInstant = moment.utc('2011-02-02 03:04:05', 'YYYY-MM-DD HH:mm:ss')
+    expect(instant.date()).toBe(2)
+    expect(instant.hour()).toBe(3)
+    expect(instant.format()).toBe('2011-02-02T03:04:05Z')
+    expect(instant.format()).toBe(momentInstant.format())
+  })
+})
+
+describe('UTC Offset', () => {
+  it('get utcOffset', () => {
+    expect(dayjs().utcOffset()).toBe(moment().utcOffset())
+    expect(dayjs().utc().utcOffset()).toBe(moment().utc().utcOffset())
+  })
+})
diff --git a/test/utils.test.js b/test/utils.test.js
--- a/test/utils.test.js
+++ b/test/utils.test.js
@@ -1,10 +1,8 @@
 import Utils from '../src/utils'
 
-const {
-  prettyUnit,
-  padZoneStr,
-  padStart
-} = Utils
+const prettyUnit = Utils.p
+const padStart = Utils.s
+const padZoneStr = Utils.z
 
 it('PrettyUnit', () => {
   expect(prettyUnit('Days')).toBe('day')
@@ -14,12 +12,19 @@ it('PrettyUnit', () => {
 })
 
 it('PadZoneStr', () => {
-  expect(padZoneStr(0)).toBe('+00:00')
-  expect(padZoneStr(1 * 60)).toBe('-01:00')
-  expect(padZoneStr(-1 * 60)).toBe('+01:00')
-  expect(padZoneStr(-10 * 60)).toBe('+10:00')
-  expect(padZoneStr(10 * 60)).toBe('-10:00')
-  expect(padZoneStr((-5 * 60) - 30)).toBe('+05:30')
+  const instance = {}
+  instance.utcOffset = () => 0 * -1
+  expect(padZoneStr(instance)).toBe('+00:00')
+  instance.utcOffset = () => 1 * 60 * -1
+  expect(padZoneStr(instance)).toBe('-01:00')
+  instance.utcOffset = () => -1 * 60 * -1
+  expect(padZoneStr(instance)).toBe('+01:00')
+  instance.utcOffset = () => -10 * 60 * -1
+  expect(padZoneStr(instance)).toBe('+10:00')
+  instance.utcOffset = () => 10 * 60 * -1
+  expect(padZoneStr(instance)).toBe('-10:00')
+  instance.utcOffset = () => ((-5 * 60) - 30) * -1
+  expect(padZoneStr(instance)).toBe('+05:30')
 })
 
 it('PadStart', () => {