@louis.jln/extract-date 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +21 -64
- package/src/calculateSpecificity.d.ts +2 -0
- package/src/calculateSpecificity.js +15 -20
- package/src/createFormats.d.ts +13 -0
- package/src/createFormats.js +131 -364
- package/src/createMovingChunks.d.ts +2 -0
- package/src/createMovingChunks.js +10 -17
- package/src/days.json +22005 -0
- package/src/extractDate.d.ts +3 -0
- package/src/extractDate.js +147 -204
- package/src/extractRelativeDate.d.ts +2 -0
- package/src/extractRelativeDate.js +20 -31
- package/src/index.d.ts +2 -0
- package/src/index.js +1 -5
- package/src/months.json +35738 -0
- package/src/normalizeInput.d.ts +2 -0
- package/src/normalizeInput.js +15 -24
- package/src/resolveLocalizedNames.d.ts +2 -0
- package/src/resolveLocalizedNames.js +82 -0
- package/src/types.d.ts +20 -0
- package/src/types.js +1 -23
- package/test/extract-date/calculateSpecificity.test.d.ts +1 -0
- package/test/extract-date/calculateSpecificity.test.js +39 -0
- package/test/extract-date/createMovingChunks.test.d.ts +1 -0
- package/test/extract-date/createMovingChunks.test.js +28 -0
- package/test/extract-date/extractDate/configuration.test.d.ts +1 -0
- package/test/extract-date/extractDate/configuration.test.js +36 -0
- package/test/extract-date/extractDate/edge-cases.test.d.ts +1 -0
- package/test/extract-date/extractDate/edge-cases.test.js +23 -0
- package/test/extract-date/extractDate/fixtures.test.d.ts +1 -0
- package/test/extract-date/extractDate/fixtures.test.js +42 -0
- package/test/extract-date/extractDate/general-formats.test.d.ts +1 -0
- package/test/extract-date/extractDate/general-formats.test.js +45 -0
- package/test/extract-date/extractDate/implied-year.test.d.ts +1 -0
- package/test/extract-date/extractDate/implied-year.test.js +105 -0
- package/test/extract-date/extractDate/localised.test.d.ts +1 -0
- package/test/extract-date/extractDate/localised.test.js +75 -0
- package/test/extract-date/extractDate/multiple-dates.test.d.ts +1 -0
- package/test/extract-date/extractDate/multiple-dates.test.js +24 -0
- package/test/extract-date/extractDate/relative-dates.test.d.ts +1 -0
- package/test/extract-date/extractDate/relative-dates.test.js +47 -0
- package/test/extract-date/extractRelativeDate.test.d.ts +1 -0
- package/test/extract-date/extractRelativeDate.test.js +29 -0
- package/test/extract-date/normalizeInput.test.d.ts +1 -0
- package/test/extract-date/normalizeInput.test.js +14 -0
- package/test/fixtures/dates.json +22574 -0
- package/.flowconfig +0 -3
- package/LICENSE +0 -24
- package/README.md +0 -184
- package/bundle/extract-date.js +0 -61276
- package/dist/calculateSpecificity.js +0 -23
- package/dist/calculateSpecificity.js.flow +0 -21
- package/dist/calculateSpecificity.js.map +0 -1
- package/dist/createFormats.js +0 -165
- package/dist/createFormats.js.flow +0 -373
- package/dist/createFormats.js.map +0 -1
- package/dist/createMovingChunks.js +0 -20
- package/dist/createMovingChunks.js.flow +0 -19
- package/dist/createMovingChunks.js.map +0 -1
- package/dist/dictionary.json +0 -3792
- package/dist/extractDate.js +0 -148
- package/dist/extractDate.js.flow +0 -214
- package/dist/extractDate.js.map +0 -1
- package/dist/extractRelativeDate.js +0 -32
- package/dist/extractRelativeDate.js.flow +0 -34
- package/dist/extractRelativeDate.js.map +0 -1
- package/dist/index.js +0 -11
- package/dist/index.js.flow +0 -6
- package/dist/index.js.map +0 -1
- package/dist/normalizeInput.js +0 -22
- package/dist/normalizeInput.js.flow +0 -26
- package/dist/normalizeInput.js.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.flow +0 -23
- package/dist/types.js.map +0 -1
- package/src/dictionary.json +0 -3792
package/src/extractDate.js
CHANGED
|
@@ -1,214 +1,157 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/* eslint-disable no-continue, no-negated-condition, import/no-namespace */
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
format as formatDate,
|
|
7
|
-
parse as parseDate,
|
|
8
|
-
isValid as isValidDate,
|
|
9
|
-
} from 'date-fns';
|
|
10
|
-
import * as locales from 'date-fns/locale';
|
|
1
|
+
import { format as formatDate, parse as parseDate, isValid as isValidDate, } from 'date-fns';
|
|
2
|
+
import { enUS as dateFnsLocale } from 'date-fns/locale';
|
|
11
3
|
import moment from 'moment-timezone';
|
|
12
4
|
import dictionary from 'relative-date-names';
|
|
13
|
-
import createMovingChunks from '
|
|
14
|
-
import extractRelativeDate from '
|
|
15
|
-
import createFormats from '
|
|
16
|
-
import normalizeInput from '
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
DateMatchType,
|
|
20
|
-
UserConfigurationType,
|
|
21
|
-
} from './types';
|
|
22
|
-
|
|
5
|
+
import createMovingChunks from '@/createMovingChunks';
|
|
6
|
+
import extractRelativeDate from '@/extractRelativeDate';
|
|
7
|
+
import createFormats from '@/createFormats';
|
|
8
|
+
import normalizeInput from '@/normalizeInput';
|
|
9
|
+
import { replaceMonthName, replaceDayName } from '@/resolveLocalizedNames';
|
|
10
|
+
import monthsData from '@/months.json';
|
|
23
11
|
const defaultConfiguration = {
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
maximumAge: Infinity,
|
|
13
|
+
minimumAge: Infinity,
|
|
26
14
|
};
|
|
27
|
-
|
|
15
|
+
const stripDiacritics = (text) => text.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
28
16
|
const formats = createFormats();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const locale = configuration.locale || 'en';
|
|
44
|
-
|
|
45
|
-
const dateFnsLocale = locales[dateFnsLocaleMap[locale] || locale];
|
|
46
|
-
|
|
47
|
-
if (!dateFnsLocale) {
|
|
48
|
-
throw new Error('No translation available for the target locale (date-fns).');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!dictionary[locale]) {
|
|
52
|
-
throw new Error('No translation available for the target locale (relative dates).');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (configuration.timezone && !moment.tz.zone(configuration.timezone)) {
|
|
56
|
-
throw new Error('Unrecognized timezone.');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (configuration.maximumAge && configuration.maximumAge < 0) {
|
|
60
|
-
throw new Error('`maximumAge` must be a positive number.');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (configuration.minimumAge && configuration.minimumAge < 0) {
|
|
64
|
-
throw new Error('`minimumAge` must be a positive number.');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let words = normalizedInput.split(' ');
|
|
68
|
-
|
|
69
|
-
const matches = [];
|
|
70
|
-
|
|
71
|
-
const baseDate = parseDate('12:00', 'HH:mm', new Date());
|
|
72
|
-
|
|
73
|
-
for (const format of formats) {
|
|
74
|
-
const movingChunks = createMovingChunks(words, format.wordCount);
|
|
75
|
-
|
|
76
|
-
let chunkIndex = 0;
|
|
77
|
-
|
|
78
|
-
for (const movingChunk of movingChunks) {
|
|
79
|
-
const wordOffset = ++chunkIndex * format.wordCount;
|
|
80
|
-
|
|
81
|
-
const subject = movingChunk.join(' ');
|
|
82
|
-
|
|
83
|
-
if (format.dateFnsFormat === 'R') {
|
|
84
|
-
if (!configuration.locale) {
|
|
85
|
-
} else if (!configuration.timezone) {
|
|
86
|
-
} else {
|
|
87
|
-
const maybeDate = extractRelativeDate(subject, configuration.locale, configuration.timezone);
|
|
88
|
-
|
|
89
|
-
if (maybeDate) {
|
|
90
|
-
words = words.slice(wordOffset);
|
|
91
|
-
|
|
92
|
-
matches.push({
|
|
93
|
-
date: maybeDate,
|
|
94
|
-
originalText: subject,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
17
|
+
const translateChunk = (subject, locale, translateAccentless) => {
|
|
18
|
+
if (locale === 'en') {
|
|
19
|
+
return subject;
|
|
20
|
+
}
|
|
21
|
+
return subject.split(' ').map((word) => {
|
|
22
|
+
// Preserve trailing punctuation (e.g. commas)
|
|
23
|
+
const punctuationMatch = word.match(/^(.+?)([,]+)$/);
|
|
24
|
+
const cleanWord = punctuationMatch ? punctuationMatch[1] : word;
|
|
25
|
+
const punctuation = punctuationMatch ? punctuationMatch[2] : '';
|
|
26
|
+
const monthReplacement = replaceMonthName(cleanWord, locale, translateAccentless);
|
|
27
|
+
if (monthReplacement) {
|
|
28
|
+
return monthReplacement + punctuation;
|
|
97
29
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
format.dateFnsFormat,
|
|
102
|
-
baseDate,
|
|
103
|
-
{
|
|
104
|
-
locale: dateFnsLocale,
|
|
105
|
-
},
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
if (isValidDate(date)) {
|
|
109
|
-
words = words.slice(wordOffset);
|
|
110
|
-
|
|
111
|
-
matches.push({
|
|
112
|
-
date: formatDate(date, 'yyyy-MM-dd'),
|
|
113
|
-
originalText: subject,
|
|
114
|
-
});
|
|
30
|
+
const dayReplacement = replaceDayName(cleanWord, locale, translateAccentless);
|
|
31
|
+
if (dayReplacement) {
|
|
32
|
+
return dayReplacement + punctuation;
|
|
115
33
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
34
|
+
return word;
|
|
35
|
+
}).join(' ');
|
|
36
|
+
};
|
|
37
|
+
export default (input, userConfiguration = defaultConfiguration) => {
|
|
38
|
+
const normalizedInput = normalizeInput(input);
|
|
39
|
+
const configuration = {
|
|
40
|
+
...defaultConfiguration,
|
|
41
|
+
...userConfiguration,
|
|
42
|
+
};
|
|
43
|
+
const locale = configuration.locale || 'en';
|
|
44
|
+
const translateAccentless = Boolean(configuration.translateAccentless);
|
|
45
|
+
if (!monthsData[locale]) {
|
|
46
|
+
throw new Error('No translation available for the target locale.');
|
|
47
|
+
}
|
|
48
|
+
const hasRelativeDateSupport = Boolean(dictionary[locale]);
|
|
49
|
+
if (configuration.timezone && !moment.tz.zone(configuration.timezone)) {
|
|
50
|
+
throw new Error('Unrecognized timezone.');
|
|
51
|
+
}
|
|
52
|
+
if (configuration.maximumAge && configuration.maximumAge < 0) {
|
|
53
|
+
throw new Error('`maximumAge` must be a positive number.');
|
|
54
|
+
}
|
|
55
|
+
if (configuration.minimumAge && configuration.minimumAge < 0) {
|
|
56
|
+
throw new Error('`minimumAge` must be a positive number.');
|
|
57
|
+
}
|
|
58
|
+
let words = normalizedInput.split(' ');
|
|
59
|
+
const matches = [];
|
|
60
|
+
const baseDate = parseDate('12:00', 'HH:mm', new Date());
|
|
61
|
+
for (const format of formats) {
|
|
62
|
+
const movingChunks = createMovingChunks(words, format.wordCount);
|
|
63
|
+
let chunkIndex = 0;
|
|
64
|
+
for (const movingChunk of movingChunks) {
|
|
65
|
+
const wordOffset = ++chunkIndex * format.wordCount;
|
|
66
|
+
const subject = movingChunk.join(' ');
|
|
67
|
+
if (format.dateFnsFormat === 'R') {
|
|
68
|
+
if (!configuration.locale) {
|
|
69
|
+
}
|
|
70
|
+
else if (!hasRelativeDateSupport) {
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const maybeDate = extractRelativeDate(subject, configuration.locale, configuration.timezone ?? '');
|
|
74
|
+
if (maybeDate) {
|
|
75
|
+
words = words.slice(wordOffset);
|
|
76
|
+
matches.push({
|
|
77
|
+
date: maybeDate,
|
|
78
|
+
originalText: stripDiacritics(subject),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (format.dateFnsFormat === 'EEE' || format.dateFnsFormat === 'EEEE') {
|
|
84
|
+
const translatedSubject = translateChunk(subject, locale, translateAccentless);
|
|
85
|
+
const date = parseDate(translatedSubject, format.dateFnsFormat, baseDate, { locale: dateFnsLocale });
|
|
86
|
+
if (isValidDate(date)) {
|
|
87
|
+
words = words.slice(wordOffset);
|
|
88
|
+
matches.push({
|
|
89
|
+
date: formatDate(date, 'yyyy-MM-dd'),
|
|
90
|
+
originalText: stripDiacritics(subject),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const yearIsExplicit = typeof format.yearIsExplicit === 'boolean' ? format.yearIsExplicit : true;
|
|
96
|
+
const translatedSubject = format.localised ? translateChunk(subject, locale, translateAccentless) : subject;
|
|
97
|
+
if (yearIsExplicit) {
|
|
98
|
+
const date = parseDate(translatedSubject, format.dateFnsFormat, baseDate, { locale: dateFnsLocale });
|
|
99
|
+
if (!isValidDate(date)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const formatDirection = format.direction;
|
|
103
|
+
const configurationDirection = configuration.direction;
|
|
104
|
+
if (formatDirection && configurationDirection && format.dateFnsFormat.includes('yyyy') && formatDirection.replace('Y', '') === configurationDirection.replace('Y', '')) {
|
|
105
|
+
}
|
|
106
|
+
else if (format.direction && format.direction !== configuration.direction) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (format.direction && !configuration.direction) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
words = words.slice(wordOffset);
|
|
113
|
+
matches.push({
|
|
114
|
+
date: formatDate(date, 'yyyy-MM-dd'),
|
|
115
|
+
originalText: stripDiacritics(subject),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const date = parseDate(translatedSubject, format.dateFnsFormat, baseDate, { locale: dateFnsLocale });
|
|
120
|
+
if (!isValidDate(date)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const currentYear = parseInt(formatDate(baseDate, 'yyyy'), 10);
|
|
124
|
+
const currentMonth = parseInt(formatDate(baseDate, 'M'), 10) + currentYear * 12;
|
|
125
|
+
const parsedMonth = parseInt(formatDate(date, 'M'), 10) + parseInt(formatDate(date, 'yyyy'), 10) * 12;
|
|
126
|
+
const difference = parsedMonth - currentMonth;
|
|
127
|
+
let useYear;
|
|
128
|
+
if (difference >= configuration.maximumAge) {
|
|
129
|
+
useYear = currentYear - 1;
|
|
130
|
+
}
|
|
131
|
+
else if (difference < 0 && Math.abs(difference) >= configuration.minimumAge) {
|
|
132
|
+
useYear = currentYear + 1;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
useYear = currentYear;
|
|
136
|
+
}
|
|
137
|
+
const maybeDate = parseDate(useYear + '-' + formatDate(date, 'MM-dd'), 'yyyy-MM-dd', baseDate, { locale: dateFnsLocale });
|
|
138
|
+
if (!isValidDate(maybeDate)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (format.direction && format.direction !== configuration.direction) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (format.direction && !configuration.direction) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
words = words.slice(wordOffset);
|
|
148
|
+
matches.push({
|
|
149
|
+
date: formatDate(maybeDate, 'yyyy-MM-dd'),
|
|
150
|
+
originalText: stripDiacritics(subject),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
208
154
|
}
|
|
209
|
-
}
|
|
210
155
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return matches;
|
|
156
|
+
return matches;
|
|
214
157
|
};
|
|
@@ -1,34 +1,23 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
|
|
3
1
|
import moment from 'moment-timezone';
|
|
4
2
|
import dictionary from 'relative-date-names';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (normalizedSubject === translation.day.relative.today) {
|
|
26
|
-
return now.format('YYYY-MM-DD');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (normalizedSubject === translation.day.relative.tomorrow) {
|
|
30
|
-
return now.add(1, 'day').format('YYYY-MM-DD');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return null;
|
|
3
|
+
export default (subject, locale, timezone) => {
|
|
4
|
+
const translation = dictionary[locale];
|
|
5
|
+
if (!translation) {
|
|
6
|
+
throw new Error('No translation available for the target locale.');
|
|
7
|
+
}
|
|
8
|
+
if (timezone && !moment.tz.zone(timezone)) {
|
|
9
|
+
throw new Error('Unrecognized timezone.');
|
|
10
|
+
}
|
|
11
|
+
const normalizedSubject = subject.toLowerCase();
|
|
12
|
+
const now = moment();
|
|
13
|
+
if (normalizedSubject === translation.day.relative.yesterday) {
|
|
14
|
+
return now.subtract(1, 'day').format('YYYY-MM-DD');
|
|
15
|
+
}
|
|
16
|
+
if (normalizedSubject === translation.day.relative.today) {
|
|
17
|
+
return now.format('YYYY-MM-DD');
|
|
18
|
+
}
|
|
19
|
+
if (normalizedSubject === translation.day.relative.tomorrow) {
|
|
20
|
+
return now.add(1, 'day').format('YYYY-MM-DD');
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
34
23
|
};
|
package/src/index.d.ts
ADDED