-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: Implement Radical class for n-th roots
- Loading branch information
Showing
2 changed files
with
215 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} | ||
gcd() {} | ||
lcm() {} | ||
geoMod(other: RadicalValue) {} | ||
gabs() {} | ||
gcr(other: RadicalValue) {} | ||
lcr(other: RadicalValue) {} | ||
log(other: RadicalValue) {} | ||
geoRoundTo(other: RadicalValue) {} | ||
|
||
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; | ||
} | ||
} |