Repository: iamkun/dayjs (JavaScript)

Issue:
`dayjs.tz()` needs to have timezone parameter as optional
**Describe the bug**
Because you can set a default timezone, the `dayjs.tz()` method should have the second parameter (timezone) as optional. It works fine in JS — just missing the correct types for TS.

**Expected behavior**
1.  Set `dayjs.tz.setDefault("America/Phoenix");`
2. `dayjs.tz('some valid date')` <- that shouldn't throw a TypeScript error
3. `dayjs('some valid date').tz()` <- that shouldn't throw a TypeScript error either

**Potential Solution**
```
declare module 'dayjs' {
  interface Dayjs {
    tz(timezone?: string): Dayjs;
  }

  interface DayjsTimezone {
    (date?: ConfigType, timezone?: string): Dayjs;
  }
}
```

**Information**
 - Day.js Version: v1.8.36
 - OS: macOS v10.15.6
 - Browser: Firefox Developer Edition v82.0b2
 - Time zone: America/Phoenix



Hints:

Patch:
diff --git a/src/index.js b/src/index.js
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,6 @@
 import * as C from './constant'
-import U from './utils'
 import en from './locale/en'
+import U from './utils'
 
 let L = 'en' // global locale
 const Ls = {} // global loaded locale
@@ -43,6 +43,7 @@ const wrapper = (date, instance) =>
   dayjs(date, {
     locale: instance.$L,
     utc: instance.$u,
+    x: instance.$x,
     $offset: instance.$offset // todo: refactor; do not use this.$offset in you code
   })
 
@@ -81,6 +82,7 @@ class Dayjs {
 
   parse(cfg) {
     this.$d = parseDate(cfg)
+    this.$x = cfg.x || {}
     this.init()
   }
 
diff --git a/src/locale/en-SG.js b/src/locale/en-sg.js
similarity index 98%
rename from src/locale/en-SG.js
rename to src/locale/en-sg.js
--- a/src/locale/en-SG.js
+++ b/src/locale/en-sg.js
@@ -2,7 +2,7 @@
 import dayjs from 'dayjs'
 
 const locale = {
-  name: 'en-SG',
+  name: 'en-sg',
   weekdays: 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
   months: 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
   weekStart: 1,
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
@@ -13,7 +13,7 @@ export default (o, c, d) => { // locale needed later
     const locale = this.$locale()
     const utils = this.$utils()
     const str = formatStr || FORMAT_DEFAULT
-    const result = str.replace(/\[([^\]]+)]|Q|wo|ww|w|gggg|Do|X|x|k{1,2}|S/g, (match) => {
+    const result = str.replace(/\[([^\]]+)]|Q|wo|ww|w|zzz|z|gggg|Do|X|x|k{1,2}|S/g, (match) => {
       switch (match) {
         case 'Q':
           return Math.ceil((this.$M + 1) / 3)
@@ -33,6 +33,10 @@ export default (o, c, d) => { // locale needed later
           return Math.floor(this.$d.getTime() / 1000)
         case 'x':
           return this.$d.getTime()
+        case 'z':
+          return `[${this.offsetName()}]`
+        case 'zzz':
+          return `[${this.offsetName('long')}]`
         default:
           return match
       }
diff --git a/src/plugin/duration/index.js b/src/plugin/duration/index.js
--- a/src/plugin/duration/index.js
+++ b/src/plugin/duration/index.js
@@ -60,7 +60,7 @@ class Duration {
 
   calMilliseconds() {
     this.$ms = Object.keys(this.$d).reduce((total, unit) => (
-      total + ((this.$d[unit] || 0) * (unitToMS[unit] || 1))
+      total + ((this.$d[unit] || 0) * (unitToMS[unit]))
     ), 0)
   }
 
@@ -106,7 +106,7 @@ class Duration {
   }
 
   as(unit) {
-    return this.$ms / (unitToMS[prettyUnit(unit)] || 1)
+    return this.$ms / (unitToMS[prettyUnit(unit)])
   }
 
   get(unit) {
diff --git a/src/plugin/timezone/index.js b/src/plugin/timezone/index.js
--- a/src/plugin/timezone/index.js
+++ b/src/plugin/timezone/index.js
@@ -9,13 +9,15 @@ const typeToPos = {
 
 const ms = 'ms'
 
-export default (o, c, d) => {
-  let defaultTimezone
-
-  const localUtcOffset = d().utcOffset()
-  const tzOffset = (timestamp, timezone) => {
-    const date = new Date(timestamp)
-    const dtf = new Intl.DateTimeFormat('en-US', {
+// Cache time-zone lookups from Intl.DateTimeFormat,
+// as it is a *very* slow method.
+const dtfCache = {}
+const getDateTimeFormat = (timezone, options = {}) => {
+  const timeZoneName = options.timeZoneName || 'short'
+  const key = `${timezone}|${timeZoneName}`
+  let dtf = dtfCache[key]
+  if (!dtf) {
+    dtf = new Intl.DateTimeFormat('en-US', {
       hour12: false,
       timeZone: timezone,
       year: 'numeric',
@@ -23,9 +25,27 @@ export default (o, c, d) => {
       day: '2-digit',
       hour: '2-digit',
       minute: '2-digit',
-      second: '2-digit'
+      second: '2-digit',
+      timeZoneName
     })
-    const formatResult = dtf.formatToParts(date)
+    dtfCache[key] = dtf
+  }
+  return dtf
+}
+
+export default (o, c, d) => {
+  let defaultTimezone
+
+  const localUtcOffset = d().utcOffset()
+
+  const makeFormatParts = (timestamp, timezone, options = {}) => {
+    const date = new Date(timestamp)
+    const dtf = getDateTimeFormat(timezone, options)
+    return dtf.formatToParts(date)
+  }
+
+  const tzOffset = (timestamp, timezone) => {
+    const formatResult = makeFormatParts(timestamp, timezone)
     const filled = []
     for (let i = 0; i < formatResult.length; i += 1) {
       const { type, value } = formatResult[i]
@@ -35,13 +55,14 @@ export default (o, c, d) => {
         filled[pos] = parseInt(value, 10)
       }
     }
+    const hour = filled[3]
     // Workaround for the same behavior in different node version
     // https://github.com/nodejs/node/issues/33027
-    const hour = filled[3]
+    /* istanbul ignore next */
     const fixedHour = hour === 24 ? 0 : hour
     const utcString = `${filled[0]}-${filled[1]}-${filled[2]} ${fixedHour}:${filled[4]}:${filled[5]}:000`
     const utcTs = d.utc(utcString).valueOf()
-    let asTS = +date
+    let asTS = +timestamp
     const over = asTS % 1000
     asTS -= over
     return (utcTs - asTS) / (60 * 1000)
@@ -76,7 +97,16 @@ export default (o, c, d) => {
   proto.tz = function (timezone = defaultTimezone) {
     const target = this.toDate().toLocaleString('en-US', { timeZone: timezone })
     const diff = Math.round((this.toDate() - new Date(target)) / 1000 / 60)
-    return d(target).utcOffset(localUtcOffset - diff, true).$set(ms, this.$ms)
+    const ins = d(target).$set(ms, this.$ms).utcOffset(localUtcOffset - diff, true)
+    ins.$x.$timezone = timezone
+    return ins
+  }
+
+  proto.offsetName = function (type) {
+    // type: short(default) / long
+    const zone = this.$x.$timezone || d.tz.guess()
+    const result = makeFormatParts(this.valueOf(), zone, { timeZoneName: type }).find(m => m.type.toLowerCase() === 'timezonename')
+    return result && result.value
   }
 
   d.tz = function (input, timezone = defaultTimezone) {
@@ -89,6 +119,7 @@ export default (o, c, d) => {
     localTs = localTs || d.utc(input).valueOf()
     const [targetTs, targetOffset] = fixOffset(localTs, previousOffset, timezone)
     const ins = d(targetTs).utcOffset(targetOffset)
+    ins.$x.$timezone = timezone
     return ins
   }
 
diff --git a/src/plugin/utc/index.js b/src/plugin/utc/index.js
--- a/src/plugin/utc/index.js
+++ b/src/plugin/utc/index.js
@@ -1,15 +1,18 @@
 import { MILLISECONDS_A_MINUTE, MIN } from '../../constant'
 
 export default (option, Dayjs, dayjs) => {
-  const localOffset = (new Date()).getTimezoneOffset()
   const proto = Dayjs.prototype
   dayjs.utc = function (date) {
     const cfg = { date, utc: true, args: arguments } // eslint-disable-line prefer-rest-params
     return new Dayjs(cfg) // eslint-disable-line no-use-before-define
   }
 
-  proto.utc = function () {
-    return dayjs(this.toDate(), { locale: this.$L, utc: true })
+  proto.utc = function (keepLocalTime) {
+    const ins = dayjs(this.toDate(), { locale: this.$L, utc: true })
+    if (keepLocalTime) {
+      return ins.add(this.utcOffset(), MIN)
+    }
+    return ins
   }
 
   proto.local = function () {
@@ -64,8 +67,11 @@ export default (option, Dayjs, dayjs) => {
       return ins
     }
     if (input !== 0) {
-      ins = this.local().add(offset + localOffset, MIN)
+      const localTimezoneOffset = this.$u
+        ? this.toDate().getTimezoneOffset() : -1 * this.utcOffset()
+      ins = this.local().add(offset + localTimezoneOffset, MIN)
       ins.$offset = offset
+      ins.$x.$localOffset = localTimezoneOffset
     } else {
       ins = this.utc()
     }
@@ -81,7 +87,7 @@ export default (option, Dayjs, dayjs) => {
 
   proto.valueOf = function () {
     const addedOffset = !this.$utils().u(this.$offset)
-      ? this.$offset + localOffset : 0
+      ? this.$offset + (this.$x.$localOffset || (new Date()).getTimezoneOffset()) : 0
     return this.$d.valueOf() - (addedOffset * MILLISECONDS_A_MINUTE)
   }
 
diff --git a/types/plugin/duration.d.ts b/types/plugin/duration.d.ts
--- a/types/plugin/duration.d.ts
+++ b/types/plugin/duration.d.ts
@@ -11,7 +11,7 @@ export declare class Duration {
 
   clone(): Duration
   
-  humanize(withSuffix: boolean): string
+  humanize(withSuffix?: boolean): string
 
   milliseconds(): number
   asMilliseconds(): number
diff --git a/types/plugin/timezone.d.ts b/types/plugin/timezone.d.ts
--- a/types/plugin/timezone.d.ts
+++ b/types/plugin/timezone.d.ts
@@ -5,12 +5,13 @@ export = plugin
 
 declare module 'dayjs' {
   interface Dayjs {
-    tz(timezone: string): Dayjs
+    tz(timezone?: string): Dayjs
   }
 
   interface DayjsTimezone {
-    (date: ConfigType, timezone: string): Dayjs
+    (date: ConfigType, timezone?: string): Dayjs
     guess(): string
+    setDefault(timezone?: string): void
   }
 
   const tz: DayjsTimezone
diff --git a/types/plugin/utc.d.ts b/types/plugin/utc.d.ts
--- a/types/plugin/utc.d.ts
+++ b/types/plugin/utc.d.ts
@@ -6,7 +6,7 @@ export = plugin
 declare module 'dayjs' {
   interface Dayjs {
     
-    utc(): Dayjs
+    utc(keepLocalTime?: boolean): Dayjs
     
     local(): Dayjs
 


Test patch:
diff --git a/test/plugin/advancedFormat.test.js b/test/plugin/advancedFormat.test.js
--- a/test/plugin/advancedFormat.test.js
+++ b/test/plugin/advancedFormat.test.js
@@ -4,8 +4,12 @@ import dayjs from '../../src'
 import advancedFormat from '../../src/plugin/advancedFormat'
 import weekOfYear from '../../src/plugin/weekOfYear'
 import weekYear from '../../src/plugin/weekYear'
+import timezone from '../../src/plugin/timezone'
+import utc from '../../src/plugin/utc'
 import '../../src/locale/zh-cn'
 
+dayjs.extend(utc)
+dayjs.extend(timezone)
 dayjs.extend(weekYear)
 dayjs.extend(weekOfYear)
 dayjs.extend(advancedFormat)
@@ -83,6 +87,14 @@ it('Format Week Year gggg', () => {
   expect(dayjs(d).format('gggg')).toBe(moment(d).format('gggg'))
 })
 
+it('Format offsetName z zzz', () => {
+  const dtz = dayjs.tz('2012-03-11 01:59:59', 'America/New_York')
+  expect(dtz.format('z')).toBe('EST')
+  expect(dtz.format('zzz')).toBe('Eastern Standard Time')
+  expect(dayjs().format('z')).toBeDefined()
+  expect(dayjs().format('zzz')).toBeDefined()
+})
+
 it('Skips format strings inside brackets', () => {
   expect(dayjs().format('[Q]')).toBe('Q')
   expect(dayjs().format('[Do]')).toBe('Do')
diff --git a/test/plugin/timezone.test.js b/test/plugin/timezone.test.js
--- a/test/plugin/timezone.test.js
+++ b/test/plugin/timezone.test.js
@@ -86,6 +86,13 @@ describe('Convert', () => {
     })
   })
 
+  it('convert from time with timezone to target time', () => {
+    const losAngelesInUTC = dayjs('2014-06-01T05:00:00-07:00').tz('UTC')
+    const MlosAngelesInUTC = moment('2014-06-01T05:00:00-07:00').tz('UTC')
+    expect(losAngelesInUTC.format()).toBe('2014-06-01T12:00:00Z')
+    expect(losAngelesInUTC.format()).toBe(MlosAngelesInUTC.format())
+  })
+
   it('DST', () => {
     [dayjs, moment].forEach((_) => {
       const jun = _('2014-06-01T12:00:00Z')
@@ -252,3 +259,17 @@ describe('set Default', () => {
     expect(tokyo.valueOf()).toBe(1401591600000)
   })
 })
+
+describe('Get offsetName', () => {
+  const dtz = dayjs.tz('2012-03-11 01:59:59', NY)
+  it('short', () => {
+    const d = dtz.offsetName('short')
+    const m = moment.tz('2012-03-11 01:59:59', NY).format('z')
+    expect(d).toBe(m)
+    expect(d).toBe('EST')
+  })
+  it('long', () => {
+    const d = dtz.offsetName('long')
+    expect(d).toBe('Eastern Standard Time')
+  })
+})
diff --git a/test/plugin/utc.test.js b/test/plugin/utc.test.js
--- a/test/plugin/utc.test.js
+++ b/test/plugin/utc.test.js
@@ -1,8 +1,8 @@
 import MockDate from 'mockdate'
 import moment from 'moment'
 import dayjs from '../../src'
-import utc from '../../src/plugin/utc'
 import customParseFormat from '../../src/plugin/customParseFormat'
+import utc from '../../src/plugin/utc'
 
 dayjs.extend(utc)
 
@@ -250,3 +250,19 @@ describe('Diff', () => {
       .toBe(moment.utc(d1).diff(d2, 'm'))
   })
 })
+
+it('utc keepLocalTime', () => {
+  const t = '2016-05-03 22:15:01'
+  const d = dayjs(t).utc(true)
+  const m = moment(t).utc(true)
+  const fd = d.format()
+  const dd = d.toDate()
+  const vd = d.valueOf()
+  const fm = m.format()
+  const dm = m.toDate()
+  const vm = m.valueOf()
+  expect(fd).toEqual(fm)
+  expect(fd).toEqual('2016-05-03T22:15:01Z')
+  expect(dd).toEqual(dm)
+  expect(vd).toEqual(vm)
+})
diff --git a/test/timezone.test.js b/test/timezone.test.js
--- a/test/timezone.test.js
+++ b/test/timezone.test.js
@@ -1,5 +1,5 @@
-import moment from 'moment'
 import MockDate from 'mockdate'
+import moment from 'moment'
 import dayjs from '../src'
 import utc from '../src/plugin/utc'
 
@@ -54,3 +54,11 @@ it('UTC add day in DST', () => {
     .toBe(momentTest.clone().add(2, 'day').format())
 })
 
+it('UTC and utcOffset', () => {
+  const test1 = 1331449199000 // 2012/3/11 14:59:59
+  expect(moment(test1).utcOffset(-300).format())
+    .toBe(dayjs(test1).utcOffset(-300).format())
+  const test2 = '2000-01-01T06:31:00Z'
+  expect(moment.utc(test2).utcOffset(-60).format())
+    .toBe(dayjs.utc(test2).utcOffset(-60).format())
+})