Skip to content

Instantly share code, notes, and snippets.

@metanomial
Last active December 13, 2019 06:32
Show Gist options
  • Save metanomial/37e1d7ad67cb461081a5ce3352cf8bc1 to your computer and use it in GitHub Desktop.
Save metanomial/37e1d7ad67cb461081a5ce3352cf8bc1 to your computer and use it in GitHub Desktop.
Rational Number Class and Utilities
/**
Check if value is a float-point number
@param {value} value
@return {*}
**/
function isFloat(value) {
const float = Number.parseFloat(value);
return !Number.isNaN(float) && float % 1 !== 0;
}
/**
Regular expression parser
@type {regex}
**/
const rationalString = /^\s*(?<sign>[-+])?\s*(?<whole>\d+(?:_\d+)*)?\s*(?:(?<numerator>\d+(?:_\d+)*)\s*\/\s*(?<denominator>\d+(?:_\d+)*))?\s*$/;
/**
Calculate the absolute value of an integer
@param {BigInt} integer
@return {BigInt}
**/
function absolute(integer) {
return integer * (integer < 0n ? -1n : 1n);
}
/**
Convert a value to an integer
@param {*} value
@return {BigInt}
**/
function toInteger(value) {
if(!Number.isFinite())
return value instanceof Rational
? value.numerator
: BigInt(value);
}
/**
Parse a string with numeric seperators into a integer
@param {string} string
@param {BigInt} fallback
@return {BigInt}
**/
function parseInteger(string, fallback = 0n) {
return string != null
? BigInt(string.replace(/_/g, ''))
: fallback;
}
/**
Find the greatest common factor of two integers
@param {BigInt} a
@param {BigInt} b
@return {BigInt}
**/
function gcf(a, b) {
let x = absolute(a);
let y = absolute(b);
while(y) [x, y] = [y, x % y];
return x;
}
/** Rational number **/
export class Rational {
/**
Construct a rational number from float-point number
@param {*} value
@return {Rational}
**/
static fromFloat(value) {
const [ integer, decimal = 0 ] = Number
.parseFloat(value)
.toString()
.split('.')
.map(value => BigInt(value));
const base = BigInt(10 ** decimal.toString().length);
return new Rational(integer * base + decimal, base);
}
/**
Parse a rational number from a string
@param {string} string
@throw {SyntaxError}
**/
static parse(string) {
if(!rationalString.test(string)) throw new SyntaxError('Invalid input string');
const {
sign,
whole,
numerator,
denominator
} = string.match(rationalString).groups;
const unit = sign === '-' ? -1n : 1n;
return new Rational(
unit * parseInteger(whole, 1n) * parseInteger(denominator, 1n) + parseInteger(numerator),
parseInteger(denominator, 1n)
);
}
/**
Fraction dividend
@type {BigInt}
**/
#numerator;
/**
Fraction divisor
@type {BigInt}
**/
#denominator;
/**
Construct a rational number
@param {Rational|BigInt|number|string} a
@param {Rational|BigInt|number|string} b
@type {Rational}
**/
constructor(a = 0n, b = 1n) {
if(isFloat(a)) return Rational.fromFloat(a);
if(isFloat(b)) throw Error('Denominator must be an integer value');
this.#numerator = toInteger(a);
this.#denominator = toInteger(b);
if(this.#denominator === 0n) throw new Error('Denominator cannot be zero');
this._reduce();
}
/**
Get the numerator of this rational number
@type {BigInt}
**/
get numerator() {
return this.#numerator;
}
/**
Get the denominator of this rational number
@type {BigInt}
**/
get denominator() {
return this.#denominator;
}
/**
Reduce this rational number to lowest terms
@private
**/
_reduce() {
if(this.#denominator < 0n) {
this.#numerator *= -1n;
this.#denominator *= -1n;
}
const factor = gcf(this.#numerator, this.#denominator);
this.#numerator /= factor;
this.#denominator /= factor;
}
/**
Check if this rational number is an integer
@return {boolean}
**/
isInteger() {
return this.#denominator === 1n;
}
/**
Check if this rational number is fractional
@return {boolean}
**/
isFractional() {
return this.#denominator !== 1n;
}
/**
Check if this rational number is a proper fraction
@return {boolean}
**/
isProper() {
return this.#numerator < this.#denominator;
}
/**
Check if this rational number is an improper fraction
@return {boolean}
**/
isImproper() {
return this.#numerator >= this.#denominator;
}
/**
Convert this rational number to a string
@return {string}
**/
toString() {
return this.#numerator + this.#denominator !== 1n
? '/' + this.#denominator
: '';
}
/**
Convert this rational number to a primitive type
@return {string|number}
**/
[Symbol.toPrimitive](hint) {
switch(hint) {
case 'string': return this.toString();
default: return Number(this.#numerator) / Number(this.#denominator);
}
}
/**
Safely add this rational number to values
@param {*[]}
@return {Rational}
**/
add(...values) {
return values
.map(value => new Rational(value))
.reduce((result, term) => new Rational(
result.numerator * term.denominator + result.denominator * term.numerator,
result.denominator * term.denominator
), this);
}
/**
Safely subtract values from this rational number
@param {*[]}
@return {Rational}
**/
subtract(...values) {
return values
.map(value => new Rational(value))
.reduce((result, term) => new Rational(
result.numerator * term.denominator - result.denominator * term.numerator,
result.denominator * term.denominator
), this);
}
/**
Safely multiply this rational number by a value
@param {*} value
@return {Rational}
**/
multiply(value) {
const factor = new Rational(value);
return new Rational(
this.#numerator * factor.#numerator,
this.#denominator * factor.#denominator
);
}
/**
Safely divide by a value
@param {*} value
@return {Rational}
**/
divide(value) {
const factor = new Rational(value);
return new Rational(
this.#numerator * factor.#denominator,
this.#denominator * factor.#numerator
);
}
}
/**
Rational number template tag
@param {string[]} strings
@param {*[]} expressions
@return {Rational}
**/
export const ℚ = function(strings, ...expressions) {
const string = strings
.flatMap((value, index) => [value, expressions[index]])
.map(value => value != null ? String(value) : '')
.join('');
return Rational.parse(string);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment