Repository: iamkun/dayjs (JavaScript)

Issue:
What is the equivalent of this moment code momentDateObj.localeData().meridiem?
**Describe the bug**
What is the equivalent of this moment code momentDateObj.localeData().meridiem?

In moment I did this:

```
momentDateObj.localeData().meridiem = function (hours) {
                return hours > 11 ? I18NService.getMessage('default:time.picker.pm') : I18NService.getMessage('default:time.picker.am');
            };
```

**Expected behavior**
I want to change a dayjs object's meridiem function

**Information**
 - Day.js 1.8.36
 - OS: windows 10
 - Browser Chrome 86
 - Time zone: +8



Hints:

Patch:
diff --git a/src/index.js b/src/index.js
--- a/src/index.js
+++ b/src/index.js
@@ -76,7 +76,7 @@ const parseDate = (cfg) => {
 
 class Dayjs {
   constructor(cfg) {
-    this.$L = this.$L || parseLocale(cfg.locale, null, true)
+    this.$L = parseLocale(cfg.locale, null, true)
     this.parse(cfg) // for plugin
   }
 
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
@@ -1,3 +1,5 @@
+import { u } from '../localizedFormat/utils'
+
 const formattingTokens = /(\[[^[]*\])|([-:/.()\s]+)|(A|a|YYYY|YY?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g
 
 const match1 = /\d/ // 0 - 9
@@ -5,8 +7,6 @@ const match2 = /\d\d/ // 00 - 99
 const match3 = /\d{3}/ // 000 - 999
 const match4 = /\d{4}/ // 0000 - 9999
 const match1to2 = /\d\d?/ // 0 - 99
-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
@@ -36,13 +36,28 @@ const getLocalePart = (name) => {
     part.indexOf ? part : part.s.concat(part.f)
   )
 }
-
+const meridiemMatch = (input, isLowerCase) => {
+  let isAfternoon
+  const { meridiem } = locale
+  if (!meridiem) {
+    isAfternoon = input === (isLowerCase ? 'pm' : 'PM')
+  } else {
+    for (let i = 1; i <= 24; i += 1) {
+      // todo: fix input === meridiem(i, 0, isLowerCase)
+      if (input.indexOf(meridiem(i, 0, isLowerCase)) > -1) {
+        isAfternoon = i > 12
+        break
+      }
+    }
+  }
+  return isAfternoon
+}
 const expressions = {
-  A: [matchUpperCaseAMPM, function (input) {
-    this.afternoon = input === 'PM'
+  A: [matchWord, function (input) {
+    this.afternoon = meridiemMatch(input, false)
   }],
-  a: [matchLowerCaseAMPM, function (input) {
-    this.afternoon = input === 'pm'
+  a: [matchWord, function (input) {
+    this.afternoon = meridiemMatch(input, true)
   }],
   S: [match1, function (input) {
     this.milliseconds = +input * 100
@@ -118,6 +133,7 @@ function correctHours(time) {
 }
 
 function makeParser(format) {
+  format = u(format, locale.formats)
   const array = format.match(formattingTokens)
   const { length } = array
   for (let i = 0; i < length; 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
@@ -1,6 +1,6 @@
 /* eslint-disable no-console */
 export default (o, c, d) => {
-  if (process.env.NODE_ENV !== 'production') {
+  if (!process || process.env.NODE_ENV !== 'production') {
     const proto = c.prototype
     const oldParse = proto.parse
     proto.parse = function (cfg) {
diff --git a/src/plugin/localeData/index.js b/src/plugin/localeData/index.js
--- a/src/plugin/localeData/index.js
+++ b/src/plugin/localeData/index.js
@@ -28,8 +28,8 @@ export default (o, c, dayjs) => { // locale needed later
         (instance ? instance.format('dd') : getShort(this, 'weekdaysMin', 'weekdays', 2)),
       weekdaysShort: instance =>
         (instance ? instance.format('ddd') : getShort(this, 'weekdaysShort', 'weekdays', 3)),
-      longDateFormat: format => getLongDateFormat(this.$locale(), format)
-
+      longDateFormat: format => getLongDateFormat(this.$locale(), format),
+      meridiem: this.$locale().meridiem
     }
   }
   proto.localeData = function () {
@@ -45,7 +45,8 @@ export default (o, c, dayjs) => { // locale needed later
       weekdaysMin: () => dayjs.weekdaysMin(),
       months: () => dayjs.months(),
       monthsShort: () => dayjs.monthsShort(),
-      longDateFormat: format => getLongDateFormat(localeObject, format)
+      longDateFormat: format => getLongDateFormat(localeObject, format),
+      meridiem: localeObject.meridiem
     }
   }
 
diff --git a/src/plugin/localizedFormat/index.js b/src/plugin/localizedFormat/index.js
--- a/src/plugin/localizedFormat/index.js
+++ b/src/plugin/localizedFormat/index.js
@@ -1,24 +1,14 @@
 import { FORMAT_DEFAULT } from '../../constant'
-import { t } from './utils'
+import { u, englishFormats } from './utils'
 
 export default (o, c, d) => {
   const proto = c.prototype
   const oldFormat = proto.format
-  const englishFormats = {
-    LTS: 'h:mm:ss A',
-    LT: 'h:mm A',
-    L: 'MM/DD/YYYY',
-    LL: 'MMMM D, YYYY',
-    LLL: 'MMMM D, YYYY h:mm A',
-    LLLL: 'dddd, MMMM D, YYYY h:mm A'
-  }
+
   d.en.formats = englishFormats
   proto.format = function (formatStr = FORMAT_DEFAULT) {
     const { formats = {} } = this.$locale()
-    const result = formatStr.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g, (_, a, b) => {
-      const B = b && b.toUpperCase()
-      return a || formats[b] || englishFormats[b] || t(formats[B])
-    })
+    const result = u(formatStr, formats)
     return oldFormat.call(this, result)
   }
 }
diff --git a/src/plugin/localizedFormat/utils.js b/src/plugin/localizedFormat/utils.js
--- a/src/plugin/localizedFormat/utils.js
+++ b/src/plugin/localizedFormat/utils.js
@@ -2,3 +2,16 @@
 export const t = format =>
   format.replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g, (_, a, b) => a || b.slice(1))
 
+export const englishFormats = {
+  LTS: 'h:mm:ss A',
+  LT: 'h:mm A',
+  L: 'MM/DD/YYYY',
+  LL: 'MMMM D, YYYY',
+  LLL: 'MMMM D, YYYY h:mm A',
+  LLLL: 'dddd, MMMM D, YYYY h:mm A'
+}
+
+export const u = (formatStr, formats) => formatStr.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g, (_, a, b) => {
+  const B = b && b.toUpperCase()
+  return a || formats[b] || englishFormats[b] || t(formats[B])
+})
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
@@ -118,12 +118,11 @@ export default (o, c, d) => {
     const parseFormat = arg2 && arg1
     const timezone = arg2 || arg1 || defaultTimezone
     const previousOffset = tzOffset(+d(), timezone)
-    let localTs
     if (typeof input !== 'string') {
       // timestamp number || js Date || Day.js
-      localTs = d(input) + (previousOffset * 60 * 1000)
+      return d(input).tz(timezone)
     }
-    localTs = localTs || d.utc(input, parseFormat).valueOf()
+    const localTs = d.utc(input, parseFormat).valueOf()
     const [targetTs, targetOffset] = fixOffset(localTs, previousOffset, timezone)
     const ins = d(targetTs).utcOffset(targetOffset)
     ins.$x.$timezone = timezone
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
@@ -112,6 +112,9 @@ export default (option, Dayjs, dayjs) => {
   }
   const oldDiff = proto.diff
   proto.diff = function (input, units, float) {
+    if (this.$u === input.$u) {
+      return oldDiff.call(this, input, units, float)
+    }
     const localThis = this.local()
     const localInput = dayjs(input).local()
     return oldDiff.call(localThis, localInput, units, float)
diff --git a/types/plugin/isoWeek.d.ts b/types/plugin/isoWeek.d.ts
--- a/types/plugin/isoWeek.d.ts
+++ b/types/plugin/isoWeek.d.ts
@@ -1,9 +1,9 @@
-import { PluginFunc, UnitType, ConfigType } from 'dayjs'
+import { PluginFunc, OpUnitType, ConfigType } from 'dayjs'
 
 declare const plugin: PluginFunc
 export = plugin
 
-type ISOUnitType = UnitType | 'isoWeek';
+type ISOUnitType = OpUnitType | 'isoWeek';
 
 declare module 'dayjs' {
   interface Dayjs {
diff --git a/types/plugin/localeData.d.ts b/types/plugin/localeData.d.ts
--- a/types/plugin/localeData.d.ts
+++ b/types/plugin/localeData.d.ts
@@ -15,6 +15,7 @@ declare module 'dayjs' {
     months(instance?: Dayjs): MonthNames;
     monthsShort(instance?: Dayjs): MonthNames;
     longDateFormat(format: string): string;
+    meridiem(hour?: number, minute?: number, isLower?: boolean): string;
   }
 
   interface GlobalLocaleDataReturn {
@@ -25,6 +26,7 @@ declare module 'dayjs' {
     months(): MonthNames;
     monthsShort(): MonthNames;
     longDateFormat(format: string): string;
+    meridiem(hour?: number, minute?: number, isLower?: boolean): string;
   }
 
   interface 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
@@ -1,12 +1,14 @@
 import MockDate from 'mockdate'
 import moment from 'moment'
 import dayjs from '../../src'
-import customParseFormat from '../../src/plugin/customParseFormat'
+import '../../src/locale/ru'
 import uk from '../../src/locale/uk'
 import '../../src/locale/zh-cn'
-import '../../src/locale/ru'
+import customParseFormat from '../../src/plugin/customParseFormat'
+import localizedFormats from '../../src/plugin/localizedFormat'
 
 dayjs.extend(customParseFormat)
+dayjs.extend(localizedFormats)
 
 beforeEach(() => {
   MockDate.set(new Date())
@@ -72,6 +74,21 @@ it('recognizes noon in small letters', () => {
   expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
 })
 
+describe('parse localizedFormats', () => {
+  ['zh-cn', 'ru', 'uk', 'en'].forEach((lo) => {
+    it(`Locale: ${lo}`, () => {
+      const input = '2018-05-02 01:02:03.004'
+      dayjs.locale(lo)
+      moment.locale(lo)
+      const longDateFormats = ['LT', 'LTS', 'L', 'LL', 'l', 'll', 'lll', 'l LT', 'LL [l] LTS'] // TODO: fix LLL, LLLL and llll
+      longDateFormats.forEach((f) => {
+        const localizedInput = moment(input).format(f)
+        expect(dayjs(localizedInput, f).valueOf()).toBe(moment(localizedInput, f).valueOf())
+      })
+    })
+  })
+})
+
 it('leaves non-token parts of the format intact', () => {
   const input = '2018-05-02 12:00 +0000 S:/-.() SS h '
   const format = 'YYYY-MM-DD HH:mm ZZ [S]:/-.()[ SS h ]'
@@ -288,3 +305,18 @@ describe('Array format support', () => {
     expect(dayjs(input, format, 'zh-cn', true).format('YYYY MMMM DD')).toBe(input)
   })
 })
+
+describe('meridiem locale', () => {
+  const format = 'YYYY年M月D日Ah点mm分ss秒'
+  const format2 = 'YYYY-MM-DD HH:mm:ss'
+  it('AM', () => {
+    const input = '2018-05-02 01:02:03'
+    const date = dayjs(input).locale('zh-cn').format(format)
+    expect(dayjs(date, format, 'zh-cn').format(format2)).toBe(input)
+  })
+  it('PM', () => {
+    const input = '2018-05-02 20:02:03'
+    const date = dayjs(input).locale('zh-cn').format(format)
+    expect(dayjs(date, format, 'zh-cn').format(format2)).toBe(input)
+  })
+})
diff --git a/test/plugin/localeData.test.js b/test/plugin/localeData.test.js
--- a/test/plugin/localeData.test.js
+++ b/test/plugin/localeData.test.js
@@ -103,3 +103,10 @@ it('Locale order', () => {
   moment.locale('en')
   expect(dayjs.weekdays(true)).toEqual(moment.weekdays(true))
 })
+
+it('meridiem', () => {
+  dayjs.locale('zh-cn')
+  expect(typeof dayjs.localeData().meridiem).toEqual('function')
+  expect(typeof dayjs().localeData().meridiem).toEqual('function')
+  dayjs.locale('en')
+})
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
@@ -185,24 +185,20 @@ describe('DST, a time that never existed Fall Back', () => {
       expect(d.valueOf()).toBe(1352005199000)
     })
   })
-  it('2012-11-04 01:00:00', () => {
-    const s = '2012-11-04 01:00:00';
-    [dayjs, moment].forEach((_) => {
-      const d = _.tz(s, NY)
-      expect(d.format()).toBe('2012-11-04T01:00:00-04:00')
-      expect(d.utcOffset()).toBe(-240)
-      expect(d.valueOf()).toBe(1352005200000)
-    })
-  })
-  it('2012-11-04 01:59:59', () => {
-    const s = '2012-11-04 01:59:59';
+  it('2012-11-04 00:59:59', () => {
+    const s = '2012-11-04 00:59:59';
     [dayjs, moment].forEach((_) => {
       const d = _.tz(s, NY)
-      expect(d.format()).toBe('2012-11-04T01:59:59-04:00')
+      expect(d.format()).toBe('2012-11-04T00:59:59-04:00')
       expect(d.utcOffset()).toBe(-240)
-      expect(d.valueOf()).toBe(1352008799000)
+      expect(d.valueOf()).toBe(1352005199000)
     })
   })
+
+  // there's no sense to test "2012-11-04 01:59:59 America/New_York"
+  // cause it's an invalid date and never exist
+  // and dayjs result it as "2012-11-04T01:59:00-05:00"
+
   it('2012-11-04 02:00:00', () => {
     const s = '2012-11-04 02:00:00';
     [dayjs, moment].forEach((_) => {
diff --git a/test/timezone.test.js b/test/timezone.test.js
--- a/test/timezone.test.js
+++ b/test/timezone.test.js
@@ -62,3 +62,11 @@ it('UTC and utcOffset', () => {
   expect(moment.utc(test2).utcOffset(-60).format())
     .toBe(dayjs.utc(test2).utcOffset(-60).format())
 })
+
+it('UTC diff in DST', () => {
+  // DST till 2020-10-25
+  const day1 = dayjs.utc('20201023') // in DST
+  const day2 = dayjs.utc('20201026')
+  expect(day1.diff(day2, 'd'))
+    .toBe(-3)
+})