Repository: iamkun/dayjs (JavaScript)

Issue:
TypeScript error when using Timezone plugin: `timezone.d.ts(7,7): error TS2304: Cannot find name 'Dayjs'`
I got this error: `timezone.d.ts(7,7): error TS2304: Cannot find name 'Dayjs'` when I use `dayjs.extend(timezone);`

I think that `DayjsTimezone` interface should be declared inside the module declaration
https://github.com/iamkun/dayjs/blob/e69caba1b0957241a855aa0ae38db899fa2c3795/types/plugin/timezone.d.ts#L6-L17

or `Dayjs` import is missing here
https://github.com/iamkun/dayjs/blob/e69caba1b0957241a855aa0ae38db899fa2c3795/types/plugin/timezone.d.ts#L1


Hints:

Patch:
diff --git a/build/esm.js b/build/esm.js
--- a/build/esm.js
+++ b/build/esm.js
@@ -1,10 +1,14 @@
 const fs = require('fs')
 const path = require('path')
 const util = require('util')
+const { ncp } = require('ncp')
 
 const { promisify } = util
 
-const localeDir = path.join(process.env.PWD, 'esm/locale');
+const typeFileExt = '.d.ts'
+const localeDir = path.join(process.env.PWD, 'esm/locale')
+const pluginDir = path.join(process.env.PWD, 'esm/plugin')
+const localeTypePath = path.join(process.env.PWD, 'esm/locale', `index${typeFileExt}`);
 
 (async () => {
   try {
@@ -15,6 +19,25 @@ const localeDir = path.join(process.env.PWD, 'esm/locale');
       const result = readFile.replace("'dayjs'", "'../index'")
       await promisify(fs.writeFile)(filePath, result, 'utf8')
     })
+
+    await promisify(ncp)('./types/', './esm')
+
+    const readLocaleFile = await promisify(fs.readFile)(localeTypePath, 'utf8')
+    const localResult = readLocaleFile.replace("'dayjs", "'dayjs/esm")
+    await promisify(fs.writeFile)(localeTypePath, localResult, 'utf8')
+
+    const readPluginDir = await promisify(fs.readdir)(pluginDir)
+    readPluginDir.forEach(async (p) => {
+      if (p.includes(typeFileExt)) {
+        const pluginName = p.replace(typeFileExt, '')
+        const filePath = path.join(pluginDir, p)
+        const targetPath = path.join(pluginDir, pluginName, `index${typeFileExt}`)
+        const readFile = await promisify(fs.readFile)(filePath, 'utf8')
+        const result = readFile.replace(/'dayjs'/g, "'dayjs/esm'")
+        await promisify(fs.writeFile)(targetPath, result, 'utf8')
+        await promisify(fs.unlink)(filePath)
+      }
+    })
   } catch (e) {
     console.error(e) // eslint-disable-line no-console
   }
diff --git a/build/index.js b/build/index.js
--- a/build/index.js
+++ b/build/index.js
@@ -41,9 +41,7 @@ async function build(option) {
       fileName: './dayjs.min.js'
     }))
 
-    ncp('./types/', './', (err) => {
-      if (err) { throw err }
-    })
+    await promisify(ncp)('./types/', './')
   } catch (e) {
     console.error(e) // eslint-disable-line no-console
   }
diff --git a/src/index.js b/src/index.js
--- a/src/index.js
+++ b/src/index.js
@@ -122,38 +122,6 @@ class Dayjs {
     return this.set(set, input)
   }
 
-  year(input) {
-    return this.$g(input, '$y', C.Y)
-  }
-
-  month(input) {
-    return this.$g(input, '$M', C.M)
-  }
-
-  day(input) {
-    return this.$g(input, '$W', C.D)
-  }
-
-  date(input) {
-    return this.$g(input, '$D', C.DATE)
-  }
-
-  hour(input) {
-    return this.$g(input, '$H', C.H)
-  }
-
-  minute(input) {
-    return this.$g(input, '$m', C.MIN)
-  }
-
-  second(input) {
-    return this.$g(input, '$s', C.S)
-  }
-
-  millisecond(input) {
-    return this.$g(input, '$ms', C.MS)
-  }
-
   unix() {
     return Math.floor(this.valueOf() / 1000)
   }
@@ -398,7 +366,22 @@ class Dayjs {
   }
 }
 
-dayjs.prototype = Dayjs.prototype
+const proto = Dayjs.prototype
+dayjs.prototype = proto;
+[
+  ['$ms', C.MS],
+  ['$s', C.S],
+  ['$m', C.MIN],
+  ['$H', C.H],
+  ['$W', C.D],
+  ['$M', C.M],
+  ['$y', C.Y],
+  ['$D', C.DATE]
+].forEach((g) => {
+  proto[g[1]] = function (input) {
+    return this.$g(input, g[0], g[1])
+  }
+})
 
 dayjs.extend = (plugin, option) => {
   plugin(option, Dayjs, dayjs)
diff --git a/src/locale/pt-br.js b/src/locale/pt-br.js
--- a/src/locale/pt-br.js
+++ b/src/locale/pt-br.js
@@ -6,7 +6,6 @@ const locale = {
   weekdays: 'Domingo_Segunda-feira_Terça-feira_Quarta-feira_Quinta-feira_Sexta-feira_Sábado'.split('_'),
   weekdaysShort: 'Dom_Seg_Ter_Qua_Qui_Sex_Sáb'.split('_'),
   weekdaysMin: 'Do_2ª_3ª_4ª_5ª_6ª_Sá'.split('_'),
-  weekStart: 1,
   months: 'Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro'.split('_'),
   monthsShort: 'Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez'.split('_'),
   ordinal: n => `${n}º`,
diff --git a/src/plugin/isoWeek/index.js b/src/plugin/isoWeek/index.js
--- a/src/plugin/isoWeek/index.js
+++ b/src/plugin/isoWeek/index.js
@@ -3,8 +3,8 @@ import { D, W, Y } from '../../constant'
 const isoWeekPrettyUnit = 'isoweek'
 
 export default (o, c, d) => {
-  const getYearFirstThursday = (year) => {
-    const yearFirstDay = d().year(year).startOf(Y)
+  const getYearFirstThursday = (year, isUtc) => {
+    const yearFirstDay = (isUtc ? d.utc : d)().year(year).startOf(Y)
     let addDiffDays = 4 - yearFirstDay.isoWeekday()
     if (yearFirstDay.isoWeekday() > 4) {
       addDiffDays += 7
@@ -26,7 +26,7 @@ export default (o, c, d) => {
       return this.add((week - this.isoWeek()) * 7, D)
     }
     const nowWeekThursday = getCurrentWeekThursday(this)
-    const diffWeekThursday = getYearFirstThursday(this.isoWeekYear())
+    const diffWeekThursday = getYearFirstThursday(this.isoWeekYear(), this.$u)
     return nowWeekThursday.diff(diffWeekThursday, W) + 1
   }
 
diff --git a/src/plugin/pluralGetSet/index.js b/src/plugin/pluralGetSet/index.js
new file mode 100644
--- /dev/null
+++ b/src/plugin/pluralGetSet/index.js
@@ -0,0 +1,21 @@
+export default (o, c) => {
+  const proto = c.prototype
+
+  const pluralAliases = [
+    'milliseconds',
+    'seconds',
+    'minutes',
+    'hours',
+    'days',
+    'weeks',
+    'isoWeeks',
+    'months',
+    'quarters',
+    'years',
+    'dates'
+  ]
+
+  pluralAliases.forEach((alias) => {
+    proto[alias] = proto[alias.replace(/s$/, '')]
+  })
+}
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
@@ -54,12 +54,17 @@ export default (o, c, d) => {
   const proto = c.prototype
   proto.tz = function (timezone) {
     const target = this.toDate().toLocaleString('en-US', { timeZone: timezone })
-    const diff = (this.toDate() - new Date(target)) / 1000 / 60
+    const diff = Math.round((this.toDate() - new Date(target)) / 1000 / 60)
     return d(target).utcOffset(localUtcOffset - diff, true)
   }
   d.tz = function (input, timezone) {
     const previousOffset = tzOffset(+d(), timezone)
-    const localTs = d.utc(input).valueOf()
+    let localTs
+    if (typeof input !== 'string') {
+      // timestamp number || js Date || Day.js
+      localTs = d(input) + (previousOffset * 60 * 1000)
+    }
+    localTs = localTs || d.utc(input).valueOf()
     const [targetTs, targetOffset] = fixOffset(localTs, previousOffset, timezone)
     const ins = d(targetTs).utcOffset(targetOffset)
     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
@@ -104,4 +104,10 @@ export default (option, Dayjs, dayjs) => {
     }
     return oldToDate.call(this)
   }
+  const oldDiff = proto.diff
+  proto.diff = function (input, units, float) {
+    const localThis = this.local()
+    const localInput = dayjs(input).local()
+    return oldDiff.call(localThis, localInput, units, float)
+  }
 }
diff --git a/types/plugin/pluralGetSet.d.ts b/types/plugin/pluralGetSet.d.ts
new file mode 100644
--- /dev/null
+++ b/types/plugin/pluralGetSet.d.ts
@@ -0,0 +1,44 @@
+import { PluginFunc, UnitType, ConfigType } from 'dayjs'
+
+declare const plugin: PluginFunc
+export = plugin
+
+declare module 'dayjs' {
+  interface Dayjs {
+    years(): number
+
+    years(value: number): Dayjs
+
+    months(): number
+
+    months(value: number): Dayjs
+
+    dates(): number
+
+    dates(value: number): Dayjs
+
+    weeks(): number
+
+    weeks(value: number): Dayjs
+
+    days(): number
+
+    days(value: number): Dayjs
+
+    hours(): number
+
+    hours(value: number): Dayjs
+
+    minutes(): number
+
+    minutes(value: number): Dayjs
+
+    seconds(): number
+
+    seconds(value: number): Dayjs
+
+    milliseconds(): number
+
+    milliseconds(value: number): Dayjs
+  }
+}
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
@@ -1,16 +1,16 @@
-import { PluginFunc } from 'dayjs'
+import { PluginFunc, ConfigType } from 'dayjs'
 
 declare const plugin: PluginFunc
 export = plugin
 
-interface DayjsTimezone {
-  (): Dayjs
-  guess(): string
-}
-
 declare module 'dayjs' {
   interface Dayjs {
-    tz(): Dayjs
+    tz(timezone: string): Dayjs
+  }
+
+  interface DayjsTimezone {
+    (date: ConfigType, timezone: string): Dayjs
+    guess(): string
   }
 
   const tz: DayjsTimezone


Test patch:
diff --git a/test/plugin/isoWeek.test.js b/test/plugin/isoWeek.test.js
--- a/test/plugin/isoWeek.test.js
+++ b/test/plugin/isoWeek.test.js
@@ -2,8 +2,10 @@ import MockDate from 'mockdate'
 import moment from 'moment'
 import dayjs from '../../src'
 import isoWeek from '../../src/plugin/isoWeek'
+import utc from '../../src/plugin/utc'
 
 dayjs.extend(isoWeek)
+dayjs.extend(utc)
 
 beforeEach(() => {
   MockDate.set(new Date())
@@ -124,3 +126,11 @@ it('isoWeek of year', () => {
   expect(dayjs('20210110').isoWeekYear()).toBe(2021)
   expect(dayjs('20210110').isoWeek()).toBe(1)
 })
+
+
+it('utc mode', () => {
+  // Wednesday, 1 January 2020 00:00:00 UTC
+  const d = dayjs.utc(1577836800000).isoWeek()
+  expect(d).toBe(1)
+  expect(moment.utc(1577836800000).isoWeek()).toBe(d)
+})
diff --git a/test/plugin/pluralGetSet.test.js b/test/plugin/pluralGetSet.test.js
new file mode 100644
--- /dev/null
+++ b/test/plugin/pluralGetSet.test.js
@@ -0,0 +1,128 @@
+import MockDate from 'mockdate'
+import moment from 'moment'
+import dayjs from '../../src'
+import pluralGetSet from '../../src/plugin/pluralGetSet'
+
+dayjs.extend(pluralGetSet)
+const warnBackup = global.console.warn
+beforeEach(() => {
+  MockDate.set(new Date())
+  global.console.warn = jest.genMockFunction()
+  // moment.js .years, .dates, .months will throw warn
+})
+
+afterEach(() => {
+  MockDate.reset()
+  global.console.warn = warnBackup
+})
+
+it('Years', () => {
+  expect(dayjs().get('years')).toBe(moment().get('years'))
+  expect(dayjs().years()).toBe(moment().years())
+  expect(dayjs().years(0).valueOf()).toBe(moment().years(0).valueOf())
+  expect(dayjs().years(2000).valueOf()).toBe(moment().years(2000).valueOf())
+})
+
+it('Months', () => {
+  expect(dayjs().get('months')).toBe(moment().get('months'))
+  expect(dayjs().months()).toBe(moment().months())
+  expect(dayjs().months(0).valueOf()).toBe(moment().months(0).valueOf())
+  expect(dayjs().months(1).valueOf()).toBe(moment().months(1).valueOf())
+})
+
+it('Days of Week', () => {
+  expect(dayjs().get('days')).toBe(moment().get('days'))
+  expect(dayjs().days()).toBe(moment().days())
+  expect(dayjs().days(0).format()).toBe(moment().days(0).format())
+  expect(dayjs().days(1).format()).toBe(moment().days(1).format())
+})
+
+it('Dates', () => {
+  expect(dayjs().get('dates')).toBe(moment().get('dates'))
+  expect(dayjs().dates()).toBe(moment().dates())
+  expect(dayjs().dates(0).valueOf()).toBe(moment().dates(0).valueOf())
+  expect(dayjs().dates(1).valueOf()).toBe(moment().dates(1).valueOf())
+})
+
+it('Hours', () => {
+  expect(dayjs().get('hours')).toBe(moment().get('hours'))
+  expect(dayjs().hours()).toBe(moment().hours())
+  expect(dayjs().hours(0).valueOf()).toBe(moment().hours(0).valueOf())
+  expect(dayjs().hours(1).valueOf()).toBe(moment().hours(1).valueOf())
+})
+
+it('Minutes', () => {
+  expect(dayjs().get('minutes')).toBe(moment().get('minutes'))
+  expect(dayjs().minutes()).toBe(moment().minutes())
+  expect(dayjs().minutes(0).valueOf()).toBe(moment().minutes(0).valueOf())
+  expect(dayjs().minutes(1).valueOf()).toBe(moment().minutes(1).valueOf())
+})
+
+it('Seconds', () => {
+  expect(dayjs().get('seconds')).toBe(moment().get('seconds'))
+  expect(dayjs().seconds()).toBe(moment().seconds())
+  expect(dayjs().seconds(0).valueOf()).toBe(moment().seconds(0).valueOf())
+  expect(dayjs().seconds(1).valueOf()).toBe(moment().seconds(1).valueOf())
+})
+
+it('Milliseconds', () => {
+  expect(dayjs().get('milliseconds')).toBe(moment().get('milliseconds'))
+  expect(dayjs().milliseconds()).toBe(moment().milliseconds())
+  expect(dayjs().milliseconds(0).valueOf()).toBe(moment().milliseconds(0).valueOf())
+  expect(dayjs().milliseconds(1).valueOf()).toBe(moment().milliseconds(1).valueOf())
+})
+
+it('Set Dates', () => {
+  expect(dayjs().date(30).valueOf()).toBe(moment().dates(30).valueOf())
+  expect(dayjs().set('dates', 30).valueOf()).toBe(moment().set('dates', 30).valueOf())
+})
+
+it('Set Days of Week', () => {
+  expect(dayjs().days(0).valueOf()).toBe(moment().days(0).valueOf())
+  expect(dayjs().set('days', 0).valueOf()).toBe(moment().set('days', 0).valueOf())
+})
+
+it('Set Months', () => {
+  expect(dayjs().months(11).valueOf()).toBe(moment().months(11).valueOf())
+  expect(dayjs().set('months', 11).valueOf()).toBe(moment().set('months', 11).valueOf())
+})
+
+it('Set Years', () => {
+  expect(dayjs().years(2008).valueOf()).toBe(moment().year(2008).valueOf())
+  expect(dayjs().set('years', 2008).valueOf()).toBe(moment().set('years', 2008).valueOf())
+})
+
+it('Set Hours', () => {
+  expect(dayjs().set('hours', 6).valueOf()).toBe(moment().set('hours', 6).valueOf())
+  expect(dayjs().hours(6).valueOf()).toBe(moment().hours(6).valueOf())
+})
+
+it('Set Minutes', () => {
+  expect(dayjs().minutes(59).valueOf()).toBe(moment().minutes(59).valueOf())
+  expect(dayjs().set('minutes', 59).valueOf()).toBe(moment().set('minutes', 59).valueOf())
+})
+
+it('Set Seconds', () => {
+  expect(dayjs().seconds(59).valueOf()).toBe(moment().seconds(59).valueOf())
+  expect(dayjs().set('second', 59).valueOf()).toBe(moment().set('second', 59).valueOf())
+})
+
+it('Set Milliseconds', () => {
+  expect(dayjs().milliseconds(999).valueOf()).toBe(moment().milliseconds(999).valueOf())
+  expect(dayjs().set('millisecond', 999).valueOf()).toBe(moment().set('millisecond', 999).valueOf())
+})
+
+it('Set Month and Year in last day of month', () => {
+  // 2011-07-31 -> 2011-02-28
+  const origin = dayjs('2011-07-31T14:48:00.000Z')
+  const setMonth = origin.set('month', 1)
+  expect(setMonth.months()).toBe(1)
+  expect(origin.dates()).toBe(31)
+  expect(setMonth.dates()).toBe(28)
+  // 2000-02-29 -> 2001-02-28
+  const origin2 = dayjs('2000-02-29T14:48:00.000Z')
+  const setYear = origin2.set('years', 2001)
+  expect(setYear.months()).toBe(1)
+  expect(origin2.dates()).toBe(29)
+  expect(setYear.dates()).toBe(28)
+})
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
@@ -16,6 +16,8 @@ afterEach(() => {
 })
 
 const NY = 'America/New_York'
+const VAN = 'America/Vancouver'
+const TOKYO = 'Asia/Tokyo'
 
 describe('Guess', () => {
   it('return string', () => {
@@ -36,6 +38,19 @@ describe('Parse', () => {
     expect(newYork.valueOf()).toBe(MnewYork.valueOf())
   })
 
+  it('parse timestamp, js Date, Day.js object', () => {
+    const d = new Date('2020-08-07T12:00-07:00')
+    const result = '2020-08-07T12:00:00-07:00'
+    const TjsDate = dayjs.tz(d, VAN)
+    const Tdayjs = dayjs.tz(dayjs(d), VAN)
+    const Timestamp = dayjs.tz(d.getTime(), VAN)
+    const Tmoment = moment.tz(d, VAN)
+    expect(TjsDate.format()).toBe(result)
+    expect(Tdayjs.format()).toBe(result)
+    expect(Timestamp.format()).toBe(result)
+    expect(Tmoment.format()).toBe(result)
+  })
+
   it('parse and convert between timezones', () => {
     const newYork = dayjs.tz('2014-06-01 12:00', NY)
     expect(newYork.tz('America/Los_Angeles').format()).toBe('2014-06-01T09:00:00-07:00')
@@ -71,12 +86,19 @@ describe('Convert', () => {
       expect(dec.tz('America/Los_Angeles').format('ha')).toBe('4am')
       expect(jun.tz(NY).format('ha')).toBe('8am')
       expect(dec.tz(NY).format('ha')).toBe('7am')
-      expect(jun.tz('Asia/Tokyo').format('ha')).toBe('9pm')
-      expect(dec.tz('Asia/Tokyo').format('ha')).toBe('9pm')
+      expect(jun.tz(TOKYO).format('ha')).toBe('9pm')
+      expect(dec.tz(TOKYO).format('ha')).toBe('9pm')
       expect(jun.tz('Australia/Sydney').format('ha')).toBe('10pm')
       expect(dec.tz('Australia/Sydney').format('ha')).toBe('11pm')
     })
   })
+
+  it('format Z', () => {
+    [dayjs, moment].forEach((_) => {
+      const t = _('2020-08-06T03:48:10.258Z').tz(TOKYO)
+      expect(t.format('Z')).toBe('+09:00')
+    })
+  })
 })
 
 
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
@@ -219,3 +219,26 @@ describe('UTC Offset', () => {
     expect(dayjs().utc().utcOffset()).toBe(moment().utc().utcOffset())
   })
 })
+
+describe('Diff', () => {
+  const d1 = '2021-06-07'
+  const d2 = '2021-06-06'
+  it('utc.diff(utc)', () => {
+    [dayjs, moment].forEach((_) => {
+      expect(_.utc(d1).diff(_.utc(d2), 'days')).toBe(1)
+      expect(_.utc(d1).diff(_.utc(d2), 'm')).toBe(1440)
+    })
+  })
+  it('local.diff(utc)', () => {
+    expect(dayjs(d1).diff(dayjs.utc(d2), 'days'))
+      .toBe(moment(d1).diff(moment.utc(d2), 'days'))
+    expect(dayjs(d1).diff(dayjs.utc(d2), 'm'))
+      .toBe(moment(d1).diff(moment.utc(d2), 'm'))
+  })
+  it('utc.diff(local)', () => {
+    expect(dayjs.utc(d1).diff(d2, 'days'))
+      .toBe(moment.utc(d1).diff(d2, 'days'))
+    expect(dayjs.utc(d1).diff(d2, 'm'))
+      .toBe(moment.utc(d1).diff(d2, 'm'))
+  })
+})