Skip to content
Open
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
202 changes: 186 additions & 16 deletions src/N.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,40 +184,210 @@ export default class N {
/* Arithmetics */

plus(Addend: NLike): N {
const Augend: N = this.clone()
const augend: bigint = Augend.value
const augend: bigint = this.value
const addend: bigint = this.rebase(Addend)
Augend.value = augend + addend
return Augend
this.value = augend + addend
return this
Comment on lines 186 to +190
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The arithmetic methods now mutate the receiver (plus/minus/mul/div), and many new BigNumber-style instance methods were added, but there are no unit tests covering the new in-place semantics or the new helpers (e.g., dividedToIntegerBy, modulo, exponentiatedBy, shiftedBy, formatting helpers). Adding targeted tests would help prevent silent behavior regressions and confirm BigNumber-compat expectations.

Copilot uses AI. Check for mistakes.
}

minus(Subtrahend: NLike): N {
const Minuend: N = this.clone()
const minuend: bigint = Minuend.value
const minuend: bigint = this.value
const subtrahend: bigint = this.rebase(Subtrahend)
Minuend.value = minuend - subtrahend
return Minuend
this.value = minuend - subtrahend
return this
}

mul(Factor: NLike): N {
const Multiplier: N = this.clone()
const multiplier: bigint = Multiplier.value
const multiplier: bigint = this.value
const multiplicand: bigint = this.rebase(Factor)
Multiplier.value = multiplier * multiplicand / this.factor
return Multiplier
this.value = multiplier * multiplicand / this.factor
return this
}

/** Alias for `mul(NLike)`. Used by BigNumber.js. */
multipliedBy(Factor: NLike): N { return this.mul(Factor) }

div(Divisor: NLike): N {
const Dividend: N = this.clone()
const dividend: bigint = Dividend.value
const dividend: bigint = this.value
const divisor: bigint = this.rebase(Divisor)
Dividend.value = this.factor * dividend / divisor
return Dividend
this.value = this.factor * dividend / divisor
return this
}

/** Alias for `div(NLike)`. Used by BigNumber.js. */
dividedBy(Divisor: NLike): N { return this.div(Divisor) }

/** Integer division: returns the integer part of the division. */
dividedToIntegerBy(Divisor: NLike): N {
const dividend: bigint = this.value
const divisor: bigint = this.rebase(Divisor)
this.value = dividend / divisor * this.factor
return this
}

/** Alias for `mul(NLike)`. Used by BigNumber.js. */
times(Factor: NLike): N { return this.mul(Factor) }

/** Alias for `sqrt()`. Used by BigNumber.js. */
squareRoot(): N { return this.sqrt() }

/** Absolute value. */
abs(): N {
if (this.value < 0n) {
this.value = -this.value
}
return this
}

/** Alias for `abs()`. Used by BigNumber.js. */
absoluteValue(): N { return this.abs() }

/** Negates the value. */
negated(): N {
this.value = -this.value
return this
}

/** Modulo/remainder. */
modulo(Divisor: NLike): N {
const divisor: bigint = this.rebase(Divisor)
this.value = this.value % divisor
return this
}

/** Alias for `modulo(NLike)`. */
mod(Divisor: NLike): N { return this.modulo(Divisor) }

/** Exponentiation by integer exponent. */
exponentiatedBy(Exponent: NLike): N {
const exponent = BigInt(this.nfy(Exponent).valueOf())
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exponentiatedBy claims to support an integer exponent, but it converts the input via valueOf(), which rounds according to the global rounding mode. That means non-integer inputs (e.g., 1.5) will be silently rounded and used as an exponent instead of being rejected. Consider validating that the exponent has no fractional part (e.g., check toPrecise() % factor === 0n) and throw a clear error when it is not an integer.

Suggested change
const exponent = BigInt(this.nfy(Exponent).valueOf())
const exponentN = this.nfy(Exponent)
const preciseExponent = exponentN.toPrecise()
if (preciseExponent % this.factor !== 0n) {
throw new Error('Exponent must be an integer.')
}
const exponent = preciseExponent / this.factor

Copilot uses AI. Check for mistakes.
if (exponent < 0n) {
throw new Error('Negative exponent is not supported.')
}
let result = 1n * this.factor
let base = this.value
let exp = exponent
while (exp > 0n) {
if (exp % 2n === 1n) {
result = result * base / this.factor
}
base = base * base / this.factor
exp /= 2n
}
this.value = result
return this
}

/** Shifts the decimal point by the given number of places. */
shiftedBy(shift: number): N {
if (shift === 0) {
return this
}
const factor = 10n ** BigInt(Math.abs(shift))
this.value = shift > 0 ? this.value * factor : this.value / factor
return this
Comment on lines +282 to +288
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shiftedBy uses BigInt(Math.abs(shift)), which will throw a RangeError for non-integer/NaN/Infinity inputs (TypeScript type is just number). If the API is intended to accept only integer shifts (as in BigNumber.js), validate Number.isFinite(shift) and Number.isInteger(shift) and throw a consistent, library-specific error message.

Copilot uses AI. Check for mistakes.
}

/** Number of decimal places or a rounded value if dp is specified. */
decimalPlaces(): number
decimalPlaces(dp: number): N
decimalPlaces(dp?: number): number | N {
if (dp === undefined) {
return this.decimals
}
return new N(this.toString(dp))
}

/** Returns the integer part of the value. */
integerValue(): N {
const integer = this.valueOf()
this.value = integer * this.factor
return this
}

/** Number of significant digits. */
precision(includeZeros: boolean = false): number {
if (this.value === 0n) {
return 1
}
const raw = this.toString(this.decimals).replace('-', '').replace('.', '')
const trimmedLeading = raw.replace(/^0+/, '')
const trimmed = includeZeros ? trimmedLeading : trimmedLeading.replace(/0+$/, '')
return trimmed.length || 1
}
Comment on lines +309 to +317
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

precision() method name conflicts with the existing public instance property precision (declared earlier in this class). This will fail TypeScript compilation (duplicate identifier) and/or make n.precision() unusable at runtime because the instance field shadows the prototype method. Rename one of them (e.g., keep precision() for BigNumber compatibility and rename the field to internalPrecision/calcPrecision, updating all internal references and tests), or choose a different method name/alias for significant digits.

Copilot uses AI. Check for mistakes.

/** Returns a plain number representation. */
toNumber(): number {
return Number(this.toString(this.decimals))
}

/** Returns a string with fixed decimal places. */
toFixed(dp: number = this.decimals): string {
return this.toString(dp)
}

/** Returns a string with an exponent. */
toExponential(dp?: number): string {
const num = Number(this.toString(this.decimals))
return dp === undefined ? num.toExponential() : num.toExponential(dp)
}

/** Returns a string with a specified precision. */
toPrecision(precision?: number): string {
const num = Number(this.toString(this.decimals))
return precision === undefined ? num.toPrecision() : num.toPrecision(precision)
Comment on lines +331 to +338
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toExponential/toPrecision are implemented by converting to a JS Number, which will overflow to Infinity and/or lose precision for large values—defeating the purpose of an arbitrary-precision type and diverging from BigNumber.js behavior. Consider implementing these using string/BigInt-based formatting (or explicitly documenting/guarding the precision limits if approximation is intended).

Suggested change
const num = Number(this.toString(this.decimals))
return dp === undefined ? num.toExponential() : num.toExponential(dp)
}
/** Returns a string with a specified precision. */
toPrecision(precision?: number): string {
const num = Number(this.toString(this.decimals))
return precision === undefined ? num.toPrecision() : num.toPrecision(precision)
// Work entirely in decimal-string space to avoid precision loss.
const full = this.toString(this.decimals)
// Extract sign.
let sign = ''
let s = full
if (s[0] === '-') {
sign = '-'
s = s.slice(1)
}
let [intPart, decPart = ''] = s.split('.')
const intPartNoLeading = intPart.replace(/^0+/, '')
let digits: string
let exponent: number
if (intPartNoLeading.length > 0) {
// Non-zero integer part: exponent is index of first significant digit in integer part.
exponent = intPartNoLeading.length - 1
digits = (intPartNoLeading + decPart)
} else {
// Integer part is zero or all zeros: look into decimal part for first non-zero.
const match = decPart.match(/[^0]/)
if (!match) {
// Value is exactly zero.
const fracZeros = dp !== undefined && dp > 0 ? '0'.repeat(dp) : ''
const mantissa = dp !== undefined && dp > 0 ? `0.${fracZeros}` : '0'
return `${sign}${mantissa}e+0`
}
const firstNonZeroIndex = match.index as number
exponent = -firstNonZeroIndex - 1
digits = decPart.slice(firstNonZeroIndex)
}
// Now `digits` holds all significant digits with no leading zeros, value != 0.
// Determine how many fraction digits to show.
const fracDigits = dp === undefined ? Math.max(digits.length - 1, 0) : dp
// Ensure we have enough digits for rounding.
const required = fracDigits + 1 // 1 digit before decimal + fracDigits
if (digits.length < required + 1) {
digits = digits.padEnd(required + 1, '0')
}
// Round to `required` significant digits (1 + fracDigits).
let rounded = digits.slice(0, required).split('').map(ch => parseInt(ch, 10))
const nextDigit = digits[required] ? parseInt(digits[required], 10) : 0
if (nextDigit >= 5) {
// Add 1 with carry.
for (let i = rounded.length - 1; i >= 0; i--) {
const sum = rounded[i] + 1
if (sum === 10) {
rounded[i] = 0
if (i === 0) {
// Carry out of most significant digit increases exponent.
rounded.unshift(1)
exponent += 1
break
}
} else {
rounded[i] = sum
break
}
}
}
const roundedStr = rounded.join('')
const intDigit = roundedStr[0]
const fracPart = roundedStr.slice(1)
// Normalize exponent sign and format.
const expSign = exponent >= 0 ? '+' : '-'
const expAbs = Math.abs(exponent).toString()
if (fracDigits > 0) {
const frac = fracPart.padEnd(fracDigits, '0').slice(0, fracDigits)
return `${sign}${intDigit}.${frac}e${expSign}${expAbs}`
} else {
return `${sign}${intDigit}e${expSign}${expAbs}`
}
}
/** Returns a string with a specified precision. */
toPrecision(precision?: number): string {
// When no precision is given, behave similarly to Number#toPrecision(undefined):
// use the default full fixed-point representation.
if (precision === undefined) {
return this.toString(this.decimals)
}
// Precision must be a positive integer, but we keep validation minimal to
// avoid changing the public API surface too much.
if (!Number.isFinite(precision) || precision < 1 || !Number.isInteger(precision)) {
throw new RangeError('toPrecision() precision must be a positive integer')
}
// First, get an exponential representation with the desired significant digits.
const expStr = this.toExponential(precision - 1)
// Parse exponential string: [-]d[.ddd]e[+/-]dd
const expMatch = expStr.match(/^(-)?(\d)(?:\.(\d+))?e([+\-]\d+)$/)
if (!expMatch) {
// Fallback: if parsing fails for any reason, return the exponential form.
return expStr
}
const [, signMatch, intDigitStr, fracDigitsStr = '', exponentStr] = expMatch
const sign = signMatch ? '-' : ''
const exponent = parseInt(exponentStr, 10)
const mantissaDigits = intDigitStr + fracDigitsStr // already rounded to `precision` digits
// Decide between fixed and exponential form:
// Mirror typical JS / BigNumber behavior:
// - use exponential if exponent < -6 or exponent >= precision
// - otherwise, fixed-point.
if (exponent < -6 || exponent >= precision) {
return expStr
}
// Convert mantissa+exponent to fixed-point decimal.
if (exponent >= 0) {
// Decimal point moves to the right of the first digit by `exponent`.
const intLen = exponent + 1
let digits = mantissaDigits
if (digits.length < intLen) {
digits = digits.padEnd(intLen, '0')
}
const intPart = digits.slice(0, intLen)
const fracPart = digits.slice(intLen)
if (fracPart.length === 0) {
return `${sign}${intPart}`
}
return `${sign}${intPart}.${fracPart}`
} else {
// Negative exponent: value is between 0 and 1.
// Decimal point moves left of the first digit.
const zeros = '0'.repeat(-exponent - 1)
const fracPart = zeros + mantissaDigits
return `${sign}0.${fracPart}`
}

Copilot uses AI. Check for mistakes.
}

/** Returns a formatted string with grouped integer digits. */
toFormat(dp: number = this.decimals, groupSeparator: string = ','): string {
const [intPart, decPart] = this.toString(dp).split('.')
const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, groupSeparator)
return decPart === undefined ? grouped : `${grouped}.${decPart}`
}

/** Returns a fraction as [numerator, denominator]. */
toFraction(): [string, string] {
let numerator = this.value
let denominator = this.factor
const gcd = (a: bigint, b: bigint): bigint => {
let x = a < 0n ? -a : a
let y = b < 0n ? -b : b
while (y !== 0n) {
const t = y
y = x % y
x = t
}
return x
}
const divisor = gcd(numerator, denominator)
numerator /= divisor
denominator /= divisor
return [numerator.toString(), denominator.toString()]
}

/** Comparisons */
comparedTo(Comparand: NLike): number {
const comparand: bigint = this.rebase(Comparand)
if (this.value === comparand) {
return 0
}
return this.value > comparand ? 1 : -1
}

isEqualTo(Comparand: NLike): boolean { return this.eq(Comparand) }
isGreaterThan(Comparand: NLike): boolean { return this.gt(Comparand) }
isGreaterThanOrEqualTo(Comparand: NLike): boolean { return this.gte(Comparand) }
isLessThan(Comparand: NLike): boolean { return this.lt(Comparand) }
isLessThanOrEqualTo(Comparand: NLike): boolean { return this.lte(Comparand) }

isZero(): boolean { return this.value === 0n }
isPositive(): boolean { return this.value > 0n }
isNegative(): boolean { return this.value < 0n }
isInteger(): boolean { return this.value % this.factor === 0n }
isBigNumber(): boolean { return true }
isFinite(): boolean { return true }
isNaN(): boolean { return false }

sq(): N {
return this.mul(this)
}
Expand Down
Loading