@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 +195 -204
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -149
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +101 -149
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +194 -190
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +6 -14
- package/src/math.ts +50 -13
- package/src/midi.ts +45 -16
- package/src/scales.ts +43 -50
- package/src/unit.ts +182 -51
- package/src/piano.ts +0 -163
- package/src/types.ts +0 -4
- package/src/util.ts +0 -42
package/src/unit.ts
CHANGED
|
@@ -1,73 +1,204 @@
|
|
|
1
|
+
import { toPrecision } from './math'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
* the smallest scale up. A value is shown in the largest unit that does not
|
|
4
|
-
* exceed it.
|
|
4
|
+
* The SI prefixes {@link unitFormat} chooses between.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Deliberately narrower than the full SI set: yocto through yotta are of no
|
|
7
|
+
* use to an audio control, and every extra prefix is one more symbol `parse`
|
|
8
|
+
* has to tell apart from a unit.
|
|
9
9
|
*/
|
|
10
|
-
export type
|
|
10
|
+
export type SIPrefix = 'p' | 'n' | 'µ' | 'm' | '' | 'k' | 'M' | 'G'
|
|
11
|
+
|
|
12
|
+
/** Ordered small to large. The empty symbol is the base unit. */
|
|
13
|
+
const PREFIXES: readonly [SIPrefix, number][] = [
|
|
14
|
+
['p', 1e-12],
|
|
15
|
+
['n', 1e-9],
|
|
16
|
+
['µ', 1e-6],
|
|
17
|
+
['m', 1e-3],
|
|
18
|
+
['', 1],
|
|
19
|
+
['k', 1e3],
|
|
20
|
+
['M', 1e6],
|
|
21
|
+
['G', 1e9],
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const PREFIX_SCALE = new Map<string, number>(PREFIXES)
|
|
11
25
|
|
|
12
26
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
27
|
+
* Micro is written three ways. `µ` (U+00B5 MICRO SIGN) is what `format`
|
|
28
|
+
* writes and what d3-format uses, `μ` (U+03BC GREEK SMALL LETTER MU) looks
|
|
29
|
+
* identical and is what a Greek keyboard produces, and `u` is what everyone
|
|
30
|
+
* actually types. All three read back the same.
|
|
15
31
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
const MICRO_ALIASES: Record<string, SIPrefix> = { μ: 'µ', u: 'µ' }
|
|
33
|
+
|
|
34
|
+
export interface UnitFormatOptions {
|
|
35
|
+
/**
|
|
36
|
+
* The prefix the stored value is already in.
|
|
37
|
+
*
|
|
38
|
+
* A control that keeps milliseconds in `value` is `{ base: 'm' }` with a
|
|
39
|
+
* unit of `'s'`: 1500 then displays as `1.5s`, and `parse` gives 1500 back.
|
|
40
|
+
*
|
|
41
|
+
* @default ''
|
|
42
|
+
*/
|
|
43
|
+
base?: SIPrefix
|
|
44
|
+
/**
|
|
45
|
+
* Whether to scale the number and pick a prefix at all.
|
|
46
|
+
*
|
|
47
|
+
* Turn it off for anything that is not an SI quantity. dB, %, cents and
|
|
48
|
+
* semitones do not take prefixes, and `-6dB` read as "-6 deci-B" is wrong
|
|
49
|
+
* rather than merely unusual.
|
|
50
|
+
*
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
53
|
+
prefixes?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* Digits after the decimal point. The number is left as-is when omitted.
|
|
56
|
+
*/
|
|
57
|
+
digits?: number
|
|
58
|
+
/**
|
|
59
|
+
* Text placed between the number and the unit.
|
|
60
|
+
* @default ''
|
|
61
|
+
*/
|
|
62
|
+
separator?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The `format` / `parse` pair a `NumberInput` takes. */
|
|
66
|
+
export interface UnitFormatter {
|
|
67
|
+
format: (value: number) => string
|
|
68
|
+
parse: (text: string) => number
|
|
22
69
|
}
|
|
23
70
|
|
|
24
71
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @param units a single symbol appended as-is, or a list to choose from.
|
|
28
|
-
* @param digit digits after the decimal point. Left as-is when omitted.
|
|
72
|
+
* Divide by a prefix scale without showing the result of doing so in binary.
|
|
29
73
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* formatValue(1.5, 'Hz') // '1.5Hz'
|
|
74
|
+
* `0.0005 / 1e-6` is 500.00000000000006, and with no `digits` to round it that
|
|
75
|
+
* lands in the input as written.
|
|
33
76
|
*/
|
|
34
|
-
|
|
35
|
-
value
|
|
36
|
-
units?: string | Units,
|
|
37
|
-
digit?: number,
|
|
38
|
-
): string {
|
|
39
|
-
const fixed = (v: number) =>
|
|
40
|
-
digit != undefined ? v.toFixed(digit) : String(v)
|
|
41
|
-
if (!units || typeof units == 'string') {
|
|
42
|
-
return fixed(value) + (units ?? '')
|
|
43
|
-
}
|
|
44
|
-
const [unit, scale] = selectUnit(units, value)
|
|
45
|
-
return fixed(value / scale) + unit
|
|
77
|
+
function scaleBy(value: number, scale: number): number {
|
|
78
|
+
return toPrecision(value / scale)
|
|
46
79
|
}
|
|
47
80
|
|
|
81
|
+
/** A number, then whatever followed it. */
|
|
82
|
+
const NUMBER_THEN_REST =
|
|
83
|
+
/^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)\s*(.*)$/
|
|
84
|
+
|
|
48
85
|
/**
|
|
49
|
-
*
|
|
86
|
+
* Build the `format` and `parse` of a unit, as one pair.
|
|
87
|
+
*
|
|
88
|
+
* They are returned together because they have to agree: a `format` that
|
|
89
|
+
* writes `1.23kHz` is only useful next to a `parse` that reads it back as
|
|
90
|
+
* 1230. Spread the result into the input.
|
|
50
91
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
92
|
+
* @example
|
|
93
|
+
* unitFormat('Hz') // 1234 -> '1.23kHz'
|
|
94
|
+
* unitFormat('s', { base: 'm' }) // value in ms. 1500 -> '1.5s'
|
|
95
|
+
* unitFormat('s', { base: 'm', digits: 2 }) // 1500 -> '1.50s'
|
|
96
|
+
* unitFormat('dB', { prefixes: false, digits: 1 }) // -6.25 -> '-6.3dB'
|
|
55
97
|
*
|
|
56
98
|
* @example
|
|
57
|
-
*
|
|
58
|
-
* parseValue('4abc') // 4
|
|
99
|
+
* <NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={v} onChange={setV}>
|
|
59
100
|
*/
|
|
60
|
-
export function
|
|
61
|
-
|
|
101
|
+
export function unitFormat(
|
|
102
|
+
unit: string,
|
|
103
|
+
options: UnitFormatOptions = {},
|
|
104
|
+
): UnitFormatter {
|
|
105
|
+
const { base = '', prefixes = true, digits, separator = '' } = options
|
|
106
|
+
if (unit === '' && base !== '') {
|
|
107
|
+
throw new RangeError('unitFormat: base requires a non-empty unit')
|
|
108
|
+
}
|
|
109
|
+
const baseScale = PREFIX_SCALE.get(base) ?? 1
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `toFixed` renders anything that rounds to zero from below as `-0`, which
|
|
113
|
+
* is never what a control should show.
|
|
114
|
+
*/
|
|
115
|
+
const fixed = (value: number) => {
|
|
116
|
+
const text = digits !== undefined ? value.toFixed(digits) : String(value)
|
|
117
|
+
return Number(text) === 0 ? text.replace('-', '') : text
|
|
118
|
+
}
|
|
62
119
|
|
|
63
|
-
if (!
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
120
|
+
if (!prefixes) {
|
|
121
|
+
// The stored value goes out untouched, so the symbol has to name the unit
|
|
122
|
+
// it is already in.
|
|
123
|
+
const symbol = base + unit
|
|
124
|
+
return {
|
|
125
|
+
format: (value) =>
|
|
126
|
+
Number.isFinite(value)
|
|
127
|
+
? fixed(value) + separator + symbol
|
|
128
|
+
: String(value),
|
|
129
|
+
// Nothing after the number can change the scale, so it is all ignored:
|
|
130
|
+
// the number in front is the value, half-typed or not.
|
|
131
|
+
parse: (text) => {
|
|
132
|
+
const match = text.trim().match(NUMBER_THEN_REST)
|
|
133
|
+
if (!match) return NaN
|
|
134
|
+
const value = Number(match[1])
|
|
135
|
+
return Number.isFinite(value) ? value : NaN
|
|
136
|
+
},
|
|
137
|
+
}
|
|
67
138
|
}
|
|
68
139
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
140
|
+
return {
|
|
141
|
+
format: (value) => {
|
|
142
|
+
if (!Number.isFinite(value)) return String(value)
|
|
143
|
+
const si = value * baseScale
|
|
144
|
+
// Zero has no magnitude to read, so it stays in the base unit.
|
|
145
|
+
let index = PREFIXES.findIndex(([, scale]) => scale === 1)
|
|
146
|
+
if (si !== 0) {
|
|
147
|
+
// The largest prefix that leaves at least one digit before the point.
|
|
148
|
+
// Below the smallest prefix the number just gets small: `p` is the
|
|
149
|
+
// floor, as `G` is the ceiling.
|
|
150
|
+
const magnitude = Math.abs(si)
|
|
151
|
+
index = 0
|
|
152
|
+
for (let i = PREFIXES.length - 1; i >= 0; i--) {
|
|
153
|
+
if (magnitude >= PREFIXES[i][1]) {
|
|
154
|
+
index = i
|
|
155
|
+
break
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
let text = fixed(scaleBy(si, PREFIXES[index][1]))
|
|
160
|
+
// Rounding can carry the number up out of its own prefix — 999.99Hz at
|
|
161
|
+
// one digit is 1000.0Hz, which should read 1.0kHz. One step is always
|
|
162
|
+
// enough, since the carry is at most a factor of ten.
|
|
163
|
+
if (Math.abs(Number(text)) >= 1000 && index < PREFIXES.length - 1) {
|
|
164
|
+
index += 1
|
|
165
|
+
text = fixed(scaleBy(si, PREFIXES[index][1]))
|
|
166
|
+
}
|
|
167
|
+
return text + separator + PREFIXES[index][0] + unit
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
parse: (text) => {
|
|
171
|
+
const match = text.trim().match(NUMBER_THEN_REST)
|
|
172
|
+
if (!match) return NaN
|
|
173
|
+
const number = Number(match[1])
|
|
174
|
+
if (!Number.isFinite(number)) return NaN
|
|
175
|
+
|
|
176
|
+
let suffix = match[2].trim()
|
|
177
|
+
const separatorText = separator.trim()
|
|
178
|
+
if (separatorText !== '' && suffix.startsWith(separatorText)) {
|
|
179
|
+
suffix = suffix.slice(separatorText.length).trim()
|
|
180
|
+
}
|
|
181
|
+
// A bare number is in the unit the value is stored in, which is what
|
|
182
|
+
// the input shows once the format is stripped.
|
|
183
|
+
if (suffix === '') return number
|
|
184
|
+
|
|
185
|
+
// The unit symbol is matched first, so a unit that is itself a prefix
|
|
186
|
+
// letter wins over the prefix reading: `5m` for a unit of `m` is five
|
|
187
|
+
// metres, not five milli-.
|
|
188
|
+
let prefix: string | null = null
|
|
189
|
+
if (unit !== '' && suffix.endsWith(unit)) {
|
|
190
|
+
prefix = suffix.slice(0, suffix.length - unit.length)
|
|
191
|
+
} else if (suffix.length <= 1) {
|
|
192
|
+
prefix = suffix
|
|
193
|
+
}
|
|
194
|
+
if (prefix === null) return number
|
|
195
|
+
|
|
196
|
+
const normalized = MICRO_ALIASES[prefix] ?? prefix
|
|
197
|
+
const scale = PREFIX_SCALE.get(normalized)
|
|
198
|
+
// Unrecognised text after the number is ignored rather than rejected,
|
|
199
|
+
// so that a half-typed entry still yields the number in front of it.
|
|
200
|
+
if (scale === undefined) return number
|
|
201
|
+
return (number * scale) / baseScale
|
|
202
|
+
},
|
|
203
|
+
}
|
|
73
204
|
}
|
package/src/piano.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { isBlackKey, isWhiteKey, noteKey, noteKeys, type NoteKey } from './midi'
|
|
2
|
-
|
|
3
|
-
export type NoteRange = {
|
|
4
|
-
first: number
|
|
5
|
-
last: number
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* `[noteRange.first, noteRange.first + 1, ..., noteRange.last]`
|
|
10
|
-
*/
|
|
11
|
-
export function getNoteRangeArray(noteRange: NoteRange): number[] {
|
|
12
|
-
return Array.from(
|
|
13
|
-
{ length: noteRange.last - noteRange.first + 1 },
|
|
14
|
-
(_, i) => i + noteRange.first,
|
|
15
|
-
)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The geometry of a drawn keyboard.
|
|
20
|
-
*
|
|
21
|
-
* One description is shared by the drawing and the hit testing, so a key cannot
|
|
22
|
-
* be drawn somewhere other than where it responds.
|
|
23
|
-
*/
|
|
24
|
-
export interface PianoLayout {
|
|
25
|
-
noteRange: NoteRange
|
|
26
|
-
|
|
27
|
-
/** Width of a white key, excluding {@link PianoLayout.keyGap}. */
|
|
28
|
-
whiteKeyWidth: number
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Space between two white keys. Part of the slot a white key occupies, so it
|
|
32
|
-
* still belongs to one of the keys for the purpose of hit testing.
|
|
33
|
-
*
|
|
34
|
-
* @default 1
|
|
35
|
-
*/
|
|
36
|
-
keyGap?: number
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Width of a black key, as a fraction of {@link PianoLayout.whiteKeyWidth}.
|
|
40
|
-
*
|
|
41
|
-
* @default 0.65
|
|
42
|
-
*/
|
|
43
|
-
blackKeyWidthRatio?: number
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Height of a black key, as a fraction of the height of the keyboard.
|
|
47
|
-
*
|
|
48
|
-
* @default 0.6
|
|
49
|
-
*/
|
|
50
|
-
blackKeyHeightRatio?: number
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const DEFAULT_KEY_GAP = 1
|
|
54
|
-
const DEFAULT_BLACK_KEY_WIDTH_RATIO = 0.65
|
|
55
|
-
const DEFAULT_BLACK_KEY_HEIGHT_RATIO = 0.6
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* How many white keys sit at or before each pitch class, counting from C.
|
|
59
|
-
*
|
|
60
|
-
* A black key shares the number of the white key to its left plus one, which
|
|
61
|
-
* puts it on the boundary between the two; {@link notePosition} then shifts it
|
|
62
|
-
* back by half its width to centre it there.
|
|
63
|
-
*/
|
|
64
|
-
const whiteKeysBefore: Record<NoteKey, number> = {
|
|
65
|
-
C: 0,
|
|
66
|
-
'C#': 1,
|
|
67
|
-
D: 1,
|
|
68
|
-
'D#': 2,
|
|
69
|
-
E: 2,
|
|
70
|
-
F: 3,
|
|
71
|
-
'F#': 4,
|
|
72
|
-
G: 4,
|
|
73
|
-
'G#': 5,
|
|
74
|
-
A: 5,
|
|
75
|
-
'A#': 6,
|
|
76
|
-
B: 6,
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Width of a black key in pixels. */
|
|
80
|
-
export function blackKeyWidth(layout: PianoLayout): number {
|
|
81
|
-
return (
|
|
82
|
-
layout.whiteKeyWidth *
|
|
83
|
-
(layout.blackKeyWidthRatio ?? DEFAULT_BLACK_KEY_WIDTH_RATIO)
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Width of the whole keyboard in pixels. */
|
|
88
|
-
export function pianoWidth(layout: PianoLayout): number {
|
|
89
|
-
const whiteKeys = getNoteRangeArray(layout.noteRange).filter(isWhiteKey)
|
|
90
|
-
return (
|
|
91
|
-
(layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)) *
|
|
92
|
-
whiteKeys.length
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Offset of the left edge of a key from the left edge of the keyboard, in
|
|
98
|
-
* pixels.
|
|
99
|
-
*
|
|
100
|
-
* Notes outside `noteRange` are placed too, so the value is negative below
|
|
101
|
-
* `noteRange.first`.
|
|
102
|
-
*/
|
|
103
|
-
export function notePosition(note: number, layout: PianoLayout): number {
|
|
104
|
-
const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)
|
|
105
|
-
const target = noteKey(note)
|
|
106
|
-
const first = noteKey(layout.noteRange.first)
|
|
107
|
-
|
|
108
|
-
const octave = Math.floor((note - layout.noteRange.first) / 12)
|
|
109
|
-
// A note whose pitch class comes before the first one belongs to the octave
|
|
110
|
-
// above the one the division above gives.
|
|
111
|
-
const octaveOffset =
|
|
112
|
-
noteKeys.indexOf(first) > noteKeys.indexOf(target) ? 1 : 0
|
|
113
|
-
|
|
114
|
-
const whiteKeysIn =
|
|
115
|
-
whiteKeysBefore[target] -
|
|
116
|
-
whiteKeysBefore[first] +
|
|
117
|
-
(octave + octaveOffset) * 7
|
|
118
|
-
|
|
119
|
-
return isBlackKey(note)
|
|
120
|
-
? whiteKeysIn * slot - blackKeyWidth(layout) / 2
|
|
121
|
-
: whiteKeysIn * slot
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* The note drawn at a point, or null where there is none.
|
|
126
|
-
*
|
|
127
|
-
* Black keys are tested first, so they win where they overlap a white one. A
|
|
128
|
-
* white key covers its gap as well as its width, so the whole width of the
|
|
129
|
-
* keyboard belongs to some key and a click cannot fall between two.
|
|
130
|
-
*
|
|
131
|
-
* @param x offset from the left edge of the keyboard, in pixels
|
|
132
|
-
* @param y offset from its top edge, in pixels
|
|
133
|
-
* @param height height of the keyboard, in pixels
|
|
134
|
-
*/
|
|
135
|
-
export function noteAt(
|
|
136
|
-
x: number,
|
|
137
|
-
y: number,
|
|
138
|
-
height: number,
|
|
139
|
-
layout: PianoLayout,
|
|
140
|
-
): number | null {
|
|
141
|
-
if (y < 0 || y >= height) return null
|
|
142
|
-
|
|
143
|
-
const notes = getNoteRangeArray(layout.noteRange)
|
|
144
|
-
const blackHeight =
|
|
145
|
-
height * (layout.blackKeyHeightRatio ?? DEFAULT_BLACK_KEY_HEIGHT_RATIO)
|
|
146
|
-
|
|
147
|
-
if (y < blackHeight) {
|
|
148
|
-
for (const note of notes) {
|
|
149
|
-
if (isWhiteKey(note)) continue
|
|
150
|
-
const left = notePosition(note, layout)
|
|
151
|
-
if (left <= x && x < left + blackKeyWidth(layout)) return note
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)
|
|
156
|
-
for (const note of notes) {
|
|
157
|
-
if (isBlackKey(note)) continue
|
|
158
|
-
const left = notePosition(note, layout)
|
|
159
|
-
if (left <= x && x < left + slot) return note
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return null
|
|
163
|
-
}
|
package/src/types.ts
DELETED
package/src/util.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
type Operator = '+' | '-' | '*' | '/'
|
|
2
|
-
|
|
3
|
-
export function styleHelper(value: string | number): string
|
|
4
|
-
export function styleHelper(
|
|
5
|
-
value: string | number,
|
|
6
|
-
op: Operator,
|
|
7
|
-
influencer?: number,
|
|
8
|
-
): string
|
|
9
|
-
export function styleHelper(
|
|
10
|
-
value: string | number,
|
|
11
|
-
op?: Operator,
|
|
12
|
-
influencer?: number,
|
|
13
|
-
) {
|
|
14
|
-
if (op && influencer) {
|
|
15
|
-
if (typeof value == 'number') {
|
|
16
|
-
if (op == '+') return `${value + influencer}px`
|
|
17
|
-
if (op == '-') return `${value - influencer}px`
|
|
18
|
-
if (op == '*') return `${value * influencer}px`
|
|
19
|
-
if (op == '/') return `${value / influencer}px`
|
|
20
|
-
} else {
|
|
21
|
-
return `calc(${value}px ${op} ${influencer})`
|
|
22
|
-
}
|
|
23
|
-
} else {
|
|
24
|
-
if (typeof value == 'number') {
|
|
25
|
-
return `${value}px`
|
|
26
|
-
} else {
|
|
27
|
-
return value
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function isEmpty(obj: object) {
|
|
33
|
-
return Object.keys(obj).length == 0
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function mod(n: number, m: number) {
|
|
37
|
-
return ((n % m) + m) % m
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function xor(a = false, b = false) {
|
|
41
|
-
return (a || b) && a != b
|
|
42
|
-
}
|