Skip to content

Commit

Permalink
Fraction from JS number (#642)
Browse files Browse the repository at this point in the history
* Add fromNumber

* Add tests

* Move buffer inside numberToFraction

* Move numberToFraction into Fraction class

* Rename numberToFraction -> numberToNumAndDen

* Add JSDoc

* Code style

* Change commit hash
  • Loading branch information
fedgiac authored Apr 8, 2020
1 parent aef6265 commit 08192f9
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 0 deletions.
49 changes: 49 additions & 0 deletions src/fraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,55 @@ export class Fraction {
);
}

/**
* Represents a Javascript number as a pair numerator/denominator without precision loss
* by retrieving mantissa, exponent, and sign from its bit representation
* @param number The Javascript number to be represented
* @return a BigInt array with two elements: numerator and denominator
*/
private static numberToNumAndDen(number: number) {
const view = new DataView(new ArrayBuffer(8));
view.setFloat64(0, number);
const bits = view.getBigUint64(0);
const sign = bits >> BigInt(63) ? BigInt(-1) : BigInt(1);
const exponent = ((bits >> BigInt(52)) & BigInt(0x7ff)) - BigInt(1023);
const one = BigInt(1) << BigInt(52);
const mantissa = bits & (one - BigInt(1));
// number is 1.mantissa * 2**exponent

switch (exponent) {
case BigInt(1024): // infinities and NaN
throw Error("Invalid number");
case BigInt(-1023):
if (mantissa == BigInt(0)) // positive and negative zero
return [BigInt(0), sign];
else // subnormal numbers
throw Error("Subnormal numbers are not supported");
}

const mantissa_plus_one = mantissa + one;
const shifted_exponent = exponent - BigInt(52);

if (shifted_exponent >= BigInt(0))
return [
sign * mantissa_plus_one * (BigInt(1) << shifted_exponent),
BigInt(1)
]
else
return [
sign * mantissa_plus_one,
BigInt(1) << -shifted_exponent
]
}

static fromNumber(number: number) {
const [numerator, denominator] = Fraction.numberToNumAndDen(number);
return new Fraction(
new BN(numerator.toString()),
new BN(denominator.toString())
);
}

toBN() {
return this.numerator.div(this.denominator);
}
Expand Down
65 changes: 65 additions & 0 deletions test/models/fraction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,71 @@ describe("Fraction", () => {
});
});

describe("fromNumber", () => {
it("converts numbers to Fraction", () => {
const testCases = [
{
number: 0.5,
expected: new Fraction(1, 2)
},
{
number: 2,
expected: new Fraction(2, 1)
}
];
for (const {number, expected} of testCases)
assert(
Fraction.fromNumber(number)
.sub(expected)
.isZero()
);
});

it("fails on bad input", () => {
const testCases = [NaN, Infinity, -Infinity];
let hasThrown = false;
for (const number of testCases) {
try {
Fraction.fromNumber(number);
} catch (error) {
assert(error.message, "Invalid number");
hasThrown = true;
}
}
assert(hasThrown);
});

it("fails with subnormal numbers", () => {
const testCases = [2 ** -1023, Number.MIN_VALUE];
let hasThrown = false;
for (const number of testCases) {
try {
Fraction.fromNumber(number);
} catch (error) {
assert(error.message, "Subnormal numbers are not supported");
hasThrown = true;
}
}
assert(hasThrown);
});

it("has toNumber as its right inverse", () => {
const testCases = [
1 / 3,
1.0,
1.1,
1000000000000000000,
0,
-0,
Number.MAX_VALUE,
1 + Number.EPSILON,
2 ** -1022
];
for (const number of testCases)
assert.equal(Fraction.fromNumber(number).toNumber(), number);
});
});

describe("clone", () => {
it("creates a deep copy", () => {
const original = new Fraction(1, 2);
Expand Down

0 comments on commit 08192f9

Please sign in to comment.