Skip to content

Commit

Permalink
WIP: Implement Radical class for n-th roots
Browse files Browse the repository at this point in the history
  • Loading branch information
frostburn committed Apr 16, 2024
1 parent 0536280 commit 024a987
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 0 deletions.
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;
}
}

0 comments on commit 024a987

Please sign in to comment.