@bbn/bbn 2.0.39 → 2.0.41

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.
@@ -4,78 +4,70 @@ import numProperties from "../../fn/object/numProperties.js";
4
4
  * Build a token pattern (YYYY, MM, DD, dddd, HH, II, SS, A, z) from Intl parts.
5
5
  * Uses Intl options to distinguish MMM vs MMMM, ddd vs dddd, etc.
6
6
  */
7
- function partsToPattern(parts, hourCycle, opts) {
7
+ function partsToPattern(parts, resolved, requestedOpts) {
8
8
  let pattern = '';
9
+ const hourCycle = resolved.hourCycle;
9
10
  const hasDayPeriod = parts.some(p => p.type === 'dayPeriod');
10
11
  const is12h = hasDayPeriod || hourCycle === 'h12' || hourCycle === 'h11';
11
- // ---- detect "all numeric date" ----
12
- const yearIsNumeric = opts.year === 'numeric' || opts.year === '2-digit';
13
- const monthIsNumeric = opts.month === 'numeric' || opts.month === '2-digit';
14
- const dayIsNumeric = opts.day === 'numeric' || opts.day === '2-digit';
15
- const hasYear = !!opts.year;
16
- const hasMonth = !!opts.month;
17
- const hasDay = !!opts.day;
18
- const hasWeekday = !!opts.weekday;
19
- const hasTextMonth = opts.month === 'short' || opts.month === 'long';
20
- const isAllNumericDate = hasYear && hasMonth && hasDay &&
21
- yearIsNumeric && monthIsNumeric && dayIsNumeric &&
22
- !hasWeekday && !hasTextMonth;
12
+ // ALL NUMERIC (not 2-digit): year, month and day resolved as "numeric"
13
+ const allNumericNonPadded = resolved.year === 'numeric' &&
14
+ resolved.month === 'numeric' &&
15
+ resolved.day === 'numeric';
23
16
  for (const p of parts) {
24
17
  switch (p.type) {
25
- case 'year':
26
- if (opts.year === '2-digit') {
18
+ case 'year': {
19
+ // Use YY only if locale actually resolved 2-digit
20
+ if (resolved.year === '2-digit') {
27
21
  pattern += 'YY';
28
22
  }
29
23
  else {
30
24
  pattern += 'YYYY';
31
25
  }
32
26
  break;
33
- case 'month':
34
- if (isAllNumericDate) {
35
- // normalize to single M when date is fully numeric
36
- pattern += 'M';
37
- }
38
- else if (opts.month === 'short') {
39
- pattern += 'MMM';
27
+ }
28
+ case 'month': {
29
+ // textual month
30
+ if (requestedOpts.month === 'short' || requestedOpts.month === 'long') {
31
+ pattern += requestedOpts.month === 'long' ? 'MMMM' : 'MMM';
32
+ break;
40
33
  }
41
- else if (opts.month === 'long') {
42
- pattern += 'MMMM';
34
+ // ALL NUMERIC and non-padded → always use M
35
+ if (allNumericNonPadded) {
36
+ pattern += 'M';
37
+ break;
43
38
  }
44
- else if (opts.month === 'numeric' || opts.month === '2-digit') {
45
- // non "all numeric" case (e.g., month+year or month+day only)
46
- pattern += /^\d{2}$/.test(p.value) ? 'MM' : 'M';
39
+ // numeric / 2-digit generic case
40
+ if (/^\d+$/.test(p.value)) {
41
+ pattern += p.value.length === 2 ? 'MM' : 'M';
47
42
  }
48
43
  else {
49
- // Fallback
50
- if (/^\d+$/.test(p.value)) {
51
- pattern += p.value.length === 2 ? 'MM' : 'M';
52
- }
53
- else {
54
- pattern += p.value.length > 3 ? 'MMMM' : 'MMM';
55
- }
44
+ // fallback (shouldn't really happen without text month)
45
+ pattern += p.value.length > 3 ? 'MMMM' : 'MMM';
56
46
  }
57
47
  break;
58
- case 'day':
59
- if (isAllNumericDate) {
60
- // normalize to single D when date is fully numeric
48
+ }
49
+ case 'day': {
50
+ // ALL NUMERIC and non-padded always use D
51
+ if (allNumericNonPadded) {
61
52
  pattern += 'D';
53
+ break;
62
54
  }
63
- else {
64
- pattern += p.value.length === 2 ? 'DD' : 'D';
65
- }
55
+ pattern += p.value.length === 2 ? 'DD' : 'D';
66
56
  break;
67
- case 'weekday':
68
- if (opts.weekday === 'short' || opts.weekday === 'narrow') {
57
+ }
58
+ case 'weekday': {
59
+ if (requestedOpts.weekday === 'short' || requestedOpts.weekday === 'narrow') {
69
60
  pattern += 'ddd';
70
61
  }
71
- else if (opts.weekday === 'long') {
62
+ else if (requestedOpts.weekday === 'long') {
72
63
  pattern += 'dddd';
73
64
  }
74
65
  else {
75
66
  pattern += p.value.length > 3 ? 'dddd' : 'ddd';
76
67
  }
77
68
  break;
78
- case 'hour':
69
+ }
70
+ case 'hour': {
79
71
  if (is12h) {
80
72
  pattern += p.value.length === 2 ? 'hh' : 'h';
81
73
  }
@@ -83,6 +75,7 @@ function partsToPattern(parts, hourCycle, opts) {
83
75
  pattern += p.value.length === 2 ? 'HH' : 'H';
84
76
  }
85
77
  break;
78
+ }
86
79
  case 'minute':
87
80
  pattern += 'II';
88
81
  break;
@@ -96,8 +89,8 @@ function partsToPattern(parts, hourCycle, opts) {
96
89
  pattern += 'z';
97
90
  break;
98
91
  case 'literal': {
99
- // Wrap literals in [ ... ] so your parser doesn't confuse them
100
- if (p.value.length && ![' ', ',', '/', '-', ':', '.'].includes(p.value)) {
92
+ // Wrap literals in [ ... ] so your parser won't confuse them with tokens
93
+ if (p.value.length) {
101
94
  const v = p.value.replace(/]/g, '\\]');
102
95
  pattern += `[${v}]`;
103
96
  }
@@ -154,7 +147,7 @@ export function getCommonFormatsForLocale(lng) {
154
147
  const fmt = new Intl.DateTimeFormat(lng, opts);
155
148
  const parts = fmt.formatToParts(sample);
156
149
  const resolved = fmt.resolvedOptions();
157
- const pattern = partsToPattern(parts, resolved.hourCycle, opts);
150
+ const pattern = partsToPattern(parts, resolved, opts);
158
151
  if (!seenDatePatterns.has(pattern)) {
159
152
  seenDatePatterns.add(pattern);
160
153
  date.push({
@@ -181,7 +174,7 @@ export function getCommonFormatsForLocale(lng) {
181
174
  const fmt = new Intl.DateTimeFormat(lng, opts);
182
175
  const parts = fmt.formatToParts(sample);
183
176
  const resolved = fmt.resolvedOptions();
184
- const pattern = partsToPattern(parts, resolved.hourCycle, opts);
177
+ const pattern = partsToPattern(parts, resolved, opts);
185
178
  if (!seenTimePatterns.has(pattern)) {
186
179
  seenTimePatterns.add(pattern);
187
180
  time.push({
@@ -198,7 +191,7 @@ export function getCommonFormatsForLocale(lng) {
198
191
  const fmt = new Intl.DateTimeFormat(lng, opts);
199
192
  const parts = fmt.formatToParts(sample);
200
193
  const resolved = fmt.resolvedOptions();
201
- const pattern = partsToPattern(parts, resolved.hourCycle, opts);
194
+ const pattern = partsToPattern(parts, resolved, opts);
202
195
  if (!seenDateTimePatterns.has(pattern)) {
203
196
  seenDateTimePatterns.add(pattern);
204
197
  datetime.push({
@@ -452,6 +452,26 @@ export default function parse(input, format, locale) {
452
452
  const applyFns = [];
453
453
  let i = 0;
454
454
  while (i < fmt.length) {
455
+ // 1) Handle [literal] blocks first
456
+ if (fmt[i] === '[') {
457
+ let j = i + 1;
458
+ let rawLiteral = '';
459
+ while (j < fmt.length && fmt[j] !== ']') {
460
+ rawLiteral += fmt[j];
461
+ j++;
462
+ }
463
+ if (j < fmt.length && fmt[j] === ']') {
464
+ // We found a closing bracket: treat content as literal
465
+ // Undo our earlier escaping of ']' (we used '\]' when building)
466
+ const literal = rawLiteral.replace(/\\]/g, ']');
467
+ pattern += escapeRegex(literal);
468
+ // No capturing group & no applyFn, we just match this text
469
+ i = j + 1;
470
+ continue;
471
+ }
472
+ // If there's no closing ']', fall through and treat '[' as normal char
473
+ }
474
+ // 2) Try to match a token at this position
455
475
  let matchedToken = null;
456
476
  for (const spec of tokensByLength) {
457
477
  if (fmt.startsWith(spec.token, i)) {
@@ -470,6 +490,7 @@ export default function parse(input, format, locale) {
470
490
  i += matchedToken.token.length;
471
491
  }
472
492
  else {
493
+ // 3) Plain literal character
473
494
  pattern += escapeRegex(fmt[i]);
474
495
  i += 1;
475
496
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbn/bbn",
3
- "version": "2.0.39",
3
+ "version": "2.0.41",
4
4
  "description": "Javascript toolkit",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",