Skip to content

Mutate receiver for binary arithmetic and add BigNumber.js-compatible instance methods#2

Open
ackvf wants to merge 1 commit into
mainfrom
codex/refactor-math-operations-to-modify-first-operand
Open

Mutate receiver for binary arithmetic and add BigNumber.js-compatible instance methods#2
ackvf wants to merge 1 commit into
mainfrom
codex/refactor-math-operations-to-modify-first-operand

Conversation

@ackvf
Copy link
Copy Markdown
Owner

@ackvf ackvf commented Feb 9, 2026

Motivation

  • Revert the recent behavior that cloned the left operand for every binary operation to avoid excessive allocations and GC pressure by mutating the receiver instead.
  • Restore the original in-place semantics for plus, minus, mul, and div so A.plus(B) updates A rather than returning a fresh clone.
  • Implement a set of missing BigNumber.js-compatible prototype methods to improve compatibility with that API and provide common numeric helpers.

Description

  • Refactored binary arithmetic methods so they update the current instance in-place: plus, minus, mul, and div now mutate this and return it.
  • Added BigNumber.js-style aliases and operations including dividedBy, dividedToIntegerBy, times, multipliedBy (existing alias preserved), squareRoot, abs/absoluteValue, negated, mod/modulo, exponentiatedBy, and shiftedBy.
  • Added formatting and inspection helpers such as decimalPlaces, precision, toNumber, toFixed, toExponential, toPrecision, toFormat, and toFraction.
  • Added comparison helpers and boolean checks including comparedTo, isEqualTo, isGreaterThan, isLessThanOrEqualTo, isZero, isPositive, isNegative, isInteger, isBigNumber, isFinite, and isNaN.

Testing

  • No automated tests were executed as part of this change.
  • The repository contains unit tests at src/N.test.ts which can be run with the project test runner (e.g. npm test or yarn test) and are recommended to validate behavior after the change.

Codex Task

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates N’s arithmetic API to reduce allocations by mutating the receiver for binary operations, and expands the N prototype surface with a set of BigNumber.js-style instance methods and formatting/comparison helpers.

Changes:

  • Switch plus, minus, mul, and div to in-place mutation semantics (this is updated and returned).
  • Add BigNumber.js-style arithmetic aliases and numeric helpers (e.g., dividedBy, dividedToIntegerBy, abs, modulo, exponentiatedBy, shiftedBy).
  • Add formatting/conversion helpers and comparison/boolean predicates (e.g., toFixed, toPrecision, toFormat, comparedTo, isZero).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/N.ts
Comment on lines +309 to +317
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
}
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.
Comment thread src/N.ts

/** 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.
Comment thread src/N.ts
Comment on lines +282 to +288
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
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.
Comment thread src/N.ts
Comment on lines +331 to +338
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)
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.
Comment thread src/N.ts
Comment on lines 186 to +190
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
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants