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