Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Implement Radical class for n-th roots #22

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
55 changes: 55 additions & 0 deletions src/__tests__/radical.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {describe, it, expect} from 'vitest';
import {Radical} from '../radical';
import {Fraction} from '../fraction';

describe('Radical', () => {
it('can represent root two', () => {
const radical = new Radical(2, 2);
expect(radical.radicand.equals(new Fraction(2))).toBe(true);
expect(radical.index).toBe(2);
expect(radical.mul(radical).equals(2)).toBe(true);
expect(radical.toString()).toBe('√2');
});

it('can represent the fifth of 12-TET', () => {
const fif = new Radical(2, '12/7');
expect(fif.radicand.equals(4096));
expect(fif.index).toBe(12);
expect(fif.valueOf()).toBeCloseTo(2 ** (7 / 12));
expect(fif.mul(fif).div(2).equals(new Radical(2, '12/2'))).toBe(true);
});

it('can represent the cube root of 5', () => {
const radical = new Radical(5, 3);
expect(radical.pow(3).equals(5)).toBe(true);
expect(radical.toString()).toBe('3√5');
});

it('can compare 5^(2/3) to 3', () => {
const radical = new Radical(5, 1.5);
expect(radical.compare(3)).toBeLessThan(0);
expect(radical.valueOf()).toBeCloseTo(2.924);
});

it('can parse root three', () => {
const radical = new Radical('√3');
expect(radical.pow(2).equals(3)).toBe(true);
});

it('can parse (3/2)√(10/7)', () => {
const radical = new Radical('(3/2)√(10/7)');
expect(radical.pow(3).equals(new Fraction(10, 7).pow(2)!)).toBe(true);
});

// TODO
it.skip('can parse 2^3', () => {
const radical = new Radical('2^3');
expect(radical.equals(8)).toBe(true);
});

// TODO
it.skip('can parse (5/3)**(3/2)', () => {
const radical = new Radical('(5/3)**(3/2)');
expect(radical.pow(2).equals(new Fraction(5, 3).pow(3)!)).toBe(true);
});
});
160 changes: 160 additions & 0 deletions src/radical.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import {Fraction, FractionValue, gcd} from './fraction';
import {toMonzo} from './monzo';
import {PRIMES} from './primes';

export type RadicalValue = FractionValue | Radical;

function stripParenthesis(str: string) {
while (str.startsWith('(') || str.startsWith(' ')) {
str = str.slice(1);
}
while (str.endsWith(')') || str.endsWith(' ')) {
str = str.slice(0, -1);
}
return str;
}

/**
* Radical expressions like 3√(10/7).
* Powerful enough to represent all n-th roots of sufficiently small arguments.
*/
export class Radical {
/**
* Non-negative radicand under the radical surd.
*/
radicand: Fraction;
/**
* Index of radication i.e. the inverse power. Always positive and never zero.
*/
index: number;

/**
* Construct a new radical value.
* @param radicand The radicand under the radical surd.
* @param index Index of radication i.e. the inverse exponent of the radicand.
*/
constructor(radicand: RadicalValue, index?: FractionValue) {
if (
index === undefined &&
typeof radicand === 'string' &&
radicand.includes('√')
) {
[index, radicand] = radicand.split('√');
index = stripParenthesis(index || '2');
radicand = stripParenthesis(radicand);
}

if (radicand instanceof Radical) {
index = new Fraction(index || 1).mul(radicand.index);
radicand = radicand.radicand;
}

this.radicand = new Fraction(radicand);

if (this.radicand.s < 0) {
throw new Error('Negative radicands not supported.');
}

const {s, n, d} = new Fraction(index || 1);
if (s < 0) {
this.radicand = this.radicand.inverse();
} else if (s === 0) {
throw new Error('Radication by zero.');
}
const r = this.radicand.pow(d);
if (r === null) {
throw new Error('Radical index denominator too large.');
}
this.radicand = r;
this.index = n;

this.reduce();
}

reduce() {
const monzo = toMonzo(this.index);
this.index = 1;
for (let i = 0; i < monzo.length; ++i) {
const root = new Fraction(1, PRIMES[i]);
while (true) {
const reduction = this.radicand.pow(root);
if (reduction === null) {
break;
} else {
this.radicand = reduction;
monzo[i]--;
}
}
this.index *= PRIMES[i] ** monzo[i];
}
}

toString() {
if (this.index === 1) {
return this.radicand.toString();
}
const result = `√${this.radicand}`;
if (this.index === 2) {
return result;
}
return this.index.toString() + result;
}

valueOf() {
return this.radicand.valueOf() ** (1 / this.index);
}

inverse() {
return new Radical(this.radicand, -this.index);
}

// TODO
floor() {}
ceil() {}
round() {}
roundTo(other: RadicalValue) {}

Check warning on line 115 in src/radical.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'other' is defined but never used
gcd() {}
lcm() {}
geoMod(other: RadicalValue) {}

Check warning on line 118 in src/radical.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'other' is defined but never used
gabs() {}
gcr(other: RadicalValue) {}

Check warning on line 120 in src/radical.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'other' is defined but never used
lcr(other: RadicalValue) {}

Check warning on line 121 in src/radical.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'other' is defined but never used
log(other: RadicalValue) {}

Check warning on line 122 in src/radical.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'other' is defined but never used
geoRoundTo(other: RadicalValue) {}

Check warning on line 123 in src/radical.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'other' is defined but never used

mul(other: RadicalValue) {
if (!(other instanceof Radical)) {
other = new Radical(other);
}
const commonFactor = gcd(this.index, other.index);
const a = this.radicand.pow(other.index / commonFactor);
const b = other.radicand.pow(this.index / commonFactor);
if (a === null || b === null) {
throw new Error('Radical multiplication failed.');
}
return new Radical(a.mul(b), (this.index / commonFactor) * other.index);
}

div(other: RadicalValue) {
return this.mul(new Radical(other).inverse());
}

pow(exponent: FractionValue) {
return new Radical(
this.radicand,
new Fraction(exponent).inverse().mul(this.index)
);
}

compare(other: RadicalValue) {
const ratio = this.div(other);
return ratio.radicand.n - ratio.radicand.d;
}

equals(other: RadicalValue) {
if (!(other instanceof Radical)) {
other = new Radical(other);
}
return this.radicand.equals(other.radicand) && this.index === other.index;
}
}
Loading