Repository: iamkun/dayjs (JavaScript)

Issue:
weekOfYear shows wrong calender week at the turn of the year
Describe the bug
The 2020-12-30 should be calender week 53 but it shows as calender week 1.
(similar issue: #760)

Calender weeks:
http://www.whatweekisit.org/

Information

Day.js Version 1.9.7
Browser Chrome latest


Hints:
Hi, have you set the correct locale?
yes, it is the same issue when i set 'us' | 'de'
https://codepen.io/tompach/pen/OJRRpNE?editors=0012

Patch:
diff --git a/src/locale/de.js b/src/locale/de.js
--- a/src/locale/de.js
+++ b/src/locale/de.js
@@ -32,6 +32,7 @@ const locale = {
   monthsShort: 'Jan_Feb_März_Apr_Mai_Juni_Juli_Aug_Sept_Okt_Nov_Dez'.split('_'),
   ordinal: n => `${n}.`,
   weekStart: 1,
+  yearStart: 4,
   formats: {
     LTS: 'HH:mm:ss',
     LT: 'HH:mm',
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|zzz|z|gggg|Do|X|x|k{1,2}|S/g, (match) => {
+    const result = str.replace(/\[([^\]]+)]|Q|wo|ww|w|WW|W|zzz|z|gggg|GGGG|Do|X|x|k{1,2}|S/g, (match) => {
       switch (match) {
         case 'Q':
           return Math.ceil((this.$M + 1) / 3)
@@ -21,11 +21,16 @@ export default (o, c, d) => { // locale needed later
           return locale.ordinal(this.$D)
         case 'gggg':
           return this.weekYear()
+        case 'GGGG':
+          return this.isoWeekYear()
         case 'wo':
           return locale.ordinal(this.week(), 'W') // W for week
         case 'w':
         case 'ww':
           return utils.s(this.week(), match === 'w' ? 1 : 2, '0')
+        case 'W':
+        case 'WW':
+          return utils.s(this.isoWeek(), match === 'W' ? 1 : 2, '0')
         case 'k':
         case 'kk':
           return utils.s(String(this.$H === 0 ? 24 : this.$H), match === 'k' ? 1 : 2, '0')
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
@@ -11,7 +11,7 @@ const matchSigned = /[+-]?\d+/ // -inf - inf
 const matchOffset = /[+-]\d\d:?(\d\d)?/ // +00:00 -00:00 +0000 or -0000 +00
 const matchWord = /\d*[^\s\d-:/()]+/ // Word
 
-let locale
+let locale = {}
 
 function offsetFromString(string) {
   if (!string) return 0
@@ -216,8 +216,9 @@ export default (o, C, d) => {
       const isStrict = isStrictWithoutLocale || isStrictWithLocale
       let pl = args[2]
       if (isStrictWithLocale) [,, pl] = args
-      if (!isStrictWithoutLocale) {
-        locale = pl ? d.Ls[pl] : this.$locale()
+      locale = this.$locale()
+      if (!isStrictWithoutLocale && pl) {
+        locale = d.Ls[pl]
       }
       this.$d = parseFormattedInput(date, format, utc)
       this.init()
@@ -226,7 +227,7 @@ export default (o, C, d) => {
         this.$d = new Date('')
       }
       // reset global locale to make parallel unit test
-      locale = undefined
+      locale = {}
     } else if (format instanceof Array) {
       const len = format.length
       for (let i = 1; i <= len; i += 1) {
diff --git a/src/plugin/devHelper/index.js b/src/plugin/devHelper/index.js
--- a/src/plugin/devHelper/index.js
+++ b/src/plugin/devHelper/index.js
@@ -16,6 +16,15 @@ export default (o, c, d) => {
       }
       return oldParse.bind(this)(cfg)
     }
+    const oldLocale = d.locale
+    d.locale = function (preset, object, isLocal) {
+      if (typeof object === 'undefined' && typeof preset === 'string') {
+        if (!d.Ls[preset]) {
+          console.warn(`Guessing you may want to use locale ${preset}, you have to load it before using it. https://day.js.org/docs/en/i18n/loading-into-nodejs`)
+        }
+      }
+      return oldLocale(preset, object, isLocal)
+    }
   }
 }
 
diff --git a/src/plugin/weekYear/index.js b/src/plugin/weekYear/index.js
--- a/src/plugin/weekYear/index.js
+++ b/src/plugin/weekYear/index.js
@@ -7,6 +7,9 @@ export default (o, c) => {
     if (weekOfYear === 1 && month === 11) {
       return year + 1
     }
+    if (month === 0 && weekOfYear >= 52) {
+      return year - 1
+    }
     return year
   }
 }


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
@@ -2,6 +2,7 @@ import MockDate from 'mockdate'
 import moment from 'moment'
 import dayjs from '../../src'
 import advancedFormat from '../../src/plugin/advancedFormat'
+import isoWeek from '../../src/plugin/isoWeek'
 import weekOfYear from '../../src/plugin/weekOfYear'
 import weekYear from '../../src/plugin/weekYear'
 import timezone from '../../src/plugin/timezone'
@@ -10,6 +11,7 @@ import '../../src/locale/zh-cn'
 
 dayjs.extend(utc)
 dayjs.extend(timezone)
+dayjs.extend(isoWeek)
 dayjs.extend(weekYear)
 dayjs.extend(weekOfYear)
 dayjs.extend(advancedFormat)
@@ -82,11 +84,29 @@ it('Format Week of Year wo', () => {
     .toBe(moment(d).locale('zh-cn').format('wo'))
 })
 
+it('Format Week of Year wo', () => {
+  const d = '2018-12-01'
+  expect(dayjs(d).format('wo')).toBe(moment(d).format('wo'))
+  expect(dayjs(d).locale('zh-cn').format('wo'))
+    .toBe(moment(d).locale('zh-cn').format('wo'))
+})
+
 it('Format Week Year gggg', () => {
   const d = '2018-12-31'
   expect(dayjs(d).format('gggg')).toBe(moment(d).format('gggg'))
 })
 
+it('Format Iso Week Year GGGG', () => {
+  const d = '2021-01-01'
+  expect(dayjs(d).format('GGGG')).toBe(moment(d).format('GGGG'))
+})
+
+it('Format Iso Week of Year', () => {
+  const d = '2021-01-01'
+  expect(dayjs(d).format('W')).toBe(moment(d).format('W'))
+  expect(dayjs(d).format('WW')).toBe(moment(d).format('WW'))
+})
+
 it('Format offsetName z zzz', () => {
   const dtz = dayjs.tz('2012-03-11 01:59:59', 'America/New_York')
   expect(dtz.format('z')).toBe('EST')
@@ -99,6 +119,11 @@ it('Skips format strings inside brackets', () => {
   expect(dayjs().format('[Q]')).toBe('Q')
   expect(dayjs().format('[Do]')).toBe('Do')
   expect(dayjs().format('[gggg]')).toBe('gggg')
+  expect(dayjs().format('[GGGG]')).toBe('GGGG')
+  expect(dayjs().format('[w]')).toBe('w')
+  expect(dayjs().format('[ww]')).toBe('ww')
+  expect(dayjs().format('[W]')).toBe('W')
+  expect(dayjs().format('[WW]')).toBe('WW')
   expect(dayjs().format('[wo]')).toBe('wo')
   expect(dayjs().format('[k]')).toBe('k')
   expect(dayjs().format('[kk]')).toBe('kk')
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
@@ -294,6 +294,8 @@ describe('Strict mode', () => {
     const format = 'YYYY-MM-DD'
     expect(dayjs(input, format).isValid()).toBe(true)
     expect(dayjs(input, format, true).isValid()).toBe(false)
+    expect(dayjs('2020-Jan-01', 'YYYY-MMM-DD', true).isValid()).toBe(true)
+    expect(dayjs('30/1/2020 10:59 PM', 'D/M/YYYY h:mm A', true).isValid()).toBe(true)
   })
   it('with locale', () => {
     const input = '2018 三月 99'
diff --git a/test/plugin/devHelper.test.js b/test/plugin/devHelper.test.js
--- a/test/plugin/devHelper.test.js
+++ b/test/plugin/devHelper.test.js
@@ -32,3 +32,9 @@ it('Warning Enable customParseFormat plugin while passing the second format para
   dayjs('2020', 'YYYY')
   expect(consoleSpy).toHaveBeenCalledWith('To parse a date-time string like 2020 using the given format, you should enable customParseFormat plugin first. https://day.js.org/docs/en/parse/string-format')
 })
+
+it('Warning: Setting locale before loading locale', () => {
+  const consoleSpy = jest.spyOn(console, 'warn')
+  dayjs.locale('zh-cn')
+  expect(consoleSpy).toHaveBeenCalledWith('Guessing you may want to use locale zh-cn, you have to load it before using it. https://day.js.org/docs/en/i18n/loading-into-nodejs')
+})
diff --git a/test/plugin/weekYear.test.js b/test/plugin/weekYear.test.js
--- a/test/plugin/weekYear.test.js
+++ b/test/plugin/weekYear.test.js
@@ -3,6 +3,7 @@ import MockDate from 'mockdate'
 import dayjs from '../../src'
 import weekYear from '../../src/plugin/weekYear'
 import weekOfYear from '../../src/plugin/weekOfYear'
+import '../../src/locale/en-gb'
 
 dayjs.extend(weekYear)
 dayjs.extend(weekOfYear)
@@ -24,7 +25,25 @@ it('Week Year', () => {
   ]
   daySet.forEach((d) => {
     const [day, result] = d
-    expect(dayjs(day).weekYear()).toBe(result)
-    expect(dayjs(day).weekYear()).toBe(moment(day).weekYear())
+    const dResult = dayjs(day).weekYear()
+    expect(dResult).toBe(result)
+    expect(dResult).toBe(moment(day).weekYear())
+  })
+})
+
+it('yearStart: 4', () => {
+  const daySet = [
+    ['2020-12-31', 2020],
+    ['2021-01-01', 2020],
+    ['2021-01-02', 2020],
+    ['2021-01-03', 2020],
+    ['2021-01-04', 2021],
+    ['2021-01-05', 2021]
+  ]
+  daySet.forEach((d) => {
+    const [day, result] = d
+    const dResult = dayjs(day).locale('en-gb').weekYear()
+    expect(dResult).toBe(result)
+    expect(dResult).toBe(moment(day).locale('en-gb').weekYear())
   })
 })