Skip to content

Commit e2588e8

Browse files
authored
feat: add support for luxon (#230)
* feat: add support for luxon * fix: remove inconsistent formatting changes * feat: bump luxon version to 2.x * feat: bump luxon version to 3.x
1 parent 13bb773 commit e2588e8

File tree

3 files changed

+159
-7
lines changed

3 files changed

+159
-7
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@testing-library/react": "^12",
5151
"@types/classnames": "^2.2.9",
5252
"@types/jest": "^26.0.0",
53+
"@types/luxon": "^3.2.0",
5354
"@types/react": "^17.0.11",
5455
"@types/react-dom": "^17.0.8",
5556
"coveralls": "^3.0.6",
@@ -65,6 +66,7 @@
6566
"father": "^4.0.0",
6667
"glob": "^7.2.0",
6768
"less": "^3.10.3",
69+
"luxon": "3.x",
6870
"mockdate": "^3.0.2",
6971
"moment": "^2.24.0",
7072
"np": "^7.1.0",

src/generate/luxon.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { DateTime, Info } from 'luxon';
2+
3+
import type { GenerateConfig } from '.';
4+
5+
const weekDayFormatMap = {
6+
zh_CN: 'narrow',
7+
zh_TW: 'narrow',
8+
};
9+
10+
const weekDayLengthMap = {
11+
en_US: 2,
12+
en_GB: 2,
13+
};
14+
15+
/**
16+
* Normalizes part of a moment format string that should
17+
* not be escaped to a luxon compatible format string.
18+
*
19+
* @param part string
20+
* @returns string
21+
*/
22+
const normalizeFormatPart = (part: string): string =>
23+
part
24+
.replace(/Y/g, 'y')
25+
.replace(/D/g, 'd')
26+
.replace(/gg/g, 'kk')
27+
.replace(/Q/g, 'q')
28+
.replace(/([Ww])o/g, 'WW');
29+
30+
/**
31+
* Normalizes a moment compatible format string to a luxon compatible format string
32+
*
33+
* @param format string
34+
* @returns string
35+
*/
36+
const normalizeFormat = (format: string): string =>
37+
format
38+
// moment escapes strings contained in brackets
39+
.split(/[[\]]/)
40+
.map((part, index) => {
41+
const shouldEscape = index % 2 > 0;
42+
43+
return shouldEscape ? part : normalizeFormatPart(part);
44+
})
45+
// luxon escapes strings contained in single quotes
46+
.join("'");
47+
48+
/**
49+
* Normalizes language tags used to luxon compatible
50+
* language tags by replacing underscores with hyphen-minus.
51+
*
52+
* @param locale string
53+
* @returns string
54+
*/
55+
const normalizeLocale = (locale: string): string => locale.replace(/_/g, '-');
56+
57+
const generateConfig: GenerateConfig<DateTime> = {
58+
// get
59+
getNow: () => DateTime.local(),
60+
getFixedDate: string => DateTime.fromFormat(string, 'yyyy-MM-dd'),
61+
getEndDate: date => date.endOf('month'),
62+
getWeekDay: date => date.weekday,
63+
getYear: date => date.year,
64+
getMonth: date => date.month - 1, // getMonth should return 0-11, luxon month returns 1-12
65+
getDate: date => date.day,
66+
getHour: date => date.hour,
67+
getMinute: date => date.minute,
68+
getSecond: date => date.second,
69+
70+
// set
71+
addYear: (date, diff) => date.plus({ year: diff }),
72+
addMonth: (date, diff) => date.plus({ month: diff }),
73+
addDate: (date, diff) => date.plus({ day: diff }),
74+
setYear: (date, year) => date.set({ year }),
75+
setMonth: (date, month) => date.set({ month: month + 1 }), // setMonth month argument is 0-11, luxon months are 1-12
76+
setDate: (date, day) => date.set({ day }),
77+
setHour: (date, hour) => date.set({ hour }),
78+
setMinute: (date, minute) => date.set({ minute }),
79+
setSecond: (date, second) => date.set({ second }),
80+
81+
// Compare
82+
isAfter: (date1, date2) => date1 > date2,
83+
isValidate: date => date.isValid,
84+
85+
locale: {
86+
getWeekFirstDate: (locale, date) => date.setLocale(normalizeLocale(locale)).startOf('week'),
87+
getWeekFirstDay: locale =>
88+
DateTime.local().setLocale(normalizeLocale(locale)).startOf('week').weekday,
89+
getWeek: (locale, date) => date.setLocale(normalizeLocale(locale)).weekNumber,
90+
getShortWeekDays: locale => {
91+
const weekdays = Info.weekdays(weekDayFormatMap[locale] || 'short', {
92+
locale: normalizeLocale(locale),
93+
});
94+
95+
const shifted = weekdays.map(weekday => weekday.slice(0, weekDayLengthMap[locale]));
96+
97+
// getShortWeekDays should return weekday labels starting from Sunday.
98+
// luxon returns them starting from Monday, so we have to shift the results.
99+
shifted.unshift(shifted.pop() as string);
100+
101+
return shifted;
102+
},
103+
getShortMonths: locale => Info.months('short', { locale: normalizeLocale(locale) }),
104+
format: (locale, date, format) => {
105+
if (!date || !date.isValid) {
106+
return null;
107+
}
108+
109+
return date.setLocale(normalizeLocale(locale)).toFormat(normalizeFormat(format));
110+
},
111+
parse: (locale, text, formats) => {
112+
for (let i = 0; i < formats.length; i += 1) {
113+
const normalizedFormat = normalizeFormat(formats[i]);
114+
115+
const date = DateTime.fromFormat(text, normalizedFormat, {
116+
locale: normalizeLocale(locale),
117+
});
118+
119+
if (date.isValid) {
120+
return date;
121+
}
122+
}
123+
124+
return null;
125+
},
126+
},
127+
};
128+
129+
export default generateConfig;

tests/generate.spec.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import MockDate from 'mockdate';
22
import momentGenerateConfig from '../src/generate/moment';
33
import dayjsGenerateConfig from '../src/generate/dayjs';
44
import dateFnsGenerateConfig from '../src/generate/dateFns';
5+
import luxonGenerateConfig from '../src/generate/luxon';
56
import { getMoment } from './util/commonUtil';
67

78
import 'dayjs/locale/zh-cn';
8-
import type { GenerateConfig } from '../src/generate';
9+
import { GenerateConfig } from '../src/generate';
910

1011
describe('Picker.Generate', () => {
1112
beforeAll(() => {
@@ -20,6 +21,7 @@ describe('Picker.Generate', () => {
2021
{ name: 'moment', generateConfig: momentGenerateConfig },
2122
{ name: 'dayjs', generateConfig: dayjsGenerateConfig },
2223
{ name: 'date-fns', generateConfig: dateFnsGenerateConfig },
24+
{ name: 'luxon', generateConfig: luxonGenerateConfig },
2325
];
2426

2527
list.forEach(({ name, generateConfig }) => {
@@ -80,7 +82,7 @@ describe('Picker.Generate', () => {
8082
describe('locale', () => {
8183
describe('parse', () => {
8284
it('basic', () => {
83-
['2000-01-02', '02/01/2000'].forEach((str) => {
85+
['2000-01-02', '02/01/2000'].forEach(str => {
8486
const date = generateConfig.locale.parse('en_US', str, ['YYYY-MM-DD', 'DD/MM/YYYY']);
8587

8688
expect(generateConfig.locale.format('en_US', date!, 'YYYY-MM-DD')).toEqual(
@@ -90,7 +92,7 @@ describe('Picker.Generate', () => {
9092
});
9193

9294
it('week', () => {
93-
if (name !== 'date-fns') {
95+
if (!['date-fns', 'luxon'].includes(name)) {
9496
expect(
9597
generateConfig.locale.format(
9698
'en_US',
@@ -116,10 +118,22 @@ describe('Picker.Generate', () => {
116118
}
117119
});
118120
});
121+
122+
describe('format', () => {
123+
it('escape strings', () => {
124+
if (name !== 'date-fns') {
125+
expect(
126+
generateConfig.locale.format('en_US', generateConfig.getNow(), 'YYYY-[Q]Q'),
127+
).toEqual('1990-Q3');
128+
}
129+
});
130+
});
119131
});
120132

121133
it('getWeekFirstDay', () => {
122-
expect(generateConfig.locale.getWeekFirstDay('en_US')).toEqual(0);
134+
const expectedUsFirstDay = name === 'luxon' ? 1 : 0;
135+
136+
expect(generateConfig.locale.getWeekFirstDay('en_US')).toEqual(expectedUsFirstDay);
123137
expect(generateConfig.locale.getWeekFirstDay('zh_CN')).toEqual(1);
124138

125139
// Should keep same weekday
@@ -142,12 +156,17 @@ describe('Picker.Generate', () => {
142156
'zh_CN',
143157
generateConfig.locale.parse('zh_CN', '2020-12-30', [formatStr]),
144158
);
145-
expect(generateConfig.locale.format('en_US', usDate, formatStr)).toEqual('2020-12-27');
159+
160+
const expectedUsFirstDate = name === 'luxon' ? '28' : '27';
161+
162+
expect(generateConfig.locale.format('en_US', usDate, formatStr)).toEqual(
163+
`2020-12-${expectedUsFirstDate}`,
164+
);
146165
expect(generateConfig.locale.format('zh_CN', cnDate, formatStr)).toEqual('2020-12-28');
147166
});
148167

149168
it('Parse format Wo', () => {
150-
if (name !== 'date-fns') {
169+
if (!['date-fns', 'luxon'].includes(name)) {
151170
expect(
152171
generateConfig.locale.parse('en_US', '2012-51st', ['YYYY-Wo']).format('Wo'),
153172
).toEqual('51st');
@@ -226,12 +245,14 @@ describe('Picker.Generate', () => {
226245
generateConfig.locale.parse('zh_CN', '2019-12-08', [formatStr]),
227246
),
228247
).toEqual(49);
248+
249+
const expectedUsWeek = name === 'luxon' ? 49 : 50;
229250
expect(
230251
generateConfig.locale.getWeek(
231252
'en_US',
232253
generateConfig.locale.parse('en_US', '2019-12-08', [formatStr]),
233254
),
234-
).toEqual(50);
255+
).toEqual(expectedUsWeek);
235256
});
236257
});
237258
});

0 commit comments

Comments
 (0)