@opendata-ai/openchart-core 7.11.0 → 8.0.0-rc.2
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/dist/index.d.ts +1154 -119
- package/dist/index.js +533 -42
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2 -1
- package/dist/styles.css.map +1 -0
- package/package.json +9 -3
- package/schema/chart.schema.json +8957 -0
- package/schema/table.schema.json +861 -0
- package/schema/vizspec.schema.json +17694 -0
- package/src/accessibility/alt-text.ts +39 -1
- package/src/accessibility/aria.ts +4 -2
- package/src/colors/__tests__/colorblind.test.ts +12 -0
- package/src/colors/colorblind.ts +11 -2
- package/src/colors/contrast.ts +11 -0
- package/src/colors/index.ts +9 -1
- package/src/colors/palettes.ts +47 -0
- package/src/helpers/__tests__/deep-merge.test.ts +54 -0
- package/src/helpers/__tests__/spec-builders.test.ts +3 -3
- package/src/helpers/deep-merge.ts +29 -0
- package/src/helpers/spec-builders.ts +7 -7
- package/src/index.ts +29 -1
- package/src/labels/collision.ts +1 -0
- package/src/layout/chrome.ts +24 -0
- package/src/layout/index.ts +1 -1
- package/src/locale/__tests__/format.test.ts +347 -11
- package/src/locale/format.ts +243 -18
- package/src/locale/index.ts +10 -1
- package/src/responsive/index.ts +2 -0
- package/src/responsive/metrics.ts +77 -10
- package/src/schema/__tests__/generated-artifacts.test.ts +42 -0
- package/src/schema/__tests__/schema.test.ts +179 -0
- package/src/styles/__tests__/reduced-motion-selectors.test.ts +28 -0
- package/src/styles/animation.css +197 -168
- package/src/styles/base.css +89 -62
- package/src/styles/chrome.css +173 -179
- package/src/styles/dark.css +38 -35
- package/src/styles/graph.css +116 -111
- package/src/styles/index.css +5 -0
- package/src/styles/keyframes.css +115 -100
- package/src/styles/legend.css +14 -12
- package/src/styles/reduced-motion.css +61 -40
- package/src/styles/series-search.css +153 -0
- package/src/styles/sparkline.css +14 -12
- package/src/styles/table-animation.css +82 -80
- package/src/styles/table.css +314 -302
- package/src/styles/token-definitions.ts +199 -0
- package/src/styles/tokens.css +132 -128
- package/src/styles/tooltip.css +109 -115
- package/src/styles/you-draw-it.css +126 -0
- package/src/types/__tests__/encoding.test.ts +86 -17
- package/src/types/__tests__/spec.test.ts +16 -12
- package/src/types/encoding.ts +69 -25
- package/src/types/events.ts +12 -0
- package/src/types/index.ts +39 -3
- package/src/types/layout.ts +414 -8
- package/src/types/schema-roots.ts +32 -0
- package/src/types/spec.ts +643 -84
- package/src/types/table.ts +3 -2
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
abbreviateNumber,
|
|
4
|
+
buildCompactStepFormatter,
|
|
4
5
|
buildD3Formatter,
|
|
5
6
|
buildTemporalFormatter,
|
|
7
|
+
computeFieldFormatContext,
|
|
8
|
+
defaultNumberFormatter,
|
|
9
|
+
formatCurrency,
|
|
6
10
|
formatDate,
|
|
7
11
|
formatNumber,
|
|
12
|
+
formatOrdinal,
|
|
13
|
+
formatPercent,
|
|
14
|
+
isYearContext,
|
|
15
|
+
isYearLikeValues,
|
|
16
|
+
resolveNumberFormatter,
|
|
8
17
|
} from '../format';
|
|
9
18
|
|
|
10
19
|
describe('formatNumber', () => {
|
|
@@ -22,7 +31,7 @@ describe('formatNumber', () => {
|
|
|
22
31
|
|
|
23
32
|
it('handles negative numbers', () => {
|
|
24
33
|
// d3-format uses unicode minus sign (U+2212), not ASCII hyphen-minus
|
|
25
|
-
expect(formatNumber(-1234)).toBe('
|
|
34
|
+
expect(formatNumber(-1234)).toBe('−1,234');
|
|
26
35
|
});
|
|
27
36
|
|
|
28
37
|
it('handles Infinity', () => {
|
|
@@ -32,6 +41,16 @@ describe('formatNumber', () => {
|
|
|
32
41
|
it('handles NaN', () => {
|
|
33
42
|
expect(formatNumber(NaN)).toBe('NaN');
|
|
34
43
|
});
|
|
44
|
+
|
|
45
|
+
it('preserves small decimals instead of rounding to 0.00', () => {
|
|
46
|
+
expect(formatNumber(0.0034)).toBe('0.0034');
|
|
47
|
+
expect(formatNumber(0.000012)).toBe('0.000012');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('still uses ,.2f for normal-sized decimals', () => {
|
|
51
|
+
expect(formatNumber(0.5)).toBe('0.50');
|
|
52
|
+
expect(formatNumber(3.14567)).toBe('3.15');
|
|
53
|
+
});
|
|
35
54
|
});
|
|
36
55
|
|
|
37
56
|
describe('abbreviateNumber', () => {
|
|
@@ -43,13 +62,13 @@ describe('abbreviateNumber', () => {
|
|
|
43
62
|
expect(abbreviateNumber(2300000000)).toBe('2.3B');
|
|
44
63
|
});
|
|
45
64
|
|
|
46
|
-
it('abbreviates thousands', () => {
|
|
47
|
-
expect(abbreviateNumber(2300)).toBe('2.
|
|
65
|
+
it('abbreviates thousands with lowercase k', () => {
|
|
66
|
+
expect(abbreviateNumber(2300)).toBe('2.3k');
|
|
48
67
|
});
|
|
49
68
|
|
|
50
69
|
it('drops trailing .0', () => {
|
|
51
70
|
expect(abbreviateNumber(1000000)).toBe('1M');
|
|
52
|
-
expect(abbreviateNumber(2000)).toBe('
|
|
71
|
+
expect(abbreviateNumber(2000)).toBe('2k');
|
|
53
72
|
});
|
|
54
73
|
|
|
55
74
|
it('does not abbreviate small numbers', () => {
|
|
@@ -63,6 +82,286 @@ describe('abbreviateNumber', () => {
|
|
|
63
82
|
it('abbreviates trillions', () => {
|
|
64
83
|
expect(abbreviateNumber(1_200_000_000_000)).toBe('1.2T');
|
|
65
84
|
});
|
|
85
|
+
|
|
86
|
+
it('trims 1020000 to 1M', () => {
|
|
87
|
+
expect(abbreviateNumber(1020000)).toBe('1M');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('renders 18900 as 18.9k', () => {
|
|
91
|
+
expect(abbreviateNumber(18900)).toBe('18.9k');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('renders 1000 as 1k', () => {
|
|
95
|
+
expect(abbreviateNumber(1000)).toBe('1k');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('rolls up 999999 to 1M', () => {
|
|
99
|
+
expect(abbreviateNumber(999999)).toBe('1M');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('rolls up 999999999 to 1B', () => {
|
|
103
|
+
expect(abbreviateNumber(999999999)).toBe('1B');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('rolls up 999950 to 1M (rounding boundary)', () => {
|
|
107
|
+
expect(abbreviateNumber(999950)).toBe('1M');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('keeps 999949 as 999.9k (below roll-up boundary)', () => {
|
|
111
|
+
expect(abbreviateNumber(999949)).toBe('999.9k');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('handles -1500000', () => {
|
|
115
|
+
expect(abbreviateNumber(-1500000)).toBe('-1.5M');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('formatPercent', () => {
|
|
120
|
+
it('formats fractions (default) with .1~%', () => {
|
|
121
|
+
expect(formatPercent(0.847)).toBe('84.7%');
|
|
122
|
+
expect(formatPercent(0.5)).toBe('50%');
|
|
123
|
+
expect(formatPercent(1)).toBe('100%');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('formats pre-scaled values with fraction: false', () => {
|
|
127
|
+
expect(formatPercent(84.7, { fraction: false })).toBe('84.7%');
|
|
128
|
+
expect(formatPercent(1200, { fraction: false })).toBe('1,200%');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('formatCurrency', () => {
|
|
133
|
+
it('compact mode abbreviates >= 1000', () => {
|
|
134
|
+
expect(formatCurrency(1900, { compact: true })).toBe('$1.9k');
|
|
135
|
+
expect(formatCurrency(12666, { compact: true })).toBe('$12.7k');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('compact mode uses ASCII minus for negatives', () => {
|
|
139
|
+
expect(formatCurrency(-1900, { compact: true })).toBe('-$1.9k');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('compact mode keeps small numbers full', () => {
|
|
143
|
+
expect(formatCurrency(42, { compact: true })).toBe('$42');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('full mode (default) formats integers without cents', () => {
|
|
147
|
+
expect(formatCurrency(12666)).toBe('$12,666');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('full mode uses ASCII minus for negatives', () => {
|
|
151
|
+
expect(formatCurrency(-12666)).toBe('-$12,666');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('full mode includes cents for non-integers', () => {
|
|
155
|
+
expect(formatCurrency(12666.5)).toBe('$12,666.50');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('handles negative non-integer in full mode', () => {
|
|
159
|
+
expect(formatCurrency(-42.5)).toBe('-$42.50');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('handles negative non-integer in compact mode', () => {
|
|
163
|
+
expect(formatCurrency(-42.5, { compact: true })).toBe('-$42.50');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('isYearLikeValues', () => {
|
|
168
|
+
it('returns true for year-like integer arrays', () => {
|
|
169
|
+
expect(isYearLikeValues([1500, 2024, 2500])).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('returns false for empty arrays', () => {
|
|
173
|
+
expect(isYearLikeValues([])).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('returns false when values are below 1500', () => {
|
|
177
|
+
expect(isYearLikeValues([1499])).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('returns false when values are above 2500', () => {
|
|
181
|
+
expect(isYearLikeValues([2501])).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('returns false for non-integer values', () => {
|
|
185
|
+
expect(isYearLikeValues([2024.5])).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('returns false when range includes zero', () => {
|
|
189
|
+
expect(isYearLikeValues([0, 2024])).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('returns false for negative values', () => {
|
|
193
|
+
expect(isYearLikeValues([-2000])).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('computeFieldFormatContext', () => {
|
|
198
|
+
it('computes extent and allIntegers from numeric values', () => {
|
|
199
|
+
const ctx = computeFieldFormatContext([10, 20, 30]);
|
|
200
|
+
expect(ctx.extent).toEqual([10, 30]);
|
|
201
|
+
expect(ctx.allIntegers).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('detects non-integers', () => {
|
|
205
|
+
const ctx = computeFieldFormatContext([10, 20.5, 30]);
|
|
206
|
+
expect(ctx.allIntegers).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('coerces numeric strings', () => {
|
|
210
|
+
const ctx = computeFieldFormatContext(['10', '20', '30']);
|
|
211
|
+
expect(ctx.extent).toEqual([10, 30]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('skips non-finite values', () => {
|
|
215
|
+
const ctx = computeFieldFormatContext([10, NaN, null, undefined, 30]);
|
|
216
|
+
expect(ctx.extent).toEqual([10, 30]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('returns empty context for empty input', () => {
|
|
220
|
+
const ctx = computeFieldFormatContext([]);
|
|
221
|
+
expect(ctx.extent).toBeUndefined();
|
|
222
|
+
expect(ctx.allIntegers).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('sets surface when provided', () => {
|
|
226
|
+
const ctx = computeFieldFormatContext([1, 2], 'table');
|
|
227
|
+
expect(ctx.surface).toBe('table');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('isYearContext', () => {
|
|
232
|
+
it('returns true for year-like integer ranges', () => {
|
|
233
|
+
expect(isYearContext({ extent: [1990, 2024], allIntegers: true })).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('returns false when non-integers', () => {
|
|
237
|
+
expect(isYearContext({ extent: [1990, 2024], allIntegers: false })).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('returns false when out of range', () => {
|
|
241
|
+
expect(isYearContext({ extent: [0, 2024], allIntegers: true })).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('returns false for undefined ctx', () => {
|
|
245
|
+
expect(isYearContext(undefined)).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('resolveNumberFormatter', () => {
|
|
250
|
+
it('returns null for empty string', () => {
|
|
251
|
+
expect(resolveNumberFormatter('')).toBeNull();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('returns null for undefined', () => {
|
|
255
|
+
expect(resolveNumberFormatter(undefined)).toBeNull();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('resolves ordinal keyword', () => {
|
|
259
|
+
const fmt = resolveNumberFormatter('ordinal');
|
|
260
|
+
expect(fmt!(1)).toBe('1st');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('resolves percent keyword with fraction detection (extent <= 1)', () => {
|
|
264
|
+
const fmt = resolveNumberFormatter('percent', { extent: [0, 0.9] });
|
|
265
|
+
expect(fmt!(0.847)).toBe('84.7%');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('resolves percent keyword with pre-scaled detection (extent > 1)', () => {
|
|
269
|
+
const fmt = resolveNumberFormatter('percent', { extent: [0, 98] });
|
|
270
|
+
expect(fmt!(84.7)).toBe('84.7%');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('resolves currency keyword for tables (full precision)', () => {
|
|
274
|
+
const fmt = resolveNumberFormatter('currency', { surface: 'table' });
|
|
275
|
+
expect(fmt!(12666)).toBe('$12,666');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('resolves currency keyword for charts (compact)', () => {
|
|
279
|
+
const fmt = resolveNumberFormatter('currency');
|
|
280
|
+
expect(fmt!(1900)).toBe('$1.9k');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('resolves d3 format strings', () => {
|
|
284
|
+
const fmt = resolveNumberFormatter('$,.0f');
|
|
285
|
+
expect(fmt!(1234)).toBe('$1,234');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('currency with step uses step-derived decimals', () => {
|
|
289
|
+
const fmt = resolveNumberFormatter('currency', {
|
|
290
|
+
step: 500000,
|
|
291
|
+
stepReference: 2000000,
|
|
292
|
+
});
|
|
293
|
+
expect(fmt!(1500000)).toBe('$1.5M');
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('defaultNumberFormatter', () => {
|
|
298
|
+
it('contextless: per-value year check (ARIA path)', () => {
|
|
299
|
+
const fmt = defaultNumberFormatter();
|
|
300
|
+
expect(fmt(2024)).toBe('2024');
|
|
301
|
+
expect(fmt(2600)).toBe('2.6k');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('year context returns bare year string', () => {
|
|
305
|
+
const fmt = defaultNumberFormatter({ extent: [1990, 2024], allIntegers: true });
|
|
306
|
+
expect(fmt(2024)).toBe('2024');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('table surface returns full precision', () => {
|
|
310
|
+
const fmt = defaultNumberFormatter({ surface: 'table' });
|
|
311
|
+
expect(fmt(1020000)).toBe('1,020,000');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('step-aware compact when step and reference >= 1000', () => {
|
|
315
|
+
const fmt = defaultNumberFormatter({ step: 500000, stepReference: 2000000 });
|
|
316
|
+
expect(fmt(500000)).toBe('500k');
|
|
317
|
+
expect(fmt(1000000)).toBe('1M');
|
|
318
|
+
expect(fmt(1500000)).toBe('1.5M');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('per-value compact/formatNumber for chart surface', () => {
|
|
322
|
+
const fmt = defaultNumberFormatter({ extent: [0, 2000000] });
|
|
323
|
+
expect(fmt(1020000)).toBe('1M');
|
|
324
|
+
expect(fmt(42)).toBe('42');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('does not apply per-value year check when context has extent', () => {
|
|
328
|
+
const fmt = defaultNumberFormatter({ extent: [-2000, 2000], allIntegers: true });
|
|
329
|
+
expect(fmt(2000)).toBe('2k');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('buildCompactStepFormatter', () => {
|
|
334
|
+
it('formats per-value units with step-derived decimals', () => {
|
|
335
|
+
const fmt = buildCompactStepFormatter(500000);
|
|
336
|
+
expect(fmt(0)).toBe('0');
|
|
337
|
+
expect(fmt(500000)).toBe('500k');
|
|
338
|
+
expect(fmt(1000000)).toBe('1M');
|
|
339
|
+
expect(fmt(1500000)).toBe('1.5M');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('step-derived decimals prevent misleading rounding', () => {
|
|
343
|
+
const fmt = buildCompactStepFormatter(1250000);
|
|
344
|
+
expect(fmt(1250000)).toBe('1.25M');
|
|
345
|
+
expect(fmt(2500000)).toBe('2.5M');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('degenerate narrow domain falls back to comma-grouped', () => {
|
|
349
|
+
const fmt = buildCompactStepFormatter(1);
|
|
350
|
+
expect(fmt(2021)).toBe('2,021');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('handles non-finite values', () => {
|
|
354
|
+
const fmt = buildCompactStepFormatter(500000);
|
|
355
|
+
expect(fmt(Infinity)).toBe('Infinity');
|
|
356
|
+
expect(fmt(NaN)).toBe('NaN');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('sub-1k ticks render plain', () => {
|
|
360
|
+
const fmt = buildCompactStepFormatter(200);
|
|
361
|
+
expect(fmt(200)).toBe('200');
|
|
362
|
+
expect(fmt(400)).toBe('400');
|
|
363
|
+
expect(fmt(1000)).toBe('1k');
|
|
364
|
+
});
|
|
66
365
|
});
|
|
67
366
|
|
|
68
367
|
describe('buildD3Formatter', () => {
|
|
@@ -117,6 +416,50 @@ describe('buildD3Formatter', () => {
|
|
|
117
416
|
expect(fmt!(1_500_000_000)).toBe('$1.5B');
|
|
118
417
|
expect(fmt!(2_000_000_000)).toBe('$2B');
|
|
119
418
|
});
|
|
419
|
+
|
|
420
|
+
it("'ordinal' returns the rank-axis ordinal formatter", () => {
|
|
421
|
+
const fmt = buildD3Formatter('ordinal');
|
|
422
|
+
expect(fmt).not.toBeNull();
|
|
423
|
+
expect(fmt!(1)).toBe('1st');
|
|
424
|
+
expect(fmt!(6)).toBe('6th');
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('formatOrdinal', () => {
|
|
429
|
+
it('formats 1, 2, 3 with st/nd/rd suffixes', () => {
|
|
430
|
+
expect(formatOrdinal(1)).toBe('1st');
|
|
431
|
+
expect(formatOrdinal(2)).toBe('2nd');
|
|
432
|
+
expect(formatOrdinal(3)).toBe('3rd');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('formats 4 through 10 with th', () => {
|
|
436
|
+
expect(formatOrdinal(4)).toBe('4th');
|
|
437
|
+
expect(formatOrdinal(10)).toBe('10th');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('uses th for the 11-13 teens, including 111-113', () => {
|
|
441
|
+
expect(formatOrdinal(11)).toBe('11th');
|
|
442
|
+
expect(formatOrdinal(12)).toBe('12th');
|
|
443
|
+
expect(formatOrdinal(13)).toBe('13th');
|
|
444
|
+
expect(formatOrdinal(111)).toBe('111th');
|
|
445
|
+
expect(formatOrdinal(112)).toBe('112th');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('applies st/nd/rd past the teens', () => {
|
|
449
|
+
expect(formatOrdinal(21)).toBe('21st');
|
|
450
|
+
expect(formatOrdinal(22)).toBe('22nd');
|
|
451
|
+
expect(formatOrdinal(23)).toBe('23rd');
|
|
452
|
+
expect(formatOrdinal(101)).toBe('101st');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('rounds non-integers to the nearest rank', () => {
|
|
456
|
+
expect(formatOrdinal(2.4)).toBe('2nd');
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('falls back to String() for non-finite values', () => {
|
|
460
|
+
expect(formatOrdinal(Number.NaN)).toBe('NaN');
|
|
461
|
+
expect(formatOrdinal(Number.POSITIVE_INFINITY)).toBe('Infinity');
|
|
462
|
+
});
|
|
120
463
|
});
|
|
121
464
|
|
|
122
465
|
describe('formatDate', () => {
|
|
@@ -149,8 +492,6 @@ describe('formatDate', () => {
|
|
|
149
492
|
});
|
|
150
493
|
|
|
151
494
|
it('infers year granularity for bare year strings regardless of timezone', () => {
|
|
152
|
-
// "2020" parses as 2020-01-01T00:00:00Z. In negative-offset timezones,
|
|
153
|
-
// local getHours()/getDate() would see Dec 31 2019 — the UTC fix prevents that.
|
|
154
495
|
const result = formatDate('2020');
|
|
155
496
|
expect(result).toBe('2020');
|
|
156
497
|
});
|
|
@@ -190,8 +531,6 @@ describe('formatDate', () => {
|
|
|
190
531
|
});
|
|
191
532
|
|
|
192
533
|
it('formats quarters with local-time year when useUtc is false', () => {
|
|
193
|
-
// Mid-quarter local date: quarter and year agree between local and UTC,
|
|
194
|
-
// exercising the getFullYear() arm of the compact branch.
|
|
195
534
|
expect(formatDate(new Date(2025, 4, 15), undefined, 'quarter', false, true)).toBe("Q2 '25");
|
|
196
535
|
});
|
|
197
536
|
|
|
@@ -214,8 +553,6 @@ describe('formatDate', () => {
|
|
|
214
553
|
});
|
|
215
554
|
|
|
216
555
|
it('full quarter year follows useUtc at year boundaries', () => {
|
|
217
|
-
// In negative-offset timezones this instant is still Dec 31 2025 locally;
|
|
218
|
-
// the UTC quarter (Q1) must pair with the UTC year (2026), not the local one.
|
|
219
556
|
expect(formatDate(new Date('2026-01-01T00:00:00Z'), undefined, 'quarter')).toBe('Q1 2026');
|
|
220
557
|
});
|
|
221
558
|
});
|
|
@@ -264,7 +601,6 @@ describe('buildTemporalFormatter', () => {
|
|
|
264
601
|
it('handles numeric timestamps', () => {
|
|
265
602
|
const fmt = buildTemporalFormatter('%Y');
|
|
266
603
|
expect(fmt).not.toBeNull();
|
|
267
|
-
// 2020-01-01T00:00:00Z
|
|
268
604
|
expect(fmt!(1577836800000)).toBe('2020');
|
|
269
605
|
});
|
|
270
606
|
});
|