@tremolo-ui/functions 0.5.0 → 0.7.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/dist/index.js CHANGED
@@ -24,23 +24,54 @@ function rawValue(normalizedValue, min, max) {
24
24
  if (min >= max) throw new RangeError("requirements: min < max");
25
25
  return min + clamp(normalizedValue, 0, 1) * (max - min);
26
26
  }
27
+ /**
28
+ * Put a value on the grid the caller asked for, rounding a half step upwards.
29
+ *
30
+ * The rounding is done on the quotient rather than by comparing the distance
31
+ * to the two neighbours, because both of those carry error of their own. The
32
+ * quotient is cleared of its artefact first: `0.15 / 0.1` is 1.4999999999999998,
33
+ * and a value sitting exactly on a half step would otherwise fall to whichever
34
+ * side the last bit happened to land on — 0.25 rounded up while 0.15 and 0.35
35
+ * rounded down.
36
+ */
27
37
  function stepValue(value, step) {
28
38
  if (step <= 0) throw new RangeError("requirements: step > 0");
29
- const quotient = Math.floor(value / step);
30
- const decimalDigits = decimalPart(step)?.length;
31
- const v = toFixed(quotient * step, decimalDigits);
32
- const next = toFixed((quotient + 1) * step, decimalDigits);
33
- return Math.abs(value - v) < Math.abs(value - next) ? v : next;
39
+ const stepped = toPrecision(Math.round(toPrecision(value / step)) * step);
40
+ return stepped === 0 ? 0 : stepped;
34
41
  }
35
42
  function toFixed(x, fractionDigits) {
36
43
  return Number(x.toFixed(fractionDigits));
37
44
  }
38
- function integerPart(x) {
39
- if (Number.isNaN(x)) return;
40
- return String(x).split(".")[0];
41
- }
42
- function decimalPart(x) {
43
- return String(x).split(".")[1];
45
+ /**
46
+ * The significant decimal digits a double actually carries. A double holds a
47
+ * little under 16, so anything past this is the binary representation showing
48
+ * through rather than information.
49
+ */
50
+ const SIGNIFICANT_DIGITS = 15;
51
+ /**
52
+ * Drop the binary artefact from a computed value.
53
+ *
54
+ * Arithmetic on doubles leaves debris in the last couple of digits, and it
55
+ * accumulates: adding 0.1 to 5 twelve times gives 5.699999999999998 rather
56
+ * than 5.7, and the display of a control shows exactly that. Rounding to the
57
+ * digits a double can carry removes it, and adds nothing back — the value was
58
+ * already the result of a calculation whose own error is that size or larger.
59
+ *
60
+ * This is not the same as rounding to a `step`. {@link stepValue} puts a value
61
+ * on a grid the caller asked for and is a decision about the value; this only
62
+ * removes what was never in the value to begin with.
63
+ *
64
+ * @param significantDigits how many digits to keep. The default is the only
65
+ * one that is purely artefact removal; a smaller number starts discarding real
66
+ * precision.
67
+ *
68
+ * @example
69
+ * toPrecision(5.1 + 0.1) // 5.2, rather than 5.199999999999999
70
+ */
71
+ function toPrecision(x, significantDigits = SIGNIFICANT_DIGITS) {
72
+ if (x === 0 || !Number.isFinite(x)) return x;
73
+ const rounded = Number(x.toPrecision(significantDigits));
74
+ return Number.isFinite(rounded) ? rounded : x;
44
75
  }
45
76
  function radian(degree) {
46
77
  return Math.PI * degree / 180;
@@ -62,6 +93,9 @@ function gainToDb(gain) {
62
93
  function assertRange(min, max) {
63
94
  if (min >= max) throw new RangeError("requirements: min < max");
64
95
  }
96
+ function assertPositiveFinite(value, name) {
97
+ if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name}: requirements: finite and greater than 0`);
98
+ }
65
99
  /**
66
100
  * Equal travel gives an equal change in value.
67
101
  *
@@ -89,6 +123,7 @@ const linearScale = {
89
123
  * @param skew the JUCE skew factor
90
124
  */
91
125
  function skewScale(skew) {
126
+ assertPositiveFinite(skew, "skewScale");
92
127
  return {
93
128
  normalize: (value, min, max) => Math.pow(normalizeValue(value, min, max), skew),
94
129
  denormalize: (position, min, max) => rawValue(skew === 1 ? position : Math.exp(Math.log(clamp(position, 0, 1)) / skew), min, max)
@@ -99,7 +134,8 @@ function skewScale(skew) {
99
134
  * of the travel — JUCE's `NormalisableRange::setSkewForCentre`.
100
135
  */
101
136
  function skewWithCenterValue(centerValue, min, max) {
102
- if (!(min <= centerValue && centerValue <= max)) throw new RangeError("requirements: min <= centerValue <= max");
137
+ assertRange(min, max);
138
+ if (!(min < centerValue && centerValue < max)) throw new RangeError("requirements: min < centerValue < max");
103
139
  return Math.log(.5) / Math.log((centerValue - min) / (max - min));
104
140
  }
105
141
  /**
@@ -116,11 +152,15 @@ function skewWithCenterValue(centerValue, min, max) {
116
152
  const exponentialScale = {
117
153
  normalize: (value, min, max) => {
118
154
  assertExponentialRange(min, max);
119
- return clamp(Math.log(clamp(value, min, max) / min) / Math.log(max / min), 0, 1);
155
+ const start = Math.log(Math.abs(min));
156
+ const end = Math.log(Math.abs(max));
157
+ return clamp((Math.log(Math.abs(clamp(value, min, max))) - start) / (end - start), 0, 1);
120
158
  },
121
159
  denormalize: (position, min, max) => {
122
160
  assertExponentialRange(min, max);
123
- return min * Math.pow(max / min, clamp(position, 0, 1));
161
+ const start = Math.log(Math.abs(min));
162
+ const magnitude = Math.exp(start + (Math.log(Math.abs(max)) - start) * clamp(position, 0, 1));
163
+ return Math.sign(min) * magnitude;
124
164
  }
125
165
  };
126
166
  function assertExponentialRange(min, max) {
@@ -149,25 +189,23 @@ function assertExponentialRange(min, max) {
149
189
  * @param curve how hard the curve bends, and in which direction
150
190
  */
151
191
  function curveScale(curve) {
192
+ if (!Number.isFinite(curve) || Math.abs(curve) > 32) throw new RangeError("curveScale: requirements: finite curve from -32 to 32");
152
193
  if (Math.abs(curve) < .001) return linearScale;
153
- const grow = Math.exp(curve);
154
- const coefficients = (min, max) => {
155
- const a = (max - min) / (1 - grow);
156
- return {
157
- a,
158
- b: min + a
159
- };
160
- };
161
194
  return {
162
195
  normalize: (value, min, max) => {
163
196
  assertRange(min, max);
164
- const { a, b } = coefficients(min, max);
165
- return clamp(Math.log((b - clamp(value, min, max)) / a) / curve, 0, 1);
197
+ const proportion = clamp((value - min) / (max - min), 0, 1);
198
+ if (proportion === 0 || proportion === 1) return proportion;
199
+ if (curve > 0) return 1 + Math.log(proportion + (1 - proportion) * Math.exp(-curve)) / curve;
200
+ return Math.log1p(proportion * Math.expm1(curve)) / curve;
166
201
  },
167
202
  denormalize: (position, min, max) => {
168
203
  assertRange(min, max);
169
- const { a, b } = coefficients(min, max);
170
- return b - a * Math.pow(grow, clamp(position, 0, 1));
204
+ const p = clamp(position, 0, 1);
205
+ if (p === 0) return min;
206
+ if (p === 1) return max;
207
+ const proportion = curve > 0 ? Math.exp(curve * (p - 1)) * (1 - Math.exp(-curve * p)) / (1 - Math.exp(-curve)) : Math.expm1(curve * p) / Math.expm1(curve);
208
+ return min + (max - min) * proportion;
171
209
  }
172
210
  };
173
211
  }
@@ -184,6 +222,7 @@ function curveScale(curve) {
184
222
  * @param skew the JUCE skew factor
185
223
  */
186
224
  function symmetricSkewScale(skew) {
225
+ assertPositiveFinite(skew, "symmetricSkewScale");
187
226
  return {
188
227
  normalize: (value, min, max) => {
189
228
  assertRange(min, max);
@@ -210,48 +249,18 @@ function curveWithCenterValue(centerValue, min, max) {
210
249
  const proportion = (centerValue - min) / (max - min);
211
250
  return 2 * Math.log(1 / proportion - 1);
212
251
  }
252
+ //#endregion
253
+ //#region src/midi.ts
213
254
  /**
214
- * Move a value by an amount of input, as reported by a wheel or an arrow key.
215
- *
216
- * The pipeline matches `createDragValue` of `@tremolo-ui/dom`: scale, then
217
- * step, then clamp. Which key or which sign of `deltaY` counts as which
218
- * direction is left to the caller, since it differs per component.
255
+ * Remainder that follows the sign of the divisor, rather than of the dividend.
219
256
  *
220
- * @param direction which way, and how many times, to apply the option. The
221
- * size of one step is `option[1]`, so this is normally `1` or `-1`.
222
- *
223
- * @example
224
- * // ArrowDown on a slider whose keyboard option is ['raw', 1]
225
- * applyDelta(value, -1, keyboard, { min, max, step, scale })
257
+ * `%` in JavaScript keeps the sign of the left operand, so `-13 % 12` is `-1`
258
+ * and cannot index a 12-element table. Every note here is placed by its
259
+ * position within the octave, and note numbers below C-1 are negative.
226
260
  */
227
- function applyDelta(value, direction, [mode, amount], { min, max, step, scale = linearScale }) {
228
- const x = direction * amount;
229
- const next = mode == "normalized" ? scale.denormalize(scale.normalize(value, min, max) + x, min, max) : value + x;
230
- return clamp(step ? stepValue(next, step) : next, min, max);
231
- }
232
- //#endregion
233
- //#region src/util.ts
234
- function styleHelper(value, op, influencer) {
235
- if (op && influencer) if (typeof value == "number") {
236
- if (op == "+") return `${value + influencer}px`;
237
- if (op == "-") return `${value - influencer}px`;
238
- if (op == "*") return `${value * influencer}px`;
239
- if (op == "/") return `${value / influencer}px`;
240
- } else return `calc(${value}px ${op} ${influencer})`;
241
- else if (typeof value == "number") return `${value}px`;
242
- else return value;
243
- }
244
- function isEmpty(obj) {
245
- return Object.keys(obj).length == 0;
246
- }
247
261
  function mod(n, m) {
248
262
  return (n % m + m) % m;
249
263
  }
250
- function xor(a = false, b = false) {
251
- return (a || b) && a != b;
252
- }
253
- //#endregion
254
- //#region src/midi.ts
255
264
  const whiteKeys = [
256
265
  "A",
257
266
  "B",
@@ -275,14 +284,19 @@ const noteKeys = [
275
284
  "A#",
276
285
  "B"
277
286
  ];
287
+ function assertSafeInteger(value, name) {
288
+ if (!Number.isSafeInteger(value)) throw new RangeError(`${name}: requirements: a safe integer`);
289
+ }
278
290
  function parseNoteName(noteName) {
279
291
  const m = noteName.match(/^([a-g])(#{0,2}|b{0,2})(-?\d+)$/i);
280
292
  if (!m) throw new Error("Invalid note name");
281
293
  const [, letter, accidental, octave] = m;
294
+ const parsedOctave = Number(octave);
295
+ assertSafeInteger(parsedOctave, "octave");
282
296
  return {
283
297
  letter: letter.toLocaleUpperCase(),
284
298
  accidental,
285
- octave: Number(octave)
299
+ octave: parsedOctave
286
300
  };
287
301
  }
288
302
  /**
@@ -291,8 +305,10 @@ function parseNoteName(noteName) {
291
305
  function noteNumber(noteName) {
292
306
  const { letter, accidental, octave } = parseNoteName(noteName);
293
307
  const noteIndex = noteKeys.indexOf(letter.toLocaleUpperCase());
294
- const accidentalValue = (accidental[0] == "b" ? -1 : 1) * accidental.length;
295
- return noteIndex + 12 * (Number(octave) + 1) + accidentalValue;
308
+ const accidentalValue = (accidental[0] === "b" ? -1 : 1) * accidental.length;
309
+ const result = noteIndex + 12 * (octave + 1) + accidentalValue;
310
+ assertSafeInteger(result, "note number");
311
+ return result;
296
312
  }
297
313
  /**
298
314
  * Convert noteNumber to noteName
@@ -301,6 +317,7 @@ function noteNumber(noteName) {
301
317
  * @param noteNumber noteNumber
302
318
  */
303
319
  function noteName(noteNumber) {
320
+ assertSafeInteger(noteNumber, "note number");
304
321
  const noteIndex = mod(noteNumber, 12);
305
322
  const octave = Math.floor(noteNumber / 12) - 1;
306
323
  return `${noteKeys[noteIndex]}${octave}`;
@@ -309,14 +326,16 @@ function noteName(noteNumber) {
309
326
  * Convert noteNumber to noteKey
310
327
  */
311
328
  function noteKey(noteNumber) {
329
+ assertSafeInteger(noteNumber, "note number");
312
330
  return noteKeys[mod(noteNumber, 12)];
313
331
  }
314
332
  /**
315
333
  * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'
316
334
  */
317
335
  function isWhiteKey(note) {
318
- const n = typeof note == "string" ? noteNumber(note) : note;
319
- return mod(n, 12) == 0 || mod(n, 12) == 2 || mod(n, 12) == 4 || mod(n, 12) == 5 || mod(n, 12) == 7 || mod(n, 12) == 9 || mod(n, 12) == 11;
336
+ const n = typeof note === "string" ? noteNumber(note) : note;
337
+ assertSafeInteger(n, "note number");
338
+ return mod(n, 12) === 0 || mod(n, 12) === 2 || mod(n, 12) === 4 || mod(n, 12) === 5 || mod(n, 12) === 7 || mod(n, 12) === 9 || mod(n, 12) === 11;
320
339
  }
321
340
  /**
322
341
  * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'
@@ -331,7 +350,8 @@ function isBlackKey(note) {
331
350
  * @returns frequency [Hz]
332
351
  */
333
352
  function noteToFrequency(note, detune = 0, a4 = 440) {
334
- const n = typeof note == "string" ? noteNumber(note) : note;
353
+ const n = typeof note === "string" ? noteNumber(note) : note;
354
+ assertSafeInteger(n, "note number");
335
355
  return a4 / 32 * 2 ** ((n - 9 + detune / 100) / 12);
336
356
  }
337
357
  /**
@@ -502,8 +522,10 @@ const scaleIntervals = {
502
522
  * ```
503
523
  */
504
524
  function inScale(note, root, name) {
505
- const n = typeof note == "string" ? noteNumber(note) : note;
506
- const r = typeof root == "string" ? noteNumber(root) : root;
525
+ const n = typeof note === "string" ? noteNumber(note) : note;
526
+ const r = typeof root === "string" ? noteNumber(root) : root;
527
+ assertSafeInteger(n, "note number");
528
+ assertSafeInteger(r, "root note number");
507
529
  return scaleIntervals[name].includes(mod(n - r, 12));
508
530
  }
509
531
  /**
@@ -521,146 +543,128 @@ function inScale(note, root, name) {
521
543
  * ```
522
544
  */
523
545
  function scaleNotes(root, name, octaves = 1) {
524
- const r = typeof root == "string" ? noteNumber(root) : root;
546
+ const r = typeof root === "string" ? noteNumber(root) : root;
547
+ assertSafeInteger(r, "root note number");
548
+ if (!Number.isSafeInteger(octaves) || octaves < 0) throw new RangeError("octaves: requirements: a non-negative safe integer");
525
549
  const intervals = scaleIntervals[name];
526
550
  return Array.from({ length: octaves }, (_, octave) => intervals.map((interval) => r + octave * 12 + interval)).flat();
527
551
  }
528
552
  //#endregion
529
- //#region src/piano.ts
530
- /**
531
- * `[noteRange.first, noteRange.first + 1, ..., noteRange.last]`
532
- */
533
- function getNoteRangeArray(noteRange) {
534
- return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
535
- }
536
- const DEFAULT_KEY_GAP = 1;
537
- const DEFAULT_BLACK_KEY_WIDTH_RATIO = .65;
538
- const DEFAULT_BLACK_KEY_HEIGHT_RATIO = .6;
553
+ //#region src/unit.ts
554
+ /** Ordered small to large. The empty symbol is the base unit. */
555
+ const PREFIXES = [
556
+ ["p", 1e-12],
557
+ ["n", 1e-9],
558
+ ["µ", 1e-6],
559
+ ["m", .001],
560
+ ["", 1],
561
+ ["k", 1e3],
562
+ ["M", 1e6],
563
+ ["G", 1e9]
564
+ ];
565
+ const PREFIX_SCALE = new Map(PREFIXES);
539
566
  /**
540
- * How many white keys sit at or before each pitch class, counting from C.
541
- *
542
- * A black key shares the number of the white key to its left plus one, which
543
- * puts it on the boundary between the two; {@link notePosition} then shifts it
544
- * back by half its width to centre it there.
567
+ * Micro is written three ways. `µ` (U+00B5 MICRO SIGN) is what `format`
568
+ * writes and what d3-format uses, `μ` (U+03BC GREEK SMALL LETTER MU) looks
569
+ * identical and is what a Greek keyboard produces, and `u` is what everyone
570
+ * actually types. All three read back the same.
545
571
  */
546
- const whiteKeysBefore = {
547
- C: 0,
548
- "C#": 1,
549
- D: 1,
550
- "D#": 2,
551
- E: 2,
552
- F: 3,
553
- "F#": 4,
554
- G: 4,
555
- "G#": 5,
556
- A: 5,
557
- "A#": 6,
558
- B: 6
572
+ const MICRO_ALIASES = {
573
+ μ: "µ",
574
+ u: "µ"
559
575
  };
560
- /** Width of a black key in pixels. */
561
- function blackKeyWidth(layout) {
562
- return layout.whiteKeyWidth * (layout.blackKeyWidthRatio ?? DEFAULT_BLACK_KEY_WIDTH_RATIO);
563
- }
564
- /** Width of the whole keyboard in pixels. */
565
- function pianoWidth(layout) {
566
- const whiteKeys = getNoteRangeArray(layout.noteRange).filter(isWhiteKey);
567
- return (layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)) * whiteKeys.length;
568
- }
569
576
  /**
570
- * Offset of the left edge of a key from the left edge of the keyboard, in
571
- * pixels.
577
+ * Divide by a prefix scale without showing the result of doing so in binary.
572
578
  *
573
- * Notes outside `noteRange` are placed too, so the value is negative below
574
- * `noteRange.first`.
579
+ * `0.0005 / 1e-6` is 500.00000000000006, and with no `digits` to round it that
580
+ * lands in the input as written.
575
581
  */
576
- function notePosition(note, layout) {
577
- const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
578
- const target = noteKey(note);
579
- const first = noteKey(layout.noteRange.first);
580
- const octave = Math.floor((note - layout.noteRange.first) / 12);
581
- const octaveOffset = noteKeys.indexOf(first) > noteKeys.indexOf(target) ? 1 : 0;
582
- const whiteKeysIn = whiteKeysBefore[target] - whiteKeysBefore[first] + (octave + octaveOffset) * 7;
583
- return isBlackKey(note) ? whiteKeysIn * slot - blackKeyWidth(layout) / 2 : whiteKeysIn * slot;
582
+ function scaleBy(value, scale) {
583
+ return toPrecision(value / scale);
584
584
  }
585
+ /** A number, then whatever followed it. */
586
+ const NUMBER_THEN_REST = /^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)\s*(.*)$/;
585
587
  /**
586
- * The note drawn at a point, or null where there is none.
588
+ * Build the `format` and `parse` of a unit, as one pair.
587
589
  *
588
- * Black keys are tested first, so they win where they overlap a white one. A
589
- * white key covers its gap as well as its width, so the whole width of the
590
- * keyboard belongs to some key and a click cannot fall between two.
591
- *
592
- * @param x offset from the left edge of the keyboard, in pixels
593
- * @param y offset from its top edge, in pixels
594
- * @param height height of the keyboard, in pixels
595
- */
596
- function noteAt(x, y, height, layout) {
597
- if (y < 0 || y >= height) return null;
598
- const notes = getNoteRangeArray(layout.noteRange);
599
- if (y < height * (layout.blackKeyHeightRatio ?? DEFAULT_BLACK_KEY_HEIGHT_RATIO)) for (const note of notes) {
600
- if (isWhiteKey(note)) continue;
601
- const left = notePosition(note, layout);
602
- if (left <= x && x < left + blackKeyWidth(layout)) return note;
603
- }
604
- const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
605
- for (const note of notes) {
606
- if (isBlackKey(note)) continue;
607
- const left = notePosition(note, layout);
608
- if (left <= x && x < left + slot) return note;
609
- }
610
- return null;
611
- }
612
- //#endregion
613
- //#region src/unit.ts
614
- /**
615
- * Pick the unit a value is displayed in: the largest one whose scale does not
616
- * exceed the magnitude of the value.
617
- */
618
- function selectUnit(units, value) {
619
- let i = 0;
620
- for (; i < units.length; i++) if (Math.abs(units[i][1]) > Math.abs(value)) break;
621
- return units[Math.max(0, i - 1)];
622
- }
623
- /**
624
- * Render a value as text, in the unit that suits its magnitude.
625
- *
626
- * @param units a single symbol appended as-is, or a list to choose from.
627
- * @param digit digits after the decimal point. Left as-is when omitted.
590
+ * They are returned together because they have to agree: a `format` that
591
+ * writes `1.23kHz` is only useful next to a `parse` that reads it back as
592
+ * 1230. Spread the result into the input.
628
593
  *
629
594
  * @example
630
- * formatValue(1234, [['Hz', 1], ['kHz', 1000]], 2) // '1.23kHz'
631
- * formatValue(1.5, 'Hz') // '1.5Hz'
632
- */
633
- function formatValue(value, units, digit) {
634
- const fixed = (v) => digit != void 0 ? v.toFixed(digit) : String(v);
635
- if (!units || typeof units == "string") return fixed(value) + (units ?? "");
636
- const [unit, scale] = selectUnit(units, value);
637
- return fixed(value / scale) + unit;
638
- }
639
- /**
640
- * Read a value back out of text, undoing the scaling of {@link formatValue}.
641
- *
642
- * With a list of units the text has to be a number followed by an optional
643
- * unit and nothing else, since the unit decides the scale; anything else reads
644
- * as 0. With a single unit, or none, the first number found anywhere in the
645
- * text is taken, so a half-typed entry still yields something.
595
+ * unitFormat('Hz') // 1234 -> '1.23kHz'
596
+ * unitFormat('s', { base: 'm' }) // value in ms. 1500 -> '1.5s'
597
+ * unitFormat('s', { base: 'm', digits: 2 }) // 1500 -> '1.50s'
598
+ * unitFormat('dB', { prefixes: false, digits: 1 }) // -6.25 -> '-6.3dB'
646
599
  *
647
600
  * @example
648
- * parseValue('1.23kHz', [['Hz', 1], ['kHz', 1000]]) // 1230
649
- * parseValue('4abc') // 4
601
+ * <NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={v} onChange={setV}>
650
602
  */
651
- function parseValue(text, units) {
652
- const str = text.trim();
653
- if (!units || typeof units == "string") {
654
- const m = str.match(/-?\d+(\.\d+)?/);
655
- const v = Number(m?.[0] ?? "0");
656
- return isNaN(v) ? 0 : v;
603
+ function unitFormat(unit, options = {}) {
604
+ const { base = "", prefixes = true, digits, separator = "" } = options;
605
+ if (unit === "" && base !== "") throw new RangeError("unitFormat: base requires a non-empty unit");
606
+ const baseScale = PREFIX_SCALE.get(base) ?? 1;
607
+ /**
608
+ * `toFixed` renders anything that rounds to zero from below as `-0`, which
609
+ * is never what a control should show.
610
+ */
611
+ const fixed = (value) => {
612
+ const text = digits !== void 0 ? value.toFixed(digits) : String(value);
613
+ return Number(text) === 0 ? text.replace("-", "") : text;
614
+ };
615
+ if (!prefixes) {
616
+ const symbol = base + unit;
617
+ return {
618
+ format: (value) => Number.isFinite(value) ? fixed(value) + separator + symbol : String(value),
619
+ parse: (text) => {
620
+ const match = text.trim().match(NUMBER_THEN_REST);
621
+ if (!match) return NaN;
622
+ const value = Number(match[1]);
623
+ return Number.isFinite(value) ? value : NaN;
624
+ }
625
+ };
657
626
  }
658
- const m = str.match(/^(-?\d+(\.\d+)?)\s*(\w*)$/);
659
- if (!m) return 0;
660
- const found = units.find(([unit]) => unit == m[3]);
661
- return (Number(m[1]) || 0) * (found ? found[1] : 1);
627
+ return {
628
+ format: (value) => {
629
+ if (!Number.isFinite(value)) return String(value);
630
+ const si = value * baseScale;
631
+ let index = PREFIXES.findIndex(([, scale]) => scale === 1);
632
+ if (si !== 0) {
633
+ const magnitude = Math.abs(si);
634
+ index = 0;
635
+ for (let i = PREFIXES.length - 1; i >= 0; i--) if (magnitude >= PREFIXES[i][1]) {
636
+ index = i;
637
+ break;
638
+ }
639
+ }
640
+ let text = fixed(scaleBy(si, PREFIXES[index][1]));
641
+ if (Math.abs(Number(text)) >= 1e3 && index < PREFIXES.length - 1) {
642
+ index += 1;
643
+ text = fixed(scaleBy(si, PREFIXES[index][1]));
644
+ }
645
+ return text + separator + PREFIXES[index][0] + unit;
646
+ },
647
+ parse: (text) => {
648
+ const match = text.trim().match(NUMBER_THEN_REST);
649
+ if (!match) return NaN;
650
+ const number = Number(match[1]);
651
+ if (!Number.isFinite(number)) return NaN;
652
+ let suffix = match[2].trim();
653
+ const separatorText = separator.trim();
654
+ if (separatorText !== "" && suffix.startsWith(separatorText)) suffix = suffix.slice(separatorText.length).trim();
655
+ if (suffix === "") return number;
656
+ let prefix = null;
657
+ if (unit !== "" && suffix.endsWith(unit)) prefix = suffix.slice(0, suffix.length - unit.length);
658
+ else if (suffix.length <= 1) prefix = suffix;
659
+ if (prefix === null) return number;
660
+ const normalized = MICRO_ALIASES[prefix] ?? prefix;
661
+ const scale = PREFIX_SCALE.get(normalized);
662
+ if (scale === void 0) return number;
663
+ return number * scale / baseScale;
664
+ }
665
+ };
662
666
  }
663
667
  //#endregion
664
- export { applyDelta, blackKeyWidth, clamp, curveScale, curveWithCenterValue, dbToGain, decimalPart, degree, exponentialScale, formatValue, gainToDb, getNoteRangeArray, inScale, integerPart, isBlackKey, isEmpty, isWhiteKey, linearScale, mapValue, mod, normalizeValue, noteAt, noteKey, noteKeys, noteName, noteNumber, notePosition, noteToFrequency, parseNoteName, parseValue, pianoWidth, radian, rawValue, scaleIntervals, scaleNotes, selectUnit, skewScale, skewWithCenterValue, stepValue, styleHelper, symmetricSkewScale, toFixed, whiteKeys, xor };
668
+ export { clamp, curveScale, curveWithCenterValue, dbToGain, degree, exponentialScale, gainToDb, inScale, isBlackKey, isWhiteKey, linearScale, mapValue, normalizeValue, noteKey, noteKeys, noteName, noteNumber, noteToFrequency, parseNoteName, radian, rawValue, scaleIntervals, scaleNotes, skewScale, skewWithCenterValue, stepValue, symmetricSkewScale, toFixed, toPrecision, unitFormat, whiteKeys };
665
669
 
666
670
  //# sourceMappingURL=index.js.map