herta
Version:
Advanced mathematics framework for scientific, engineering, and financial applications
408 lines (344 loc) • 11.4 kB
JavaScript
/**
* fraction.js
* Advanced fraction operations for Herta.js
*/
/**
* Greatest common divisor (GCD) of two numbers
* @param {Number} a - First number
* @param {Number} b - Second number
* @returns {Number} - GCD of a and b
*/
function gcd(a, b) {
a = Math.abs(a);
b = Math.abs(b);
while (b !== 0) {
const temp = b;
b = a % b;
a = temp;
}
return a;
}
/**
* Least common multiple (LCM) of two numbers
* @param {Number} a - First number
* @param {Number} b - Second number
* @returns {Number} - LCM of a and b
*/
function lcm(a, b) {
return Math.abs(a * b) / gcd(a, b);
}
/**
* Fraction class for representing and manipulating fractions
*/
class Fraction {
/**
* Create a new fraction
* @param {Number} numerator - Numerator of the fraction
* @param {Number} denominator - Denominator of the fraction
*/
constructor(numerator, denominator = 1) {
if (denominator === 0) {
throw new Error('Denominator cannot be zero');
}
this.numerator = numerator;
this.denominator = denominator;
// Normalize sign (negative sign always goes in numerator)
if (this.denominator < 0) {
this.numerator = -this.numerator;
this.denominator = -this.denominator;
}
this.simplify();
}
/**
* Simplify the fraction by dividing both numerator and denominator by their GCD
* @returns {Fraction} - Simplified fraction (this)
*/
simplify() {
const divisor = gcd(this.numerator, this.denominator);
this.numerator /= divisor;
this.denominator /= divisor;
return this;
}
/**
* Add another fraction to this fraction
* @param {Fraction|Number} other - Fraction or number to add
* @returns {Fraction} - New fraction representing the sum
*/
add(other) {
if (typeof other === 'number') {
other = new Fraction(other, 1);
}
const commonDenominator = lcm(this.denominator, other.denominator);
const thisScaleFactor = commonDenominator / this.denominator;
const otherScaleFactor = commonDenominator / other.denominator;
const newNumerator = this.numerator * thisScaleFactor + other.numerator * otherScaleFactor;
return new Fraction(newNumerator, commonDenominator);
}
/**
* Subtract another fraction from this fraction
* @param {Fraction|Number} other - Fraction or number to subtract
* @returns {Fraction} - New fraction representing the difference
*/
subtract(other) {
if (typeof other === 'number') {
other = new Fraction(other, 1);
}
return this.add(new Fraction(-other.numerator, other.denominator));
}
/**
* Multiply this fraction by another fraction
* @param {Fraction|Number} other - Fraction or number to multiply by
* @returns {Fraction} - New fraction representing the product
*/
multiply(other) {
if (typeof other === 'number') {
other = new Fraction(other, 1);
}
return new Fraction(
this.numerator * other.numerator,
this.denominator * other.denominator
);
}
/**
* Divide this fraction by another fraction
* @param {Fraction|Number} other - Fraction or number to divide by
* @returns {Fraction} - New fraction representing the quotient
*/
divide(other) {
if (typeof other === 'number') {
other = new Fraction(other, 1);
}
if (other.numerator === 0) {
throw new Error('Division by zero');
}
return new Fraction(
this.numerator * other.denominator,
this.denominator * other.numerator
);
}
/**
* Raise this fraction to a power
* @param {Number} exponent - Exponent to raise the fraction to
* @returns {Fraction} - New fraction representing the power
*/
pow(exponent) {
if (!Number.isInteger(exponent)) {
throw new Error('Exponent must be an integer for Fraction.pow');
}
if (exponent === 0) {
return new Fraction(1, 1);
} else if (exponent < 0) {
// Negative exponent means reciprocal
return new Fraction(
Math.pow(this.denominator, -exponent),
Math.pow(this.numerator, -exponent)
);
} else {
return new Fraction(
Math.pow(this.numerator, exponent),
Math.pow(this.denominator, exponent)
);
}
}
/**
* Get the absolute value of this fraction
* @returns {Fraction} - New fraction representing the absolute value
*/
abs() {
return new Fraction(Math.abs(this.numerator), this.denominator);
}
/**
* Get the reciprocal of this fraction
* @returns {Fraction} - New fraction representing the reciprocal
*/
reciprocal() {
if (this.numerator === 0) {
throw new Error('Cannot take reciprocal of zero');
}
return new Fraction(this.denominator, this.numerator);
}
/**
* Check if this fraction is equal to another fraction
* @param {Fraction|Number} other - Fraction or number to compare with
* @returns {Boolean} - True if fractions are equal
*/
equals(other) {
if (typeof other === 'number') {
other = new Fraction(other, 1);
}
// Compare simplified versions
const a = new Fraction(this.numerator, this.denominator);
const b = new Fraction(other.numerator, other.denominator);
return a.numerator === b.numerator && a.denominator === b.denominator;
}
/**
* Check if this fraction is less than another fraction
* @param {Fraction|Number} other - Fraction or number to compare with
* @returns {Boolean} - True if this fraction is less than other
*/
lessThan(other) {
if (typeof other === 'number') {
other = new Fraction(other, 1);
}
return this.numerator * other.denominator < other.numerator * this.denominator;
}
/**
* Check if this fraction is greater than another fraction
* @param {Fraction|Number} other - Fraction or number to compare with
* @returns {Boolean} - True if this fraction is greater than other
*/
greaterThan(other) {
if (typeof other === 'number') {
other = new Fraction(other, 1);
}
return this.numerator * other.denominator > other.numerator * this.denominator;
}
/**
* Convert this fraction to a decimal number
* @returns {Number} - Decimal representation of the fraction
*/
toDecimal() {
return this.numerator / this.denominator;
}
/**
* Convert this fraction to a string representation
* @returns {String} - String representation of the fraction
*/
toString() {
if (this.denominator === 1) {
return `${this.numerator}`;
} else {
return `${this.numerator}/${this.denominator}`;
}
}
/**
* Convert this fraction to a mixed number string representation
* @returns {String} - Mixed number representation of the fraction
*/
toMixedString() {
if (Math.abs(this.numerator) < this.denominator || this.denominator === 1) {
return this.toString();
} else {
const wholePart = Math.floor(Math.abs(this.numerator) / this.denominator);
const remainder = Math.abs(this.numerator) % this.denominator;
const sign = this.numerator < 0 ? '-' : '';
if (remainder === 0) {
return `${sign}${wholePart}`;
} else {
return `${sign}${wholePart} ${remainder}/${this.denominator}`;
}
}
}
/**
* Create a fraction from a decimal number
* @param {Number} decimal - Decimal number to convert
* @param {Number} maxDenominator - Maximum denominator to use (default: 1000000)
* @returns {Fraction} - Fraction approximating the decimal
*/
static fromDecimal(decimal, maxDenominator = 1000000) {
if (isNaN(decimal)) {
throw new Error('Cannot create fraction from NaN');
}
if (!isFinite(decimal)) {
throw new Error('Cannot create fraction from Infinity');
}
// Handle negative numbers
const sign = decimal < 0 ? -1 : 1;
decimal = Math.abs(decimal);
// Handle integers
if (Number.isInteger(decimal)) {
return new Fraction(sign * decimal, 1);
}
// Best approximation algorithm
let bestNumerator = 0;
let bestDenominator = 1;
let bestError = Math.abs(decimal);
// Try simple fractions first with small denominators
for (let denominator = 1; denominator <= 100; denominator++) {
const numerator = Math.round(decimal * denominator);
const error = Math.abs(decimal - numerator / denominator);
if (error < bestError) {
bestNumerator = numerator;
bestDenominator = denominator;
bestError = error;
if (error < 1e-10) break;
}
}
// For more precision, use continued fraction algorithm if needed
if (bestError > 1e-6 && maxDenominator > 100) {
// Continued fraction algorithm
let x = decimal;
let a = Math.floor(x);
let h1 = 1, h2 = a;
let k1 = 0, k2 = 1;
while (k2 <= maxDenominator) {
x = 1 / (x - a);
a = Math.floor(x);
const h3 = a * h2 + h1;
const k3 = a * k2 + k1;
if (k3 > maxDenominator) break;
h1 = h2; h2 = h3;
k1 = k2; k2 = k3;
const error = Math.abs(decimal - h2 / k2);
if (error < bestError) {
bestNumerator = h2;
bestDenominator = k2;
bestError = error;
}
if (error < 1e-10) break;
}
}
return new Fraction(sign * bestNumerator, bestDenominator);
}
/**
* Create a fraction from a mixed number
* @param {Number} whole - Whole part of the mixed number
* @param {Number} numerator - Numerator of the fractional part
* @param {Number} denominator - Denominator of the fractional part
* @returns {Fraction} - Equivalent improper fraction
*/
static fromMixed(whole, numerator, denominator) {
if (denominator === 0) {
throw new Error('Denominator cannot be zero');
}
const sign = whole < 0 ? -1 : 1;
whole = Math.abs(whole);
const improperNumerator = whole * denominator + numerator;
return new Fraction(sign * improperNumerator, denominator);
}
/**
* Create a fraction from a string representation
* @param {String} str - String representation of a fraction (e.g., "3/4", "5", "2 1/2")
* @returns {Fraction} - Parsed fraction
*/
static fromString(str) {
str = str.trim();
// Mixed number (e.g., "2 1/2")
const mixedMatch = str.match(/^(-?\d+)\s+(\d+)\/(\d+)$/);
if (mixedMatch) {
const whole = parseInt(mixedMatch[1], 10);
const numerator = parseInt(mixedMatch[2], 10);
const denominator = parseInt(mixedMatch[3], 10);
return Fraction.fromMixed(whole, numerator, denominator);
}
// Simple fraction (e.g., "3/4")
const fractionMatch = str.match(/^(-?\d+)\/(\d+)$/);
if (fractionMatch) {
const numerator = parseInt(fractionMatch[1], 10);
const denominator = parseInt(fractionMatch[2], 10);
return new Fraction(numerator, denominator);
}
// Integer (e.g., "5")
const integerMatch = str.match(/^(-?\d+)$/);
if (integerMatch) {
const numerator = parseInt(integerMatch[1], 10);
return new Fraction(numerator, 1);
}
throw new Error(`Invalid fraction string: ${str}`);
}
}
module.exports = {
Fraction,
gcd,
lcm
};