Validating Custom Date Format Parsing in dayjs
This test suite validates the custom parse format plugin functionality in dayjs, focusing on date string parsing with various format patterns and locale support. The tests ensure accurate date parsing across different format tokens, timezones, and localization scenarios.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
iamkun/dayjs
test/plugin/customParseFormat.test.js
import MockDate from 'mockdate'
import moment from 'moment'
import dayjs from '../../src'
import '../../src/locale/ru'
import uk from '../../src/locale/uk'
import '../../src/locale/zh-cn'
import customParseFormat from '../../src/plugin/customParseFormat'
import advancedFormat from '../../src/plugin/advancedFormat'
import localizedFormats from '../../src/plugin/localizedFormat'
import weekOfYear from '../../src/plugin/weekOfYear'
dayjs.extend(customParseFormat)
dayjs.extend(localizedFormats)
dayjs.extend(weekOfYear) // test parse w, ww
beforeEach(() => {
MockDate.set(new Date())
})
afterEach(() => {
MockDate.reset()
})
it('does not break the built-in parsing', () => {
const input = '2018-05-02 01:02:03.004'
expect(dayjs(input).valueOf()).toBe(moment(input).valueOf())
})
it('parse padded string', () => {
const input = '2018-05-02 01:02:03.004 AM +01:00'
const format = 'YYYY-MM-DD HH:mm:ss.SSS A Z'
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())
const input2 = '21-Dec-18'
const format2 = 'D-MMM-YY'
expect(dayjs(input2, format2).valueOf()).toBe(moment(input2, format2).valueOf())
})
it('parse string January (getMonth() = 0)', () => {
const input = '01/01/2019'
const format = 'DD/MM/YYYY'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
it('parse unpadded string', () => {
const input = '2.5.18 1:2:3.4 PM -0100'
const format = 'D.M.YY H:m:s.S A ZZ'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
it('parse time zone abbreviation', () => {
const input = '05/02/69 1:02:03.004 AM +01:00 (CET)'
const format = 'MM/DD/YY h:mm:ss.SSS A Z (z)'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
it('parse time zone abbreviation2', () => {
const input = '05/02/69 1:02:03.04 AM +01:00 (CET)'
const format = 'MM/DD/YY h:mm:ss.SS A Z (z)'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
it('recognizes midnight in small letters', () => {
const input = '2018-05-02 12:00 am'
const format = 'YYYY-MM-DD hh:mm a'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
it('recognizes noon in small letters', () => {
const input = '2018-05-02 12:00 pm'
const format = 'YYYY-MM-DD hh:mm a'
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 ]'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
it('timezone with no hour', () => {
const input = '2018-05-02 +0000'
const format = 'YYYY-MM-DD ZZ'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
describe('Timezone Offset', () => {
it('timezone with 2-digit offset', () => {
const input = '2020-12-01T20:00:00+09'
const format = 'YYYY-MM-DD[T]HH:mm:ssZZ'
const result = dayjs(input, format)
expect(result.valueOf()).toBe(moment(input, format).valueOf())
expect(result.valueOf()).toBe(1606820400000)
})
it('zulu', () => {
const input = '2021-01-26T15:38:43.000Z'
const format = 'YYYY-MM-DDTHH:mm:ss.SSSZ'
const result = dayjs(input, format)
expect(result.valueOf()).toBe(moment(input, format).valueOf())
expect(result.valueOf()).toBe(1611675523000)
})
it('no timezone format token should parse in local time', () => {
const input = '2020-12-01T20:00:00+01:00'
const format = 'YYYY-MM-DD[T]HH:mm:ss'
const result = dayjs(input, format)
expect(result.valueOf()).toBe(moment(input, format).valueOf())
})
})
it('parse hh:mm', () => {
const input = '12:00'
const format = 'hh:mm'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
it('parse HH:mm:ss', () => {
const input = '00:27:21'
const format = 'HH:mm:ss'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
it('parse HH:mm:ss but only one digit', () => {
const input = '0:0:1'
const format = 'HH:mm:ss'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
describe('parse YYYY / YYYY-MM only', () => {
it('YYYY', () => {
const input = '2001 +08:00'
const format = 'YYYY Z'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
const input2 = '2001'
const format2 = 'YYYY'
expect(dayjs(input2, format2).valueOf()).toBe(moment(input2, format2).valueOf())
})
it('YYYY-MM', () => {
const input = '2001-01 +08:00'
const format = 'YYYY-MM Z'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
const input2 = '2001-01'
const format2 = 'YYYY-MM'
expect(dayjs(input2, format2).valueOf()).toBe(moment(input2, format2).valueOf())
})
})
it('parse hh:mm:ss but only one digit', () => {
const input = '0:0:1'
const format = 'hh:mm:ss'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
it('fails with an invalid format', () => {
const input = '2018-05-02 12:00 PM'
const format = 'C'
expect(dayjs(input, format).format().toLowerCase())
.toBe(moment(input, format).format().toLowerCase())
})
it('parse month from string', () => {
const input = '2018 February 03'
const format = 'YYYY MMMM DD'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
const input2 = '21-December-18'
const format2 = 'D-MMMM-YY'
expect(dayjs(input2, format2).valueOf()).toBe(moment(input2, format2).valueOf())
})
it('parse month from short string', () => {
const input = '2018 Feb 03'
const format = 'YYYY MMM DD'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
})
it('parse month from string with locale in config', () => {
const input = '2018 лютий 03'
const format = 'YYYY MMMM DD'
expect(dayjs(input, format, 'uk').valueOf()).toBe(moment(input, format, 'uk').valueOf())
})
it('parse month from short string with locale in config', () => {
const input = '2018 трав 03'
const format = 'YYYY MMM DD'
expect(dayjs(input, format, 'uk').valueOf()).toBe(moment(input, format, 'uk').valueOf())
})
it('parse month from short string with locale in argument', () => {
const input = '2018 трав 03'
const format = 'YYYY MMM DD'
expect(dayjs(input, format, 'uk').valueOf()).toBe(moment(input, format, 'uk').valueOf())
})
it('parse month from string with locale in argument', () => {
const input = '2018 лютий 03'
const format = 'YYYY MMMM DD'
expect(dayjs(input, format, 'uk').valueOf()).toBe(moment(input, format, 'uk').valueOf())
})
it('return Invalid Date when parse corrupt string', () => {
const input = '2018 Turnip 03'
const format = 'YYYY MMMM DD'
expect(dayjs(input, format).format()).toBe('Invalid Date')
})
it('return Invalid Date when parse corrupt short string', () => {
const input = '2018 Dog 03'
const format = 'YYYY MMM DD'
expect(dayjs(input, format).format()).toBe('Invalid Date')
})
it('YYYY-MM set 1st day of the month', () => {
expect(dayjs('2019-02', 'YYYY-MM').format('YYYY-MM-DD')).toBe('2019-02-01')
})
it('Invalid Dates', () => {
expect(dayjs('10/12/2014', 'YYYY-MM-DD').format('MM-DD-YYYY')).toBe('Invalid Date')
expect(dayjs('10-12-2014', 'YYYY-MM-DD').format('MM-DD-YYYY')).toBe('Invalid Date')
})
it('Valid Date', () => {
expect(dayjs('2014/10/12', 'YYYY-MM-DD').format('MM-DD-YYYY')).toBe('10-12-2014')
})
it('correctly parse month from string after changing locale globally', () => {
const input = '2018 лютий 03'
const format = 'YYYY MMMM DD'
const dayjsLocale = dayjs().$locale()
const momentLocale = moment.locale()
try {
dayjs.locale(uk)
moment.locale('uk')
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
} finally {
dayjs.locale(dayjsLocale)
moment.locale(momentLocale)
}
})
it('correctly parse ordinal', () => {
const input = '7th March 2019'
const input2 = '17th March 2019'
const inputFalse = '7st March 2019'
const inputZHCN = '7日 三月 2019'
const format = 'Do MMMM YYYY'
const displayFormatWithLocale = 'MMMM dddd'
expect(dayjs(input, format).valueOf())
.toBe(moment(input, format).valueOf())
expect(dayjs(input2, format).valueOf())
.toBe(moment(input2, format).valueOf())
expect(dayjs(inputFalse, format).valueOf())
.toBe(moment(inputFalse, format).valueOf())
const dayjsCN = dayjs(inputZHCN, format, 'zh-cn')
const momentCN = moment(inputZHCN, format, 'zh-cn')
expect(dayjsCN.valueOf())
.toBe(momentCN.valueOf())
expect(dayjsCN.format(displayFormatWithLocale))
.toBe(momentCN.format(displayFormatWithLocale))
expect(dayjsCN.locale())
.toBe(momentCN.locale())
})
describe('month function locale', () => {
it('MMMM', () => {
const input = '08 мая 2020'
const input2 = '08 май 2020'
const format = 'DD MMMM YYYY'
expect(dayjs(input, format, 'ru').valueOf()).toBe(moment(input, format, 'ru').valueOf())
expect(dayjs(input2, format, 'ru').valueOf()).toBe(moment(input2, format, 'ru').valueOf())
})
it('MMM', () => {
const input = '08 февр. 2020'
const format = 'DD MMM YYYY'
expect(dayjs(input, format, 'ru').valueOf()).toBe(moment(input, format, 'ru').valueOf())
})
})
describe('Strict mode', () => {
it('without locale', () => {
const input = '1970-00-00'
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'
const format = 'YYYY MMMM DD'
expect(dayjs(input, format, 'zh-cn').isValid()).toBe(true)
expect(dayjs(input, format, 'zh-cn', true).isValid()).toBe(false)
})
})
describe('Array format support', () => {
it('second ok', () => {
const input = '2012-05-28'
const format = ['YYYY', 'YYYY-MM-DD']
expect(dayjs(input, format).isValid()).toBe(true)
expect(dayjs(input, format, true).format('YYYY-MM-DD')).toBe('2012-05-28')
})
it('all invalid', () => {
const input = '2012-05-28'
const format = ['DD', 'MM-DD']
expect(dayjs(input, format, true).isValid()).toBe(false)
})
it('with locale', () => {
const input = '2018 三月 12'
const format = ['YYYY', 'MM', 'YYYY MMMM DD']
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)
})
})
it('parse a string for MMM month format with underscore delimiter', () => {
const input = 'Jan_2021'
const format = 'MMM_YYYY'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
const input2 = '21_Jan_2021_123523'
const format2 = 'DD_MMM_YYYY_hhmmss'
expect(dayjs(input2, format2).valueOf()).toBe(moment(input2, format2).valueOf())
})
it('custom two-digit year parse function', () => {
delete customParseFormat.$i // this allow plugin to be installed again
dayjs.extend(customParseFormat, {
parseTwoDigitYear: yearString => (+yearString) + 1800
})
const format = 'YY-MM-DD'
const input = '00-05-02'
expect(dayjs(input, format).year()).toBe(1800)
const input2 = '50-05-02'
expect(dayjs(input2, format).year()).toBe(1850)
const input3 = '99-05-02'
expect(dayjs(input3, format).year()).toBe(1899)
})
// issue 1852
describe('parse with special separator characters', () => {
it('Output is NaN for a specific date format', () => {
const input = '20 Nov, 2022'
const format = 'DD MMM, YYYY'
const locale = 'en'
const resultDayjs = dayjs(input, format, locale)
const resultMoment = moment(input, format, locale)
expect(resultMoment.isValid()).toBe(true)
expect(resultDayjs.isValid()).toBe(true)
expect(resultDayjs.format('DD-MM-YYYY')).toBe('20-11-2022')
expect(resultMoment.format('DD-MM-YYYY')).toBe('20-11-2022')
})
it('parse comma separated date', () => {
const input = '20,11,2022'
const format = 'DD,MM,YYYY'
const resultDayjs = dayjs(input, format)
const resultMoment = moment(input, format)
expect(resultMoment.isValid()).toBe(true)
expect(resultDayjs.isValid()).toBe(true)
expect(resultDayjs.format('DD-MM-YYYY')).toBe('20-11-2022')
expect(resultMoment.format('DD-MM-YYYY')).toBe('20-11-2022')
})
it('parse comma separated date in strict mode', () => {
const input = '20,11,2022'
const format = 'DD,MM,YYYY'
const resultDayjs = dayjs(input, format, true)
const resultMoment = moment(input, format, true)
expect(resultMoment.isValid()).toBe(true)
expect(resultDayjs.isValid()).toBe(true)
expect(resultDayjs.format('DD-MM-YYYY')).toBe('20-11-2022')
expect(resultMoment.format('DD-MM-YYYY')).toBe('20-11-2022')
})
it('parse date with multi character separator', () => {
const input = '20---11---2022'
const format = 'DD-/-MM-#-YYYY'
const resultDayjs = dayjs(input, format)
const resultMoment = moment(input, format)
expect(resultMoment.isValid()).toBe(true)
expect(resultDayjs.isValid()).toBe(true)
expect(resultDayjs.format('DD-MM-YYYY')).toBe('20-11-2022')
expect(resultMoment.format('DD-MM-YYYY')).toBe('20-11-2022')
})
it('parse date with multi character separator in strict mode', () => {
const input = '20-/-11-#-2022'
const format = 'DD-/-MM-#-YYYY'
const resultDayjs = dayjs(input, format, true)
const resultMoment = moment(input, format, true)
expect(resultMoment.isValid()).toBe(true)
expect(resultDayjs.isValid()).toBe(true)
expect(resultDayjs.format('DD-MM-YYYY')).toBe('20-11-2022')
expect(resultMoment.format('DD-MM-YYYY')).toBe('20-11-2022')
})
})
it('parse X x', () => {
const input = '1410715640.579'
const format = 'X'
expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
const input2 = '1410715640579'
const format2 = 'x'
expect(dayjs(input2, format2).valueOf()).toBe(moment(input2, format2).valueOf())
// x X starct parse requires advancedFormat plugin
dayjs.extend(advancedFormat)
expect(dayjs(input2, format2, true).valueOf()).toBe(moment(input2, format2, true).valueOf())
})
it('parse Q, [Q]', () => {
const input1 = '2024-Q1'
const input2 = '2024-Q2'
const input3 = '2024-Q3'
const input4 = '2024-Q4'
const format = 'YYYY-[Q]Q'
expect(dayjs(input1, format).valueOf()).toBe(moment(input1, format).valueOf())
expect(dayjs(input2, format).valueOf()).toBe(moment(input2, format).valueOf())
expect(dayjs(input3, format).valueOf()).toBe(moment(input3, format).valueOf())
expect(dayjs(input4, format).valueOf()).toBe(moment(input4, format).valueOf())
})
it('parse w, ww', () => {
const input = '2024-w1'
const format1 = 'YYYY-[w]w'
expect(dayjs(input, format1).format(format1)).toBe(input)
const input2 = '2024-w32'
const format2 = 'YYYY-[w]ww'
expect(dayjs(input2, format2).format(format1)).toBe(input2)
})