@fhss-web-team/fuzzy-dates 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 FHSS Web Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # CFHG Fuzzy Dates
2
+
3
+ Parse imprecise, human-entered date strings into a structured, consistent model that you can normalize, search, sort, and serialize.
4
+
5
+ ## Features
6
+ - Handles modifiers like `before`, `after`, `about`, `between`, `from`, `early`, `mid`, `late`.
7
+ - Normalizes many input shapes (`1st of February 1900`, `Feb 1 1900`, `winter 1890`, `1800s`).
8
+ - Produces inclusive lower/upper bounds for range filtering and a collation key for stable chronological sorting.
9
+ - Outputs a canonical JSON model plus GEDCOM X formal dates for genealogy interoperability.
10
+ - ESM + TypeScript ready (`.d.ts` shipped with the package).
11
+
12
+ ## Installation
13
+ ```bash
14
+ npm install cfhg-fuzzy-dates
15
+ ```
16
+
17
+ ## Quick start
18
+ ```ts
19
+ import { FuzzyDate } from 'cfhg-fuzzy-dates';
20
+
21
+ const parsed = FuzzyDate.parse('about Feb 1900');
22
+ if (!parsed.ok) throw parsed.error;
23
+
24
+ const date = parsed.value;
25
+ console.log(date.normalized); // "about 1 February 1900"
26
+ console.log(date.lowerBound); // 1899-02-01T00:00:00.000Z (Date)
27
+ console.log(date.upperBound); // 1901-01-31T23:59:59.999Z (Date)
28
+ console.log(date.collationKey); // deterministic string for sorting
29
+
30
+ // Serialize for storage and hydrate later
31
+ const json = date.toJSON();
32
+ const hydrated = FuzzyDate.fromJSON(json);
33
+ ```
34
+
35
+ ## API snapshot
36
+ - `FuzzyDate.parse(input: string): Result<FuzzyDate, string>` — non-throwing parse; errors come back as `{ ok: false, error }`.
37
+ - `FuzzyDate.fromJSON(model: FuzzyDateModel)` — rebuild from the canonical JSON model.
38
+ - `FuzzyDate` instance properties:
39
+ - `original` — original input string.
40
+ - `normalized` — normalized human-readable string.
41
+ - `lowerBound` / `upperBound` — inclusive Date bounds (or `null` for unbounded).
42
+ - `collationKey` — sortable string aligned to chronological order.
43
+ - `formal` — GEDCOM X formal date representation.
44
+ - `toJSON()` — returns the canonical model.
45
+
46
+ ## Development
47
+ - Tests: `npm test` (Vitest)
48
+ - Build: `npm run build` (TypeScript -> `dist/`)
49
+ - Pack locally (publish dry-run): `npm pack` then install the generated `.tgz` in a temp project to verify exports and typings.
50
+
51
+ ## License
52
+ MIT © FHSS Web Team
@@ -0,0 +1,2 @@
1
+ import { FuzzyDateModel } from '../types';
2
+ export declare function collate(model: FuzzyDateModel): string;
@@ -0,0 +1,15 @@
1
+ import { FORMAT_ORDER, MODIFIER_ORDER } from '../types';
2
+ export function collate(model) {
3
+ const date1 = model.start.minDate.toISOString();
4
+ const modifier = MODIFIER_ORDER.indexOf(model.modifier)
5
+ .toString()
6
+ .padStart(2, '0');
7
+ const format1 = FORMAT_ORDER.indexOf(model.start.format)
8
+ .toString()
9
+ .padStart(2, '0');
10
+ const date2 = model.end.minDate.toISOString();
11
+ const format2 = FORMAT_ORDER.indexOf(model.end.format)
12
+ .toString()
13
+ .padStart(2, '0');
14
+ return `${date1}|${modifier}|${format1}|${date2}|${format2}`;
15
+ }
@@ -0,0 +1,206 @@
1
+ import { FuzzyDateModel } from './types';
2
+ /**
3
+ * Represents an immutable, parsed fuzzy date suitable for genealogy and
4
+ * historical data contexts.
5
+ *
6
+ * A `FuzzyDate` encapsulates a human-entered date expression
7
+ * (e.g. `"before 1900"`, `"between 1 Jan 1900 and 31 Dec 1901"`,
8
+ * `"March 1890"`) and exposes multiple *derived representations* optimized
9
+ * for different use cases:
10
+ *
11
+ * - A canonical JSON model for storage and transport
12
+ * - Query bounds for database filtering
13
+ * - A collation key for stable chronological ordering
14
+ * - A normalized, human-readable string form
15
+ * - A GEDCOM X formal date representation
16
+ *
17
+ * ---
18
+ *
19
+ * ### Design principles
20
+ *
21
+ * - **Immutable**: Instances cannot be mutated after construction.
22
+ * - **Explicit construction**: Instances must be created via `parse()` or
23
+ * `fromJSON()`. Direct construction via `new` is intentionally disallowed.
24
+ * - **Derived projections**: Query bounds, collation keys, and serializations
25
+ * are all derived from a single internal model to guarantee consistency.
26
+ * - **UTC semantics**: All date calculations and comparisons are performed
27
+ * using UTC to avoid timezone-related ambiguity.
28
+ *
29
+ * ---
30
+ *
31
+ * ### Intended usage
32
+ *
33
+ * - Use `parse()` for untrusted, human-entered input.
34
+ * - Use `fromJSON()` when hydrating from a trusted serialized model
35
+ * (e.g. database JSON).
36
+ * - Store the JSON model as the canonical representation.
37
+ * - Store `lowerBound`, `upperBound`, and `collationKey` separately for
38
+ * efficient querying and ordering.
39
+ */
40
+ export declare class FuzzyDate {
41
+ /**
42
+ * Canonical internal model representing the parsed fuzzy date.
43
+ *
44
+ * This model is considered the single source of truth; all other
45
+ * representations are derived from it.
46
+ */
47
+ private _model;
48
+ /**
49
+ * Private constructor to enforce factory-based creation.
50
+ *
51
+ * Consumers must use {@link FuzzyDate.parse} or {@link FuzzyDate.fromJSON}.
52
+ */
53
+ private constructor();
54
+ /**
55
+ * The original human-readable input string used to construct this fuzzy date.
56
+ *
57
+ * This value is preserved verbatim and is not normalized or modified.
58
+ */
59
+ get original(): string;
60
+ /**
61
+ * GEDCOM X formal date representation of this fuzzy date.
62
+ *
63
+ * This value is suitable for interoperability with genealogy systems
64
+ * that support the GEDCOM X date specification.
65
+ *
66
+ * @see https://github.com/FamilySearch/gedcomx/blob/master/specifications/date-format-specification.md
67
+ *
68
+ * @remarks
69
+ * This representation is derived and may evolve as GEDCOM X mapping
70
+ * rules are refined.
71
+ */
72
+ get formal(): string;
73
+ /**
74
+ * Normalized, human-readable representation of the fuzzy date.
75
+ *
76
+ * This format represents the interpreted meaning of the original input
77
+ * using a standardized vocabulary and ordering.
78
+ *
79
+ * Example outputs:
80
+ * - `"before 1900"`
81
+ * - `"March 1890"`
82
+ * - `"between 1 Jan 1900 and 31 Dec 1901"`
83
+ */
84
+ get normalized(): string;
85
+ /**
86
+ * The inclusive lower bound of this fuzzy date's possible interval (UTC).
87
+ *
88
+ * This bound is intended for **endpoint-in-window** searching:
89
+ * a fuzzy date matches an inclusive search range `[searchStart, searchEnd]`
90
+ * if **either** its `lowerBound` **or** its `upperBound` falls within the
91
+ * search range.
92
+ *
93
+ * Formally (inclusive):
94
+ *
95
+ * ```text
96
+ * (searchStart <= lowerBound <= searchEnd) OR (searchStart <= upperBound <= searchEnd)
97
+ * ```
98
+ *
99
+ * ## Unbounded intervals
100
+ *
101
+ * Open-ended intervals are represented with `null`:
102
+ * - `lowerBound === null` means unbounded in the past (−∞)
103
+ * - `upperBound === null` means unbounded in the future (+∞)
104
+ *
105
+ * When using the predicate above, treat a `null` bound as not satisfying
106
+ * the endpoint check (i.e., it is not “within” any finite window).
107
+ *
108
+ * @remarks
109
+ * - Derived from the canonical model.
110
+ * - Always interpreted as UTC.
111
+ */
112
+ get lowerBound(): Date | null;
113
+ /**
114
+ * The inclusive upper bound of this fuzzy date's possible interval (UTC).
115
+ *
116
+ * This bound is intended for **endpoint-in-window** searching:
117
+ * a fuzzy date matches an inclusive search range `[searchStart, searchEnd]`
118
+ * if **either** its `lowerBound` **or** its `upperBound` falls within the
119
+ * search range.
120
+ *
121
+ * Formally (inclusive):
122
+ *
123
+ * ```text
124
+ * (searchStart <= lowerBound <= searchEnd) OR (searchStart <= upperBound <= searchEnd)
125
+ * ```
126
+ *
127
+ * ## Unbounded intervals
128
+ *
129
+ * Open-ended intervals are represented with `null`:
130
+ * - `lowerBound === null` means unbounded in the past (−∞)
131
+ * - `upperBound === null` means unbounded in the future (+∞)
132
+ *
133
+ * When using the predicate above, treat a `null` bound as not satisfying
134
+ * the endpoint check (i.e., it is not “within” any finite window).
135
+ *
136
+ * @remarks
137
+ * - Derived from the canonical model.
138
+ * - Always interpreted as UTC.
139
+ */
140
+ get upperBound(): Date | null;
141
+ /**
142
+ * Collation key used for chronological ordering.
143
+ *
144
+ * The collation key is a derived string whose lexicographic ordering
145
+ * is guaranteed to match the semantic chronological ordering of fuzzy dates.
146
+ *
147
+ * ```text
148
+ * a occurs before b ⇔ a.collationKey < b.collationKey
149
+ * ```
150
+ *
151
+ * This property is intended for:
152
+ * - database indexing
153
+ * - `ORDER BY` clauses
154
+ * - stable sorting without parsing
155
+ *
156
+ * @remarks
157
+ * - Not human-readable
158
+ * - Not intended for round-tripping
159
+ * - Derived entirely from the canonical model
160
+ */
161
+ get collationKey(): string;
162
+ /**
163
+ * Serializes this fuzzy date to its canonical JSON representation.
164
+ *
165
+ * The returned object is suitable for long-term storage,
166
+ * transport, and later reconstruction via {@link fromJSON}.
167
+ */
168
+ toJSON(): FuzzyDateModel;
169
+ /**
170
+ * Reconstructs a `FuzzyDate` from a previously serialized canonical model.
171
+ *
172
+ * @param json A trusted `FuzzyDateModel`, typically loaded from storage.
173
+ */
174
+ static fromJSON(json: FuzzyDateModel): FuzzyDate;
175
+ /**
176
+ * Parses a human-readable date string into a `FuzzyDate`.
177
+ *
178
+ * This method should be used for all untrusted or user-provided input.
179
+ *
180
+ * @param input Human-readable date expression.
181
+ * @returns A result object containing either a `FuzzyDate` or parse issues.
182
+ *
183
+ * @remarks
184
+ * - Parsing is non-throwing; errors are returned as structured issues.
185
+ * - Successful results are guaranteed to produce a valid canonical model.
186
+ */
187
+ static parse(input: string): {
188
+ ok: false;
189
+ error: "Year is required.";
190
+ } | {
191
+ ok: false;
192
+ error: "Unknown month.";
193
+ } | {
194
+ ok: false;
195
+ error: "Unknown date format.";
196
+ } | {
197
+ ok: false;
198
+ error: "Invalid \"BETWEEN\" modifier.";
199
+ } | {
200
+ ok: false;
201
+ error: "Invalid \"FROM\" modifier.";
202
+ } | {
203
+ ok: true;
204
+ value: FuzzyDate;
205
+ };
206
+ }
@@ -0,0 +1,220 @@
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, ok, } from './types';
6
+ /**
7
+ * Represents an immutable, parsed fuzzy date suitable for genealogy and
8
+ * historical data contexts.
9
+ *
10
+ * A `FuzzyDate` encapsulates a human-entered date expression
11
+ * (e.g. `"before 1900"`, `"between 1 Jan 1900 and 31 Dec 1901"`,
12
+ * `"March 1890"`) and exposes multiple *derived representations* optimized
13
+ * for different use cases:
14
+ *
15
+ * - A canonical JSON model for storage and transport
16
+ * - Query bounds for database filtering
17
+ * - A collation key for stable chronological ordering
18
+ * - A normalized, human-readable string form
19
+ * - A GEDCOM X formal date representation
20
+ *
21
+ * ---
22
+ *
23
+ * ### Design principles
24
+ *
25
+ * - **Immutable**: Instances cannot be mutated after construction.
26
+ * - **Explicit construction**: Instances must be created via `parse()` or
27
+ * `fromJSON()`. Direct construction via `new` is intentionally disallowed.
28
+ * - **Derived projections**: Query bounds, collation keys, and serializations
29
+ * are all derived from a single internal model to guarantee consistency.
30
+ * - **UTC semantics**: All date calculations and comparisons are performed
31
+ * using UTC to avoid timezone-related ambiguity.
32
+ *
33
+ * ---
34
+ *
35
+ * ### Intended usage
36
+ *
37
+ * - Use `parse()` for untrusted, human-entered input.
38
+ * - Use `fromJSON()` when hydrating from a trusted serialized model
39
+ * (e.g. database JSON).
40
+ * - Store the JSON model as the canonical representation.
41
+ * - Store `lowerBound`, `upperBound`, and `collationKey` separately for
42
+ * efficient querying and ordering.
43
+ */
44
+ export class FuzzyDate {
45
+ /**
46
+ * Canonical internal model representing the parsed fuzzy date.
47
+ *
48
+ * This model is considered the single source of truth; all other
49
+ * representations are derived from it.
50
+ */
51
+ _model;
52
+ /**
53
+ * Private constructor to enforce factory-based creation.
54
+ *
55
+ * Consumers must use {@link FuzzyDate.parse} or {@link FuzzyDate.fromJSON}.
56
+ */
57
+ constructor(model) {
58
+ this._model = model;
59
+ }
60
+ /**
61
+ * The original human-readable input string used to construct this fuzzy date.
62
+ *
63
+ * This value is preserved verbatim and is not normalized or modified.
64
+ */
65
+ get original() {
66
+ return this._model.original;
67
+ }
68
+ /**
69
+ * GEDCOM X formal date representation of this fuzzy date.
70
+ *
71
+ * This value is suitable for interoperability with genealogy systems
72
+ * that support the GEDCOM X date specification.
73
+ *
74
+ * @see https://github.com/FamilySearch/gedcomx/blob/master/specifications/date-format-specification.md
75
+ *
76
+ * @remarks
77
+ * This representation is derived and may evolve as GEDCOM X mapping
78
+ * rules are refined.
79
+ */
80
+ get formal() {
81
+ return toGedcomX(this._model);
82
+ }
83
+ /**
84
+ * Normalized, human-readable representation of the fuzzy date.
85
+ *
86
+ * This format represents the interpreted meaning of the original input
87
+ * using a standardized vocabulary and ordering.
88
+ *
89
+ * Example outputs:
90
+ * - `"before 1900"`
91
+ * - `"March 1890"`
92
+ * - `"between 1 Jan 1900 and 31 Dec 1901"`
93
+ */
94
+ get normalized() {
95
+ return normalize(this._model);
96
+ }
97
+ /**
98
+ * The inclusive lower bound of this fuzzy date's possible interval (UTC).
99
+ *
100
+ * This bound is intended for **endpoint-in-window** searching:
101
+ * a fuzzy date matches an inclusive search range `[searchStart, searchEnd]`
102
+ * if **either** its `lowerBound` **or** its `upperBound` falls within the
103
+ * search range.
104
+ *
105
+ * Formally (inclusive):
106
+ *
107
+ * ```text
108
+ * (searchStart <= lowerBound <= searchEnd) OR (searchStart <= upperBound <= searchEnd)
109
+ * ```
110
+ *
111
+ * ## Unbounded intervals
112
+ *
113
+ * Open-ended intervals are represented with `null`:
114
+ * - `lowerBound === null` means unbounded in the past (−∞)
115
+ * - `upperBound === null` means unbounded in the future (+∞)
116
+ *
117
+ * When using the predicate above, treat a `null` bound as not satisfying
118
+ * the endpoint check (i.e., it is not “within” any finite window).
119
+ *
120
+ * @remarks
121
+ * - Derived from the canonical model.
122
+ * - Always interpreted as UTC.
123
+ */
124
+ get lowerBound() {
125
+ return this._model.start.minDate === DATE_NEG_INFINITY
126
+ ? null
127
+ : this._model.start.minDate;
128
+ }
129
+ /**
130
+ * The inclusive upper bound of this fuzzy date's possible interval (UTC).
131
+ *
132
+ * This bound is intended for **endpoint-in-window** searching:
133
+ * a fuzzy date matches an inclusive search range `[searchStart, searchEnd]`
134
+ * if **either** its `lowerBound` **or** its `upperBound` falls within the
135
+ * search range.
136
+ *
137
+ * Formally (inclusive):
138
+ *
139
+ * ```text
140
+ * (searchStart <= lowerBound <= searchEnd) OR (searchStart <= upperBound <= searchEnd)
141
+ * ```
142
+ *
143
+ * ## Unbounded intervals
144
+ *
145
+ * Open-ended intervals are represented with `null`:
146
+ * - `lowerBound === null` means unbounded in the past (−∞)
147
+ * - `upperBound === null` means unbounded in the future (+∞)
148
+ *
149
+ * When using the predicate above, treat a `null` bound as not satisfying
150
+ * the endpoint check (i.e., it is not “within” any finite window).
151
+ *
152
+ * @remarks
153
+ * - Derived from the canonical model.
154
+ * - Always interpreted as UTC.
155
+ */
156
+ get upperBound() {
157
+ return this._model.start.maxDate === DATE_POS_INFINITY
158
+ ? null
159
+ : this._model.start.maxDate;
160
+ }
161
+ /**
162
+ * Collation key used for chronological ordering.
163
+ *
164
+ * The collation key is a derived string whose lexicographic ordering
165
+ * is guaranteed to match the semantic chronological ordering of fuzzy dates.
166
+ *
167
+ * ```text
168
+ * a occurs before b ⇔ a.collationKey < b.collationKey
169
+ * ```
170
+ *
171
+ * This property is intended for:
172
+ * - database indexing
173
+ * - `ORDER BY` clauses
174
+ * - stable sorting without parsing
175
+ *
176
+ * @remarks
177
+ * - Not human-readable
178
+ * - Not intended for round-tripping
179
+ * - Derived entirely from the canonical model
180
+ */
181
+ get collationKey() {
182
+ return collate(this._model);
183
+ }
184
+ /**
185
+ * Serializes this fuzzy date to its canonical JSON representation.
186
+ *
187
+ * The returned object is suitable for long-term storage,
188
+ * transport, and later reconstruction via {@link fromJSON}.
189
+ */
190
+ toJSON() {
191
+ return this._model;
192
+ }
193
+ /**
194
+ * Reconstructs a `FuzzyDate` from a previously serialized canonical model.
195
+ *
196
+ * @param json A trusted `FuzzyDateModel`, typically loaded from storage.
197
+ */
198
+ static fromJSON(json) {
199
+ return new FuzzyDate(json);
200
+ }
201
+ /**
202
+ * Parses a human-readable date string into a `FuzzyDate`.
203
+ *
204
+ * This method should be used for all untrusted or user-provided input.
205
+ *
206
+ * @param input Human-readable date expression.
207
+ * @returns A result object containing either a `FuzzyDate` or parse issues.
208
+ *
209
+ * @remarks
210
+ * - Parsing is non-throwing; errors are returned as structured issues.
211
+ * - Successful results are guaranteed to produce a valid canonical model.
212
+ */
213
+ static parse(input) {
214
+ const result = parse(input);
215
+ if (!result.ok)
216
+ return result;
217
+ const date = new FuzzyDate(result.value);
218
+ return ok(date);
219
+ }
220
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,158 @@
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
+ });
@@ -0,0 +1,2 @@
1
+ import { FuzzyDateModel } from '../types';
2
+ export declare function toGedcomX(model: FuzzyDateModel): string;