From 209875a601479a656bed425d84f64d2e8afc27bd Mon Sep 17 00:00:00 2001 From: Sylvain Boulade Date: Thu, 11 Mar 2021 09:16:27 +0100 Subject: [PATCH 1/4] feat: add support for luxon --- package.json | 2 + src/generate/luxon.ts | 129 ++++++++++++++++++++++++++++++++++++++++ tests/generate.spec.tsx | 31 ++++++++-- 3 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 src/generate/luxon.ts diff --git a/package.json b/package.json index f1a2092ba..9c6eed159 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@testing-library/react": "^12", "@types/classnames": "^2.2.9", "@types/jest": "^26.0.0", + "@types/luxon": "^1.26.2", "@types/react": "^17.0.11", "@types/react-dom": "^17.0.8", "coveralls": "^3.0.6", @@ -65,6 +66,7 @@ "father": "^4.0.0", "glob": "^7.2.0", "less": "^3.10.3", + "luxon": "^1.26.0", "mockdate": "^3.0.2", "moment": "^2.24.0", "np": "^7.1.0", diff --git a/src/generate/luxon.ts b/src/generate/luxon.ts new file mode 100644 index 000000000..cc284cf28 --- /dev/null +++ b/src/generate/luxon.ts @@ -0,0 +1,129 @@ +import { DateTime, Info } from 'luxon'; + +import type { GenerateConfig } from '.'; + +const weekDayFormatMap = { + zh_CN: 'narrow', + zh_TW: 'narrow', +}; + +const weekDayLengthMap = { + en_US: 2, + en_GB: 2, +}; + +/** + * Normalizes part of a moment format string that should + * not be escaped to a luxon compatible format string. + * + * @param part string + * @returns string + */ +const normalizeFormatPart = (part: string): string => + part + .replace(/Y/g, 'y') + .replace(/D/g, 'd') + .replace(/gg/g, 'kk') + .replace(/Q/g, 'q') + .replace(/([Ww])o/g, 'WW'); + +/** + * Normalizes a moment compatible format string to a luxon compatible format string + * + * @param format string + * @returns string + */ +const normalizeFormat = (format: string): string => + format + // moment escapes strings contained in brackets + .split(/[[\]]/) + .map((part, index) => { + const shouldEscape = index % 2 > 0; + + return shouldEscape ? part : normalizeFormatPart(part); + }) + // luxon escapes strings contained in single quotes + .join("'"); + +/** + * Normalizes language tags used to luxon compatible + * language tags by replacing underscores with hyphen-minus. + * + * @param locale string + * @returns string + */ +const normalizeLocale = (locale: string): string => locale.replace(/_/g, '-'); + +const generateConfig: GenerateConfig = { + // get + getNow: () => DateTime.local(), + getFixedDate: (string) => DateTime.fromFormat(string, 'yyyy-MM-dd'), + getEndDate: (date) => date.endOf('month'), + getWeekDay: (date) => date.weekday, + getYear: (date) => date.year, + getMonth: (date) => date.month - 1, // getMonth should return 0-11, luxon month returns 1-12 + getDate: (date) => date.day, + getHour: (date) => date.hour, + getMinute: (date) => date.minute, + getSecond: (date) => date.second, + + // set + addYear: (date, diff) => date.plus({ year: diff }), + addMonth: (date, diff) => date.plus({ month: diff }), + addDate: (date, diff) => date.plus({ day: diff }), + setYear: (date, year) => date.set({ year }), + setMonth: (date, month) => date.set({ month: month + 1 }), // setMonth month argument is 0-11, luxon months are 1-12 + setDate: (date, day) => date.set({ day }), + setHour: (date, hour) => date.set({ hour }), + setMinute: (date, minute) => date.set({ minute }), + setSecond: (date, second) => date.set({ second }), + + // Compare + isAfter: (date1, date2) => date1 > date2, + isValidate: (date) => date.isValid, + + locale: { + getWeekFirstDate: (locale, date) => date.setLocale(normalizeLocale(locale)).startOf('week'), + getWeekFirstDay: (locale) => + DateTime.local().setLocale(normalizeLocale(locale)).startOf('week').weekday, + getWeek: (locale, date) => date.setLocale(normalizeLocale(locale)).weekNumber, + getShortWeekDays: (locale) => { + const weekdays = Info.weekdays(weekDayFormatMap[locale] || 'short', { + locale: normalizeLocale(locale), + }); + + const shifted = weekdays.map((weekday) => weekday.slice(0, weekDayLengthMap[locale])); + + // getShortWeekDays should return weekday labels starting from Sunday. + // luxon returns them starting from Monday, so we have to shift the results. + shifted.unshift(shifted.pop() as string); + + return shifted; + }, + getShortMonths: (locale) => Info.months('short', { locale: normalizeLocale(locale) }), + format: (locale, date, format) => { + if (!date || !date.isValid) { + return null; + } + + return date.setLocale(normalizeLocale(locale)).toFormat(normalizeFormat(format)); + }, + parse: (locale, text, formats) => { + for (let i = 0; i < formats.length; i += 1) { + const normalizedFormat = normalizeFormat(formats[i]); + + const date = DateTime.fromFormat(text, normalizedFormat, { + locale: normalizeLocale(locale), + }); + + if (date.isValid) { + return date; + } + } + + return null; + }, + }, +}; + +export default generateConfig; diff --git a/tests/generate.spec.tsx b/tests/generate.spec.tsx index b36732d72..21da2bca1 100644 --- a/tests/generate.spec.tsx +++ b/tests/generate.spec.tsx @@ -2,6 +2,7 @@ import MockDate from 'mockdate'; import momentGenerateConfig from '../src/generate/moment'; import dayjsGenerateConfig from '../src/generate/dayjs'; import dateFnsGenerateConfig from '../src/generate/dateFns'; +import luxonGenerateConfig from '../src/generate/luxon'; import { getMoment } from './util/commonUtil'; import 'dayjs/locale/zh-cn'; @@ -20,6 +21,7 @@ describe('Picker.Generate', () => { { name: 'moment', generateConfig: momentGenerateConfig }, { name: 'dayjs', generateConfig: dayjsGenerateConfig }, { name: 'date-fns', generateConfig: dateFnsGenerateConfig }, + { name: 'luxon', generateConfig: luxonGenerateConfig }, ]; list.forEach(({ name, generateConfig }) => { @@ -90,7 +92,7 @@ describe('Picker.Generate', () => { }); it('week', () => { - if (name !== 'date-fns') { + if (!['date-fns', 'luxon'].includes(name)) { expect( generateConfig.locale.format( 'en_US', @@ -116,10 +118,22 @@ describe('Picker.Generate', () => { } }); }); + + describe('format', () => { + it('escape strings', () => { + if (name !== 'date-fns') { + expect( + generateConfig.locale.format('en_US', generateConfig.getNow(), 'YYYY-[Q]Q'), + ).toEqual('1990-Q3'); + } + }); + }); }); it('getWeekFirstDay', () => { - expect(generateConfig.locale.getWeekFirstDay('en_US')).toEqual(0); + const expectedUsFirstDay = name === 'luxon' ? 1 : 0; + + expect(generateConfig.locale.getWeekFirstDay('en_US')).toEqual(expectedUsFirstDay); expect(generateConfig.locale.getWeekFirstDay('zh_CN')).toEqual(1); // Should keep same weekday @@ -142,12 +156,17 @@ describe('Picker.Generate', () => { 'zh_CN', generateConfig.locale.parse('zh_CN', '2020-12-30', [formatStr]), ); - expect(generateConfig.locale.format('en_US', usDate, formatStr)).toEqual('2020-12-27'); + + const expectedUsFirstDate = name === 'luxon' ? '28' : '27'; + + expect(generateConfig.locale.format('en_US', usDate, formatStr)).toEqual( + `2020-12-${expectedUsFirstDate}`, + ); expect(generateConfig.locale.format('zh_CN', cnDate, formatStr)).toEqual('2020-12-28'); }); it('Parse format Wo', () => { - if (name !== 'date-fns') { + if (!['date-fns', 'luxon'].includes(name)) { expect( generateConfig.locale.parse('en_US', '2012-51st', ['YYYY-Wo']).format('Wo'), ).toEqual('51st'); @@ -226,12 +245,14 @@ describe('Picker.Generate', () => { generateConfig.locale.parse('zh_CN', '2019-12-08', [formatStr]), ), ).toEqual(49); + + const expectedUsWeek = name === 'luxon' ? 49 : 50; expect( generateConfig.locale.getWeek( 'en_US', generateConfig.locale.parse('en_US', '2019-12-08', [formatStr]), ), - ).toEqual(50); + ).toEqual(expectedUsWeek); }); }); }); From 2c9be2eded7ab77131302cb71734fcb326e57b92 Mon Sep 17 00:00:00 2001 From: Sylvain Boulade Date: Thu, 11 Mar 2021 18:24:55 +0100 Subject: [PATCH 2/4] fix: remove inconsistent formatting changes --- src/generate/luxon.ts | 28 ++++++++++++++-------------- tests/generate.spec.tsx | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/generate/luxon.ts b/src/generate/luxon.ts index cc284cf28..8017fa099 100644 --- a/src/generate/luxon.ts +++ b/src/generate/luxon.ts @@ -57,15 +57,15 @@ const normalizeLocale = (locale: string): string => locale.replace(/_/g, '-'); const generateConfig: GenerateConfig = { // get getNow: () => DateTime.local(), - getFixedDate: (string) => DateTime.fromFormat(string, 'yyyy-MM-dd'), - getEndDate: (date) => date.endOf('month'), - getWeekDay: (date) => date.weekday, - getYear: (date) => date.year, - getMonth: (date) => date.month - 1, // getMonth should return 0-11, luxon month returns 1-12 - getDate: (date) => date.day, - getHour: (date) => date.hour, - getMinute: (date) => date.minute, - getSecond: (date) => date.second, + getFixedDate: string => DateTime.fromFormat(string, 'yyyy-MM-dd'), + getEndDate: date => date.endOf('month'), + getWeekDay: date => date.weekday, + getYear: date => date.year, + getMonth: date => date.month - 1, // getMonth should return 0-11, luxon month returns 1-12 + getDate: date => date.day, + getHour: date => date.hour, + getMinute: date => date.minute, + getSecond: date => date.second, // set addYear: (date, diff) => date.plus({ year: diff }), @@ -80,19 +80,19 @@ const generateConfig: GenerateConfig = { // Compare isAfter: (date1, date2) => date1 > date2, - isValidate: (date) => date.isValid, + isValidate: date => date.isValid, locale: { getWeekFirstDate: (locale, date) => date.setLocale(normalizeLocale(locale)).startOf('week'), - getWeekFirstDay: (locale) => + getWeekFirstDay: locale => DateTime.local().setLocale(normalizeLocale(locale)).startOf('week').weekday, getWeek: (locale, date) => date.setLocale(normalizeLocale(locale)).weekNumber, - getShortWeekDays: (locale) => { + getShortWeekDays: locale => { const weekdays = Info.weekdays(weekDayFormatMap[locale] || 'short', { locale: normalizeLocale(locale), }); - const shifted = weekdays.map((weekday) => weekday.slice(0, weekDayLengthMap[locale])); + const shifted = weekdays.map(weekday => weekday.slice(0, weekDayLengthMap[locale])); // getShortWeekDays should return weekday labels starting from Sunday. // luxon returns them starting from Monday, so we have to shift the results. @@ -100,7 +100,7 @@ const generateConfig: GenerateConfig = { return shifted; }, - getShortMonths: (locale) => Info.months('short', { locale: normalizeLocale(locale) }), + getShortMonths: locale => Info.months('short', { locale: normalizeLocale(locale) }), format: (locale, date, format) => { if (!date || !date.isValid) { return null; diff --git a/tests/generate.spec.tsx b/tests/generate.spec.tsx index 21da2bca1..939e76968 100644 --- a/tests/generate.spec.tsx +++ b/tests/generate.spec.tsx @@ -6,7 +6,7 @@ import luxonGenerateConfig from '../src/generate/luxon'; import { getMoment } from './util/commonUtil'; import 'dayjs/locale/zh-cn'; -import type { GenerateConfig } from '../src/generate'; +import { GenerateConfig } from '../src/generate'; describe('Picker.Generate', () => { beforeAll(() => { @@ -82,7 +82,7 @@ describe('Picker.Generate', () => { describe('locale', () => { describe('parse', () => { it('basic', () => { - ['2000-01-02', '02/01/2000'].forEach((str) => { + ['2000-01-02', '02/01/2000'].forEach(str => { const date = generateConfig.locale.parse('en_US', str, ['YYYY-MM-DD', 'DD/MM/YYYY']); expect(generateConfig.locale.format('en_US', date!, 'YYYY-MM-DD')).toEqual( From 341f71b77de3a25f66790c7254f2c4e975a7b89c Mon Sep 17 00:00:00 2001 From: Sylvain Boulade Date: Mon, 31 Jan 2022 17:08:51 +0100 Subject: [PATCH 3/4] feat: bump luxon version to 2.x --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9c6eed159..da6725ba1 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@testing-library/react": "^12", "@types/classnames": "^2.2.9", "@types/jest": "^26.0.0", - "@types/luxon": "^1.26.2", + "@types/luxon": "^2.0.9", "@types/react": "^17.0.11", "@types/react-dom": "^17.0.8", "coveralls": "^3.0.6", @@ -66,7 +66,7 @@ "father": "^4.0.0", "glob": "^7.2.0", "less": "^3.10.3", - "luxon": "^1.26.0", + "luxon": "2.x", "mockdate": "^3.0.2", "moment": "^2.24.0", "np": "^7.1.0", From 555c557df8272f2124095daef877ae914952ef1e Mon Sep 17 00:00:00 2001 From: Sylvain Boulade Date: Thu, 30 Mar 2023 09:23:50 +0200 Subject: [PATCH 4/4] feat: bump luxon version to 3.x --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index da6725ba1..d3f5c89bc 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@testing-library/react": "^12", "@types/classnames": "^2.2.9", "@types/jest": "^26.0.0", - "@types/luxon": "^2.0.9", + "@types/luxon": "^3.2.0", "@types/react": "^17.0.11", "@types/react-dom": "^17.0.8", "coveralls": "^3.0.6", @@ -66,7 +66,7 @@ "father": "^4.0.0", "glob": "^7.2.0", "less": "^3.10.3", - "luxon": "2.x", + "luxon": "3.x", "mockdate": "^3.0.2", "moment": "^2.24.0", "np": "^7.1.0",