1import { pipe } from "effect/Function"
2import * as N from "effect/Number"
3import assert from "node:assert/strict"
4import { describe, it } from "vitest"
5import { assertNone, assertSome } from "./utils/assert.ts"
6
7const assertNaN = (value: number): void => {
8 assert.ok(Number.isNaN(value))
9}
10
11const assertNegativeZero = (value: number): void => {
12 assert.ok(Object.is(value, -0))
13}
14
15const iterableThatFailsAfterZero = (): Iterable<number> => ({
16 [Symbol.iterator]: function*() {
17 yield 2
18 yield 0
19 throw new Error("iteration continued past zero")
20 }
21})
22
23describe("Number", () => {
24 it("re-exports the global Number constructor", () => {
25 assert.strictEqual(N.Number, globalThis.Number)
26 })
27
28 it("coerces values with Number semantics", () => {
29 assert.strictEqual(N.Number("42"), 42)
30 assert.strictEqual(N.Number("3.14"), 3.14)
31 assert.strictEqual(N.Number(true), 1)
32 assert.strictEqual(N.Number(null), 0)
33 })
34})
35
36describe("isNumber", () => {
37 it("returns true for numbers, including NaN and infinities", () => {
38 assert.strictEqual(N.isNumber(0), true)
39 assert.strictEqual(N.isNumber(-1.5), true)
40 assert.strictEqual(N.isNumber(NaN), true)
41 assert.strictEqual(N.isNumber(Infinity), true)
42 })
43
44 it("returns false for non-number values", () => {
45 assert.strictEqual(N.isNumber("1"), false)
46 assert.strictEqual(N.isNumber(new globalThis.Number(1)), false)
47 assert.strictEqual(N.isNumber(null), false)
48 assert.strictEqual(N.isNumber(undefined), false)
49 })
50})
51
52describe("sum", () => {
53 it("adds numbers in data-first and data-last forms", () => {
54 assert.strictEqual(N.sum(2, 3), 5)
55 assert.strictEqual(pipe(2, N.sum(3)), 5)
56 })
57
58 it("preserves zero and supports negative operands", () => {
59 assert.strictEqual(N.sum(0, 5), 5)
60 assert.strictEqual(N.sum(-2, 5), 3)
61 })
62})
63
64describe("multiply", () => {
65 it("multiplies numbers in data-first and data-last forms", () => {
66 assert.strictEqual(N.multiply(2, 3), 6)
67 assert.strictEqual(pipe(2, N.multiply(3)), 6)
68 })
69
70 it("handles zero and negative operands", () => {
71 assert.strictEqual(N.multiply(0, 5), 0)
72 assert.strictEqual(N.multiply(-2, 5), -10)
73 })
74})
75
76describe("subtract", () => {
77 it("subtracts numbers in data-first and data-last forms", () => {
78 assert.strictEqual(N.subtract(5, 3), 2)
79 assert.strictEqual(pipe(5, N.subtract(3)), 2)
80 })
81
82 it("supports negative results", () => {
83 assert.strictEqual(N.subtract(2, 5), -3)
84 assert.strictEqual(N.subtract(-2, -5), 3)
85 })
86})
87
88describe("divide", () => {
89 it("divides numbers in data-first and data-last forms", () => {
90 assertSome(N.divide(6, 3), 2)
91 assertSome(pipe(6, N.divide(3)), 2)
92 assertSome(N.divide(0, 3), 0)
93 })
94
95 it("returns none for zero and negative-zero divisors", () => {
96 assertNone(N.divide(6, 0))
97 assertNone(N.divide(6, -0))
98 })
99})
100
101describe("increment", () => {
102 it("adds one to the input", () => {
103 assert.strictEqual(N.increment(2), 3)
104 assert.strictEqual(N.increment(-1), 0)
105 })
106})
107
108describe("decrement", () => {
109 it("subtracts one from the input", () => {
110 assert.strictEqual(N.decrement(3), 2)
111 assert.strictEqual(N.decrement(0), -1)
112 })
113})
114
115describe("Order", () => {
116 it("compares less than, equal, and greater than values", () => {
117 assert.strictEqual(N.Order(1, 2), -1)
118 assert.strictEqual(N.Order(2, 2), 0)
119 assert.strictEqual(N.Order(3, 2), 1)
120 })
121
122 it("treats NaN as less than any non-NaN and equal to NaN", () => {
123 assert.strictEqual(N.Order(NaN, 1), -1)
124 assert.strictEqual(N.Order(1, NaN), 1)
125 assert.strictEqual(N.Order(NaN, NaN), 0)
126 })
127
128 it("treats 0 and -0 as equal", () => {
129 assert.strictEqual(N.Order(0, -0), 0)
130 assert.strictEqual(N.Order(-0, 0), 0)
131 })
132})
133
134describe("Equivalence", () => {
135 it("returns true for equal numbers and false for different numbers", () => {
136 assert.strictEqual(N.Equivalence(1, 1), true)
137 assert.strictEqual(N.Equivalence(1, 2), false)
138 })
139
140 it("treats NaN as equivalent to NaN and 0 as equivalent to -0", () => {
141 assert.strictEqual(N.Equivalence(NaN, NaN), true)
142 assert.strictEqual(N.Equivalence(0, -0), true)
143 })
144})
145
146describe("isLessThan", () => {
147 it("returns true only when the first operand is strictly smaller", () => {
148 assert.strictEqual(N.isLessThan(2, 3), true)
149 assert.strictEqual(N.isLessThan(3, 3), false)
150 assert.strictEqual(N.isLessThan(4, 3), false)
151 })
152
153 it("supports the data-last form", () => {
154 assert.strictEqual(pipe(2, N.isLessThan(3)), true)
155 })
156})
157
158describe("isLessThanOrEqualTo", () => {
159 it("returns true for smaller or equal values", () => {
160 assert.strictEqual(N.isLessThanOrEqualTo(2, 3), true)
161 assert.strictEqual(N.isLessThanOrEqualTo(3, 3), true)
162 assert.strictEqual(N.isLessThanOrEqualTo(4, 3), false)
163 })
164
165 it("supports the data-last form", () => {
166 assert.strictEqual(pipe(2, N.isLessThanOrEqualTo(3)), true)
167 })
168})
169
170describe("isGreaterThan", () => {
171 it("returns true only when the first operand is strictly greater", () => {
172 assert.strictEqual(N.isGreaterThan(2, 3), false)
173 assert.strictEqual(N.isGreaterThan(3, 3), false)
174 assert.strictEqual(N.isGreaterThan(4, 3), true)
175 })
176
177 it("supports the data-last form", () => {
178 assert.strictEqual(pipe(4, N.isGreaterThan(3)), true)
179 })
180})
181
182describe("isGreaterThanOrEqualTo", () => {
183 it("returns true for greater or equal values", () => {
184 assert.strictEqual(N.isGreaterThanOrEqualTo(2, 3), false)
185 assert.strictEqual(N.isGreaterThanOrEqualTo(3, 3), true)
186 assert.strictEqual(N.isGreaterThanOrEqualTo(4, 3), true)
187 })
188
189 it("supports the data-last form", () => {
190 assert.strictEqual(pipe(4, N.isGreaterThanOrEqualTo(3)), true)
191 })
192})
193
194describe("between", () => {
195 it("includes both bounds and excludes values outside the range", () => {
196 const options = { minimum: 0, maximum: 5 }
197
198 assert.strictEqual(N.between(0, options), true)
199 assert.strictEqual(N.between(3, options), true)
200 assert.strictEqual(N.between(5, options), true)
201 assert.strictEqual(N.between(-1, options), false)
202 assert.strictEqual(N.between(6, options), false)
203 })
204
205 it("supports the data-last form", () => {
206 assert.strictEqual(pipe(3, N.between({ minimum: 0, maximum: 5 })), true)
207 })
208})
209
210describe("clamp", () => {
211 it("returns the input when it is within bounds and clamps out-of-range values", () => {
212 const options = { minimum: 1, maximum: 5 }
213
214 assert.strictEqual(N.clamp(3, options), 3)
215 assert.strictEqual(N.clamp(0, options), 1)
216 assert.strictEqual(N.clamp(6, options), 5)
217 })
218
219 it("supports the data-last form", () => {
220 assert.strictEqual(pipe(3, N.clamp({ minimum: 1, maximum: 5 })), 3)
221 })
222
223 it("clamps NaN to the minimum according to Number.Order", () => {
224 assert.strictEqual(N.clamp(Number.NaN, { minimum: 1, maximum: 5 }), 1)
225 })
226})
227
228describe("min", () => {
229 it("returns the smaller value in data-first and data-last forms", () => {
230 assert.strictEqual(N.min(2, 3), 2)
231 assert.strictEqual(pipe(2, N.min(3)), 2)
232 assert.strictEqual(N.min(5, 0.1), 0.1)
233 })
234
235 it("returns the first argument when both values compare as equal", () => {
236 assertNegativeZero(N.min(-0, 0))
237 assert.strictEqual(Object.is(N.min(0, -0), 0), true)
238 })
239})
240
241describe("max", () => {
242 it("returns the larger value in data-first and data-last forms", () => {
243 assert.strictEqual(N.max(2, 3), 3)
244 assert.strictEqual(pipe(2, N.max(3)), 3)
245 assert.strictEqual(N.max(0.005, 3), 3)
246 })
247
248 it("returns the first argument when both values compare as equal", () => {
249 assert.strictEqual(Object.is(N.max(-0, 0), -0), true)
250 assert.strictEqual(Object.is(N.max(0, -0), 0), true)
251 })
252})
253
254describe("sign", () => {
255 it("returns -1, 0, or 1 for negative, zero, and positive numbers", () => {
256 assert.strictEqual(N.sign(-5), -1)
257 assert.strictEqual(N.sign(0), 0)
258 assert.strictEqual(N.sign(-0), 0)
259 assert.strictEqual(N.sign(5), 1)
260 })
261
262 it("treats NaN as less than zero because it delegates to Number.Order", () => {
263 assert.strictEqual(N.sign(NaN), -1)
264 })
265})
266
267describe("sumAll", () => {
268 it("sums arrays and arbitrary iterables", () => {
269 assert.strictEqual(N.sumAll([2, 3, 4]), 9)
270 assert.strictEqual(N.sumAll(new Set([1, 2, 3])), 6)
271 })
272
273 it("returns zero for an empty iterable", () => {
274 assert.strictEqual(N.sumAll([]), 0)
275 })
276})
277
278describe("multiplyAll", () => {
279 it("multiplies arrays and returns one for an empty iterable", () => {
280 assert.strictEqual(N.multiplyAll([2, 3, 4]), 24)
281 assert.strictEqual(N.multiplyAll([]), 1)
282 })
283
284 it("stops iterating once it encounters zero", () => {
285 assert.strictEqual(N.multiplyAll(iterableThatFailsAfterZero()), 0)
286 })
287})
288
289describe("remainder", () => {
290 it("computes integer and decimal remainders in data-first and data-last forms", () => {
291 assert.strictEqual(N.remainder(3, 2), 1)
292 assert.strictEqual(N.remainder(0.3, 0.2), 0.1)
293 assert.strictEqual(pipe(0.3, N.remainder(0.2)), 0.1)
294 })
295
296 it("preserves the dividend sign, including negative zero", () => {
297 assert.strictEqual(N.remainder(-5, 2), -1)
298 assertNegativeZero(N.remainder(-4, 2))
299 assertNegativeZero(N.remainder(-0, 2))
300 })
301
302 it("preserves the dividend sign with negative divisors", () => {
303 assert.strictEqual(N.remainder(5, -2), 1)
304 assert.strictEqual(N.remainder(-5, -2), -1)
305 assertNegativeZero(N.remainder(-4, -2))
306 })
307
308 it("returns NaN when the divisor is zero", () => {
309 assertNaN(N.remainder(5, 0))
310 assertNaN(N.remainder(Number("1e-101"), 0))
311 })
312
313 it("returns NaN for non-finite operands", () => {
314 assertNaN(N.remainder(NaN, 1))
315 assertNaN(N.remainder(Infinity, 1))
316 assertNaN(N.remainder(-Infinity, 1))
317 assertNaN(N.remainder(1, Infinity))
318 assertNaN(N.remainder(1, -Infinity))
319 })
320
321 it("handles small floats / scientific notation", () => {
322 const divisor = 1e-7
323
324 // Valid multiples (integer * 1e-7)
325 assert.strictEqual(N.remainder(0, divisor), 0)
326 assert.strictEqual(N.remainder(1e-7, divisor), 0)
327 assert.strictEqual(N.remainder(2e-7, divisor), 0)
328 assert.strictEqual(N.remainder(3e-7, divisor), 0)
329
330 // Invalid — 2.5 and 1.5 are not integers
331 assert.strictEqual(N.remainder(2.5e-7, divisor), 5e-8)
332 assert.strictEqual(N.remainder(1.5e-7, divisor), 5e-8)
333 })
334
335 it("preserves signs in scientific notation", () => {
336 const divisor = 1e-7
337
338 assert.strictEqual(N.remainder(-2.5e-7, divisor), -5e-8)
339 assert.strictEqual(N.remainder(2.5e-7, -divisor), 5e-8)
340 assert.strictEqual(N.remainder(-2.5e-7, -divisor), -5e-8)
341 assertNegativeZero(N.remainder(-0, divisor))
342 })
343
344 it("handles scientific notation beyond the toFixed precision limit", () => {
345 const divisor = Number("1e-101")
346
347 assert.strictEqual(N.remainder(0, divisor), 0)
348 assert.strictEqual(N.remainder(divisor, divisor), 0)
349 assert.strictEqual(N.remainder(Number("3e-101"), divisor), 0)
350 assert.strictEqual(N.remainder(Number("2.5e-101"), divisor), Number("5e-102"))
351 assertNegativeZero(N.remainder(Number("-3e-101"), divisor))
352 })
353
354 it("handles subnormal values", () => {
355 const min = Number.MIN_VALUE
356
357 assert.strictEqual(N.remainder(min, min), 0)
358 assert.strictEqual(N.remainder(min * 2, min), 0)
359 assert.strictEqual(N.remainder(min, min * 2), min)
360 })
361
362 it("preserves nonzero subnormal remainders", () => {
363 const divisor = Number("1e-323")
364
365 assert.strictEqual(N.remainder(Number("1.042e-321"), divisor), Number.MIN_VALUE)
366 assert.strictEqual(N.remainder(Number("-1.042e-321"), divisor), -Number.MIN_VALUE)
367 })
368
369 it("handles large values formatted in scientific notation", () => {
370 const large = Number("1e21")
371
372 assert.strictEqual(N.remainder(large, 2), 0)
373 assert.strictEqual(N.remainder(large, 3), 1)
374 assert.strictEqual(N.remainder(3, large), 3)
375 assertNegativeZero(N.remainder(-large, 2))
376 })
377
378 it("handles decimal coefficients beyond the safe integer range", () => {
379 assert.strictEqual(N.remainder(100000000000000.05, 0.03), 0)
380 assert.strictEqual(N.remainder(100000000000000.1, 0.03), 0.02)
381 })
382
383 it("preserves exact integer remainders beyond the safe integer range", () => {
384 const large = 2 ** 60
385
386 assert.strictEqual(N.remainder(large, 5), 1)
387 assert.strictEqual(N.remainder(-large, 5), -1)
388 assert.strictEqual(N.remainder(large, -5), 1)
389 assert.strictEqual(N.remainder(large + 256, large), 256)
390 assertNegativeZero(N.remainder(-large, 2))
391 })
392
393 it("preserves exact integer dividends with fractional divisors", () => {
394 const large = 2 ** 60
395
396 assert.strictEqual(N.remainder(large, 2.5), 1)
397 assert.strictEqual(N.remainder(large, 1.25), 1)
398 assert.strictEqual(N.remainder(large, 3.5), 1)
399 assert.strictEqual(N.remainder(-large, 2.5), -1)
400 assert.strictEqual(N.remainder(large, -2.5), 1)
401 assertNegativeZero(N.remainder(-large, 0.5))
402 })
403
404 it("preserves exact integers formatted in scientific notation", () => {
405 const large = 2 ** 70
406
407 assert.strictEqual(N.remainder(large, 3), 1)
408 assert.strictEqual(N.remainder(large, 2.5), 1.5)
409 })
410})
411
412describe("nextPow2", () => {
413 it("rounds non-powers of two up to the next power of two", () => {
414 assert.strictEqual(N.nextPow2(5), 8)
415 assert.strictEqual(N.nextPow2(17), 32)
416 })
417
418 it("returns the same value for powers of two and a minimum of two otherwise", () => {
419 assert.strictEqual(N.nextPow2(8), 8)
420 assert.strictEqual(N.nextPow2(1), 2)
421 assert.strictEqual(N.nextPow2(0), 2)
422 assert.strictEqual(N.nextPow2(1.5), 2)
423 })
424
425 it("returns NaN for negative inputs", () => {
426 assertNaN(N.nextPow2(-1))
427 })
428})
429
430describe("parse", () => {
431 it("parses valid numeric strings using Number semantics", () => {
432 assertSome(N.parse("42"), 42)
433 assertSome(N.parse("3.14"), 3.14)
434 assertSome(N.parse(" 42 "), 42)
435 })
436
437 it("supports NaN and infinities as explicit string literals", () => {
438 assertSome(N.parse("NaN"), NaN)
439 assertSome(N.parse("Infinity"), Infinity)
440 assertSome(N.parse("-Infinity"), -Infinity)
441 })
442
443 it("returns none for empty or invalid strings", () => {
444 assertNone(N.parse(""))
445 assertNone(N.parse(" "))
446 assertNone(N.parse("not a number"))
447 })
448})
449
450describe("round", () => {
451 it("rounds with positive precision in data-first and data-last forms", () => {
452 assert.strictEqual(N.round(1.1234, 2), 1.12)
453 assert.strictEqual(N.round(1.567, 2), 1.57)
454 assert.strictEqual(pipe(1.567, N.round(2)), 1.57)
455 })
456
457 it("supports zero and negative precision", () => {
458 assert.strictEqual(N.round(1.5, 0), 2)
459 assert.strictEqual(N.round(145, -1), 150)
460 })
461
462 it("follows Math.round semantics for negative halves", () => {
463 assert.strictEqual(N.round(-1.25, 1), -1.2)
464 })
465})
466
467describe("ReducerSum", () => {
468 it("combines values and uses zero as the identity", () => {
469 assert.strictEqual(N.ReducerSum.combine(1, 2), 3)
470 assert.strictEqual(N.ReducerSum.combine(N.ReducerSum.initialValue, 2), 2)
471 assert.strictEqual(N.ReducerSum.combine(2, N.ReducerSum.initialValue), 2)
472 })
473
474 it("reduces iterables with combineAll", () => {
475 assert.strictEqual(N.ReducerSum.combineAll([1, 2, 3]), 6)
476 assert.strictEqual(N.ReducerSum.combineAll([]), 0)
477 })
478})
479
480describe("ReducerMultiply", () => {
481 it("combines values and uses one as the identity", () => {
482 assert.strictEqual(N.ReducerMultiply.combine(2, 3), 6)
483 assert.strictEqual(N.ReducerMultiply.combine(N.ReducerMultiply.initialValue, 2), 2)
484 assert.strictEqual(N.ReducerMultiply.combine(2, N.ReducerMultiply.initialValue), 2)
485 })
486
487 it("reduces iterables and short-circuits on zero", () => {
488 assert.strictEqual(N.ReducerMultiply.combineAll([2, 3, 4]), 24)
489 assert.strictEqual(N.ReducerMultiply.combineAll([]), 1)
490 assert.strictEqual(N.ReducerMultiply.combineAll(iterableThatFailsAfterZero()), 0)
491 })
492})
493
494describe("ReducerMax", () => {
495 it("combines values and uses negative infinity as the identity", () => {
496 assert.strictEqual(N.ReducerMax.combine(1, 2), 2)
497 assert.strictEqual(N.ReducerMax.combine(N.ReducerMax.initialValue, 2), 2)
498 assert.strictEqual(N.ReducerMax.combine(2, N.ReducerMax.initialValue), 2)
499 })
500
501 it("reduces iterables with combineAll", () => {
502 assert.strictEqual(N.ReducerMax.combineAll([1, 5, 3]), 5)
503 assert.strictEqual(N.ReducerMax.combineAll([]), -Infinity)
504 })
505})
506
507describe("ReducerMin", () => {
508 it("combines values and uses infinity as the identity", () => {
509 assert.strictEqual(N.ReducerMin.combine(1, 2), 1)
510 assert.strictEqual(N.ReducerMin.combine(N.ReducerMin.initialValue, 2), 2)
511 assert.strictEqual(N.ReducerMin.combine(2, N.ReducerMin.initialValue), 2)
512 })
513
514 it("reduces iterables with combineAll", () => {
515 assert.strictEqual(N.ReducerMin.combineAll([1, 5, 3]), 1)
516 assert.strictEqual(N.ReducerMin.combineAll([]), Infinity)
517 })
518})