Repository: iamkun/dayjs (JavaScript)

Issue:
Typescript definition allows `quarter` as unit of time
**Describe the bug**

According to the typescript definition, this is valid:

```ts
val now = dayjs()
val nextQuarter = now.add(1, 'quarter')

now.valueOf() == nextQuarter.valueOf() // true
```

**Expected behavior**
Either the quarter calculation should work or the typescript definition should not allow passing `'quarter'` as unit of time.

**Information**
 - Day.js Version: 1.8.10



Hints:
Thanks, will fix soon

Patch:
diff --git a/src/locale/az.js b/src/locale/az.js
new file mode 100644
--- /dev/null
+++ b/src/locale/az.js
@@ -0,0 +1,39 @@
+import dayjs from 'dayjs'
+
+const locale = {
+  name: 'az',
+  weekdays: 'Bazar_Bazar ertəsi_Çərşənbə axşamı_Çərşənbə_Cümə axşamı_Cümə_Şənbə'.split('_'),
+  weekdaysShort: 'Baz_BzE_ÇAx_Çər_CAx_Cüm_Şən'.split('_'),
+  weekdaysMin: 'Bz_BE_ÇA_Çə_CA_Cü_Şə'.split('_'),
+  months: 'yanvar_fevral_mart_aprel_may_iyun_iyul_avqust_sentyabr_oktyabr_noyabr_dekabr'.split('_'),
+  monthsShort: 'yan_fev_mar_apr_may_iyn_iyl_avq_sen_okt_noy_dek'.split('_'),
+  weekStart: 1,
+  formats: {
+    LT: 'H:mm',
+    LTS: 'H:mm:ss',
+    L: 'DD.MM.YYYY',
+    LL: 'D MMMM YYYY г.',
+    LLL: 'D MMMM YYYY г., H:mm',
+    LLLL: 'dddd, D MMMM YYYY г., H:mm'
+  },
+  relativeTime: {
+    future: '%s sonra',
+    past: '%s əvvəl',
+    s: 'bir neçə saniyə',
+    m: 'bir dəqiqə',
+    mm: '%d dəqiqə',
+    h: 'bir saat',
+    hh: '%d saat',
+    d: 'bir gün',
+    dd: '%d gün',
+    M: 'bir ay',
+    MM: '%d ay',
+    y: 'bir il',
+    yy: '%d il'
+  },
+  ordinal: n => n
+}
+
+dayjs.locale(locale, null, true)
+
+export default locale
diff --git a/src/locale/pt.js b/src/locale/pt.js
--- a/src/locale/pt.js
+++ b/src/locale/pt.js
@@ -3,9 +3,19 @@ import dayjs from 'dayjs'
 const locale = {
   name: 'pt',
   weekdays: 'Domingo_Segunda-feira_Terça-feira_Quarta-feira_Quinta-feira_Sexta-feira_Sábado'.split('_'),
+  weekdaysShort: 'Dom_Seg_Ter_Qua_Qui_Sex_Sab'.split('_'),
   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}º`,
   weekStart: 1,
+  formats: {
+    LT: 'HH:mm',
+    LTS: 'HH:mm:ss',
+    L: 'DD/MM/YYYY',
+    LL: 'D [de] MMMM [de] YYYY',
+    LLL: 'D [de] MMMM [de] YYYY [às] HH:mm',
+    LLLL: 'dddd, D [de] MMMM [de] YYYY [às] HH:mm'
+  },
   relativeTime: {
     future: 'em %s',
     past: 'há %s',
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
@@ -9,7 +9,7 @@ const matchUpperCaseAMPM = /[AP]M/
 const matchLowerCaseAMPM = /[ap]m/
 const matchSigned = /[+-]?\d+/ // -inf - inf
 const matchOffset = /[+-]\d\d:?\d\d/ // +00:00 -00:00 +0000 or -0000
-const matchWord = /\d*[^\s\d]+/ // Word
+const matchWord = /\d*[^\s\d-:/.()]+/ // Word
 
 let locale
 
diff --git a/src/plugin/quarterOfYear/index.js b/src/plugin/quarterOfYear/index.js
--- a/src/plugin/quarterOfYear/index.js
+++ b/src/plugin/quarterOfYear/index.js
@@ -1,6 +1,35 @@
+import { Q, M, D } from '../../constant'
+
 export default (o, c) => {
   const proto = c.prototype
-  proto.quarter = function () {
+  proto.quarter = function (quarter) {
+    if (!this.$utils().u(quarter)) {
+      return this.add((quarter - 1) * 3, M)
+    }
     return Math.ceil((this.month() + 1) / 3)
   }
+
+  const oldAdd = proto.add
+  proto.add = function (number, units) {
+    number = Number(number) // eslint-disable-line no-param-reassign
+    const unit = this.$utils().p(units)
+    if (unit === Q) {
+      return this.add(number * 3, M)
+    }
+    return oldAdd.bind(this)(number, units)
+  }
+
+  const oldStartOf = proto.startOf
+  proto.startOf = function (units, startOf) {
+    const utils = this.$utils()
+    const isStartOf = !utils.u(startOf) ? startOf : true
+    const unit = utils.p(units)
+    if (unit === Q) {
+      const quarter = this.quarter() - 1
+      return isStartOf ? this.month(quarter * 3)
+        .startOf(M).startOf(D) :
+        this.month((quarter * 3) + 2).endOf(M).endOf(D)
+    }
+    return oldStartOf.bind(this)(units, startOf)
+  }
 }
diff --git a/src/utils.js b/src/utils.js
--- a/src/utils.js
+++ b/src/utils.js
@@ -35,7 +35,8 @@ const prettyUnit = (u) => {
     h: C.H,
     m: C.MIN,
     s: C.S,
-    ms: C.MS
+    ms: C.MS,
+    Q: C.Q
   }
   return special[u] || String(u || '').toLowerCase().replace(/s$/, '')
 }
diff --git a/types/index.d.ts b/types/index.d.ts
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -7,10 +7,10 @@ declare namespace dayjs {
   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;
+  export type UnitType = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' | 'date' | UnitTypeShort;
 
-  type OpUnitTypeShort = 'w'
-  export type OpUnitType = UnitType | "week" | OpUnitTypeShort;
+  export type OpUnitType = UnitType | "week" | 'w';
+  export type QUnitType = UnitType | "quarter" | 'Q';
 
   class Dayjs {
     constructor (config?: ConfigType)
@@ -63,7 +63,7 @@ declare namespace dayjs {
 
     format(template?: string): string
 
-    diff(date: ConfigType, unit: OpUnitType, float?: boolean): number
+    diff(date: ConfigType, unit: QUnitType, float?: boolean): number
 
     valueOf(): number
 
diff --git a/types/plugin/quarterOfYear.d.ts b/types/plugin/quarterOfYear.d.ts
--- a/types/plugin/quarterOfYear.d.ts
+++ b/types/plugin/quarterOfYear.d.ts
@@ -1,4 +1,4 @@
-import { PluginFunc } from 'dayjs'
+import { PluginFunc, QUnitType } from 'dayjs'
 
 declare const plugin: PluginFunc
 export = plugin
@@ -6,5 +6,15 @@ export = plugin
 declare module 'dayjs' {
   interface Dayjs {
     quarter(): number
+    
+    quarter(quarter: number): Dayjs
+
+    add(value: number, unit: QUnitType): Dayjs
+
+    subtract(value: number, unit: QUnitType): Dayjs
+
+    startOf(unit: QUnitType): Dayjs
+
+    endOf(unit: QUnitType): Dayjs
   }
 }


Test patch:
diff --git a/test/plugin/customParseFormat.test.js b/test/plugin/customParseFormat.test.js
--- a/test/plugin/customParseFormat.test.js
+++ b/test/plugin/customParseFormat.test.js
@@ -26,6 +26,12 @@ it('parse padded string', () => {
   expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
 })
 
+it('parse string for MMM month format', () => {
+  const input = '4/Mar/2019:11:16:26 +0800'
+  const format = 'D/MMM/YYYY:H:m:s zz'
+  expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
+})
+
 it('parse string January (getMonth() = 0)', () => {
   const input = '01/01/2019'
   const format = 'DD/MM/YYYY'
diff --git a/test/plugin/quarterOfYear.test.js b/test/plugin/quarterOfYear.test.js
--- a/test/plugin/quarterOfYear.test.js
+++ b/test/plugin/quarterOfYear.test.js
@@ -1,4 +1,5 @@
 import MockDate from 'mockdate'
+import moment from 'moment'
 import dayjs from '../../src'
 import quarterOfYear from '../../src/plugin/quarterOfYear'
 
@@ -12,7 +13,7 @@ afterEach(() => {
   MockDate.reset()
 })
 
-it('QuarterOfYear', () => {
+it('get QuarterOfYear', () => {
   expect(dayjs('2013-01-01T00:00:00.000').quarter()).toBe(1)
   expect(dayjs('2013-04-01T00:00:00.000').subtract(1, 'ms').quarter()).toBe(1)
   expect(dayjs('2013-04-01T00:00:00.000').quarter()).toBe(2)
@@ -22,3 +23,26 @@ it('QuarterOfYear', () => {
   expect(dayjs('2013-10-01T00:00:00.000').quarter()).toBe(4)
   expect(dayjs('2014-01-01T00:00:00.000').subtract(1, 'ms').quarter()).toBe(4)
 })
+
+it('set QuarterOfYear', () => {
+  const d1 = '2013-01-01T00:00:00.000'
+  expect(dayjs(d1).quarter(2).format())
+    .toBe(moment(d1).quarter(2).format())
+  const d2 = '2013-02-05T05:06:07.000'
+  expect(dayjs(d2).quarter(2).format())
+    .toBe(moment(d2).quarter(2).format())
+})
+
+it('add subtract quarter', () => {
+  expect(dayjs().add(2, 'quarter').format())
+    .toBe(moment().add(2, 'quarter').format())
+  expect(dayjs().subtract(2, 'quarter').format())
+    .toBe(moment().subtract(2, 'quarter').format())
+})
+
+it('startOf endOf quarter', () => {
+  expect(dayjs().startOf('quarter').format())
+    .toBe(moment().startOf('quarter').format())
+  expect(dayjs().endOf('quarter').format())
+    .toBe(moment().endOf('quarter').format())
+})
diff --git a/test/utils.test.js b/test/utils.test.js
--- a/test/utils.test.js
+++ b/test/utils.test.js
@@ -8,6 +8,9 @@ it('PrettyUnit', () => {
   expect(prettyUnit('Days')).toBe('day')
   expect(prettyUnit('days')).toBe('day')
   expect(prettyUnit('day')).toBe('day')
+  expect(prettyUnit('Q')).toBe('quarter')
+  expect(prettyUnit('quarter')).toBe('quarter')
+  expect(prettyUnit('quarters')).toBe('quarter')
   expect(prettyUnit()).toBe('')
 })