@optique/core 1.0.0-dev.693 → 1.0.0-dev.705

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.
@@ -86,7 +86,7 @@ function dependency(parser) {
86
86
  return createSyncDerivedParser(id, options);
87
87
  },
88
88
  deriveAsync(options) {
89
- return createDerivedValueParser(id, parser, options);
89
+ return createAsyncDerivedParserFromAsyncFactory(id, options);
90
90
  }
91
91
  };
92
92
  return result;
@@ -212,8 +212,17 @@ function createSyncDerivedFromParser(sourceId, options) {
212
212
  [dependencyIds]: alldependencyIds,
213
213
  [defaultValues]: options.defaultValues,
214
214
  parse(input) {
215
- const sourceValues = options.defaultValues();
216
- const derivedParser = options.factory(...sourceValues);
215
+ let derivedParser;
216
+ try {
217
+ const sourceValues = options.defaultValues();
218
+ derivedParser = options.factory(...sourceValues);
219
+ } catch (e) {
220
+ const msg = e instanceof Error ? e.message : String(e);
221
+ return {
222
+ success: false,
223
+ error: require_message.message`Derived parser error: ${msg}`
224
+ };
225
+ }
217
226
  if (isAsyncModeParser(derivedParser)) return {
218
227
  success: false,
219
228
  error: require_message.message`Factory returned an async parser where a sync parser is required.`
@@ -277,9 +286,18 @@ function createAsyncDerivedFromParserFromAsyncFactory(sourceId, options) {
277
286
  [dependencyIds]: alldependencyIds,
278
287
  [defaultValues]: options.defaultValues,
279
288
  parse(input) {
280
- const sourceValues = options.defaultValues();
281
- const derivedParser = options.factory(...sourceValues);
282
- return derivedParser.parse(input);
289
+ let derivedParser;
290
+ try {
291
+ const sourceValues = options.defaultValues();
292
+ derivedParser = options.factory(...sourceValues);
293
+ } catch (e) {
294
+ const msg = e instanceof Error ? e.message : String(e);
295
+ return Promise.resolve({
296
+ success: false,
297
+ error: require_message.message`Derived parser error: ${msg}`
298
+ });
299
+ }
300
+ return Promise.resolve(derivedParser.parse(input));
283
301
  },
284
302
  [parseWithDependency](input, dependencyValue) {
285
303
  let derivedParser;
@@ -292,7 +310,7 @@ function createAsyncDerivedFromParserFromAsyncFactory(sourceId, options) {
292
310
  error: require_message.message`Factory error: ${msg}`
293
311
  });
294
312
  }
295
- return derivedParser.parse(input);
313
+ return Promise.resolve(derivedParser.parse(input));
296
314
  },
297
315
  format(value) {
298
316
  const sourceValues = options.defaultValues();
@@ -334,8 +352,17 @@ function createAsyncDerivedFromParserFromSyncFactory(sourceId, options) {
334
352
  [dependencyIds]: alldependencyIds,
335
353
  [defaultValues]: options.defaultValues,
336
354
  parse(input) {
337
- const sourceValues = options.defaultValues();
338
- const derivedParser = options.factory(...sourceValues);
355
+ let derivedParser;
356
+ try {
357
+ const sourceValues = options.defaultValues();
358
+ derivedParser = options.factory(...sourceValues);
359
+ } catch (e) {
360
+ const msg = e instanceof Error ? e.message : String(e);
361
+ return Promise.resolve({
362
+ success: false,
363
+ error: require_message.message`Derived parser error: ${msg}`
364
+ });
365
+ }
339
366
  return Promise.resolve(derivedParser.parse(input));
340
367
  },
341
368
  [parseWithDependency](input, dependencyValue) {
@@ -402,8 +429,17 @@ function createSyncDerivedParser(sourceId, options) {
402
429
  [derivedValueParserMarker]: true,
403
430
  [dependencyId]: sourceId,
404
431
  parse(input) {
405
- const sourceValue = options.defaultValue();
406
- const derivedParser = options.factory(sourceValue);
432
+ let derivedParser;
433
+ try {
434
+ const sourceValue = options.defaultValue();
435
+ derivedParser = options.factory(sourceValue);
436
+ } catch (e) {
437
+ const msg = e instanceof Error ? e.message : String(e);
438
+ return {
439
+ success: false,
440
+ error: require_message.message`Derived parser error: ${msg}`
441
+ };
442
+ }
407
443
  if (isAsyncModeParser(derivedParser)) return {
408
444
  success: false,
409
445
  error: require_message.message`Factory returned an async parser where a sync parser is required.`
@@ -463,9 +499,18 @@ function createAsyncDerivedParserFromAsyncFactory(sourceId, options) {
463
499
  [derivedValueParserMarker]: true,
464
500
  [dependencyId]: sourceId,
465
501
  parse(input) {
466
- const sourceValue = options.defaultValue();
467
- const derivedParser = options.factory(sourceValue);
468
- return derivedParser.parse(input);
502
+ let derivedParser;
503
+ try {
504
+ const sourceValue = options.defaultValue();
505
+ derivedParser = options.factory(sourceValue);
506
+ } catch (e) {
507
+ const msg = e instanceof Error ? e.message : String(e);
508
+ return Promise.resolve({
509
+ success: false,
510
+ error: require_message.message`Derived parser error: ${msg}`
511
+ });
512
+ }
513
+ return Promise.resolve(derivedParser.parse(input));
469
514
  },
470
515
  [parseWithDependency](input, dependencyValue) {
471
516
  let derivedParser;
@@ -478,7 +523,7 @@ function createAsyncDerivedParserFromAsyncFactory(sourceId, options) {
478
523
  error: require_message.message`Factory error: ${msg}`
479
524
  });
480
525
  }
481
- return derivedParser.parse(input);
526
+ return Promise.resolve(derivedParser.parse(input));
482
527
  },
483
528
  format(value) {
484
529
  const sourceValue = options.defaultValue();
@@ -516,8 +561,17 @@ function createAsyncDerivedParserFromSyncFactory(sourceId, options) {
516
561
  [derivedValueParserMarker]: true,
517
562
  [dependencyId]: sourceId,
518
563
  parse(input) {
519
- const sourceValue = options.defaultValue();
520
- const derivedParser = options.factory(sourceValue);
564
+ let derivedParser;
565
+ try {
566
+ const sourceValue = options.defaultValue();
567
+ derivedParser = options.factory(sourceValue);
568
+ } catch (e) {
569
+ const msg = e instanceof Error ? e.message : String(e);
570
+ return Promise.resolve({
571
+ success: false,
572
+ error: require_message.message`Derived parser error: ${msg}`
573
+ });
574
+ }
521
575
  return Promise.resolve(derivedParser.parse(input));
522
576
  },
523
577
  [parseWithDependency](input, dependencyValue) {
@@ -86,7 +86,7 @@ function dependency(parser) {
86
86
  return createSyncDerivedParser(id, options);
87
87
  },
88
88
  deriveAsync(options) {
89
- return createDerivedValueParser(id, parser, options);
89
+ return createAsyncDerivedParserFromAsyncFactory(id, options);
90
90
  }
91
91
  };
92
92
  return result;
@@ -212,8 +212,17 @@ function createSyncDerivedFromParser(sourceId, options) {
212
212
  [dependencyIds]: alldependencyIds,
213
213
  [defaultValues]: options.defaultValues,
214
214
  parse(input) {
215
- const sourceValues = options.defaultValues();
216
- const derivedParser = options.factory(...sourceValues);
215
+ let derivedParser;
216
+ try {
217
+ const sourceValues = options.defaultValues();
218
+ derivedParser = options.factory(...sourceValues);
219
+ } catch (e) {
220
+ const msg = e instanceof Error ? e.message : String(e);
221
+ return {
222
+ success: false,
223
+ error: message`Derived parser error: ${msg}`
224
+ };
225
+ }
217
226
  if (isAsyncModeParser(derivedParser)) return {
218
227
  success: false,
219
228
  error: message`Factory returned an async parser where a sync parser is required.`
@@ -277,9 +286,18 @@ function createAsyncDerivedFromParserFromAsyncFactory(sourceId, options) {
277
286
  [dependencyIds]: alldependencyIds,
278
287
  [defaultValues]: options.defaultValues,
279
288
  parse(input) {
280
- const sourceValues = options.defaultValues();
281
- const derivedParser = options.factory(...sourceValues);
282
- return derivedParser.parse(input);
289
+ let derivedParser;
290
+ try {
291
+ const sourceValues = options.defaultValues();
292
+ derivedParser = options.factory(...sourceValues);
293
+ } catch (e) {
294
+ const msg = e instanceof Error ? e.message : String(e);
295
+ return Promise.resolve({
296
+ success: false,
297
+ error: message`Derived parser error: ${msg}`
298
+ });
299
+ }
300
+ return Promise.resolve(derivedParser.parse(input));
283
301
  },
284
302
  [parseWithDependency](input, dependencyValue) {
285
303
  let derivedParser;
@@ -292,7 +310,7 @@ function createAsyncDerivedFromParserFromAsyncFactory(sourceId, options) {
292
310
  error: message`Factory error: ${msg}`
293
311
  });
294
312
  }
295
- return derivedParser.parse(input);
313
+ return Promise.resolve(derivedParser.parse(input));
296
314
  },
297
315
  format(value) {
298
316
  const sourceValues = options.defaultValues();
@@ -334,8 +352,17 @@ function createAsyncDerivedFromParserFromSyncFactory(sourceId, options) {
334
352
  [dependencyIds]: alldependencyIds,
335
353
  [defaultValues]: options.defaultValues,
336
354
  parse(input) {
337
- const sourceValues = options.defaultValues();
338
- const derivedParser = options.factory(...sourceValues);
355
+ let derivedParser;
356
+ try {
357
+ const sourceValues = options.defaultValues();
358
+ derivedParser = options.factory(...sourceValues);
359
+ } catch (e) {
360
+ const msg = e instanceof Error ? e.message : String(e);
361
+ return Promise.resolve({
362
+ success: false,
363
+ error: message`Derived parser error: ${msg}`
364
+ });
365
+ }
339
366
  return Promise.resolve(derivedParser.parse(input));
340
367
  },
341
368
  [parseWithDependency](input, dependencyValue) {
@@ -402,8 +429,17 @@ function createSyncDerivedParser(sourceId, options) {
402
429
  [derivedValueParserMarker]: true,
403
430
  [dependencyId]: sourceId,
404
431
  parse(input) {
405
- const sourceValue = options.defaultValue();
406
- const derivedParser = options.factory(sourceValue);
432
+ let derivedParser;
433
+ try {
434
+ const sourceValue = options.defaultValue();
435
+ derivedParser = options.factory(sourceValue);
436
+ } catch (e) {
437
+ const msg = e instanceof Error ? e.message : String(e);
438
+ return {
439
+ success: false,
440
+ error: message`Derived parser error: ${msg}`
441
+ };
442
+ }
407
443
  if (isAsyncModeParser(derivedParser)) return {
408
444
  success: false,
409
445
  error: message`Factory returned an async parser where a sync parser is required.`
@@ -463,9 +499,18 @@ function createAsyncDerivedParserFromAsyncFactory(sourceId, options) {
463
499
  [derivedValueParserMarker]: true,
464
500
  [dependencyId]: sourceId,
465
501
  parse(input) {
466
- const sourceValue = options.defaultValue();
467
- const derivedParser = options.factory(sourceValue);
468
- return derivedParser.parse(input);
502
+ let derivedParser;
503
+ try {
504
+ const sourceValue = options.defaultValue();
505
+ derivedParser = options.factory(sourceValue);
506
+ } catch (e) {
507
+ const msg = e instanceof Error ? e.message : String(e);
508
+ return Promise.resolve({
509
+ success: false,
510
+ error: message`Derived parser error: ${msg}`
511
+ });
512
+ }
513
+ return Promise.resolve(derivedParser.parse(input));
469
514
  },
470
515
  [parseWithDependency](input, dependencyValue) {
471
516
  let derivedParser;
@@ -478,7 +523,7 @@ function createAsyncDerivedParserFromAsyncFactory(sourceId, options) {
478
523
  error: message`Factory error: ${msg}`
479
524
  });
480
525
  }
481
- return derivedParser.parse(input);
526
+ return Promise.resolve(derivedParser.parse(input));
482
527
  },
483
528
  format(value) {
484
529
  const sourceValue = options.defaultValue();
@@ -516,8 +561,17 @@ function createAsyncDerivedParserFromSyncFactory(sourceId, options) {
516
561
  [derivedValueParserMarker]: true,
517
562
  [dependencyId]: sourceId,
518
563
  parse(input) {
519
- const sourceValue = options.defaultValue();
520
- const derivedParser = options.factory(sourceValue);
564
+ let derivedParser;
565
+ try {
566
+ const sourceValue = options.defaultValue();
567
+ derivedParser = options.factory(sourceValue);
568
+ } catch (e) {
569
+ const msg = e instanceof Error ? e.message : String(e);
570
+ return Promise.resolve({
571
+ success: false,
572
+ error: message`Derived parser error: ${msg}`
573
+ });
574
+ }
521
575
  return Promise.resolve(derivedParser.parse(input));
522
576
  },
523
577
  [parseWithDependency](input, dependencyValue) {
@@ -20,31 +20,68 @@ function choice(choices, options = {}) {
20
20
  if (isNumberChoice) {
21
21
  const numberChoices = choices;
22
22
  const numberOptions = options;
23
+ const hasNaN = numberChoices.some((v) => Number.isNaN(v));
24
+ const validNumberChoices = hasNaN ? numberChoices.filter((v) => !Number.isNaN(v)) : numberChoices;
25
+ const numberStrings = numberChoices.map((v) => Object.is(v, -0) ? "-0" : String(v));
23
26
  return {
24
27
  $mode: "sync",
25
28
  metavar,
26
- choices,
29
+ choices: hasNaN ? validNumberChoices : numberChoices,
27
30
  parse(input) {
28
- const parsed = Number(input);
29
- if (Number.isNaN(parsed)) return {
30
- success: false,
31
- error: formatNumberChoiceError(input, numberChoices, numberOptions)
32
- };
33
- const index = numberChoices.indexOf(parsed);
34
- if (index < 0) return {
35
- success: false,
36
- error: formatNumberChoiceError(input, numberChoices, numberOptions)
37
- };
38
- return {
31
+ const index = numberStrings.indexOf(input);
32
+ if (index >= 0 && !Number.isNaN(numberChoices[index])) return {
39
33
  success: true,
40
34
  value: numberChoices[index]
41
35
  };
36
+ if (/^[+-]?(\d+\.?\d*|\.\d+)$/.test(input)) {
37
+ const parsed = Number(input);
38
+ if (Number.isFinite(parsed)) {
39
+ const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
40
+ const normalizedInput = normalizeDecimal(input);
41
+ const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
42
+ if (normalizedInput === normalizedCanonical) {
43
+ const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
44
+ if (fallbackIndex >= 0) return {
45
+ success: true,
46
+ value: numberChoices[fallbackIndex]
47
+ };
48
+ }
49
+ if (parsed === 0 && normalizedInput.replace(/^-/, "") === "0" && !numberChoices.some((v) => Object.is(v, -0))) {
50
+ const zeroIndex = numberChoices.indexOf(0);
51
+ if (zeroIndex >= 0) return {
52
+ success: true,
53
+ value: numberChoices[zeroIndex]
54
+ };
55
+ }
56
+ }
57
+ }
58
+ if (/^[+-]?(\d+\.?\d*|\.\d+)[eE][+-]?\d+$/.test(input)) {
59
+ const parsed = Number(input);
60
+ if (Number.isFinite(parsed)) {
61
+ const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
62
+ if (/[eE]/.test(canonical)) {
63
+ const normalizedInput = normalizeDecimal(expandScientific(input));
64
+ const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
65
+ if (normalizedInput === normalizedCanonical) {
66
+ const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
67
+ if (fallbackIndex >= 0) return {
68
+ success: true,
69
+ value: numberChoices[fallbackIndex]
70
+ };
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return {
76
+ success: false,
77
+ error: formatNumberChoiceError(input, validNumberChoices, numberChoices, numberOptions)
78
+ };
42
79
  },
43
80
  format(value) {
44
- return String(value);
81
+ return Object.is(value, -0) ? "-0" : String(value);
45
82
  },
46
83
  suggest(prefix) {
47
- return numberChoices.map((value) => String(value)).filter((valueStr) => valueStr.startsWith(prefix)).map((valueStr) => ({
84
+ return numberStrings.filter((valueStr, i) => !Number.isNaN(numberChoices[i]) && valueStr.startsWith(prefix)).map((valueStr) => ({
48
85
  kind: "literal",
49
86
  text: valueStr
50
87
  }));
@@ -86,6 +123,54 @@ function choice(choices, options = {}) {
86
123
  };
87
124
  }
88
125
  /**
126
+ * Expands a numeric string in scientific notation (e.g., `"1e+21"`,
127
+ * `"1.5e-3"`, `".1e-6"`) into plain decimal form for normalization.
128
+ * Used for both canonical `String(number)` output and user input.
129
+ * Returns the input unchanged if it does not contain scientific notation.
130
+ */
131
+ function expandScientific(s) {
132
+ const match = /^([+-]?)(\d+\.?\d*|\.\d+)[eE]([+-]?\d+)$/.exec(s);
133
+ if (!match) return s;
134
+ const [, rawSign, mantissa, expStr] = match;
135
+ const sign = rawSign === "-" ? "-" : "";
136
+ const exp = parseInt(expStr, 10);
137
+ const dotPos = mantissa.indexOf(".");
138
+ const digits = mantissa.replace(".", "");
139
+ const intLen = dotPos >= 0 ? dotPos : digits.length;
140
+ const newIntLen = intLen + exp;
141
+ let result;
142
+ if (newIntLen >= digits.length) result = digits + "0".repeat(newIntLen - digits.length);
143
+ else if (newIntLen <= 0) result = "0." + "0".repeat(-newIntLen) + digits;
144
+ else result = digits.slice(0, newIntLen) + "." + digits.slice(newIntLen);
145
+ return sign + result;
146
+ }
147
+ /**
148
+ * Normalizes a plain decimal string by stripping leading zeros from the
149
+ * integer part and trailing zeros from the fractional part, so that two
150
+ * strings representing the same mathematical value compare as equal.
151
+ */
152
+ function normalizeDecimal(s) {
153
+ let sign = "";
154
+ let str = s;
155
+ if (str.startsWith("-") || str.startsWith("+")) {
156
+ if (str[0] === "-") sign = "-";
157
+ str = str.slice(1);
158
+ }
159
+ const dot = str.indexOf(".");
160
+ let int;
161
+ let frac;
162
+ if (dot >= 0) {
163
+ int = str.slice(0, dot);
164
+ frac = str.slice(dot + 1);
165
+ } else {
166
+ int = str;
167
+ frac = "";
168
+ }
169
+ int = int.replace(/^0+/, "") || "0";
170
+ frac = frac.replace(/0+$/, "");
171
+ return sign + (frac ? int + "." + frac : int);
172
+ }
173
+ /**
89
174
  * Formats error message for string choice parser.
90
175
  */
91
176
  function formatStringChoiceError(input, choices, options) {
@@ -95,15 +180,16 @@ function formatStringChoiceError(input, choices, options) {
95
180
  /**
96
181
  * Formats error message for number choice parser.
97
182
  */
98
- function formatNumberChoiceError(input, choices, options) {
99
- if (options.errors?.invalidChoice) return typeof options.errors.invalidChoice === "function" ? options.errors.invalidChoice(input, choices) : options.errors.invalidChoice;
100
- return formatDefaultChoiceError(input, choices);
183
+ function formatNumberChoiceError(input, validChoices, allChoices, options) {
184
+ if (options.errors?.invalidChoice) return typeof options.errors.invalidChoice === "function" ? options.errors.invalidChoice(input, validChoices) : options.errors.invalidChoice;
185
+ return formatDefaultChoiceError(input, allChoices);
101
186
  }
102
187
  /**
103
188
  * Formats default error message for choice parser.
104
189
  */
105
190
  function formatDefaultChoiceError(input, choices) {
106
- const choiceStrings = choices.map((c) => String(c));
191
+ const choiceStrings = choices.filter((c) => typeof c === "string" || !Number.isNaN(c)).map((c) => Object.is(c, -0) ? "-0" : String(c));
192
+ if (choiceStrings.length === 0 && choices.length > 0) return require_message.message`No valid choices are configured, but got ${input}.`;
107
193
  return require_message.message`Expected one of ${require_message.valueSet(choiceStrings, { locale: "en-US" })}, but got ${input}.`;
108
194
  }
109
195
  /**
@@ -20,31 +20,68 @@ function choice(choices, options = {}) {
20
20
  if (isNumberChoice) {
21
21
  const numberChoices = choices;
22
22
  const numberOptions = options;
23
+ const hasNaN = numberChoices.some((v) => Number.isNaN(v));
24
+ const validNumberChoices = hasNaN ? numberChoices.filter((v) => !Number.isNaN(v)) : numberChoices;
25
+ const numberStrings = numberChoices.map((v) => Object.is(v, -0) ? "-0" : String(v));
23
26
  return {
24
27
  $mode: "sync",
25
28
  metavar,
26
- choices,
29
+ choices: hasNaN ? validNumberChoices : numberChoices,
27
30
  parse(input) {
28
- const parsed = Number(input);
29
- if (Number.isNaN(parsed)) return {
30
- success: false,
31
- error: formatNumberChoiceError(input, numberChoices, numberOptions)
32
- };
33
- const index = numberChoices.indexOf(parsed);
34
- if (index < 0) return {
35
- success: false,
36
- error: formatNumberChoiceError(input, numberChoices, numberOptions)
37
- };
38
- return {
31
+ const index = numberStrings.indexOf(input);
32
+ if (index >= 0 && !Number.isNaN(numberChoices[index])) return {
39
33
  success: true,
40
34
  value: numberChoices[index]
41
35
  };
36
+ if (/^[+-]?(\d+\.?\d*|\.\d+)$/.test(input)) {
37
+ const parsed = Number(input);
38
+ if (Number.isFinite(parsed)) {
39
+ const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
40
+ const normalizedInput = normalizeDecimal(input);
41
+ const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
42
+ if (normalizedInput === normalizedCanonical) {
43
+ const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
44
+ if (fallbackIndex >= 0) return {
45
+ success: true,
46
+ value: numberChoices[fallbackIndex]
47
+ };
48
+ }
49
+ if (parsed === 0 && normalizedInput.replace(/^-/, "") === "0" && !numberChoices.some((v) => Object.is(v, -0))) {
50
+ const zeroIndex = numberChoices.indexOf(0);
51
+ if (zeroIndex >= 0) return {
52
+ success: true,
53
+ value: numberChoices[zeroIndex]
54
+ };
55
+ }
56
+ }
57
+ }
58
+ if (/^[+-]?(\d+\.?\d*|\.\d+)[eE][+-]?\d+$/.test(input)) {
59
+ const parsed = Number(input);
60
+ if (Number.isFinite(parsed)) {
61
+ const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
62
+ if (/[eE]/.test(canonical)) {
63
+ const normalizedInput = normalizeDecimal(expandScientific(input));
64
+ const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
65
+ if (normalizedInput === normalizedCanonical) {
66
+ const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
67
+ if (fallbackIndex >= 0) return {
68
+ success: true,
69
+ value: numberChoices[fallbackIndex]
70
+ };
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return {
76
+ success: false,
77
+ error: formatNumberChoiceError(input, validNumberChoices, numberChoices, numberOptions)
78
+ };
42
79
  },
43
80
  format(value) {
44
- return String(value);
81
+ return Object.is(value, -0) ? "-0" : String(value);
45
82
  },
46
83
  suggest(prefix) {
47
- return numberChoices.map((value) => String(value)).filter((valueStr) => valueStr.startsWith(prefix)).map((valueStr) => ({
84
+ return numberStrings.filter((valueStr, i) => !Number.isNaN(numberChoices[i]) && valueStr.startsWith(prefix)).map((valueStr) => ({
48
85
  kind: "literal",
49
86
  text: valueStr
50
87
  }));
@@ -86,6 +123,54 @@ function choice(choices, options = {}) {
86
123
  };
87
124
  }
88
125
  /**
126
+ * Expands a numeric string in scientific notation (e.g., `"1e+21"`,
127
+ * `"1.5e-3"`, `".1e-6"`) into plain decimal form for normalization.
128
+ * Used for both canonical `String(number)` output and user input.
129
+ * Returns the input unchanged if it does not contain scientific notation.
130
+ */
131
+ function expandScientific(s) {
132
+ const match = /^([+-]?)(\d+\.?\d*|\.\d+)[eE]([+-]?\d+)$/.exec(s);
133
+ if (!match) return s;
134
+ const [, rawSign, mantissa, expStr] = match;
135
+ const sign = rawSign === "-" ? "-" : "";
136
+ const exp = parseInt(expStr, 10);
137
+ const dotPos = mantissa.indexOf(".");
138
+ const digits = mantissa.replace(".", "");
139
+ const intLen = dotPos >= 0 ? dotPos : digits.length;
140
+ const newIntLen = intLen + exp;
141
+ let result;
142
+ if (newIntLen >= digits.length) result = digits + "0".repeat(newIntLen - digits.length);
143
+ else if (newIntLen <= 0) result = "0." + "0".repeat(-newIntLen) + digits;
144
+ else result = digits.slice(0, newIntLen) + "." + digits.slice(newIntLen);
145
+ return sign + result;
146
+ }
147
+ /**
148
+ * Normalizes a plain decimal string by stripping leading zeros from the
149
+ * integer part and trailing zeros from the fractional part, so that two
150
+ * strings representing the same mathematical value compare as equal.
151
+ */
152
+ function normalizeDecimal(s) {
153
+ let sign = "";
154
+ let str = s;
155
+ if (str.startsWith("-") || str.startsWith("+")) {
156
+ if (str[0] === "-") sign = "-";
157
+ str = str.slice(1);
158
+ }
159
+ const dot = str.indexOf(".");
160
+ let int;
161
+ let frac;
162
+ if (dot >= 0) {
163
+ int = str.slice(0, dot);
164
+ frac = str.slice(dot + 1);
165
+ } else {
166
+ int = str;
167
+ frac = "";
168
+ }
169
+ int = int.replace(/^0+/, "") || "0";
170
+ frac = frac.replace(/0+$/, "");
171
+ return sign + (frac ? int + "." + frac : int);
172
+ }
173
+ /**
89
174
  * Formats error message for string choice parser.
90
175
  */
91
176
  function formatStringChoiceError(input, choices, options) {
@@ -95,15 +180,16 @@ function formatStringChoiceError(input, choices, options) {
95
180
  /**
96
181
  * Formats error message for number choice parser.
97
182
  */
98
- function formatNumberChoiceError(input, choices, options) {
99
- if (options.errors?.invalidChoice) return typeof options.errors.invalidChoice === "function" ? options.errors.invalidChoice(input, choices) : options.errors.invalidChoice;
100
- return formatDefaultChoiceError(input, choices);
183
+ function formatNumberChoiceError(input, validChoices, allChoices, options) {
184
+ if (options.errors?.invalidChoice) return typeof options.errors.invalidChoice === "function" ? options.errors.invalidChoice(input, validChoices) : options.errors.invalidChoice;
185
+ return formatDefaultChoiceError(input, allChoices);
101
186
  }
102
187
  /**
103
188
  * Formats default error message for choice parser.
104
189
  */
105
190
  function formatDefaultChoiceError(input, choices) {
106
- const choiceStrings = choices.map((c) => String(c));
191
+ const choiceStrings = choices.filter((c) => typeof c === "string" || !Number.isNaN(c)).map((c) => Object.is(c, -0) ? "-0" : String(c));
192
+ if (choiceStrings.length === 0 && choices.length > 0) return message`No valid choices are configured, but got ${input}.`;
107
193
  return message`Expected one of ${valueSet(choiceStrings, { locale: "en-US" })}, but got ${input}.`;
108
194
  }
109
195
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.0-dev.693+6ee99e0d",
3
+ "version": "1.0.0-dev.705+5cb74130",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",