@fhss-web-team/fuzzy-dates 1.2.2 → 2.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/README.md +99 -36
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -2
- package/dist/src/collation/collationKey.d.ts +2 -0
- package/dist/src/collation/collationKey.js +44 -0
- package/dist/{fuzzyDate → src}/fuzzyDate.d.ts +85 -69
- package/dist/src/fuzzyDate.js +238 -0
- package/dist/src/gedcomX/toGedcomX.js +28 -0
- package/dist/src/helpers/constants.d.ts +2 -0
- package/dist/src/helpers/constants.js +2 -0
- package/dist/src/helpers/types.d.ts +12 -0
- package/dist/src/helpers/types.js +3 -0
- package/dist/src/normalize/normalize.js +51 -0
- package/dist/src/parse/index.d.ts +47 -0
- package/dist/src/parse/index.js +41 -0
- package/dist/src/parse/modifiers.d.ts +182 -0
- package/dist/src/parse/modifiers.js +62 -0
- package/dist/{fuzzyDate/parse/inputDateFormats.d.ts → src/parse/simpleDate/formats.d.ts} +33 -50
- package/dist/{fuzzyDate/parse/inputDateFormats.js → src/parse/simpleDate/formats.js} +69 -88
- package/dist/src/parse/simpleDate/helpers.d.ts +21 -0
- package/dist/src/parse/simpleDate/helpers.js +58 -0
- package/dist/src/parse/simpleDate/parse.d.ts +31 -0
- package/dist/{fuzzyDate/parse/stringToDate.js → src/parse/simpleDate/parse.js} +4 -5
- package/package.json +1 -11
- package/dist/fuzzyDate/collate/collate.d.ts +0 -2
- package/dist/fuzzyDate/collate/collate.js +0 -15
- package/dist/fuzzyDate/fuzzyDate.js +0 -246
- package/dist/fuzzyDate/fuzzyDate.spec.d.ts +0 -1
- package/dist/fuzzyDate/fuzzyDate.spec.js +0 -158
- package/dist/fuzzyDate/gedcomX/toGedcomX.js +0 -31
- package/dist/fuzzyDate/helpers/constants.d.ts +0 -4
- package/dist/fuzzyDate/helpers/constants.js +0 -20
- package/dist/fuzzyDate/helpers/schemas.d.ts +0 -36
- package/dist/fuzzyDate/helpers/schemas.js +0 -12
- package/dist/fuzzyDate/helpers/types.d.ts +0 -16
- package/dist/fuzzyDate/helpers/types.js +0 -1
- package/dist/fuzzyDate/normalize/normalize.js +0 -47
- package/dist/fuzzyDate/parse/index.d.ts +0 -178
- package/dist/fuzzyDate/parse/index.js +0 -92
- package/dist/fuzzyDate/parse/modifiers.d.ts +0 -231
- package/dist/fuzzyDate/parse/modifiers.js +0 -185
- package/dist/fuzzyDate/parse/stringToDate.d.ts +0 -38
- /package/dist/{fuzzyDate → src}/gedcomX/toGedcomX.d.ts +0 -0
- /package/dist/{fuzzyDate → src}/helpers/result.d.ts +0 -0
- /package/dist/{fuzzyDate → src}/helpers/result.js +0 -0
- /package/dist/{fuzzyDate → src}/normalize/normalize.d.ts +0 -0
- /package/dist/{fuzzyDate/helpers → src/parse/simpleDate}/maps.d.ts +0 -0
- /package/dist/{fuzzyDate/helpers → src/parse/simpleDate}/maps.js +0 -0
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { parse } from './parse/index';
|
|
2
|
-
import { normalize } from './normalize/normalize';
|
|
3
|
-
import { collate } from './collate/collate';
|
|
4
|
-
import { toGedcomX } from './gedcomX/toGedcomX';
|
|
5
|
-
import { DATE_NEG_INFINITY, DATE_POS_INFINITY } from './helpers/constants';
|
|
6
|
-
import { ok, err } from './helpers/result';
|
|
7
|
-
import { fuzzyDateJsonSchema } from './helpers/schemas';
|
|
8
|
-
/**
|
|
9
|
-
* Represents an immutable, parsed fuzzy date suitable for genealogy and
|
|
10
|
-
* historical data contexts.
|
|
11
|
-
*
|
|
12
|
-
* A `FuzzyDate` encapsulates a human-entered date expression
|
|
13
|
-
* (e.g. `"before 1900"`, `"between 1 Jan 1900 and 31 Dec 1901"`,
|
|
14
|
-
* `"March 1890"`) and exposes multiple *derived representations* optimized
|
|
15
|
-
* for different use cases:
|
|
16
|
-
*
|
|
17
|
-
* - A canonical JSON model for storage and transport
|
|
18
|
-
* - Query bounds for database filtering
|
|
19
|
-
* - A collation key for stable chronological ordering
|
|
20
|
-
* - A normalized, human-readable string form
|
|
21
|
-
* - A GEDCOM X formal date representation
|
|
22
|
-
*
|
|
23
|
-
* ---
|
|
24
|
-
*
|
|
25
|
-
* ### Design principles
|
|
26
|
-
*
|
|
27
|
-
* - **Immutable**: Instances cannot be mutated after construction.
|
|
28
|
-
* - **Explicit construction**: Instances must be created via `parse()` or
|
|
29
|
-
* `fromJSON()`. Direct construction via `new` is intentionally disallowed.
|
|
30
|
-
* - **Derived projections**: Query bounds, collation keys, and serializations
|
|
31
|
-
* are all derived from a single internal model to guarantee consistency.
|
|
32
|
-
* - **UTC semantics**: All date calculations and comparisons are performed
|
|
33
|
-
* using UTC to avoid timezone-related ambiguity.
|
|
34
|
-
*
|
|
35
|
-
* ---
|
|
36
|
-
*
|
|
37
|
-
* ### Intended usage
|
|
38
|
-
*
|
|
39
|
-
* - Use `parse()` for untrusted, human-entered input.
|
|
40
|
-
* - Use `fromJSON()` when hydrating from a trusted serialized model
|
|
41
|
-
* (e.g. database JSON).
|
|
42
|
-
* - Store the JSON model as the canonical representation.
|
|
43
|
-
* - Store `lowerBound`, `upperBound`, and `collationKey` separately for
|
|
44
|
-
* efficient querying and ordering.
|
|
45
|
-
*/
|
|
46
|
-
export class FuzzyDate {
|
|
47
|
-
/**
|
|
48
|
-
* Canonical internal model representing the parsed fuzzy date.
|
|
49
|
-
*
|
|
50
|
-
* This model is considered the single source of truth; all other
|
|
51
|
-
* representations are derived from it.
|
|
52
|
-
*/
|
|
53
|
-
_model;
|
|
54
|
-
/**
|
|
55
|
-
* Private constructor to enforce factory-based creation.
|
|
56
|
-
*
|
|
57
|
-
* Consumers must use {@link FuzzyDate.parse} or {@link FuzzyDate.fromJSON}.
|
|
58
|
-
*/
|
|
59
|
-
constructor(model) {
|
|
60
|
-
this._model = model;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* GEDCOM X formal date representation of this fuzzy date.
|
|
64
|
-
*
|
|
65
|
-
* This value is suitable for interoperability with genealogy systems
|
|
66
|
-
* that support the GEDCOM X date specification.
|
|
67
|
-
*
|
|
68
|
-
* @see https://github.com/FamilySearch/gedcomx/blob/master/specifications/date-format-specification.md
|
|
69
|
-
*
|
|
70
|
-
* @remarks
|
|
71
|
-
* This representation is derived and may evolve as GEDCOM X mapping
|
|
72
|
-
* rules are refined.
|
|
73
|
-
*/
|
|
74
|
-
get formal() {
|
|
75
|
-
return toGedcomX(this._model);
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Normalized, human-readable representation of the fuzzy date.
|
|
79
|
-
*
|
|
80
|
-
* This format represents the interpreted meaning of the original input
|
|
81
|
-
* using a standardized vocabulary and ordering.
|
|
82
|
-
*
|
|
83
|
-
* Example outputs:
|
|
84
|
-
* - `"before 1900"`
|
|
85
|
-
* - `"March 1890"`
|
|
86
|
-
* - `"between 1 Jan 1900 and 31 Dec 1901"`
|
|
87
|
-
*/
|
|
88
|
-
get normalized() {
|
|
89
|
-
return normalize(this._model);
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* The inclusive lower bound of this fuzzy date's possible interval (UTC).
|
|
93
|
-
*
|
|
94
|
-
* This bound is intended for **endpoint-in-window** searching:
|
|
95
|
-
* a fuzzy date matches an inclusive search range `[searchStart, searchEnd]`
|
|
96
|
-
* if **either** its `lowerBound` **or** its `upperBound` falls within the
|
|
97
|
-
* search range.
|
|
98
|
-
*
|
|
99
|
-
* Formally (inclusive):
|
|
100
|
-
*
|
|
101
|
-
* ```text
|
|
102
|
-
* (searchStart <= lowerBound <= searchEnd) OR (searchStart <= upperBound <= searchEnd)
|
|
103
|
-
* ```
|
|
104
|
-
*
|
|
105
|
-
* ## Unbounded intervals
|
|
106
|
-
*
|
|
107
|
-
* Open-ended intervals are represented with `null`:
|
|
108
|
-
* - `lowerBound === null` means unbounded in the past (−∞)
|
|
109
|
-
* - `upperBound === null` means unbounded in the future (+∞)
|
|
110
|
-
*
|
|
111
|
-
* When using the predicate above, treat a `null` bound as not satisfying
|
|
112
|
-
* the endpoint check (i.e., it is not “within” any finite window).
|
|
113
|
-
*
|
|
114
|
-
* @remarks
|
|
115
|
-
* - Derived from the canonical model.
|
|
116
|
-
* - Always interpreted as UTC.
|
|
117
|
-
*/
|
|
118
|
-
get lowerBound() {
|
|
119
|
-
return this._model.start.minDate === DATE_NEG_INFINITY
|
|
120
|
-
? null
|
|
121
|
-
: this._model.start.minDate;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* The inclusive upper bound of this fuzzy date's possible interval (UTC).
|
|
125
|
-
*
|
|
126
|
-
* This bound is intended for **endpoint-in-window** searching:
|
|
127
|
-
* a fuzzy date matches an inclusive search range `[searchStart, searchEnd]`
|
|
128
|
-
* if **either** its `lowerBound` **or** its `upperBound` falls within the
|
|
129
|
-
* search range.
|
|
130
|
-
*
|
|
131
|
-
* Formally (inclusive):
|
|
132
|
-
*
|
|
133
|
-
* ```text
|
|
134
|
-
* (searchStart <= lowerBound <= searchEnd) OR (searchStart <= upperBound <= searchEnd)
|
|
135
|
-
* ```
|
|
136
|
-
*
|
|
137
|
-
* ## Unbounded intervals
|
|
138
|
-
*
|
|
139
|
-
* Open-ended intervals are represented with `null`:
|
|
140
|
-
* - `lowerBound === null` means unbounded in the past (−∞)
|
|
141
|
-
* - `upperBound === null` means unbounded in the future (+∞)
|
|
142
|
-
*
|
|
143
|
-
* When using the predicate above, treat a `null` bound as not satisfying
|
|
144
|
-
* the endpoint check (i.e., it is not “within” any finite window).
|
|
145
|
-
*
|
|
146
|
-
* @remarks
|
|
147
|
-
* - Derived from the canonical model.
|
|
148
|
-
* - Always interpreted as UTC.
|
|
149
|
-
*/
|
|
150
|
-
get upperBound() {
|
|
151
|
-
return this._model.start.maxDate === DATE_POS_INFINITY
|
|
152
|
-
? null
|
|
153
|
-
: this._model.start.maxDate;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Collation key used for chronological ordering.
|
|
157
|
-
*
|
|
158
|
-
* The collation key is a derived string whose lexicographic ordering
|
|
159
|
-
* is guaranteed to match the semantic chronological ordering of fuzzy dates.
|
|
160
|
-
*
|
|
161
|
-
* ```text
|
|
162
|
-
* a occurs before b ⇔ a.collationKey < b.collationKey
|
|
163
|
-
* ```
|
|
164
|
-
*
|
|
165
|
-
* This property is intended for:
|
|
166
|
-
* - database indexing
|
|
167
|
-
* - `ORDER BY` clauses
|
|
168
|
-
* - stable sorting without parsing
|
|
169
|
-
*
|
|
170
|
-
* @remarks
|
|
171
|
-
* - Not human-readable
|
|
172
|
-
* - Not intended for round-tripping
|
|
173
|
-
* - Derived entirely from the canonical model
|
|
174
|
-
*/
|
|
175
|
-
get collationKey() {
|
|
176
|
-
return collate(this._model);
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Serializes this fuzzy date to its canonical JSON representation.
|
|
180
|
-
*
|
|
181
|
-
* The returned object is suitable for long-term storage,
|
|
182
|
-
* transport, and later reconstruction via {@link fromJSON}.
|
|
183
|
-
*/
|
|
184
|
-
toJSON() {
|
|
185
|
-
return {
|
|
186
|
-
modifier: this._model.modifier,
|
|
187
|
-
start: {
|
|
188
|
-
format: this._model.start.format,
|
|
189
|
-
minDate: this._model.start.minDate.toISOString(),
|
|
190
|
-
maxDate: this._model.start.maxDate.toISOString(),
|
|
191
|
-
},
|
|
192
|
-
end: {
|
|
193
|
-
format: this._model.end.format,
|
|
194
|
-
minDate: this._model.end.minDate.toISOString(),
|
|
195
|
-
maxDate: this._model.end.maxDate.toISOString(),
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Reconstructs a `FuzzyDate` from a previously serialized canonical model.
|
|
201
|
-
*
|
|
202
|
-
* @param data A trusted `FuzzyDateModel`, typically loaded from storage.
|
|
203
|
-
*/
|
|
204
|
-
static fromJSON(data) {
|
|
205
|
-
try {
|
|
206
|
-
const json = typeof data === 'string' ? JSON.parse(data) : data;
|
|
207
|
-
const safeJson = fuzzyDateJsonSchema.parse(json);
|
|
208
|
-
const model = {
|
|
209
|
-
modifier: safeJson.modifier,
|
|
210
|
-
start: {
|
|
211
|
-
format: safeJson.start.format,
|
|
212
|
-
minDate: new Date(safeJson.start.minDate),
|
|
213
|
-
maxDate: new Date(safeJson.start.maxDate),
|
|
214
|
-
},
|
|
215
|
-
end: {
|
|
216
|
-
format: safeJson.end.format,
|
|
217
|
-
minDate: new Date(safeJson.end.minDate),
|
|
218
|
-
maxDate: new Date(safeJson.end.maxDate),
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
return ok(new FuzzyDate(model));
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
return err('Invalid JSON data.');
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Parses a human-readable date string into a `FuzzyDate`.
|
|
229
|
-
*
|
|
230
|
-
* This method should be used for all untrusted or user-provided input.
|
|
231
|
-
*
|
|
232
|
-
* @param input Human-readable date expression.
|
|
233
|
-
* @returns A result object containing either a `FuzzyDate` or parse issues.
|
|
234
|
-
*
|
|
235
|
-
* @remarks
|
|
236
|
-
* - Parsing is non-throwing; errors are returned as structured issues.
|
|
237
|
-
* - Successful results are guaranteed to produce a valid canonical model.
|
|
238
|
-
*/
|
|
239
|
-
static parse(input) {
|
|
240
|
-
const result = parse(input);
|
|
241
|
-
if (!result.ok)
|
|
242
|
-
return result;
|
|
243
|
-
const date = new FuzzyDate(result.value);
|
|
244
|
-
return ok(date);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { assert, describe, expect, it } from 'vitest';
|
|
2
|
-
import { FuzzyDate } from './fuzzyDate';
|
|
3
|
-
describe('Fuzzy Date', () => {
|
|
4
|
-
it('errs if it parses an invalid date', () => {
|
|
5
|
-
const unknownDate = FuzzyDate.parse('a');
|
|
6
|
-
expect(unknownDate).toStrictEqual({
|
|
7
|
-
ok: false,
|
|
8
|
-
error: 'Unknown date format.',
|
|
9
|
-
});
|
|
10
|
-
const invalidBetween = FuzzyDate.parse('between 1900');
|
|
11
|
-
expect(invalidBetween).toStrictEqual({
|
|
12
|
-
ok: false,
|
|
13
|
-
error: 'Invalid "BETWEEN" modifier.',
|
|
14
|
-
});
|
|
15
|
-
const invalidFrom = FuzzyDate.parse('from 1900');
|
|
16
|
-
expect(invalidFrom).toStrictEqual({
|
|
17
|
-
ok: false,
|
|
18
|
-
error: 'Invalid "FROM" modifier.',
|
|
19
|
-
});
|
|
20
|
-
const invalidMonth = FuzzyDate.parse('jam 1900');
|
|
21
|
-
expect(invalidMonth).toStrictEqual({
|
|
22
|
-
ok: false,
|
|
23
|
-
error: 'Unknown month.',
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
it('parses date inputs', () => {
|
|
27
|
-
const formats = {
|
|
28
|
-
'1800s': '1800s',
|
|
29
|
-
'1800': '1800',
|
|
30
|
-
'Winter 1800': 'winter 1800',
|
|
31
|
-
'1800 Winter': 'winter 1800',
|
|
32
|
-
'Jan 1800': 'January 1800',
|
|
33
|
-
'January 1800': 'January 1800',
|
|
34
|
-
'1800 Jan': 'January 1800',
|
|
35
|
-
'1800 January': 'January 1800',
|
|
36
|
-
'1 Jan 1800': '1 January 1800',
|
|
37
|
-
'1 1800': 'January 1800',
|
|
38
|
-
'01 1800': 'January 1800',
|
|
39
|
-
'1800 1': 'January 1800',
|
|
40
|
-
'1800 01': 'January 1800',
|
|
41
|
-
'1 January 1800': '1 January 1800',
|
|
42
|
-
'01 Jan 1800': '1 January 1800',
|
|
43
|
-
'01 January 1800': '1 January 1800',
|
|
44
|
-
'Jan 1 1800': '1 January 1800',
|
|
45
|
-
'January 1 1800': '1 January 1800',
|
|
46
|
-
'Jan 01 1800': '1 January 1800',
|
|
47
|
-
'January 01 1800': '1 January 1800',
|
|
48
|
-
'1800 Jan 1': '1 January 1800',
|
|
49
|
-
'1800 January 1': '1 January 1800',
|
|
50
|
-
'1800 Jan 01': '1 January 1800',
|
|
51
|
-
'1800 January 01': '1 January 1800',
|
|
52
|
-
'1800 1 Jan': '1 January 1800',
|
|
53
|
-
'1800 1 January': '1 January 1800',
|
|
54
|
-
'1800 01 Jan': '1 January 1800',
|
|
55
|
-
'1800 01 January': '1 January 1800',
|
|
56
|
-
'1 1 1800': '1 January 1800',
|
|
57
|
-
'01 1 1800': '1 January 1800',
|
|
58
|
-
'1 01 1800': '1 January 1800',
|
|
59
|
-
'1800 1 1': '1 January 1800',
|
|
60
|
-
'1800 01 1': '1 January 1800',
|
|
61
|
-
'1800 1 01': '1 January 1800',
|
|
62
|
-
' 1st of February 1900 ': '1 February 1900',
|
|
63
|
-
'feb 1st, 1900': '1 February 1900',
|
|
64
|
-
'February 1st, 1900': '1 February 1900',
|
|
65
|
-
' ---___////,,,, sEpTeMbEr 30 th ,,, ---// 1984': '30 September 1984',
|
|
66
|
-
};
|
|
67
|
-
for (const [key, value] of Object.entries(formats)) {
|
|
68
|
-
const result = FuzzyDate.parse(key);
|
|
69
|
-
let normal;
|
|
70
|
-
if (result.ok) {
|
|
71
|
-
normal = result.value.normalized;
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
normal = result.error;
|
|
75
|
-
}
|
|
76
|
-
expect(normal).toStrictEqual(value);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
it('test modifiers parsing', () => {
|
|
80
|
-
const modifiers = [
|
|
81
|
-
'',
|
|
82
|
-
'before',
|
|
83
|
-
'after',
|
|
84
|
-
'about',
|
|
85
|
-
'between',
|
|
86
|
-
'from',
|
|
87
|
-
'early',
|
|
88
|
-
'mid',
|
|
89
|
-
'late',
|
|
90
|
-
'or',
|
|
91
|
-
];
|
|
92
|
-
const parse = {
|
|
93
|
-
'before 1st of February 1900': 'before 1 February 1900',
|
|
94
|
-
'after 1st of February 1900': 'after 1 February 1900',
|
|
95
|
-
'about 1st of February 1900': 'about 1 February 1900',
|
|
96
|
-
'between 1st of February 1900 and 18/03/1905': 'between 1 February 1900 and 18 March 1905',
|
|
97
|
-
'from 1st of February 1900 to 18/03/1905': 'from 1 February 1900 to 18 March 1905',
|
|
98
|
-
};
|
|
99
|
-
Object.entries(parse).forEach(([input, expected]) => {
|
|
100
|
-
const result = FuzzyDate.parse(input);
|
|
101
|
-
let normal;
|
|
102
|
-
if (result.ok) {
|
|
103
|
-
normal = result.value.normalized;
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
normal = result.error;
|
|
107
|
-
}
|
|
108
|
-
expect(normal).toStrictEqual(expected);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
it('orders serialized dates in chronological order', () => {
|
|
112
|
-
const order = [
|
|
113
|
-
'before 2000',
|
|
114
|
-
'before jan 2000',
|
|
115
|
-
'before jan 1 2000',
|
|
116
|
-
'about 2000',
|
|
117
|
-
'about jan 2000',
|
|
118
|
-
'about jan 1 2000',
|
|
119
|
-
'2000',
|
|
120
|
-
'jan 2000',
|
|
121
|
-
'jan 1 2000',
|
|
122
|
-
'from 2000 to 2001',
|
|
123
|
-
'from 2000 to jan 2001',
|
|
124
|
-
'from 2000 to 1 jan 2001',
|
|
125
|
-
'from jan 2000 to 2001',
|
|
126
|
-
'from jan 2000 to jan 2001',
|
|
127
|
-
'from jan 2000 to 1 jan 2001',
|
|
128
|
-
'from 1 jan 2000 to 2001',
|
|
129
|
-
'from 1 jan 2000 to jan 2001',
|
|
130
|
-
'from 1 jan 2000 to 1 jan 2001',
|
|
131
|
-
'between 2000 and 2001',
|
|
132
|
-
'between 2000 and jan 2001',
|
|
133
|
-
'between 2000 and 1 jan 2001',
|
|
134
|
-
'between jan 2000 and 2001',
|
|
135
|
-
'between jan 2000 and jan 2001',
|
|
136
|
-
'between jan 2000 and 1 jan 2001',
|
|
137
|
-
'between 1 jan 2000 and 2001',
|
|
138
|
-
'between 1 jan 2000 and jan 2001',
|
|
139
|
-
'between 1 jan 2000 and 1 jan 2001',
|
|
140
|
-
'after 2000',
|
|
141
|
-
'after jan 2000',
|
|
142
|
-
'after jan 1 2000',
|
|
143
|
-
'jan 2 2000',
|
|
144
|
-
'dec 31 2000',
|
|
145
|
-
'',
|
|
146
|
-
];
|
|
147
|
-
for (let i = 1; i < order.length - 1; i++) {
|
|
148
|
-
const previous = FuzzyDate.parse(order[i - 1]);
|
|
149
|
-
const current = FuzzyDate.parse(order[i]);
|
|
150
|
-
if (previous.ok && current.ok) {
|
|
151
|
-
expect(previous.value.collationKey < current.value.collationKey).toBeTruthy();
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
assert.fail('failed to parse input');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export function toGedcomX(model) {
|
|
2
|
-
const startDate = toSimpleDate(model.start);
|
|
3
|
-
const endDate = toSimpleDate(model.end);
|
|
4
|
-
const gedcomModifierMap = {
|
|
5
|
-
NONE: startDate,
|
|
6
|
-
BEFORE: `/${endDate}`,
|
|
7
|
-
AFTER: `${startDate}/`,
|
|
8
|
-
EARLY: `${startDate}/${endDate}`,
|
|
9
|
-
MID: `${startDate}/${endDate}`,
|
|
10
|
-
LATE: `${startDate}/${endDate}`,
|
|
11
|
-
FROM: `${startDate}/${endDate}`,
|
|
12
|
-
BETWEEN: `${startDate}/${endDate}`,
|
|
13
|
-
ABOUT: `A${startDate}`,
|
|
14
|
-
};
|
|
15
|
-
return gedcomModifierMap[model.modifier];
|
|
16
|
-
}
|
|
17
|
-
function toSimpleDate(dateValue) {
|
|
18
|
-
const year = dateValue.minDate.getUTCFullYear();
|
|
19
|
-
const month = dateValue.minDate.getUTCMonth() + 1;
|
|
20
|
-
const day = dateValue.minDate.getUTCDate();
|
|
21
|
-
const sign = year >= 0 ? '+' : '-';
|
|
22
|
-
const yearAbs = Math.abs(year);
|
|
23
|
-
const yyyy = String(yearAbs).padStart(4, '0');
|
|
24
|
-
if (dateValue.format === 'YYYY' || dateValue.format === 'YYYYs')
|
|
25
|
-
return `${sign}${yyyy}`;
|
|
26
|
-
const mm = String(month).padStart(2, '0');
|
|
27
|
-
if (dateValue.format === 'MMMM_YYYY' || dateValue.format === 'SEASON_YYYY')
|
|
28
|
-
return `${sign}${yyyy}-${mm}`;
|
|
29
|
-
const dd = String(day).padStart(2, '0');
|
|
30
|
-
return `${sign}${yyyy}-${mm}-${dd}`;
|
|
31
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export declare const DATE_NEG_INFINITY: Date;
|
|
2
|
-
export declare const DATE_POS_INFINITY: Date;
|
|
3
|
-
export declare const FORMAT_ORDER: readonly ["YYYYs", "YYYY", "SEASON_YYYY", "MMMM_YYYY", "D_MMMM_YYYY"];
|
|
4
|
-
export declare const MODIFIER_ORDER: readonly ["BEFORE", "ABOUT", "NONE", "EARLY", "MID", "LATE", "FROM", "BETWEEN", "AFTER"];
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export const DATE_NEG_INFINITY = new Date(-8640000000000000);
|
|
2
|
-
export const DATE_POS_INFINITY = new Date(8640000000000000);
|
|
3
|
-
export const FORMAT_ORDER = [
|
|
4
|
-
'YYYYs',
|
|
5
|
-
'YYYY',
|
|
6
|
-
'SEASON_YYYY',
|
|
7
|
-
'MMMM_YYYY',
|
|
8
|
-
'D_MMMM_YYYY',
|
|
9
|
-
];
|
|
10
|
-
export const MODIFIER_ORDER = [
|
|
11
|
-
'BEFORE',
|
|
12
|
-
'ABOUT',
|
|
13
|
-
'NONE',
|
|
14
|
-
'EARLY',
|
|
15
|
-
'MID',
|
|
16
|
-
'LATE',
|
|
17
|
-
'FROM',
|
|
18
|
-
'BETWEEN',
|
|
19
|
-
'AFTER',
|
|
20
|
-
];
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import z from 'zod';
|
|
2
|
-
export declare const fuzzyDateJsonSchema: z.ZodObject<{
|
|
3
|
-
modifier: z.ZodEnum<{
|
|
4
|
-
BEFORE: "BEFORE";
|
|
5
|
-
ABOUT: "ABOUT";
|
|
6
|
-
NONE: "NONE";
|
|
7
|
-
EARLY: "EARLY";
|
|
8
|
-
MID: "MID";
|
|
9
|
-
LATE: "LATE";
|
|
10
|
-
FROM: "FROM";
|
|
11
|
-
BETWEEN: "BETWEEN";
|
|
12
|
-
AFTER: "AFTER";
|
|
13
|
-
}>;
|
|
14
|
-
start: z.ZodObject<{
|
|
15
|
-
format: z.ZodEnum<{
|
|
16
|
-
YYYYs: "YYYYs";
|
|
17
|
-
YYYY: "YYYY";
|
|
18
|
-
SEASON_YYYY: "SEASON_YYYY";
|
|
19
|
-
MMMM_YYYY: "MMMM_YYYY";
|
|
20
|
-
D_MMMM_YYYY: "D_MMMM_YYYY";
|
|
21
|
-
}>;
|
|
22
|
-
minDate: z.ZodISODateTime;
|
|
23
|
-
maxDate: z.ZodISODateTime;
|
|
24
|
-
}, z.core.$strip>;
|
|
25
|
-
end: z.ZodObject<{
|
|
26
|
-
format: z.ZodEnum<{
|
|
27
|
-
YYYYs: "YYYYs";
|
|
28
|
-
YYYY: "YYYY";
|
|
29
|
-
SEASON_YYYY: "SEASON_YYYY";
|
|
30
|
-
MMMM_YYYY: "MMMM_YYYY";
|
|
31
|
-
D_MMMM_YYYY: "D_MMMM_YYYY";
|
|
32
|
-
}>;
|
|
33
|
-
minDate: z.ZodISODateTime;
|
|
34
|
-
maxDate: z.ZodISODateTime;
|
|
35
|
-
}, z.core.$strip>;
|
|
36
|
-
}, z.core.$strip>;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import z from 'zod';
|
|
2
|
-
import { FORMAT_ORDER, MODIFIER_ORDER } from './constants';
|
|
3
|
-
const fuzzyDateValueSchema = z.object({
|
|
4
|
-
format: z.enum(FORMAT_ORDER),
|
|
5
|
-
minDate: z.iso.datetime(),
|
|
6
|
-
maxDate: z.iso.datetime(),
|
|
7
|
-
});
|
|
8
|
-
export const fuzzyDateJsonSchema = z.object({
|
|
9
|
-
modifier: z.enum(MODIFIER_ORDER),
|
|
10
|
-
start: fuzzyDateValueSchema,
|
|
11
|
-
end: fuzzyDateValueSchema,
|
|
12
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import z from 'zod';
|
|
2
|
-
import { FORMAT_ORDER, MODIFIER_ORDER } from './constants';
|
|
3
|
-
import { fuzzyDateJsonSchema } from './schemas';
|
|
4
|
-
export type FuzzyDateFormat = (typeof FORMAT_ORDER)[number];
|
|
5
|
-
export type FuzzyDate = (typeof MODIFIER_ORDER)[number];
|
|
6
|
-
export type FuzzyDateValue = {
|
|
7
|
-
format: FuzzyDateFormat;
|
|
8
|
-
minDate: Date;
|
|
9
|
-
maxDate: Date;
|
|
10
|
-
};
|
|
11
|
-
export type FuzzyDateModel = {
|
|
12
|
-
modifier: FuzzyDate;
|
|
13
|
-
start: FuzzyDateValue;
|
|
14
|
-
end: FuzzyDateValue;
|
|
15
|
-
};
|
|
16
|
-
export type FuzzyDateJson = z.infer<typeof fuzzyDateJsonSchema>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { isSeasonMonth, SEASON_MONTH_MAP } from '../helpers/maps';
|
|
2
|
-
// Main function
|
|
3
|
-
export function normalize(model) {
|
|
4
|
-
const startDate = normalizeDate(model.start.minDate, model.start.format);
|
|
5
|
-
const endDate = normalizeDate(model.end.minDate, model.end.format);
|
|
6
|
-
const normalModifierMap = {
|
|
7
|
-
NONE: startDate,
|
|
8
|
-
BEFORE: `before ${endDate}`,
|
|
9
|
-
AFTER: `after ${startDate}`,
|
|
10
|
-
EARLY: `early ${startDate}`,
|
|
11
|
-
MID: `mid ${startDate}`,
|
|
12
|
-
LATE: `late ${startDate}`,
|
|
13
|
-
FROM: `from ${startDate} to ${endDate}`,
|
|
14
|
-
BETWEEN: `between ${startDate} and ${endDate}`,
|
|
15
|
-
ABOUT: `about ${startDate}`,
|
|
16
|
-
};
|
|
17
|
-
return normalModifierMap[model.modifier];
|
|
18
|
-
}
|
|
19
|
-
// Helpers
|
|
20
|
-
function normalizeDate(date, format) {
|
|
21
|
-
const options = { timeZone: 'UTC' };
|
|
22
|
-
switch (format) {
|
|
23
|
-
case 'YYYY':
|
|
24
|
-
options.year = 'numeric';
|
|
25
|
-
break;
|
|
26
|
-
case 'MMMM_YYYY':
|
|
27
|
-
options.month = 'long';
|
|
28
|
-
options.year = 'numeric';
|
|
29
|
-
break;
|
|
30
|
-
case 'D_MMMM_YYYY':
|
|
31
|
-
options.day = 'numeric';
|
|
32
|
-
options.month = 'long';
|
|
33
|
-
options.year = 'numeric';
|
|
34
|
-
break;
|
|
35
|
-
case 'SEASON_YYYY': {
|
|
36
|
-
const month = date.getUTCMonth() + 1;
|
|
37
|
-
if (!isSeasonMonth(month))
|
|
38
|
-
return ''; // month always returns 1 - 12 so this should never happen
|
|
39
|
-
return `${SEASON_MONTH_MAP[month]} ${date.getUTCFullYear()}`;
|
|
40
|
-
}
|
|
41
|
-
case 'YYYYs': {
|
|
42
|
-
const year = date.getUTCFullYear();
|
|
43
|
-
return `${year}s`;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return date.toLocaleDateString('en-GB', options);
|
|
47
|
-
}
|