measurable 1.1.0 → 2.0.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/CHANGELOG.md +100 -0
- package/README.md +179 -35
- package/dist/dimensions/angle.js +2 -2
- package/dist/dimensions/area.d.ts +14 -0
- package/dist/dimensions/area.js +25 -0
- package/dist/dimensions/data.d.ts +10 -0
- package/dist/dimensions/data.js +36 -0
- package/dist/dimensions/energy.d.ts +14 -0
- package/dist/dimensions/energy.js +22 -0
- package/dist/dimensions/force.js +2 -2
- package/dist/dimensions/frequency.d.ts +7 -0
- package/dist/dimensions/frequency.js +11 -0
- package/dist/dimensions/illuminance.d.ts +9 -0
- package/dist/dimensions/illuminance.js +13 -0
- package/dist/dimensions/index.d.ts +9 -0
- package/dist/dimensions/index.js +9 -0
- package/dist/dimensions/length.js +2 -2
- package/dist/dimensions/luminance.d.ts +7 -0
- package/dist/dimensions/luminance.js +16 -0
- package/dist/dimensions/luminousIntensity.d.ts +9 -0
- package/dist/dimensions/luminousIntensity.js +16 -0
- package/dist/dimensions/mass.js +2 -2
- package/dist/dimensions/power.d.ts +9 -0
- package/dist/dimensions/power.js +13 -0
- package/dist/dimensions/pressure.d.ts +14 -0
- package/dist/dimensions/pressure.js +18 -0
- package/dist/dimensions/temperature.js +7 -1
- package/dist/dimensions/time.js +2 -2
- package/dist/dimensions/volume.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lib/Dimension.d.ts +37 -11
- package/dist/lib/Dimension.js +49 -14
- package/dist/lib/MeasurementSystem.js +2 -2
- package/dist/lib/Quantity.d.ts +24 -10
- package/dist/lib/Quantity.js +39 -37
- package/dist/lib/Rational.d.ts +58 -0
- package/dist/lib/Rational.js +174 -0
- package/dist/lib/Unit.d.ts +41 -7
- package/dist/lib/Unit.js +35 -6
- package/dist/lib/prefixes.d.ts +2 -2
- package/dist/lib/prefixes.js +1 -1
- package/dist/systems/imperial.js +19 -1
- package/dist/systems/metric.js +25 -1
- package/dist/systems/usCustomary.js +19 -1
- package/dist/utils/definePrefixed.d.ts +35 -0
- package/dist/utils/definePrefixed.js +61 -0
- package/dist/utils/scaleOf.d.ts +3 -0
- package/dist/utils/scaleOf.js +6 -0
- package/package.json +8 -3
package/dist/lib/Quantity.js
CHANGED
|
@@ -3,20 +3,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Quantity = void 0;
|
|
4
4
|
const AmbiguousUnitError_1 = require("../errors/AmbiguousUnitError");
|
|
5
5
|
const UnknownUnitError_1 = require("../errors/UnknownUnitError");
|
|
6
|
-
const
|
|
6
|
+
const scaleOf_1 = require("../utils/scaleOf");
|
|
7
|
+
const Rational_1 = require("./Rational");
|
|
7
8
|
/** A magnitude paired with a unit (e.g. `5` `kilometer`). */
|
|
8
9
|
class Quantity {
|
|
9
10
|
constructor(magnitude, unit) {
|
|
10
|
-
this.magnitude = magnitude;
|
|
11
11
|
this.unit = unit;
|
|
12
|
+
this.rational = Rational_1.Rational.from(magnitude);
|
|
13
|
+
}
|
|
14
|
+
/** The magnitude as a `number`, derived from {@link rational}. */
|
|
15
|
+
get magnitude() {
|
|
16
|
+
return this.rational.toNumber();
|
|
12
17
|
}
|
|
13
18
|
/** Return an equivalent quantity expressed in `target`. */
|
|
14
19
|
to(target) {
|
|
15
|
-
return new Quantity(this.
|
|
20
|
+
return new Quantity(this.inRational(target), target);
|
|
16
21
|
}
|
|
17
22
|
/** Return this quantity's raw magnitude expressed in `target`. */
|
|
18
23
|
in(target) {
|
|
19
|
-
return this.
|
|
24
|
+
return this.inRational(target).toNumber();
|
|
25
|
+
}
|
|
26
|
+
/** This quantity's exact magnitude expressed in `target`. */
|
|
27
|
+
inRational(target) {
|
|
28
|
+
return this.unit.dimension.convertRational(this.rational, this.unit, target);
|
|
20
29
|
}
|
|
21
30
|
/** Render as `"<magnitude> <unit name>"`, e.g. `"5 kilometer"`. */
|
|
22
31
|
toString() {
|
|
@@ -33,19 +42,19 @@ class Quantity {
|
|
|
33
42
|
* difference.
|
|
34
43
|
*/
|
|
35
44
|
plus(other) {
|
|
36
|
-
return new Quantity(this.
|
|
45
|
+
return new Quantity(this.rational.plus(other.inRational(this.unit)), this.unit);
|
|
37
46
|
}
|
|
38
47
|
/** Subtract another quantity, returned in this quantity's unit. */
|
|
39
48
|
minus(other) {
|
|
40
|
-
return new Quantity(this.
|
|
49
|
+
return new Quantity(this.rational.minus(other.inRational(this.unit)), this.unit);
|
|
41
50
|
}
|
|
42
51
|
/** Scale this quantity by a dimensionless factor. */
|
|
43
52
|
times(factor) {
|
|
44
|
-
return new Quantity(this.
|
|
53
|
+
return new Quantity(this.rational.times(Rational_1.Rational.from(factor)), this.unit);
|
|
45
54
|
}
|
|
46
55
|
/** Divide this quantity by a dimensionless divisor. */
|
|
47
56
|
dividedBy(divisor) {
|
|
48
|
-
return new Quantity(this.
|
|
57
|
+
return new Quantity(this.rational.dividedBy(Rational_1.Rational.from(divisor)), this.unit);
|
|
49
58
|
}
|
|
50
59
|
/**
|
|
51
60
|
* Divide this quantity by `other` of the same dimension, yielding the
|
|
@@ -54,15 +63,15 @@ class Quantity {
|
|
|
54
63
|
* Throws {@link InvalidConversionError} across dimensions.
|
|
55
64
|
*/
|
|
56
65
|
ratioTo(other) {
|
|
57
|
-
return this.
|
|
66
|
+
return this.rational.dividedBy(other.inRational(this.unit)).toNumber();
|
|
58
67
|
}
|
|
59
68
|
/** Return this quantity with its magnitude negated. */
|
|
60
69
|
negate() {
|
|
61
|
-
return new Quantity(
|
|
70
|
+
return new Quantity(this.rational.negate(), this.unit);
|
|
62
71
|
}
|
|
63
72
|
/** Return this quantity with a non-negative magnitude. */
|
|
64
73
|
abs() {
|
|
65
|
-
return new Quantity(
|
|
74
|
+
return new Quantity(this.rational.abs(), this.unit);
|
|
66
75
|
}
|
|
67
76
|
/** Clamp this quantity to the range [`lower`, `upper`], returned in this unit. */
|
|
68
77
|
clamp(lower, upper) {
|
|
@@ -98,11 +107,12 @@ class Quantity {
|
|
|
98
107
|
/**
|
|
99
108
|
* Whether this quantity equals `other`, compared in this quantity's unit.
|
|
100
109
|
* Throws {@link InvalidConversionError} if the operands belong to different
|
|
101
|
-
* dimensions. Comparison is exact, so
|
|
102
|
-
*
|
|
110
|
+
* dimensions. Comparison is exact rational equality, so quantities that are
|
|
111
|
+
* mathematically equal compare equal even if reaching them involved a
|
|
112
|
+
* conversion that would have drifted in floating point.
|
|
103
113
|
*/
|
|
104
114
|
equals(other) {
|
|
105
|
-
return this.
|
|
115
|
+
return this.rational.equals(other.inRational(this.unit));
|
|
106
116
|
}
|
|
107
117
|
/** Whether this quantity does not equal `other`. */
|
|
108
118
|
notEquals(other) {
|
|
@@ -110,19 +120,19 @@ class Quantity {
|
|
|
110
120
|
}
|
|
111
121
|
/** Whether this quantity is less than `other`. */
|
|
112
122
|
lessThan(other) {
|
|
113
|
-
return this.
|
|
123
|
+
return this.rational.compare(other.inRational(this.unit)) < 0;
|
|
114
124
|
}
|
|
115
125
|
/** Whether this quantity is greater than `other`. */
|
|
116
126
|
greaterThan(other) {
|
|
117
|
-
return this.
|
|
127
|
+
return this.rational.compare(other.inRational(this.unit)) > 0;
|
|
118
128
|
}
|
|
119
129
|
/** Whether this quantity is less than or equal to `other`. */
|
|
120
130
|
lessThanOrEqual(other) {
|
|
121
|
-
return this.
|
|
131
|
+
return this.rational.compare(other.inRational(this.unit)) <= 0;
|
|
122
132
|
}
|
|
123
133
|
/** Whether this quantity is greater than or equal to `other`. */
|
|
124
134
|
greaterThanOrEqual(other) {
|
|
125
|
-
return this.
|
|
135
|
+
return this.rational.compare(other.inRational(this.unit)) >= 0;
|
|
126
136
|
}
|
|
127
137
|
/** Alias for {@link equals}. */
|
|
128
138
|
eq(other) {
|
|
@@ -153,26 +163,19 @@ class Quantity {
|
|
|
153
163
|
* if larger, `0` if equal. Suitable as an `Array#sort` comparator.
|
|
154
164
|
*/
|
|
155
165
|
compareTo(other) {
|
|
156
|
-
|
|
157
|
-
if (this.magnitude < value) {
|
|
158
|
-
return -1;
|
|
159
|
-
}
|
|
160
|
-
if (this.magnitude > value) {
|
|
161
|
-
return 1;
|
|
162
|
-
}
|
|
163
|
-
return 0;
|
|
166
|
+
return this.rational.compare(other.inRational(this.unit));
|
|
164
167
|
}
|
|
165
168
|
/** Whether this quantity's magnitude is exactly zero. */
|
|
166
169
|
isZero() {
|
|
167
|
-
return this.
|
|
170
|
+
return this.rational.sign() === 0;
|
|
168
171
|
}
|
|
169
172
|
/** Whether this quantity's magnitude is greater than zero. */
|
|
170
173
|
isPositive() {
|
|
171
|
-
return this.
|
|
174
|
+
return this.rational.sign() > 0;
|
|
172
175
|
}
|
|
173
176
|
/** Whether this quantity's magnitude is less than zero. */
|
|
174
177
|
isNegative() {
|
|
175
|
-
return this.
|
|
178
|
+
return this.rational.sign() < 0;
|
|
176
179
|
}
|
|
177
180
|
/**
|
|
178
181
|
* Parse a string into a `Quantity` using a dimension's known units and aliases.
|
|
@@ -181,7 +184,7 @@ class Quantity {
|
|
|
181
184
|
* - `"5 hr"` -> `Quantity(5, hour)`
|
|
182
185
|
* - `"5hr 20min"` -> `Quantity(320, minute)`
|
|
183
186
|
*
|
|
184
|
-
* Compound inputs are summed
|
|
187
|
+
* Compound inputs are summed (exactly) and returned in the *finest*
|
|
185
188
|
* (smallest-scale) unit present, so `"5hr 20min"` collapses to `320 minute`.
|
|
186
189
|
*
|
|
187
190
|
* When a token is a shared alias (e.g. `"ton"` → short ton & long ton), pass
|
|
@@ -189,22 +192,21 @@ class Quantity {
|
|
|
189
192
|
*/
|
|
190
193
|
static parse(str, dimension, options = {}) {
|
|
191
194
|
const pattern = /(-?\d+(?:\.\d+)?)\s*([^\d\s]+)/g;
|
|
192
|
-
let total
|
|
195
|
+
let total;
|
|
193
196
|
let finest;
|
|
194
|
-
let count = 0;
|
|
195
197
|
for (let match = pattern.exec(str); match !== null; match = pattern.exec(str)) {
|
|
196
198
|
const value = Number.parseFloat(match[1]);
|
|
197
199
|
const unit = resolve(match[2], dimension, options.prefer);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
const quantity = new Quantity(value, unit);
|
|
201
|
+
total = total ? total.plus(quantity) : quantity;
|
|
202
|
+
if (!finest || (0, scaleOf_1.scaleOf)(unit) < (0, scaleOf_1.scaleOf)(finest)) {
|
|
200
203
|
finest = unit;
|
|
201
204
|
}
|
|
202
|
-
count += 1;
|
|
203
205
|
}
|
|
204
|
-
if (
|
|
206
|
+
if (!total || !finest) {
|
|
205
207
|
throw new Error(`Could not parse a quantity from "${str}"`);
|
|
206
208
|
}
|
|
207
|
-
return
|
|
209
|
+
return total.to(finest);
|
|
208
210
|
}
|
|
209
211
|
/** The smallest of the given quantities (by value); requires at least one. */
|
|
210
212
|
static min(first, ...rest) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An exact rational number (`n / d`) used to make conversions between linear
|
|
3
|
+
* and affine units lossless. Such conversions are inherently rational — a foot
|
|
4
|
+
* is exactly `3048/10000` m and an inch exactly `254/10000` m, so foot → inch is
|
|
5
|
+
* exactly `12` — but baking those ratios into binary `number` scales rounds the
|
|
6
|
+
* result (`12.000000000000002`). Doing the arithmetic over integer numerator /
|
|
7
|
+
* denominator pairs and collapsing to a float only at the end avoids the drift.
|
|
8
|
+
*
|
|
9
|
+
* Instances are immutable and always stored in lowest terms with a positive
|
|
10
|
+
* denominator. `bigint` is used so cross-multiplying large scales (e.g. a
|
|
11
|
+
* mile's `1609344`) cannot overflow.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Rational {
|
|
14
|
+
readonly n: bigint;
|
|
15
|
+
readonly d: bigint;
|
|
16
|
+
/**
|
|
17
|
+
* Build a rational from integer numerator and denominator. Pass exact ratios
|
|
18
|
+
* the literal way — `new Rational(5, 9)` — using `bigint` or integer `number`.
|
|
19
|
+
* For a decimal value, use {@link Rational.fromNumber} instead.
|
|
20
|
+
*/
|
|
21
|
+
constructor(numerator: bigint | number, denominator?: bigint | number);
|
|
22
|
+
/** Coerce a `number | Rational` to a `Rational`, parsing numbers as decimals. */
|
|
23
|
+
static from(value: number | Rational): Rational;
|
|
24
|
+
/**
|
|
25
|
+
* Convert a `number` to the exact rational the author *wrote*, by reading its
|
|
26
|
+
* shortest round-tripping decimal (`(0.0254).toString() === "0.0254"` →
|
|
27
|
+
* `254/10000`). This recovers the intended terminating decimal. A value that
|
|
28
|
+
* was never terminating in source (e.g. Fahrenheit's `5 / 9`) is captured as
|
|
29
|
+
* the exact rational of its nearest double — no worse than a raw float, and
|
|
30
|
+
* the conversion is still done in one rounding instead of several. To avoid
|
|
31
|
+
* that, pass an exact {@link Rational} (e.g. `new Rational(5, 9)`) instead.
|
|
32
|
+
*/
|
|
33
|
+
private static fromNumber;
|
|
34
|
+
plus(other: Rational): Rational;
|
|
35
|
+
minus(other: Rational): Rational;
|
|
36
|
+
times(other: Rational): Rational;
|
|
37
|
+
dividedBy(other: Rational): Rational;
|
|
38
|
+
negate(): Rational;
|
|
39
|
+
abs(): Rational;
|
|
40
|
+
/** Whether this equals `other` exactly (both are stored in canonical form). */
|
|
41
|
+
equals(other: Rational): boolean;
|
|
42
|
+
/** Alias for {@link plus}. */
|
|
43
|
+
add(other: Rational): Rational;
|
|
44
|
+
/** Alias for {@link minus}. */
|
|
45
|
+
sub(other: Rational): Rational;
|
|
46
|
+
/** Alias for {@link times}. */
|
|
47
|
+
mul(other: Rational): Rational;
|
|
48
|
+
/** Alias for {@link dividedBy}. */
|
|
49
|
+
div(other: Rational): Rational;
|
|
50
|
+
/** Alias for {@link equals}. */
|
|
51
|
+
eq(other: Rational): boolean;
|
|
52
|
+
/** `-1` if this is smaller than `other`, `1` if larger, `0` if equal. */
|
|
53
|
+
compare(other: Rational): number;
|
|
54
|
+
/** Sign of the value: `-1`, `0`, or `1`. */
|
|
55
|
+
sign(): number;
|
|
56
|
+
/** Collapse to the nearest `number`. */
|
|
57
|
+
toNumber(): number;
|
|
58
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Rational = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* An exact rational number (`n / d`) used to make conversions between linear
|
|
6
|
+
* and affine units lossless. Such conversions are inherently rational — a foot
|
|
7
|
+
* is exactly `3048/10000` m and an inch exactly `254/10000` m, so foot → inch is
|
|
8
|
+
* exactly `12` — but baking those ratios into binary `number` scales rounds the
|
|
9
|
+
* result (`12.000000000000002`). Doing the arithmetic over integer numerator /
|
|
10
|
+
* denominator pairs and collapsing to a float only at the end avoids the drift.
|
|
11
|
+
*
|
|
12
|
+
* Instances are immutable and always stored in lowest terms with a positive
|
|
13
|
+
* denominator. `bigint` is used so cross-multiplying large scales (e.g. a
|
|
14
|
+
* mile's `1609344`) cannot overflow.
|
|
15
|
+
*/
|
|
16
|
+
class Rational {
|
|
17
|
+
/**
|
|
18
|
+
* Build a rational from integer numerator and denominator. Pass exact ratios
|
|
19
|
+
* the literal way — `new Rational(5, 9)` — using `bigint` or integer `number`.
|
|
20
|
+
* For a decimal value, use {@link Rational.fromNumber} instead.
|
|
21
|
+
*/
|
|
22
|
+
constructor(numerator, denominator = 1n) {
|
|
23
|
+
let n = toBigInt(numerator);
|
|
24
|
+
let d = toBigInt(denominator);
|
|
25
|
+
if (d === 0n) {
|
|
26
|
+
throw new Error("Rational denominator cannot be zero");
|
|
27
|
+
}
|
|
28
|
+
if (d < 0n) {
|
|
29
|
+
n = -n;
|
|
30
|
+
d = -d;
|
|
31
|
+
}
|
|
32
|
+
const g = gcd(n, d) || 1n;
|
|
33
|
+
this.n = n / g;
|
|
34
|
+
this.d = d / g;
|
|
35
|
+
}
|
|
36
|
+
/** Coerce a `number | Rational` to a `Rational`, parsing numbers as decimals. */
|
|
37
|
+
static from(value) {
|
|
38
|
+
return value instanceof Rational ? new this(value.n, value.d) : Rational.fromNumber(value);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Convert a `number` to the exact rational the author *wrote*, by reading its
|
|
42
|
+
* shortest round-tripping decimal (`(0.0254).toString() === "0.0254"` →
|
|
43
|
+
* `254/10000`). This recovers the intended terminating decimal. A value that
|
|
44
|
+
* was never terminating in source (e.g. Fahrenheit's `5 / 9`) is captured as
|
|
45
|
+
* the exact rational of its nearest double — no worse than a raw float, and
|
|
46
|
+
* the conversion is still done in one rounding instead of several. To avoid
|
|
47
|
+
* that, pass an exact {@link Rational} (e.g. `new Rational(5, 9)`) instead.
|
|
48
|
+
*/
|
|
49
|
+
static fromNumber(value) {
|
|
50
|
+
if (!Number.isFinite(value)) {
|
|
51
|
+
throw new Error(`Cannot derive a rational from ${value}`);
|
|
52
|
+
}
|
|
53
|
+
const match = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(value.toString());
|
|
54
|
+
if (!match) {
|
|
55
|
+
throw new Error(`Cannot derive a rational from ${value}`);
|
|
56
|
+
}
|
|
57
|
+
const [, sign, intPart, fracPart = "", expPart] = match;
|
|
58
|
+
let n = BigInt(intPart + fracPart);
|
|
59
|
+
let d = 10n ** BigInt(fracPart.length);
|
|
60
|
+
const exp = expPart ? Number(expPart) : 0;
|
|
61
|
+
if (exp > 0) {
|
|
62
|
+
n *= 10n ** BigInt(exp);
|
|
63
|
+
}
|
|
64
|
+
else if (exp < 0) {
|
|
65
|
+
d *= 10n ** BigInt(-exp);
|
|
66
|
+
}
|
|
67
|
+
return new Rational(sign === "-" ? -n : n, d);
|
|
68
|
+
}
|
|
69
|
+
plus(other) {
|
|
70
|
+
return new Rational(this.n * other.d + other.n * this.d, this.d * other.d);
|
|
71
|
+
}
|
|
72
|
+
minus(other) {
|
|
73
|
+
return new Rational(this.n * other.d - other.n * this.d, this.d * other.d);
|
|
74
|
+
}
|
|
75
|
+
times(other) {
|
|
76
|
+
return new Rational(this.n * other.n, this.d * other.d);
|
|
77
|
+
}
|
|
78
|
+
dividedBy(other) {
|
|
79
|
+
return new Rational(this.n * other.d, this.d * other.n);
|
|
80
|
+
}
|
|
81
|
+
negate() {
|
|
82
|
+
return new Rational(-this.n, this.d);
|
|
83
|
+
}
|
|
84
|
+
abs() {
|
|
85
|
+
return new Rational(this.n < 0n ? -this.n : this.n, this.d);
|
|
86
|
+
}
|
|
87
|
+
/** Whether this equals `other` exactly (both are stored in canonical form). */
|
|
88
|
+
equals(other) {
|
|
89
|
+
return this.n === other.n && this.d === other.d;
|
|
90
|
+
}
|
|
91
|
+
/** Alias for {@link plus}. */
|
|
92
|
+
add(other) {
|
|
93
|
+
return this.plus(other);
|
|
94
|
+
}
|
|
95
|
+
/** Alias for {@link minus}. */
|
|
96
|
+
sub(other) {
|
|
97
|
+
return this.minus(other);
|
|
98
|
+
}
|
|
99
|
+
/** Alias for {@link times}. */
|
|
100
|
+
mul(other) {
|
|
101
|
+
return this.times(other);
|
|
102
|
+
}
|
|
103
|
+
/** Alias for {@link dividedBy}. */
|
|
104
|
+
div(other) {
|
|
105
|
+
return this.dividedBy(other);
|
|
106
|
+
}
|
|
107
|
+
/** Alias for {@link equals}. */
|
|
108
|
+
eq(other) {
|
|
109
|
+
return this.equals(other);
|
|
110
|
+
}
|
|
111
|
+
/** `-1` if this is smaller than `other`, `1` if larger, `0` if equal. */
|
|
112
|
+
compare(other) {
|
|
113
|
+
// Denominators are always positive, so cross-multiplication preserves order.
|
|
114
|
+
const lhs = this.n * other.d;
|
|
115
|
+
const rhs = other.n * this.d;
|
|
116
|
+
return lhs < rhs ? -1 : lhs > rhs ? 1 : 0;
|
|
117
|
+
}
|
|
118
|
+
/** Sign of the value: `-1`, `0`, or `1`. */
|
|
119
|
+
sign() {
|
|
120
|
+
return this.n < 0n ? -1 : this.n > 0n ? 1 : 0;
|
|
121
|
+
}
|
|
122
|
+
/** Collapse to the nearest `number`. */
|
|
123
|
+
toNumber() {
|
|
124
|
+
const { n, d } = this;
|
|
125
|
+
if (n === 0n) {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
// Fast path: both operands are exactly representable as doubles, so a single
|
|
129
|
+
// IEEE division is correctly rounded.
|
|
130
|
+
if (n >= -MAX_EXACT_INT && n <= MAX_EXACT_INT && d <= MAX_EXACT_INT) {
|
|
131
|
+
return Number(n) / Number(d);
|
|
132
|
+
}
|
|
133
|
+
// Otherwise converting each operand to a double first would round it before
|
|
134
|
+
// dividing and lose precision (e.g. denominators like 10^24). Long-divide to
|
|
135
|
+
// SIG significant digits and let Number()'s correctly-rounded decimal parser
|
|
136
|
+
// produce the nearest double, independent of magnitude.
|
|
137
|
+
const SIG = 20;
|
|
138
|
+
const negative = n < 0n;
|
|
139
|
+
const num = negative ? -n : n;
|
|
140
|
+
// floor(log10(num / d)), accurate to within 1 — enough to capture SIG digits.
|
|
141
|
+
const exponent = num.toString().length - d.toString().length;
|
|
142
|
+
const shift = SIG - 1 - exponent;
|
|
143
|
+
const scaledNum = shift >= 0 ? num * 10n ** BigInt(shift) : num;
|
|
144
|
+
const scaledDen = shift >= 0 ? d : d * 10n ** BigInt(-shift);
|
|
145
|
+
let digits = scaledNum / scaledDen;
|
|
146
|
+
if (2n * (scaledNum % scaledDen) >= scaledDen) {
|
|
147
|
+
digits += 1n; // round half up on the last significant digit
|
|
148
|
+
}
|
|
149
|
+
return Number(`${negative ? "-" : ""}${digits}e${exponent - (SIG - 1)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.Rational = Rational;
|
|
153
|
+
/**
|
|
154
|
+
* One past `Number.MAX_SAFE_INTEGER` (i.e. 2^53): every integer with magnitude
|
|
155
|
+
* at most this is exactly representable as a double, so `Number(n)` is lossless.
|
|
156
|
+
*/
|
|
157
|
+
const MAX_EXACT_INT = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
|
|
158
|
+
function gcd(a, b) {
|
|
159
|
+
let x = a < 0n ? -a : a;
|
|
160
|
+
let y = b < 0n ? -b : b;
|
|
161
|
+
while (y) {
|
|
162
|
+
[x, y] = [y, x % y];
|
|
163
|
+
}
|
|
164
|
+
return x;
|
|
165
|
+
}
|
|
166
|
+
function toBigInt(value) {
|
|
167
|
+
if (typeof value === "bigint") {
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
if (!Number.isInteger(value)) {
|
|
171
|
+
throw new Error(`Rational numerator and denominator must be integers; got ${value}`);
|
|
172
|
+
}
|
|
173
|
+
return BigInt(value);
|
|
174
|
+
}
|
package/dist/lib/Unit.d.ts
CHANGED
|
@@ -1,19 +1,44 @@
|
|
|
1
1
|
import type { Dimension } from "./Dimension";
|
|
2
|
-
|
|
2
|
+
import type { Rational } from "./Rational";
|
|
3
|
+
/**
|
|
4
|
+
* An affine transform to the base unit, `base = value * scale + offset`, with
|
|
5
|
+
* both terms held as exact rationals. Covers the base unit (`1·x + 0`), plain
|
|
6
|
+
* linear units (`scale·x + 0`), and offset units like Celsius (`1·x + 273.15`).
|
|
7
|
+
*/
|
|
8
|
+
export interface LinearTransform {
|
|
9
|
+
scale: Rational;
|
|
10
|
+
offset: Rational;
|
|
11
|
+
}
|
|
12
|
+
interface BaseUnitOptions {
|
|
3
13
|
name: string;
|
|
4
14
|
dimension: Dimension;
|
|
15
|
+
}
|
|
16
|
+
interface LinearConversionOptions {
|
|
17
|
+
/** Exact transform for linear / affine units (the common case). */
|
|
18
|
+
linear: LinearTransform;
|
|
19
|
+
}
|
|
20
|
+
interface CustomConversionOptions {
|
|
21
|
+
/** Hand-written transform for non-linear units; mutually exclusive with `linear`. */
|
|
5
22
|
toBase: (value: number) => number;
|
|
6
23
|
fromBase: (value: number) => number;
|
|
7
24
|
}
|
|
25
|
+
export type UnitConversionOptions = LinearConversionOptions | CustomConversionOptions;
|
|
26
|
+
type UnitOptions = BaseUnitOptions & UnitConversionOptions;
|
|
8
27
|
/**
|
|
9
28
|
* A single unit of measurement (e.g. "meter", "celsius").
|
|
10
29
|
*
|
|
11
30
|
* A `Unit` is a passive handle: it knows its name, its home {@link Dimension},
|
|
12
31
|
* and how to transform a value to and from that dimension's canonical base
|
|
13
32
|
* unit. It does NOT know about other units or store pairwise conversions — all
|
|
14
|
-
* conversion math lives in {@link Dimension}, derived from
|
|
15
|
-
*
|
|
16
|
-
*
|
|
33
|
+
* conversion math lives in {@link Dimension}, derived from this transform.
|
|
34
|
+
*
|
|
35
|
+
* Almost every unit relates to the base by an affine map (`value * scale +
|
|
36
|
+
* offset`), so the transform is normally a {@link LinearTransform} of exact
|
|
37
|
+
* rationals — a single source of truth from which {@link toBase} / {@link
|
|
38
|
+
* fromBase} are derived, and which lets {@link Dimension.convert} stay exact.
|
|
39
|
+
* Only genuinely non-linear units (e.g. logarithmic scales, defined via
|
|
40
|
+
* `Dimension.custom`) fall back to a hand-written `toBase` / `fromBase` pair,
|
|
41
|
+
* since no `scale`/`offset` can express a curve like `10^x`.
|
|
17
42
|
*
|
|
18
43
|
* A unit belongs to exactly one dimension, but may belong to many
|
|
19
44
|
* {@link MeasurementSystem}s (metric/imperial/…); that membership lives on the
|
|
@@ -28,8 +53,17 @@ interface UnitOptions {
|
|
|
28
53
|
export declare class Unit {
|
|
29
54
|
readonly name: string;
|
|
30
55
|
readonly dimension: Dimension;
|
|
31
|
-
readonly
|
|
32
|
-
|
|
33
|
-
|
|
56
|
+
private readonly conversion;
|
|
57
|
+
constructor({ name, dimension, ...conversionOptions }: UnitOptions);
|
|
58
|
+
/**
|
|
59
|
+
* Exact affine transform to the base unit, present for all but non-linear
|
|
60
|
+
* units. When both ends of a conversion have one, {@link Dimension.convert}
|
|
61
|
+
* routes through exact rational arithmetic instead of lossy float scaling.
|
|
62
|
+
*/
|
|
63
|
+
get linear(): LinearTransform | undefined;
|
|
64
|
+
/** Convert a value in this unit to the dimension's base unit. */
|
|
65
|
+
toBase(value: number): number;
|
|
66
|
+
/** Convert a value in the dimension's base unit to this unit. */
|
|
67
|
+
fromBase(value: number): number;
|
|
34
68
|
}
|
|
35
69
|
export {};
|
package/dist/lib/Unit.js
CHANGED
|
@@ -7,9 +7,15 @@ exports.Unit = void 0;
|
|
|
7
7
|
* A `Unit` is a passive handle: it knows its name, its home {@link Dimension},
|
|
8
8
|
* and how to transform a value to and from that dimension's canonical base
|
|
9
9
|
* unit. It does NOT know about other units or store pairwise conversions — all
|
|
10
|
-
* conversion math lives in {@link Dimension}, derived from
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* conversion math lives in {@link Dimension}, derived from this transform.
|
|
11
|
+
*
|
|
12
|
+
* Almost every unit relates to the base by an affine map (`value * scale +
|
|
13
|
+
* offset`), so the transform is normally a {@link LinearTransform} of exact
|
|
14
|
+
* rationals — a single source of truth from which {@link toBase} / {@link
|
|
15
|
+
* fromBase} are derived, and which lets {@link Dimension.convert} stay exact.
|
|
16
|
+
* Only genuinely non-linear units (e.g. logarithmic scales, defined via
|
|
17
|
+
* `Dimension.custom`) fall back to a hand-written `toBase` / `fromBase` pair,
|
|
18
|
+
* since no `scale`/`offset` can express a curve like `10^x`.
|
|
13
19
|
*
|
|
14
20
|
* A unit belongs to exactly one dimension, but may belong to many
|
|
15
21
|
* {@link MeasurementSystem}s (metric/imperial/…); that membership lives on the
|
|
@@ -22,11 +28,34 @@ exports.Unit = void 0;
|
|
|
22
28
|
* (`base`, `unit`, `affine`, `custom`) rather than constructed directly.
|
|
23
29
|
*/
|
|
24
30
|
class Unit {
|
|
25
|
-
constructor({ name, dimension,
|
|
31
|
+
constructor({ name, dimension, ...conversionOptions }) {
|
|
26
32
|
this.name = name;
|
|
27
33
|
this.dimension = dimension;
|
|
28
|
-
this.
|
|
29
|
-
|
|
34
|
+
this.conversion = conversionOptions;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Exact affine transform to the base unit, present for all but non-linear
|
|
38
|
+
* units. When both ends of a conversion have one, {@link Dimension.convert}
|
|
39
|
+
* routes through exact rational arithmetic instead of lossy float scaling.
|
|
40
|
+
*/
|
|
41
|
+
get linear() {
|
|
42
|
+
return "linear" in this.conversion ? this.conversion.linear : undefined;
|
|
43
|
+
}
|
|
44
|
+
/** Convert a value in this unit to the dimension's base unit. */
|
|
45
|
+
toBase(value) {
|
|
46
|
+
if ("linear" in this.conversion) {
|
|
47
|
+
const linear = this.conversion.linear;
|
|
48
|
+
return value * linear.scale.toNumber() + linear.offset.toNumber();
|
|
49
|
+
}
|
|
50
|
+
return this.conversion.toBase(value);
|
|
51
|
+
}
|
|
52
|
+
/** Convert a value in the dimension's base unit to this unit. */
|
|
53
|
+
fromBase(value) {
|
|
54
|
+
if ("linear" in this.conversion) {
|
|
55
|
+
const linear = this.conversion.linear;
|
|
56
|
+
return (value - linear.offset.toNumber()) / linear.scale.toNumber();
|
|
57
|
+
}
|
|
58
|
+
return this.conversion.fromBase(value);
|
|
30
59
|
}
|
|
31
60
|
}
|
|
32
61
|
exports.Unit = Unit;
|
package/dist/lib/prefixes.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ export interface PrefixReference {
|
|
|
16
16
|
name: string;
|
|
17
17
|
/** Primary symbol, e.g. "m" → "km", "mm", … */
|
|
18
18
|
symbol: string;
|
|
19
|
-
/** Scale of the reference relative to its dimension's base (meter → 1, gram → 0.001). */
|
|
20
|
-
scale
|
|
19
|
+
/** Scale of the reference relative to its dimension's base (meter → 1, gram → 0.001) (default 1). */
|
|
20
|
+
scale?: number;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Define metric-prefixed variants of a reference unit on a dimension. Each
|
package/dist/lib/prefixes.js
CHANGED
|
@@ -50,7 +50,7 @@ function definePrefixed(dimension, reference, prefixes = exports.SI_PREFIXES) {
|
|
|
50
50
|
if (prefix.name === "micro") {
|
|
51
51
|
aliases.push(`u${reference.symbol}`);
|
|
52
52
|
}
|
|
53
|
-
units[name] = dimension.unit(name, reference.scale * prefix.factor, aliases);
|
|
53
|
+
units[name] = dimension.unit(name, (reference.scale ?? 1) * prefix.factor, aliases);
|
|
54
54
|
}
|
|
55
55
|
return units;
|
|
56
56
|
}
|
package/dist/systems/imperial.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.imperial = void 0;
|
|
4
|
+
const area_1 = require("../dimensions/area");
|
|
5
|
+
const energy_1 = require("../dimensions/energy");
|
|
6
|
+
const illuminance_1 = require("../dimensions/illuminance");
|
|
4
7
|
const length_1 = require("../dimensions/length");
|
|
8
|
+
const luminousIntensity_1 = require("../dimensions/luminousIntensity");
|
|
5
9
|
const mass_1 = require("../dimensions/mass");
|
|
10
|
+
const power_1 = require("../dimensions/power");
|
|
11
|
+
const pressure_1 = require("../dimensions/pressure");
|
|
6
12
|
const temperature_1 = require("../dimensions/temperature");
|
|
7
13
|
const volume_1 = require("../dimensions/volume");
|
|
8
14
|
const MeasurementSystem_1 = require("../lib/MeasurementSystem");
|
|
@@ -10,9 +16,21 @@ const MeasurementSystem_1 = require("../lib/MeasurementSystem");
|
|
|
10
16
|
exports.imperial = new MeasurementSystem_1.MeasurementSystem("imperial").add(
|
|
11
17
|
// length
|
|
12
18
|
length_1.inch, length_1.foot, length_1.yard, length_1.mile,
|
|
19
|
+
// area
|
|
20
|
+
area_1.squareInch, area_1.squareFoot, area_1.squareYard, area_1.acre, area_1.squareMile,
|
|
13
21
|
// mass
|
|
14
22
|
mass_1.pound, mass_1.ounce, mass_1.stone, mass_1.longTon,
|
|
15
23
|
// volume
|
|
16
24
|
volume_1.imperialGallon, volume_1.imperialQuart, volume_1.imperialPint, volume_1.imperialGill, volume_1.imperialFluidOunce,
|
|
17
25
|
// temperature
|
|
18
|
-
temperature_1.fahrenheit
|
|
26
|
+
temperature_1.fahrenheit,
|
|
27
|
+
// energy
|
|
28
|
+
energy_1.britishThermalUnit,
|
|
29
|
+
// power
|
|
30
|
+
power_1.horsepower,
|
|
31
|
+
// pressure
|
|
32
|
+
pressure_1.psi, pressure_1.inchOfMercury, pressure_1.inchOfWater,
|
|
33
|
+
// illuminance
|
|
34
|
+
illuminance_1.footCandle,
|
|
35
|
+
// luminous intensity
|
|
36
|
+
luminousIntensity_1.candlepower);
|
package/dist/systems/metric.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.metric = void 0;
|
|
4
|
+
const area_1 = require("../dimensions/area");
|
|
5
|
+
const energy_1 = require("../dimensions/energy");
|
|
6
|
+
const frequency_1 = require("../dimensions/frequency");
|
|
7
|
+
const illuminance_1 = require("../dimensions/illuminance");
|
|
4
8
|
const length_1 = require("../dimensions/length");
|
|
9
|
+
const luminance_1 = require("../dimensions/luminance");
|
|
10
|
+
const luminousIntensity_1 = require("../dimensions/luminousIntensity");
|
|
5
11
|
const mass_1 = require("../dimensions/mass");
|
|
12
|
+
const power_1 = require("../dimensions/power");
|
|
13
|
+
const pressure_1 = require("../dimensions/pressure");
|
|
6
14
|
const temperature_1 = require("../dimensions/temperature");
|
|
7
15
|
const volume_1 = require("../dimensions/volume");
|
|
8
16
|
const MeasurementSystem_1 = require("../lib/MeasurementSystem");
|
|
@@ -10,9 +18,25 @@ const MeasurementSystem_1 = require("../lib/MeasurementSystem");
|
|
|
10
18
|
exports.metric = new MeasurementSystem_1.MeasurementSystem("metric").add(
|
|
11
19
|
// length
|
|
12
20
|
length_1.meter, ...Object.values(length_1.metricLength),
|
|
21
|
+
// area
|
|
22
|
+
area_1.squareMeter, area_1.squareKilometer, area_1.hectare, area_1.are, area_1.squareCentimeter, area_1.squareMillimeter,
|
|
13
23
|
// mass
|
|
14
24
|
mass_1.gram, mass_1.tonne, ...Object.values(mass_1.metricMass),
|
|
15
25
|
// volume
|
|
16
26
|
volume_1.liter, ...Object.values(volume_1.metricVolume),
|
|
17
27
|
// temperature
|
|
18
|
-
temperature_1.kelvin, temperature_1.celsius
|
|
28
|
+
temperature_1.kelvin, temperature_1.celsius,
|
|
29
|
+
// energy
|
|
30
|
+
energy_1.joule, ...Object.values(energy_1.metricEnergy), energy_1.wattHour, ...Object.values(energy_1.metricWattHour), energy_1.calorie, energy_1.kilocalorie,
|
|
31
|
+
// power
|
|
32
|
+
power_1.watt, ...Object.values(power_1.metricPower), power_1.metricHorsepower,
|
|
33
|
+
// pressure
|
|
34
|
+
pressure_1.pascal, ...Object.values(pressure_1.metricPressure), pressure_1.bar, pressure_1.millibar,
|
|
35
|
+
// frequency
|
|
36
|
+
frequency_1.hertz, ...Object.values(frequency_1.metricFrequency),
|
|
37
|
+
// illuminance
|
|
38
|
+
illuminance_1.lux, ...Object.values(illuminance_1.metricIlluminance), illuminance_1.phot,
|
|
39
|
+
// luminance
|
|
40
|
+
luminance_1.candelaPerSquareMeter, luminance_1.stilb,
|
|
41
|
+
// luminous intensity
|
|
42
|
+
luminousIntensity_1.candela, ...Object.values(luminousIntensity_1.metricLuminousIntensity));
|