@bbn/bbn 2.0.34 → 2.0.35

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.
@@ -16,15 +16,13 @@ type CommonFormats = {
16
16
  }>;
17
17
  };
18
18
  /**
19
- * Enumerate common date, time and datetime formats for a locale, by iterating
20
- * over combinations of:
21
- * - weekday / year / month / day
22
- * - hour / minute / second / timeZoneName
19
+ * Get a curated set of *common* date, time and datetime formats
20
+ * for the given locale, without exploding into thousands of combos.
23
21
  *
24
- * Constraints:
25
- * - no minutes/seconds if you don't have hours
26
- * - no seconds if you don't have minutes
27
- * - no timezone if you don't have time
22
+ * Rules:
23
+ * - Date: only sensible combos (Y-M-D ± weekday, Y-M, M-D).
24
+ * - Time: hour / hour:minute / hour:minute:second (+ optional TZ).
25
+ * - Datetime: only full dates (Y-M-D ± weekday) combined with time.
28
26
  */
29
27
  export declare function getCommonFormatsForLocale(lng: string | string[]): CommonFormats;
30
28
  export default function buildLocaleFromIntl(): void;
@@ -2,8 +2,9 @@ import extend from "../../fn/object/extend.js";
2
2
  import numProperties from "../../fn/object/numProperties.js";
3
3
  /**
4
4
  * Build a token pattern (YYYY, MM, DD, dddd, HH, II, SS, A, z) from Intl parts.
5
+ * Uses Intl options to distinguish MMM vs MMMM, ddd vs dddd, etc.
5
6
  */
6
- function partsToPattern(parts, hourCycle) {
7
+ function partsToPattern(parts, hourCycle, opts) {
7
8
  let pattern = '';
8
9
  const hasDayPeriod = parts.some(p => p.type === 'dayPeriod');
9
10
  const is12h = hasDayPeriod || hourCycle === 'h12' || hourCycle === 'h11';
@@ -13,18 +14,38 @@ function partsToPattern(parts, hourCycle) {
13
14
  pattern += 'YYYY';
14
15
  break;
15
16
  case 'month':
16
- if (/^\d+$/.test(p.value)) {
17
- pattern += p.value.length === 2 ? 'MM' : 'M';
17
+ if (opts.month === 'short') {
18
+ pattern += 'MMM';
19
+ }
20
+ else if (opts.month === 'long') {
21
+ pattern += 'MMMM';
22
+ }
23
+ else if (opts.month === 'numeric' || opts.month === '2-digit') {
24
+ pattern += /^\d{2}$/.test(p.value) ? 'MM' : 'M';
18
25
  }
19
26
  else {
20
- pattern += p.value.length > 3 ? 'MMMM' : 'MMM';
27
+ // Fallback
28
+ if (/^\d+$/.test(p.value)) {
29
+ pattern += p.value.length === 2 ? 'MM' : 'M';
30
+ }
31
+ else {
32
+ pattern += p.value.length > 3 ? 'MMMM' : 'MMM';
33
+ }
21
34
  }
22
35
  break;
23
36
  case 'day':
24
37
  pattern += p.value.length === 2 ? 'DD' : 'D';
25
38
  break;
26
39
  case 'weekday':
27
- pattern += p.value.length > 3 ? 'dddd' : 'ddd';
40
+ if (opts.weekday === 'short' || opts.weekday === 'narrow') {
41
+ pattern += 'ddd';
42
+ }
43
+ else if (opts.weekday === 'long') {
44
+ pattern += 'dddd';
45
+ }
46
+ else {
47
+ pattern += p.value.length > 3 ? 'dddd' : 'ddd';
48
+ }
28
49
  break;
29
50
  case 'hour':
30
51
  if (is12h) {
@@ -55,152 +76,102 @@ function partsToPattern(parts, hourCycle) {
55
76
  return pattern;
56
77
  }
57
78
  /**
58
- * Enumerate common date, time and datetime formats for a locale, by iterating
59
- * over combinations of:
60
- * - weekday / year / month / day
61
- * - hour / minute / second / timeZoneName
79
+ * Get a curated set of *common* date, time and datetime formats
80
+ * for the given locale, without exploding into thousands of combos.
62
81
  *
63
- * Constraints:
64
- * - no minutes/seconds if you don't have hours
65
- * - no seconds if you don't have minutes
66
- * - no timezone if you don't have time
82
+ * Rules:
83
+ * - Date: only sensible combos (Y-M-D ± weekday, Y-M, M-D).
84
+ * - Time: hour / hour:minute / hour:minute:second (+ optional TZ).
85
+ * - Datetime: only full dates (Y-M-D ± weekday) combined with time.
67
86
  */
68
87
  export function getCommonFormatsForLocale(lng) {
69
- // Fixed sample: 2 Jan 2000, 13:45:30 UTC
70
88
  const sample = new Date(Date.UTC(2000, 0, 2, 13, 45, 30));
71
89
  const date = [];
72
90
  const time = [];
73
91
  const datetime = [];
74
- // Dedupe by options (not just pattern), so we don't lose combinations like
75
- // { day: "numeric", month: "short", year: "numeric" }.
76
- const seenDateOptions = new Set();
77
- const seenTimeOptions = new Set();
78
- const seenDateTimeOptions = new Set();
79
- // ---- 1) DATE formats: combinations of weekday/year/month/day ----
80
- const weekdayOptions = [
81
- undefined,
82
- 'short',
83
- 'long'
92
+ const seenDatePatterns = new Set();
93
+ const seenTimePatterns = new Set();
94
+ const seenDateTimePatterns = new Set();
95
+ // ---- 1) DATE: curated list of useful patterns ----
96
+ // Includes your important one: { day: "numeric", month: "short", year: "numeric" }
97
+ const dateOptionsList = [
98
+ // Full dates
99
+ { year: 'numeric', month: '2-digit', day: '2-digit' },
100
+ { year: 'numeric', month: 'numeric', day: 'numeric' },
101
+ { year: 'numeric', month: 'short', day: 'numeric' },
102
+ { year: 'numeric', month: 'long', day: 'numeric' },
103
+ // Full dates with weekday
104
+ { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' },
105
+ { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' },
106
+ // Year–month
107
+ { year: 'numeric', month: 'numeric' },
108
+ { year: 'numeric', month: '2-digit' },
109
+ { year: 'numeric', month: 'short' },
110
+ { year: 'numeric', month: 'long' },
111
+ // Month–day (no year)
112
+ { month: 'numeric', day: 'numeric' },
113
+ { month: '2-digit', day: '2-digit' },
114
+ { month: 'short', day: 'numeric' }, // ← e.g. "22 janv."
115
+ { month: 'long', day: 'numeric' }
84
116
  ];
85
- const yearOptions = [
86
- undefined,
87
- 'numeric',
88
- '2-digit'
89
- ];
90
- const monthOptions = [undefined, 'numeric', '2-digit', 'short', 'long'];
91
- const dayOptions = [
92
- undefined,
93
- 'numeric',
94
- '2-digit'
95
- ];
96
- const dateOptionsList = [];
97
- for (const weekday of weekdayOptions) {
98
- for (const year of yearOptions) {
99
- for (const month of monthOptions) {
100
- for (const day of dayOptions) {
101
- // Skip combos with no actual date fields
102
- if (!year && !month && !day) {
103
- continue;
104
- }
105
- const options = {};
106
- if (weekday)
107
- options.weekday = weekday;
108
- if (year)
109
- options.year = year;
110
- if (month)
111
- options.month = month;
112
- if (day)
113
- options.day = day;
114
- const key = JSON.stringify(options);
115
- if (seenDateOptions.has(key)) {
116
- continue;
117
- }
118
- seenDateOptions.add(key);
119
- const fmt = new Intl.DateTimeFormat(lng, options);
120
- const parts = fmt.formatToParts(sample);
121
- const resolved = fmt.resolvedOptions();
122
- const pattern = partsToPattern(parts, resolved.hourCycle);
123
- dateOptionsList.push(options);
124
- date.push({
125
- pattern,
126
- sample: fmt.format(sample),
127
- options
128
- });
129
- }
130
- }
117
+ const fullDateOptions = []; // Y+M+D (± weekday)
118
+ for (const opts of dateOptionsList) {
119
+ const fmt = new Intl.DateTimeFormat(lng, opts);
120
+ const parts = fmt.formatToParts(sample);
121
+ const resolved = fmt.resolvedOptions();
122
+ const pattern = partsToPattern(parts, resolved.hourCycle, opts);
123
+ if (!seenDatePatterns.has(pattern)) {
124
+ seenDatePatterns.add(pattern);
125
+ date.push({
126
+ pattern,
127
+ sample: fmt.format(sample),
128
+ options: opts
129
+ });
130
+ }
131
+ // keep track of "full dates" (year+month+day) for datetime
132
+ if (opts.year && opts.month && opts.day) {
133
+ fullDateOptions.push(opts);
131
134
  }
132
135
  }
133
- const hourOptions = ['numeric', '2-digit'];
134
- const minuteOptions = [
135
- undefined,
136
- 'numeric',
137
- '2-digit'
138
- ];
139
- const secondOptions = [
140
- undefined,
141
- 'numeric',
142
- '2-digit'
143
- ];
144
- const tzNameOptions = [
145
- undefined,
146
- 'short',
147
- 'long'
136
+ // ---- 2) TIME: curated, valid combos (always have hour, then minute/second) ----
137
+ const timeOptionsList = [
138
+ { hour: 'numeric' },
139
+ { hour: '2-digit', minute: '2-digit' },
140
+ { hour: '2-digit', minute: '2-digit', second: '2-digit' },
141
+ { hour: '2-digit', minute: '2-digit', timeZoneName: 'short' },
142
+ { hour: '2-digit', minute: '2-digit', second: '2-digit', timeZoneName: 'short' },
143
+ { hour: '2-digit', minute: '2-digit', timeZoneName: 'long' }
148
144
  ];
149
- const timeOptionsList = [];
150
- for (const hour of hourOptions) {
151
- for (const minute of minuteOptions) {
152
- for (const second of secondOptions) {
153
- for (const tzName of tzNameOptions) {
154
- // Constraints:
155
- // - we always have hour (by design)
156
- // - if we have second, we must have minute
157
- if (second && !minute) {
158
- continue;
159
- }
160
- const options = { hour };
161
- if (minute)
162
- options.minute = minute;
163
- if (second)
164
- options.second = second;
165
- if (tzName)
166
- options.timeZoneName = tzName;
167
- const key = JSON.stringify(options);
168
- if (seenTimeOptions.has(key)) {
169
- continue;
170
- }
171
- seenTimeOptions.add(key);
172
- const fmt = new Intl.DateTimeFormat(lng, options);
173
- const parts = fmt.formatToParts(sample);
174
- const resolved = fmt.resolvedOptions();
175
- const pattern = partsToPattern(parts, resolved.hourCycle);
176
- timeOptionsList.push(options);
177
- time.push({
178
- pattern,
179
- sample: fmt.format(sample),
180
- options
181
- });
182
- }
183
- }
145
+ for (const opts of timeOptionsList) {
146
+ const fmt = new Intl.DateTimeFormat(lng, opts);
147
+ const parts = fmt.formatToParts(sample);
148
+ const resolved = fmt.resolvedOptions();
149
+ const pattern = partsToPattern(parts, resolved.hourCycle, opts);
150
+ if (!seenTimePatterns.has(pattern)) {
151
+ seenTimePatterns.add(pattern);
152
+ time.push({
153
+ pattern,
154
+ sample: fmt.format(sample),
155
+ options: opts
156
+ });
184
157
  }
185
158
  }
186
- // ---- 3) DATETIME formats: each dateOption × each timeOption ----
187
- for (const dateOpts of dateOptionsList) {
188
- for (const timeOpts of timeOptionsList) {
189
- const options = Object.assign(Object.assign({}, dateOpts), timeOpts);
190
- const key = JSON.stringify(options);
191
- if (seenDateTimeOptions.has(key)) {
192
- continue;
193
- }
194
- seenDateTimeOptions.add(key);
195
- const fmt = new Intl.DateTimeFormat(lng, options);
159
+ // ---- 3) DATETIME: only full dates (Y-M-D ± weekday) × time
160
+ for (const dOpts of fullDateOptions) {
161
+ for (const tOpts of timeOptionsList) {
162
+ const opts = Object.assign(Object.assign({}, dOpts), tOpts);
163
+ const fmt = new Intl.DateTimeFormat(lng, opts);
196
164
  const parts = fmt.formatToParts(sample);
197
165
  const resolved = fmt.resolvedOptions();
198
- const pattern = partsToPattern(parts, resolved.hourCycle);
199
- datetime.push({
200
- pattern,
201
- sample: fmt.format(sample),
202
- options
203
- });
166
+ const pattern = partsToPattern(parts, resolved.hourCycle, opts);
167
+ if (!seenDateTimePatterns.has(pattern)) {
168
+ seenDateTimePatterns.add(pattern);
169
+ datetime.push({
170
+ pattern,
171
+ sample: fmt.format(sample),
172
+ options: opts
173
+ });
174
+ }
204
175
  }
205
176
  }
206
177
  return { date, time, datetime };
@@ -1,12 +1 @@
1
- /**
2
- * Guess a date format string for the given input.
3
- *
4
- * - If `formats` is provided, it will try those formats in order and return
5
- * the first one that successfully parses.
6
- * - If `formats` is not provided, it will try a set of built-in common formats
7
- * (MySQL, ISO/JS, EU/US, full-text using bbn.dt.locales).
8
- * - Returns `null` if no format matches.
9
- *
10
- * NOTE: It relies on `this.parse(input, format)` NOT throwing when the format is correct.
11
- */
12
- export default function guessFormat(input: string, formats?: string[] | string): string | null;
1
+ export default function guessFormat(input: string, formats?: string | string[], lng?: string): string | null;
@@ -1,120 +1,45 @@
1
- import buildLocaleFromIntl from './buildLocaleFromIntl.js';
2
1
  import parse from './parse.js';
3
- /**
4
- * Guess a date format string for the given input.
5
- *
6
- * - If `formats` is provided, it will try those formats in order and return
7
- * the first one that successfully parses.
8
- * - If `formats` is not provided, it will try a set of built-in common formats
9
- * (MySQL, ISO/JS, EU/US, full-text using bbn.dt.locales).
10
- * - Returns `null` if no format matches.
11
- *
12
- * NOTE: It relies on `this.parse(input, format)` NOT throwing when the format is correct.
13
- */
14
- export default function guessFormat(input, formats) {
15
- var _a;
2
+ import { getCommonFormatsForLocale } from './buildLocaleFromIntl.js';
3
+ export default function guessFormat(input, formats, lng) {
16
4
  const str = input.trim();
17
5
  if (!str) {
18
6
  return null;
19
7
  }
20
- const tryFormats = (formatsToTry) => {
21
- for (const fmt of formatsToTry) {
8
+ // helper: try a list of formats with your parse()
9
+ const tryFormats = (fmts) => {
10
+ for (const fmt of fmts) {
22
11
  try {
23
- // We only care that it parses without throwing
24
- parse(str, fmt);
12
+ parse(str, fmt); // will throw if not matching
25
13
  return fmt;
26
14
  }
27
15
  catch (_a) {
28
- // ignore
16
+ // ignore and continue
29
17
  }
30
18
  }
31
19
  return null;
32
20
  };
33
- // If user provided formats, restrict to those only
21
+ // if user provided formats, restrict to those
34
22
  if (formats) {
35
23
  const list = Array.isArray(formats) ? formats : [formats];
36
24
  return tryFormats(list);
37
25
  }
38
- // -------- Autodetection mode (no user-provided formats) --------
39
- const lower = str.toLowerCase();
40
- // Access locales for full-text formats (months / weekdays)
41
- buildLocaleFromIntl();
42
- const loc = ((_a = bbn === null || bbn === void 0 ? void 0 : bbn.dt) === null || _a === void 0 ? void 0 : _a.locales) || {};
43
- const monthsLong = loc.monthsLong || [];
44
- const monthsShort = loc.monthsShort || [];
45
- const weekdaysLong = loc.weekdaysLong || [];
46
- const weekdaysShort = loc.weekdaysShort || [];
47
- const timeFormats = loc.time || [];
48
- const dateFormats = loc.date || [];
49
- const datetimeFormats = loc.datetime || [];
50
- const hasMonthName = monthsLong.some(m => lower.includes(m.toLowerCase())) ||
51
- monthsShort.some(m => lower.includes(m.toLowerCase()));
52
- const hasWeekdayName = weekdaysLong.some(w => lower.includes(w.toLowerCase())) ||
53
- weekdaysShort.some(w => lower.includes(w.toLowerCase()));
54
- const hasLetterTZ = /gmt|utc|[+-]\d{2}:?\d{2}|z$/i.test(str);
55
- const looksISO = /^\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}(\.\d+)?(z|[+\-]\d{2}:?\d{2})?$/i.test(str);
56
- const looksMySQLDateTime = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2}(\.\d+)?)?$/i.test(str);
57
- const looksMySQLDate = /^\d{4}-\d{2}-\d{2}$/.test(str);
58
- const looksTimeOnly = /^\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/.test(str);
59
- // Start building candidate formats (most specific first)
60
- const candidates = [
61
- ...datetimeFormats.map(f => f.pattern),
62
- ];
63
- // --- Full-text / locale-based formats ---
64
- if (hasMonthName || hasWeekdayName) {
65
- // e.g. "Monday 15 January 2024"
66
- candidates.push('dddd, DD MMMM YYYY HH:II:SSZ', 'dddd, DD MMMM YYYY HH:II:SS', 'dddd, DD MMMM YYYY', 'DD MMMM YYYY HH:II:SSZ', 'DD MMMM YYYY HH:II:SS', 'DD MMMM YYYY', 'ddd, DD MMM YYYY HH:II:SSZ', 'ddd, DD MMM YYYY HH:II:SS', 'ddd, DD MMM YYYY');
67
- // JS Date.toString() / toUTCString()-like
68
- // "Tue Oct 29 2024 14:30:00 GMT+0200"
69
- candidates.push('ddd MMM DD YYYY HH:II:SSZ', 'ddd, DD MMM YYYY HH:II:SS z');
70
- }
71
- // --- ISO / JS-like default formats ---
72
- if (looksISO || str.includes('T')) {
73
- candidates.push('YYYY-MM-DDTHH:II:SS.msZ', 'YYYY-MM-DDTHH:II:SSZ', 'YYYY-MM-DDTHH:II:SS.ms', 'YYYY-MM-DDTHH:II:SS', 'YYYY-MM-DDTHH:II:Z', 'YYYY-MM-DDTHH:II');
74
- }
75
- // --- MySQL classic formats ---
76
- if (looksMySQLDateTime) {
77
- candidates.push('YYYY-MM-DD HH:II:SS.msZ', 'YYYY-MM-DD HH:II:SSZ', 'YYYY-MM-DD HH:II:SS.ms', 'YYYY-MM-DD HH:II:SS', 'YYYY-MM-DD HH:II');
78
- }
79
- if (looksMySQLDate) {
80
- candidates.push('YYYY-MM-DD');
81
- }
82
- // --- Time-only strings ---
83
- if (looksTimeOnly) {
84
- candidates.push(...timeFormats.map(f => f.pattern), 'HH:II:SS.msZ', 'HH:II:SS.ms', 'HH:II:SS', 'HH:II');
85
- }
86
- // --- Common EU / US formats ---
87
- candidates.push(...dateFormats.map(f => f.pattern),
88
- // European style
89
- 'DD/MM/YYYY HH:II:SSZ', 'DD/MM/YYYY HH:II:SS', 'DD/MM/YYYY HH:II', 'DD/MM/YYYY', 'DD-MM-YYYY HH:II:SSZ', 'DD-MM-YYYY HH:II:SS', 'DD-MM-YYYY HH:II', 'DD-MM-YYYY',
90
- // US style
91
- 'MM/DD/YYYY HH:II:SSZ', 'MM/DD/YYYY HH:II:SS', 'MM/DD/YYYY HH:II', 'MM/DD/YYYY',
92
- // Dot-separated
93
- 'YYYY.MM.DD HH:II:SSZ', 'YYYY.MM.DD HH:II:SS', 'YYYY.MM.DD',
94
- // MySQL-ish (if we haven't already pushed them by detection)
95
- 'YYYY-MM-DD HH:II:SSZ', 'YYYY-MM-DD HH:II:SS', 'YYYY-MM-DD HH:II', 'YYYY-MM-DD');
96
- // If we see clear timezone indicators, prioritize formats with Z / z
97
- if (hasLetterTZ) {
98
- const withTZ = candidates.filter(f => f.includes('Z') || f.includes('z'));
99
- const withoutTZ = candidates.filter(f => !f.includes('Z') && !f.includes('z'));
100
- const reordered = [...withTZ, ...withoutTZ];
101
- const fmt = tryFormats(reordered);
102
- if (fmt) {
103
- return fmt;
104
- }
105
- }
106
- else {
107
- const fmt = tryFormats(candidates);
108
- if (fmt) {
109
- return fmt;
110
- }
111
- }
112
- // --- Last resort: native JS parsing ---
113
- const jsDate = new Date(str);
114
- if (!isNaN(jsDate.getTime())) {
115
- // You can treat "native" as a special keyword meaning:
116
- // "use Date/Temporal to parse directly".
117
- return 'native';
118
- }
26
+ // autodetect via Intl-derived formats
27
+ const resolvedLocale = lng ||
28
+ (typeof navigator !== 'undefined'
29
+ ? navigator.language
30
+ : Intl.DateTimeFormat().resolvedOptions().locale);
31
+ const common = getCommonFormatsForLocale(resolvedLocale);
32
+ const candidates = [];
33
+ // prioritize datetime patterns first, then date, then time
34
+ candidates.push(...common.datetime.map(f => f.pattern), ...common.date.map(f => f.pattern), ...common.time.map(f => f.pattern));
35
+ const fmt = tryFormats(candidates);
36
+ if (fmt) {
37
+ return fmt;
38
+ }
39
+ // last resort: if you *really* want a "native" fallback, do it here
40
+ // const d = new Date(str);
41
+ // if (!Number.isNaN(d.getTime())) {
42
+ // return 'native';
43
+ // }
119
44
  return null;
120
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbn/bbn",
3
- "version": "2.0.34",
3
+ "version": "2.0.35",
4
4
  "description": "Javascript toolkit",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",