Skip to content

Commit d5bf82d

Browse files
committed
Add comprehensive tests for decimal utility functions and rounding methods
- Implement tests for formatBigIntAsDecimal covering various scenarios including zero, positive, negative, large coefficients, and boundary conditions. - Introduce tests for roundHalfUp, ceilRound, and floorRound functions, ensuring correct behavior for positive, negative, and edge cases. - Validate integration of rounding functions with formatBigIntAsDecimal. - Enhance trial tests for debugging with Decimal class operations.
1 parent 2a4191b commit d5bf82d

File tree

6 files changed

+1401
-0
lines changed

6 files changed

+1401
-0
lines changed

src/core/decimal-utils.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,138 @@ export function scaleDown(coefficient: bigint, scaleFactor: number): bigint {
9797
}
9898

9999
return coefficient / (10n ** BigInt(scaleFactor));
100+
}
101+
102+
/**
103+
* Rounds a coefficient using round-half-up behavior when scaling down.
104+
*
105+
* @param coefficient The BigInt coefficient to round
106+
* @param currentScale The current scale of the coefficient
107+
* @param targetScale The target scale after rounding
108+
* @returns The rounded coefficient
109+
* @throws Error if targetScale is negative or currentScale is negative
110+
*/
111+
export function roundHalfUp(coefficient: bigint, currentScale: number, targetScale: number): bigint {
112+
if (currentScale < 0 || targetScale < 0) {
113+
throw new Error('Scales must be non-negative');
114+
}
115+
116+
if (currentScale <= targetScale) {
117+
// Scale up by padding zeros
118+
return scaleUp(coefficient, targetScale - currentScale);
119+
}
120+
121+
// Scale down with rounding
122+
const scaleDiff = currentScale - targetScale;
123+
const divisor = 10n ** BigInt(scaleDiff);
124+
const quotient = coefficient / divisor;
125+
const remainder = coefficient % divisor;
126+
127+
// Round half up logic
128+
const halfDivisor = divisor / 2n;
129+
const absRemainder = remainder < 0n ? -remainder : remainder;
130+
131+
if (absRemainder >= halfDivisor) {
132+
return quotient + (coefficient >= 0n ? 1n : -1n);
133+
}
134+
135+
return quotient;
136+
}
137+
138+
/**
139+
* Rounds a coefficient using ceiling behavior (always round up) when scaling down.
140+
*
141+
* @param coefficient The BigInt coefficient to round
142+
* @param currentScale The current scale of the coefficient
143+
* @param targetScale The target scale after rounding
144+
* @returns The ceiling rounded coefficient
145+
* @throws Error if targetScale is negative or currentScale is negative
146+
*/
147+
export function ceilRound(coefficient: bigint, currentScale: number, targetScale: number): bigint {
148+
if (currentScale < 0 || targetScale < 0) {
149+
throw new Error('Scales must be non-negative');
150+
}
151+
152+
if (currentScale <= targetScale) {
153+
// Scale up by padding zeros
154+
return scaleUp(coefficient, targetScale - currentScale);
155+
}
156+
157+
// Scale down with ceiling
158+
const scaleDiff = currentScale - targetScale;
159+
const divisor = 10n ** BigInt(scaleDiff);
160+
const quotient = coefficient / divisor;
161+
const remainder = coefficient % divisor;
162+
163+
// Ceiling logic: if there's any remainder and coefficient is positive, round up
164+
// If coefficient is negative and there's remainder, don't round (towards zero)
165+
if (remainder !== 0n && coefficient > 0n) {
166+
return quotient + 1n;
167+
}
168+
169+
return quotient;
170+
}
171+
172+
/**
173+
* Rounds a coefficient using floor behavior (always round down) when scaling down.
174+
*
175+
* @param coefficient The BigInt coefficient to round
176+
* @param currentScale The current scale of the coefficient
177+
* @param targetScale The target scale after rounding
178+
* @returns The floor rounded coefficient
179+
* @throws Error if targetScale is negative or currentScale is negative
180+
*/
181+
export function floorRound(coefficient: bigint, currentScale: number, targetScale: number): bigint {
182+
if (currentScale < 0 || targetScale < 0) {
183+
throw new Error('Scales must be non-negative');
184+
}
185+
186+
if (currentScale <= targetScale) {
187+
// Scale up by padding zeros
188+
return scaleUp(coefficient, targetScale - currentScale);
189+
}
190+
191+
// Scale down with floor
192+
const scaleDiff = currentScale - targetScale;
193+
const divisor = 10n ** BigInt(scaleDiff);
194+
const quotient = coefficient / divisor;
195+
const remainder = coefficient % divisor;
196+
197+
// Floor logic: if there's any remainder and coefficient is negative, round down
198+
// If coefficient is positive and there's remainder, don't round (towards zero)
199+
if (remainder !== 0n && coefficient < 0n) {
200+
return quotient - 1n;
201+
}
202+
203+
return quotient;
204+
}
205+
206+
/**
207+
* Formats a BigInt coefficient as a decimal string with the specified scale.
208+
*
209+
* @param coefficient The BigInt coefficient to format
210+
* @param scale The number of decimal places
211+
* @returns The formatted decimal string
212+
*/
213+
export function formatBigIntAsDecimal(coefficient: bigint, scale: number): string {
214+
// Handle zero case
215+
if (coefficient === 0n) {
216+
return scale > 0 ? `0.${'0'.repeat(scale)}` : '0';
217+
}
218+
219+
// Extract sign and work with absolute value
220+
const sign = coefficient < 0n ? '-' : '';
221+
const absCoeff = coefficient < 0n ? -coefficient : coefficient;
222+
let coeffStr = absCoeff.toString();
223+
224+
// Pad with leading zeros if needed
225+
while (coeffStr.length <= scale) {
226+
coeffStr = '0' + coeffStr;
227+
}
228+
229+
// Split into integer and fractional parts
230+
const integerPart = coeffStr.slice(0, coeffStr.length - scale) || '0';
231+
const fractionalPart = scale > 0 ? coeffStr.slice(-scale) : '';
232+
233+
return sign + integerPart + (scale > 0 ? '.' + fractionalPart : '');
100234
}

src/core/decimal.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,96 @@ class Decimal {
682682
}
683683
}
684684

685+
/**
686+
* Rounds this Decimal to the specified precision and scale using round-half-up behavior.
687+
* @param targetPrecision The total number of significant digits (M)
688+
* @param targetScale The number of digits after the decimal point (D)
689+
* @returns A new Decimal with the specified precision and scale, rounded using round-half-up
690+
* @throws {DecimalError} If targetScale > targetPrecision or if parameters are invalid
691+
*/
692+
round(targetPrecision: number, targetScale: number): Decimal {
693+
// Import the utility function
694+
const { roundHalfUp, formatBigIntAsDecimal } = require('./decimal-utils');
695+
696+
// Validate parameters
697+
if (targetScale > targetPrecision) {
698+
throw new DecimalError("Scale must be less than or equal to precision.");
699+
}
700+
if (targetPrecision < 1) {
701+
throw new DecimalError("Precision must be at least 1.");
702+
}
703+
if (targetScale < 0) {
704+
throw new DecimalError("Scale must be non-negative.");
705+
}
706+
707+
// Round the coefficient to the target scale
708+
const roundedCoeff = roundHalfUp(this.coefficient, this.scale, targetScale);
709+
710+
// Format as decimal string and create new Decimal
711+
const decimalStr = formatBigIntAsDecimal(roundedCoeff, targetScale);
712+
return new Decimal(decimalStr, targetPrecision, targetScale);
713+
}
714+
715+
/**
716+
* Rounds this Decimal to the specified precision and scale using ceiling behavior (always round up).
717+
* @param targetPrecision The total number of significant digits (M)
718+
* @param targetScale The number of digits after the decimal point (D)
719+
* @returns A new Decimal with the specified precision and scale, rounded using ceiling
720+
* @throws {DecimalError} If targetScale > targetPrecision or if parameters are invalid
721+
*/
722+
ceil(targetPrecision: number, targetScale: number): Decimal {
723+
// Import the utility function
724+
const { ceilRound, formatBigIntAsDecimal } = require('./decimal-utils');
725+
726+
// Validate parameters
727+
if (targetScale > targetPrecision) {
728+
throw new DecimalError("Scale must be less than or equal to precision.");
729+
}
730+
if (targetPrecision < 1) {
731+
throw new DecimalError("Precision must be at least 1.");
732+
}
733+
if (targetScale < 0) {
734+
throw new DecimalError("Scale must be non-negative.");
735+
}
736+
737+
// Round the coefficient using ceiling behavior
738+
const ceiledCoeff = ceilRound(this.coefficient, this.scale, targetScale);
739+
740+
// Format as decimal string and create new Decimal
741+
const decimalStr = formatBigIntAsDecimal(ceiledCoeff, targetScale);
742+
return new Decimal(decimalStr, targetPrecision, targetScale);
743+
}
744+
745+
/**
746+
* Rounds this Decimal to the specified precision and scale using floor behavior (always round down).
747+
* @param targetPrecision The total number of significant digits (M)
748+
* @param targetScale The number of digits after the decimal point (D)
749+
* @returns A new Decimal with the specified precision and scale, rounded using floor
750+
* @throws {DecimalError} If targetScale > targetPrecision or if parameters are invalid
751+
*/
752+
floor(targetPrecision: number, targetScale: number): Decimal {
753+
// Import the utility function
754+
const { floorRound, formatBigIntAsDecimal } = require('./decimal-utils');
755+
756+
// Validate parameters
757+
if (targetScale > targetPrecision) {
758+
throw new DecimalError("Scale must be less than or equal to precision.");
759+
}
760+
if (targetPrecision < 1) {
761+
throw new DecimalError("Precision must be at least 1.");
762+
}
763+
if (targetScale < 0) {
764+
throw new DecimalError("Scale must be non-negative.");
765+
}
766+
767+
// Round the coefficient using floor behavior
768+
const flooredCoeff = floorRound(this.coefficient, this.scale, targetScale);
769+
770+
// Format as decimal string and create new Decimal
771+
const decimalStr = formatBigIntAsDecimal(flooredCoeff, targetScale);
772+
return new Decimal(decimalStr, targetPrecision, targetScale);
773+
}
774+
685775
/**
686776
* Adds this Decimal to another and returns a new Decimal.
687777
* The result will match the scale of the first operand (this), and will be rounded if necessary.

0 commit comments

Comments
 (0)