@slnknrr/num-ez
v1.0.0
Published
The friendly face of @slnknrr/num-im: the same exact, any-base number engine behind familiar names, plain booleans and ready arrays. Exact arithmetic past 2^53, base conversion, canonical validation, modular arithmetic, number theory, bit metrics, digit s
Downloads
114
Maintainers
Readme
@slnknrr/num-ez
The friendly face of @slnknrr/num-im: the same exact, any-base number engine behind familiar names, plain booleans and ready arrays.
Floats leave you num-b? Take num-ez. Want every digit num-bered, exactly, past 2⁵³? Take num-im. Same engine, two accents:
num-ezsaysequalsand returnstrue;num-imsayseqand returns-1because it has more to tell you.
num-im is precise and terse: isnum, powmod, rotdig, a tri-state eq that separates value from written form, lazy digit generators, libc-style names throughout. That is the right contract for an engine. It is not the contract you want in application code, where a number utility should read like isInteger, return true, and hand you an array you can map over.
num-ez is that layer — and nothing more. Every function is one call into the engine plus a translation at the boundary. No arithmetic of its own, no state, no configuration.
import { add, equals, toHex, isInteger, sortAscending, factorial, format } from '@slnknrr/num-ez';
add('9007199254740992', '1'); // '9007199254740993' — 2⁵³ + 1, exact where + starts lying
add('0.1', '0.2'); // '0.3' — not 0.30000000000000004
equals('0', '0.0'); // true — equal in value (the engine says -1: same value, different form)
toHex('18446744073709551616'); // '10000000000000000' — 2⁶⁴, exact
isInteger('007'); // false — a leading zero is a costume, not a number
sortAscending(['10', '9', '100']); // ['9', '10', '100'] — plain sort gives ['10', '100', '9']
factorial('25'); // '15511210043330985984000000' — 26 digits, 10 past what a double holds exactly
format('1234567.891', { group: true, fracDigits: 2 }); // '1,234,567.89'What changes at the boundary
Six rules. Everything else is the engine's behavior, unchanged.
| | num-im | num-ez |
|---|---|---|
| Equality | eq is 1 same value & form · -1 same value only · 0 unequal | equals — boolean, 1 and -1 are both a yes; identical keeps the strict 1 |
| Order | cmp is -1 / 0 / 1 | compare — unchanged: that is the familiar shape, sort wants it; comparator() packs it, greaterThan & co. give the booleans |
| Lazy digit generators | ladd … yield one digit per next() | addDigits … return string[] |
| Result objects | { q, r }, { sign, int, frac } | { quotient, remainder }, { sign, integer, fraction } |
| Values | digit-strings in, digit-strings out | unchanged — a Number would round past 2⁵³; toNumber is the one exit, and it throws rather than lose digits |
| SyntaxError / RangeError / TypeError | thrown at the call | unchanged — the caller is wrong, and hiding it is the opposite of easy; check input with isNumber first |
undefined for an empty aggregate, null for a missing modular inverse and NaN for the sign of a non-number pass through too; the boolean companions (isZero, isPositive, isNegative, isSafeInteger) answer the yes/no form directly. Options travel in the same trailing object the engine takes: { base } (default 10), { precision } (default 20 fraction digits for a non-terminating division), and the per-method extras listed below. A numeric argument is always a decimal value; only strings are read in base.
What it is NOT
- Not a second engine. Every algorithm lives in
num-im. If you need the tri-state, the generators or the raw regex machinery, importnum-imdirectly; both packages can be used side by side. - Not a
Numberreplacement. For eager, in-range, base-10 math,NumberandBigIntare a compiled C++ path and they win. Reach for this where the answer must be exact, in a non-decimal base, or in a canonical, validatable form. - Not a decimal / money / units type.
formatdoes grouping, fixed fraction and sign style; a locale engine is your layer. - Not constant-time crypto.
powerModis correct, not hardened. Do not use it on secret exponents.
Install
npm install @slnknrr/num-ezimport ez from '@slnknrr/num-ez'; // one frozen object with every function
import { add, isInteger, toHex } from '@slnknrr/num-ez'; // or named imports — tree-shakeableRequirements: Node ≥ 20, ESM only. @slnknrr/num-im ≥ 1.0.1 is the single dependency and is installed with the package.
API
123 functions: 105 wrappers, one per engine method, and 18 compositions built from them (marked +). The num-im column names the engine method behind each wrapper.
Every value argument (a, b, n, k, m, width, …) is a Numeric: a digit-string read in opts.base, or a number / bigint understood as a decimal value. xs is an Enumerable: an Array, Set, Map (its values) or array-like. opts is optional everywhere and carries base unless stated otherwise.
1 · Validators — the regex is the grammar
| Function | num-im | Returns |
|---|---|---|
| numberPattern(base = 10, { sign, float }?) | re | anchored RegExp for a canonical number; sign / float: true require, false forbid, default optional |
| integerPattern(base = 10) | reint | integers only |
| floatPattern(base = 10) | refloat | a fractional part is required |
base is positional here, as in the engine: a radix 2–36, a custom alphabet string, or an array whose entries may be glyph-sets (['0', '1', ['a', 'A']]).
2 · Equality and order — cross-base, no float math
| Function | num-im | Returns |
|---|---|---|
| equals(a, b, { sign }?) | eq | boolean — equal in value; sign: false ignores the sign |
| + identical(a, b, opts?) | | boolean — equal in value and written the same way |
| notEquals(a, b) | ne | boolean — provably different |
| compare(a, b) | cmp | -1 / 0 / 1 |
| greaterOrEqual(a, b) / lessOrEqual(a, b) | ge / le | boolean |
| + greaterThan(a, b) / lessThan(a, b) | | boolean |
| + comparator(opts?) | | (a, b) => -1 / 0 / 1 for sort, with the base fixed once |
| + sortAscending(xs) / sortDescending(xs) | | a sorted copy by numeric value; the items come back as given |
| + isZero(n) / isPositive(n) / isNegative(n) | | boolean; all three are false for a non-number |
| + isBetween(n, min, max) | | min ≤ n ≤ max, inclusive; min > max throws |
3 · Bit-weight order — by how it packs, not how big it is
| Function | num-im | Returns |
|---|---|---|
| compareBitWeight(a, b) | cmpbw | -1 / 0 / 1 by a signed bit-count of the integer part |
| equalsBitWeight / notEqualsBitWeight / greaterOrEqualBitWeight / lessOrEqualBitWeight | eqbw / nebw / gebw / lebw | boolean |
4 · Digit streams — the lazy arithmetic, as arrays
| Function | num-im | Returns |
|---|---|---|
| addDigits / subtractDigits / multiplyDigits (a, b) | ladd / lsub / lmul | string[] — one glyph per element, least-significant first |
| divideDigits(a, b) | ldiv | string[] — most-significant first, fraction digits included |
5 · Arithmetic — exact past 2⁵³ and across bases
| Function | num-im | Returns |
|---|---|---|
| add / subtract / multiply / divide (a, b, { precision }?) | add / sub / mul / div | canonical string; a non-terminating divide stops at precision (default 20) |
| negate(n) / increment(n) / decrement(n) | neg / inc / dec | never '-0' |
| remainder(a, b) | rem | truncated remainder — the sign of the dividend, like % |
| modulo(a, b) | mod | Euclidean — always in [0, |b|) |
| divideWithRemainder(a, b) | divrem | { quotient, remainder } |
| power(a, k) | pow | non-negative integer exponent, exact |
| integerSqrt(n) / integerRoot(n, k) | isqrt / root | floor(√n) / floor(n^(1/k)) |
| gcd(a, b) / lcm(a, b) | gcd / lcm | |
6 · Modular arithmetic
| Function | num-im | Returns |
|---|---|---|
| multiplyMod(a, b, m) | mulmod | (a·b) mod m, non-negative |
| powerMod(a, k, m) | powmod | a^k mod m — the RSA/DH primitive (⚠ not constant-time) |
| modInverse(a, m) | modinv | the inverse, or null when gcd(a, m) ≠ 1 |
| isPrime(n) | isprime | boolean, deterministic Miller–Rabin |
7 · Rounding — to precision fraction digits (default 0)
| Function | num-im | Notes |
|---|---|---|
| round(n, { precision, mode }?) | round | mode: 'half-up' (default) · 'half-even' · 'half-odd' · 'floor' · 'ceil' · 'trunc' |
| floor(n) / ceil(n) / truncate(n) | floor / ceil / trunc | toward −∞ · +∞ · zero |
"Half" means base/2, so ties exist only in even bases.
8 · Shifts and bitwise
| Function | num-im | Returns |
|---|---|---|
| shiftLeft(n, k) / shiftRight(n, k) | shl / shr | multiply / divide by base^k — the count k is itself read in-base |
| bitAnd / bitOr / bitXor (a, b) | and / or / xor | on the magnitudes, base-2 semantics |
| bitNot(n, width) | not | ones-complement inside a width-digit field; width is mandatory, overflow throws |
9 · Bit metrics — from the magnitude, never via Number
| Function | num-im | Returns |
|---|---|---|
| bitWidth(n) | bw | bits needed for |n|; 0 for 0 |
| bitCeil(n) | bc | smallest power of two ≥ bitWidth(n) |
| fitsBits(n, width) | fits | boolean |
| leadingZeros(n, width?) / leadingOnes(n, width?) | clz / clo | in a width-bit view |
| trailingZeros(n) | ctz | trailing zero digits in the base |
| trailingTopDigits(n) | cto | trailing top-glyph digits: ones in binary, nines in decimal |
| popCount(n, { digit }?) | popcnt | set bits in base 2; occurrences of digit (default: the top glyph) elsewhere |
10 · Digit surgery — zeros are not silently trimmed
| Function | num-im | Returns |
|---|---|---|
| rotateDigits(n, k) | rotdig | circular rotation; negative k rotates right |
| reverseDigits(n, { keepLeadingZeros }?) | mirror | '1200' → '21', or '0021' |
| digitCount(n, base = 10) | digits | digits of the integer part — base is positional here, as in the engine |
| digitAt(n, k, { from }?) | digitat | value of the k-th digit; from: 'lsd' (default) or 'msd' |
| setDigit(n, k, d) | setdigit | replace the k-th (least-significant-indexed) digit, re-canonicalized |
11 · Format and parse — presentation is not value
| Function | num-im | Returns |
|---|---|---|
| format(n, { group, groupSize, fracDigits, signStyle }?) | fmt | grouped, fixed-fraction, styled output |
| scientific(n, { marker, sigdigits, eng }?) | sci | scientific / engineering notation; the exponent is a power of base |
| canonical(n) | trim | cosmetic noise stripped: '007.50' → '7.5' |
| parts(n) | split | { sign, integer, fraction } |
| parseLeading(str, { max, sign, float }?) | getnum | the leading number: { value, end }, or null |
| parse(str) | parsenum | the whole string; trailing junk throws SyntaxError |
| toBigInt(str) | parsebig | a canonical decimal integer → BigInt |
| stringify(n, { from, base }?) | stringify | render a value read in from into base |
| convertBase(n, { from, to }) | rebase | the headline: the same value in another notation |
| + toBinary / toOctal / toHex (n, { base, precision }?) | | n read in base (default 10), rendered in 2 / 8 / 16 |
| + fromBinary / fromOctal / fromHex (n, { base, precision }?) | | the other way round |
| + toNumber(n, opts?) | | a JS number; throws RangeError when |n| > 2⁵³ − 1 — a fraction becomes the nearest double |
12 · Predicates — canonical form, by regex
| Function | num-im | Asks |
|---|---|---|
| isNumber / isInteger / isFloat (str, { base, sign, float }?) | isnum / isint / isfloat | valid number / integer / float in base? |
| isHex / isDecimal / isOctal / isBinary (str) | ishex / isdec / isoct / isbin | valid in base 16 / 10 / 8 / 2? |
| isEven(str) / isOdd(str) | iseven / isodd | value parity; false for a fraction or a non-number |
| isPower(n, k = 2) | ispow | exact k-th power? |
| + isSafeInteger(str) | | a canonical integer within ±(2⁵³ − 1) — Number.isSafeInteger for digit-strings, in any base |
13 · Aggregates — exact, base-aware; offset trims the input first
| Function | num-im | Returns |
|---|---|---|
| min(xs) / max(xs) | min / max | undefined on empty input |
| sum(xs) / product(xs) | sum / prod | |
| cumulativeSum(xs) | cumsum | string[] — running totals |
| mean(xs, { unique }?) / median(xs) | avg / med | undefined on empty input |
| mode(xs, { all }?) | mode | the most frequent value; all: true returns every tied winner |
| spread(xs) | range | max − min |
| variance(xs, { sample }?) / standardDeviation(xs) | variance / stdev | population by default |
| quantile(xs, q) | quantile | interpolated, q in [0, 1] |
| clamp(n, max, { min }?) | clamp | the lower bound is optional, as in the engine |
| abs(n) / sign(n) / signedInfinity(n) | abs / sign / infinity | sign → -1 / 0 / 1, or NaN |
| quantize(n, size, { origin, rounding }?) | step | snap to a grid |
| differences(xs, { order }?) | diff | string[] — the discrete derivative |
| normalize(xs) | norm | string[] — min-max into [0, 1] |
| rescale(n, { inLo, inHi, outLo, outHi }) | scale | linear rescale |
14 · Sequences and combinatorics — exact where a double overflows
| Function | num-im | Returns |
|---|---|---|
| range(n, { start, step }?) | iota | string[] — [start, start + step, …) of length n |
| factorial(n) / fibonacci(n) / binomial(n, k) | fact / fib / choose | exact |
| random({ base, digits, max, secure }?) | random | a random canonical number; secure uses Web Crypto |
15 · Checksums
| Function | num-im | Returns |
|---|---|---|
| digitSum(n) | digitsum | sum of the digit values of the integer part |
| digitalRoot(n) | droot | 1 + (n − 1) mod (base − 1) |
TypeScript
Declarations ship with the package; nothing to install from @types. Option and result types are the engine's own, re-exported: Numeric, BaseSpec, Trit, Enumerable, RoundMode, every *Options interface and LeadingNumber, plus QuotientRemainder, Parts, RoundOptions, ConvertOptions and NumEz (the shape of the default export).
import ez, { compare, divideWithRemainder, toHex } from '@slnknrr/num-ez';
import type { Numeric, Trit } from '@slnknrr/num-ez';
const order: Trit = compare('10', '9'); // -1 | 0 | 1
divideWithRemainder('17', '5').quotient; // string
toHex('255', { to: 2 }); // error: `to` is convertBase's key, not toHex's
ez.add = () => ''; // error: the default export is frozentypes/num-ez.d.ts is written by hand and checked against types/api.test-d.ts with npm run types.
When to reach for num-im instead
- You want the tri-state
eq— "equal in value but written differently" as a distinct answer. - You want one digit at a time from a lazy generator, not an array.
- You want the engine's own names, or the exported base-10 oracle regexes (
intre,uintre,floatre,ufloatre).
Both packages can be used side by side; num-ez never shadows anything in the engine.
Scripts
| Command | Does |
|---|---|
| npm test | behavioral suite (node --test, zero dev dependencies) |
| npm run types | type-check the shipped declarations (tsc --noEmit) |
Links
- Source: https://codeberg.org/slnknrr/numez_lib.js-for-pm_npm/src/branch/main
- npm: https://www.npmjs.com/package/@slnknrr/num-ez
- The engine: @slnknrr/num-im
Author
Yury Slinkin (Юрий Слинкин)
- Email: [email protected]
- Codeberg: https://codeberg.org/slnknrr
License
MIT. See LICENSE.md.
