Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# cmath-js
Implementation of parts of C's & C++'s numerics libraries in TypeScript/JavaScript.
Implementation of parts of C++'s numerics libraries in TypeScript/JavaScript, including functions C++ inherits from C.

## Floating-point functions
- [`copysign`](https://en.cppreference.com/w/c/numeric/math/copysign)
Expand Down Expand Up @@ -50,6 +50,6 @@ These functions accept either a `bigint` or an integer `number`:
The test coverage is a perfect 100% and enforced by the publishing and pull request verification workflows.

## Contributing
Contributions are welcomed! Feel free to make a pull request. Please add your name to `contributors` in `package.json` and run `npm run build-and-verify` before submitting your PR. By making a pull request you agree to license your contribution under [the CC0 license](https://creativecommons.org/publicdomain/zero/1.0/legalcode.en#legal-code-title) unless otherwise specified.
Contributions are welcomed! Feel free to make a pull request. Please add your name to `contributors` in `package.json` and run `npm run build-and-verify` before submitting your PR. By opening a pull request you agree to license your contribution under [the CC0 license](https://creativecommons.org/publicdomain/zero/1.0/legalcode.en#legal-code-title) unless you specify otherwise.

ESLint is used to enforce code quality and consistent formatting (with the help of Prettier). If ESLint complains when you run `npm run build-and-verify`, you can run `npm run lint-fix` to apply automatic fixes and then fix the rest of the errors manually. I recommend configuring your IDE for ESLint and Prettier. If you are using Visual Studio Code, simply installing [Microsoft's ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [the official Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) takes care of that.
ESLint is used to enforce code quality and consistent formatting (with the help of Prettier). If ESLint complains when you run `npm run build-and-verify`, you can run `npm run lint-fix` to apply automatic fixes, and then fix the rest of the errors manually. It is recommend to configure your IDE for ESLint and Prettier. If you are using Visual Studio Code, simply installing [Microsoft's ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [the official Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) takes care of that.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/internal/floatOctets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Constructing these can take a little bit of time so let's reuse them
const floatArray = new Float64Array(1);
const octets = new Uint8Array(floatArray.buffer);
const octets: Uint8Array = new Uint8Array(floatArray.buffer);

// DO NOT STORE THE Uint8Array RETURN VALUE FROM THIS CALL!
// It will be mutated on the next call to this function
Expand Down
14 changes: 7 additions & 7 deletions src/namespace-std-numbers/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// Mathematical constants defined in the namespace std::numbers
// cppreference: https://en.cppreference.com/w/cpp/numeric/constants

export const e = Math.E;
export const e: number = Math.E;
export const egamma = 0.5772156649015329; // The Euler-Mascheroni constant
export const inv_pi = 0.3183098861837907; // 1 / pi
export const inv_sqrt3 = 0.5773502691896257; // 1 / sqrt(3)
export const inv_sqrtpi = 0.5641895835477563; // 1 / sqrt(pi)
export const ln10 = Math.LN10;
export const ln2 = Math.LN2;
export const log10 = Math.LOG10E;
export const log2e = Math.LOG2E;
export const ln10: number = Math.LN10;
export const ln2: number = Math.LN2;
export const log10: number = Math.LOG10E;
export const log2e: number = Math.LOG2E;
export const phi = 1.618033988749895; // The golden ratio
export const pi = Math.PI;
export const sqrt2 = Math.SQRT2;
export const pi: number = Math.PI;
export const sqrt2: number = Math.SQRT2;
export const sqrt3 = 1.7320508075688772; // sqrt(3)
9 changes: 6 additions & 3 deletions src/namespace-std/all-numbers/iota.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Fills an array with sequentially increasing values
// Cppreference: https://en.cppreference.com/w/c/numeric/math/iota

/**
* Fills an array with sequentially increasing values
*
* Read more about the original function on
* - {@link https://en.cppreference.com/w/c/numeric/math/iota|Cppreference}
*/
export function iota(mut_arrayToFill: bigint[], startValue: bigint): void;
export function iota(mut_arrayToFill: number[], startValue: number): void;
export function iota(mut_arrayToFill: (bigint | number)[], startValue: bigint | number): void {
Expand Down
11 changes: 9 additions & 2 deletions src/namespace-std/all-numbers/signbit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { floatOctets } from "../../internal/index.js";

// Cppreference: https://en.cppreference.com/w/c/numeric/math/signbit
/**
* Determines if a number is negative or NaN with the sign bit set.
* Note that the ECMAScript standard does not guarantee that +NaN and
* -NaN are different so this function may give unexpected results
* (such as always false or always true) in some JavaScript engines.
*
* Read more about the original function on
* - {@link https://en.cppreference.com/w/cpp/numeric/math/signbit|Cppreference}
*/
export function signbit(num: bigint | number): boolean {
return floatOctets(Number(num))[7] > 127;
}
17 changes: 11 additions & 6 deletions src/namespace-std/doubles/copysign.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { signbit } from "../index.js";

// copysign produces a value with the magnitude of 'num' and the sign 'sign'
// C spec: https://web.archive.org/web/20181230041359if_/http://www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf#subsection.7.12.11
// Cppreference: https://en.cppreference.com/w/c/numeric/math/copysign
// This implementation handles positive and negative zero and positive and negative
// NaNs (in JS engines where that difference is observable when writing NaNs to a Float64Array)
export function copysign(/*double*/ num: number, /*double*/ sign: number): /*double*/ number {
/**
* Produces a value with the magnitude of 'num' and the sign 'sign'.
* This implementation handles positive and negative zero and positive and negative
* NaNs (in JS engines where that difference is observable when writing NaNs to a Float64Array).
*
* Read more about the original function on
* - {@link https://en.cppreference.com/w/cpp/numeric/math/copysign|Cppreference}
* - {@link https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf#subsection.7.12.11|The C23 final draft specification}
*/

export function copysign(num: number, sign: number): number {
return signbit(num) === signbit(sign) ? num : -num;
}
11 changes: 8 additions & 3 deletions src/namespace-std/doubles/fabs.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// fabs is just like JavaScript's Math.abs
// Cppreference: https://en.cppreference.com/w/c/numeric/math/fabs
export const fabs = Math.abs;
/**
* fabs is just like JavaScript's Math.abs.
*
* Read more about the original function on
* - {@link https://en.cppreference.com/w/cpp/numeric/math/fabs|Cppreference}
*/

export const fabs: (number: number) => number = Math.abs;
18 changes: 9 additions & 9 deletions src/namespace-std/doubles/frexp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { frexp } from "./index.js";

describe(frexp.name, () => {
it("decomposes a number into a normalized fraction and an integral power of two", () => {
expect(frexp(1)).toStrictEqual([0.5, 1]);
expect(frexp(1.5)).toStrictEqual([0.75, 1]);
expect(frexp(3 * 2 ** 500)).toStrictEqual([0.75, 502]);
expect(frexp(-4)).toStrictEqual([-0.5, 3]);
expect(frexp(Number.MAX_VALUE)).toStrictEqual([0.9999999999999999, 1024]);
expect(frexp(Number.MIN_VALUE)).toStrictEqual([0.5, -1073]);
expect(frexp(-Infinity)).toStrictEqual([-Infinity, 0]);
expect(frexp(-0)).toStrictEqual([-0, 0]);
expect(frexp(NaN)).toStrictEqual([NaN, 0]);
expect(frexp(1)).toStrictEqual({ exponent: 1, fraction: 0.5 });
expect(frexp(1.5)).toStrictEqual({ exponent: 1, fraction: 0.75 });
expect(frexp(3 * 2 ** 500)).toStrictEqual({ exponent: 502, fraction: 0.75 });
expect(frexp(-4)).toStrictEqual({ exponent: 3, fraction: -0.5 });
expect(frexp(Number.MAX_VALUE)).toStrictEqual({ exponent: 1024, fraction: 0.9999999999999999 });
expect(frexp(Number.MIN_VALUE)).toStrictEqual({ exponent: -1073, fraction: 0.5 });
expect(frexp(-Infinity)).toStrictEqual({ exponent: 0, fraction: -Infinity });
expect(frexp(-0)).toStrictEqual({ exponent: 0, fraction: -0 });
expect(frexp(NaN)).toStrictEqual({ exponent: 0, fraction: NaN });
});
});
42 changes: 24 additions & 18 deletions src/namespace-std/doubles/frexp.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,45 @@
// Note: Instead of "double frexp(double arg, int* exp)" this is built as "[double, int] frexp(double arg)" due to ECMAScripts's lack of pointers
// A hypothetical issue with this implementation is that the precision the ** operator is not defined in the ECMAScript standard,
// however, sane ECMAScript implementations should give precise results for 2**<integer> expressions
// Cppreference: https://en.cppreference.com/w/c/numeric/math/frexp for a more detailed description
export interface frexp_result {
exponent: number;
fraction: number;
}

// Note: Instead of "double frexp(double arg, int* exp)" this is built as "{ exponent, fraction } frexp(double arg)" due to ECMAScripts's lack of pointers
// A hypothetical issue with this implementation is that the precision of the ** operator is not defined in the ECMAScript standard,
// however, it is hard to imagine a sane ECMAScript implementation would give imprecise results for 2**<integer> expressions.
// Cppreference: https://en.cppreference.com/w/cpp/numeric/math/frexp
// Object.is(n, frexp(n)[0] * 2 ** frexp(n)[1]) for all number values of n except when Math.isFinite(n) && Math.abs(n) > 2**1023
// Object.is(n, (2 * frexp(n)[0]) * 2 ** (frexp(n)[1] - 1)) for all number values of n
// Object.is(n, frexp(n)[0]) for these values of n: 0, -0, NaN, Infinity, -Infinity
// Math.abs(frexp(n)[0]) is >= 0.5 and < 1.0 for any other number-type value of n
export function frexp(
/*double*/ num: number,
): [/*double*/ fraction: number, /*int*/ exponent: number] {
export function frexp(num: number): frexp_result {
if (num === 0 || !Number.isFinite(num)) {
return [num, 0];
return { exponent: 0, fraction: num };
}

const absNum: number = Math.abs(num);

let exp: number = Math.max(-1023, Math.floor(Math.log2(absNum)) + 1);
let x: number = absNum * 2 ** -exp;
let exponent: number = Math.max(-1023, Math.floor(Math.log2(absNum)) + 1);
let fraction: number = absNum * 2 ** -exponent;

// These while loops compensate for rounding errors that may occur because of ECMAScript's Math.log2's undefined precision
// and the first one also helps work around the issue of 2 ** -exp === Infinity when exp <= -1024
while (x < 0.5) {
x *= 2;
--exp;
while (fraction < 0.5) {
fraction *= 2;
--exponent;
}

// istanbul ignore next - This might never run and that's okay. See the above comment
while (x >= 1) {
x *= 0.5;
++exp;
while (fraction >= 1) {
fraction *= 0.5;
++exponent;
}

if (num < 0) {
x = -x;
fraction = -fraction;
}

return [x, exp];
return {
exponent,
fraction,
};
}
2 changes: 1 addition & 1 deletion src/namespace-std/doubles/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { copysign } from "./copysign.js";
export { fabs } from "./fabs.js";
export { frexp } from "./frexp.js";
export { frexp, type frexp_result } from "./frexp.js";
export { hypot } from "./hypot.js";
export { ldexp } from "./ldexp.js";
export { nan } from "./nan.js";
Expand Down
15 changes: 13 additions & 2 deletions src/namespace-std/doubles/ldexp.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
// ldexp multiplies a floating-point number by an integral power of 2
// ldexp returns factor * 2**exponent
// C spec: https://web.archive.org/web/20181230041359if_/http://www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf#subsection.7.12.6
// Cppreference: https://en.cppreference.com/w/c/numeric/math/ldexp
// Implementation is complicated by the need to avoid underflow/overflow given a large exponent (-1075< >1023)

/**
* Multiplies a floating-point number by an integral power of 2.
*
*
* Read more about the original function on
* - {@link https://en.cppreference.com/w/c/numeric/math/ldexp|Cppreference}
* - {@link https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf#subsection.7.12.6|The C23 final draft specification}
*
* @returns `factor * 2**exponent` but avoiding overflow and underflow
*/
export function ldexp(/*double*/ factor: number, /*int*/ exponent: number): /*double*/ number {
// Implementation is complicated by the need to avoid underflow/overflow
// given a large exponent (less than -1075 or greater than 1023).
const halfPowerRoundedTowardZero: number = 2 ** Math.trunc(exponent * 0.5);
return (
factor * halfPowerRoundedTowardZero * halfPowerRoundedTowardZero * 2 ** Math.sign(exponent % 2)
Expand Down
44 changes: 28 additions & 16 deletions src/namespace-std/doubles/nan.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import { floatFromBits } from "../../internal/index.js";

// Converts the string arg into the corresponding quiet NaN value.
// https://en.cppreference.com/w/cpp/numeric/math/nan
// This is always true: Object.is(nan(<any string>), NaN).
// Important note: JavaScript engines do not have to keep the NaN value
// and at the time of writing (2024) at least one engine (Firefox's) only
// supports two different NaNs (+NaN and -NaN), so in Firefox this function
// will always return the exact same NaN. Chrome's JS engine supports all
// possible NaN values, though, which can be observed like this:
// (
// new Uint8Array(new Float64Array([nan("0")]).buffer)[0] === 0 &&
// new Uint8Array(new Float64Array([nan("92")]).buffer)[0] === 92
// )

/**
* Converts the string argument into the corresponding quiet NaN value.
*
* The accepted format is not defined in the C23 standard, except that
* if a character other than [0-9a-zA-Z_] appears, the argument is ignored.
* This implementation accepts simple decimal and hexadecimal integer strings
* like `"1234"` or `"0xF00D"`.
*
* The following is always `true`:
* ```ts
* Object.is(nan(<any string>), NaN)
* ```
*
* Important note: JavaScript engines do not have to keep the NaN value
* and at the time of writing (2024) at least one engine (Firefox's) only
* supports two different NaNs (+NaN and -NaN), so in Firefox this function
* will always return the exact same NaN. Chrome's JS engine supports all
* possible NaN values, though, which can be observed like this:
* ```ts
* (
* new Uint8Array(new Float64Array([nan("0")]).buffer)[0] === 0 &&
* new Uint8Array(new Float64Array([nan("92")]).buffer)[0] === 92
* )
* ```
*
* Read more about the original function on
* - {@link https://en.cppreference.com/w/cpp/numeric/math/nan|Cppreference}
*/
export function nan(arg: string): number {
let bits = 0n;

// The accepted format is not defined in the C23 standard, except that
// if a character other than [0-9a-zA-Z_] appears, the argument is ignored.
// So let's just accept simple decimal and hexadecimal integers.
const isIntegerString = /^(0|[1-9]\d*|0x[0-9a-zA-Z]+)$/.test(arg);

if (isIntegerString) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ describe(create_countl_zero.name, () => {
expect(countl_zero_u32(0b0)).toStrictEqual(32);
expect(countl_zero_u32(0xff_ff_ff_ff)).toStrictEqual(0);
expect(countl_zero_u32(0xff_ff_ff_fe)).toStrictEqual(0);
expect(countl_zero_u32(-1)).toStrictEqual(0);
expect(countl_zero_u32(-2n)).toStrictEqual(0);
expect(countl_zero_u32(0x7f_ff_ff_ff)).toStrictEqual(1);
expect(countl_zero_u32(0x00_00_00_01)).toStrictEqual(31);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ describe(create_countr_zero.name, () => {
expect(countr_zero_u32(0xff_ff_ff_fe)).toStrictEqual(1);
expect(countr_zero_u32(0x7f_ff_ff_ff)).toStrictEqual(0);
expect(countr_zero_u32(0x80_00_00_00)).toStrictEqual(31);
expect(countr_zero_u32(-1)).toStrictEqual(0);
expect(countr_zero_u32(-2n)).toStrictEqual(1);

expect(countr_zero_u64(0x0)).toStrictEqual(64);
expect(countr_zero_u64(0x0n)).toStrictEqual(64);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ export function create_countr_zero({ bits }: { bits: number }): countr_zero_func
const truncated =
(typeof integer === "number" ? integer : Number(BigInt.asIntN(32, integer))) & mask;

if (truncated === 0) {
return bits;
}

// Turn the trailing 0s - plus the bit just to the left of them - to 1s, and the rest of the bits to 0s.
const zeroesAsOnes = truncated ^ (truncated - 1);

const trailLength = 31 - Math.clz32(zeroesAsOnes);
return truncated === 0 ? bits : trailLength;
return trailLength;
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { gcd } from "./index.js";
import { gcd } from "../index.js";

describe(gcd.name, () => {
it("finds the greatest common divisor", () => {
Expand Down
3 changes: 3 additions & 0 deletions src/namespace-std/integers/gcd/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { gcd } from "./gcd.js";
export { gcd_bigint } from "./gcd_bigint.js";
export { gcd_number } from "./gcd_number.js";
4 changes: 2 additions & 2 deletions src/namespace-std/integers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { abs } from "./abs.js";
export * from "./count-left-or-right-bits/index.js";
export { div, type div_t, type div_t_bigint, type div_t_number } from "./div/index.js";
export { gcd } from "./gcd.js";
export { lcm } from "./lcm.js";
export { gcd } from "./gcd/index.js";
export { lcm } from "./lcm/index.js";
export { popcount } from "./popcount.js";
1 change: 1 addition & 0 deletions src/namespace-std/integers/lcm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { lcm } from "./lcm.js";
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { lcm } from "./index.js";
import { lcm } from "../index.js";

describe(lcm.name, () => {
it("finds the least common multiple", () => {
Expand Down Expand Up @@ -40,7 +40,7 @@ describe(lcm.name, () => {
}
});

it(`returns 0 if at least one of the arguments are 0`, () => {
it("returns 0 if at least one of the arguments are 0", () => {
expect(lcm(0, 0)).toStrictEqual(0);
expect(lcm(-0, -0)).toStrictEqual(0);
expect(lcm(-4, 0)).toStrictEqual(0);
Expand All @@ -50,7 +50,7 @@ describe(lcm.name, () => {
expect(lcm(0n, 4n)).toStrictEqual(0n);
});

it(`returns 0 if one or both parameters are non-integers`, () => {
it("returns 0 if one or both parameters are non-integers", () => {
expect(lcm(9, 4.5)).toStrictEqual(0);
expect(lcm(4.5, Infinity)).toStrictEqual(0);
expect(lcm(-Infinity, 4)).toStrictEqual(0);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { gcd_bigint } from "./gcd_bigint.js";
import { gcd_bigint } from "../gcd/index.js";

export function lcm_bigint(aInteger: bigint, bInteger: bigint): bigint {
const a = aInteger < 0n ? -aInteger : aInteger;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { gcd_number } from "./gcd_number.js";
import { gcd_number } from "../gcd/index.js";

export function lcm_number(aInteger: number, bInteger: number): number {
if (!Number.isSafeInteger(aInteger) || !Number.isSafeInteger(bInteger)) {
Expand Down
Loading
Loading