Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@testing-library/react": "^12",
"@types/classnames": "^2.2.9",
"@types/jest": "^26.0.0",
"@types/luxon": "^3.2.0",
"@types/react": "^17.0.11",
"@types/react-dom": "^17.0.8",
"coveralls": "^3.0.6",
Expand All @@ -65,6 +66,7 @@
"father": "^4.0.0",
"glob": "^7.2.0",
"less": "^3.10.3",
"luxon": "3.x",
"mockdate": "^3.0.2",
"moment": "^2.24.0",
"np": "^7.1.0",
Expand Down
129 changes: 129 additions & 0 deletions src/generate/luxon.ts
Original file line number Diff line number Diff line change
@@ -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<DateTime> = {
// 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]));
Comment on lines +91 to +95
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the PR description, luxon (through Intl API) formatting for week days does not match moment's 1:1.

I made some static adaptations so that the main antd locales are formatted the same, but I didn't think it was proper to bloat the implementation with formatting for all locales.

I have indicated in the PR description how we could provide easier customization for users, or simply document how to do it with the current implementation.


// 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;
35 changes: 28 additions & 7 deletions tests/generate.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ 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';
import type { GenerateConfig } from '../src/generate';
import { GenerateConfig } from '../src/generate';

describe('Picker.Generate', () => {
beforeAll(() => {
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -80,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(
Expand All @@ -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',
Expand All @@ -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
Expand All @@ -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');
Expand Down Expand Up @@ -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);
});
});
});
Expand Down