mirror of
https://github.com/fluffos/fluffos
synced 2026-08-12 18:26:06 -04:00
A pure-LPC decimal library (integer math, no driver/VM change and no
external dependency). A decimal is a 2-element array ({ mant, scale })
with value mant / 10^scale, so arithmetic is exact -- 0.1 + 0.2 == 0.3 --
which binary floats can't do. Backed by 64-bit LPC ints (~18 significant
digits); mantissa overflow is detected and raised, not silently wrapped.
API (simul-efuns via inherit "std/decimal"): to_decimal(string|int|
float|decimal), decimal_add/sub/mul/div/mod/neg, decimal_cmp/eq/lt/gt,
decimal_to_string/to_int/to_float, decimalp. Decimals are immutable
(every op returns a fresh array). An array rather than a `class` on
purpose: a simul_efun class would leak into every object's global
classes()/num_classes().
Pinned by /single/tests/std/decimal.lpc (33 checks: construction,
exact add/sub, scale-aligned compare, mul/div with trailing-zero trim,
repeating-division truncation, mod, negation, conversions, float
construction, overflow + bad-input errors).
Verified: testsuite x3 (ASan Debug) + ctest 297, RelWithDebInfo 298.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
190 lines
6.2 KiB
Text
190 lines
6.2 KiB
Text
/**
|
|
* std/decimal -- exact fixed-point decimal arithmetic in pure LPC.
|
|
*
|
|
* A decimal is a 2-element array ({ mant, scale }) whose value is
|
|
* mant / 10^scale
|
|
* so arithmetic is EXACT (no binary floating-point rounding), which is
|
|
* what money / game-economy code wants. Backed by 64-bit LPC integers:
|
|
* ~18 significant digits total; scale (fractional places) is capped at
|
|
* DECIMAL_MAX_SCALE. Overflow of the 64-bit mantissa is detected and
|
|
* raised rather than silently wrapping.
|
|
*
|
|
* An array (not a class) so the type does not leak into every object's
|
|
* global class namespace via the simul_efun; decimals are immutable
|
|
* (every operation returns a fresh array). Construct with to_decimal();
|
|
* never build the array by hand.
|
|
*
|
|
* Inherited by /single/simul_efun so to_decimal()/decimal_*() are global
|
|
* simul-efuns. Tested by /single/tests/std/decimal.lpc.
|
|
*/
|
|
|
|
#define DECIMAL_MAX_SCALE 18
|
|
#define DECIMAL_DIV_SCALE 12
|
|
|
|
// Field indices into the ({ mant, scale }) representation.
|
|
#define DEC_MANT 0
|
|
#define DEC_SCALE 1
|
|
|
|
private int _pow10(int n) {
|
|
int r = 1;
|
|
if (n < 0 || n > DECIMAL_MAX_SCALE)
|
|
error("decimal: exponent " + n + " out of range.\n");
|
|
while (n-- > 0) r *= 10;
|
|
return r;
|
|
}
|
|
|
|
private int _mul_checked(int a, int b) {
|
|
int p;
|
|
if (a == 0 || b == 0) return 0;
|
|
p = a * b;
|
|
if (p / a != b) error("decimal: integer overflow.\n");
|
|
return p;
|
|
}
|
|
|
|
private int _add_checked(int a, int b) {
|
|
int r = a + b;
|
|
// 64-bit two's-complement overflow: result sign is inconsistent.
|
|
if ((b > 0 && r < a) || (b < 0 && r > a)) error("decimal: integer overflow.\n");
|
|
return r;
|
|
}
|
|
|
|
private int *_make(int mant, int scale) {
|
|
if (scale < 0 || scale > DECIMAL_MAX_SCALE)
|
|
error("decimal: illegal scale " + scale + ".\n");
|
|
return ({ mant, scale });
|
|
}
|
|
|
|
// Is v a decimal value produced by this library?
|
|
int decimalp(mixed v) {
|
|
return arrayp(v) && sizeof(v) == 2 && intp(v[DEC_MANT]) && intp(v[DEC_SCALE]);
|
|
}
|
|
|
|
private int *_from_string(string s) {
|
|
int i, len = strlen(s), sign = 1, mant = 0, scale = 0, seen_dot = 0, seen_digit = 0;
|
|
int c;
|
|
i = 0;
|
|
while (i < len && (s[i] == ' ' || s[i] == '\t')) i++;
|
|
if (i < len && (s[i] == '+' || s[i] == '-')) {
|
|
if (s[i] == '-') sign = -1;
|
|
i++;
|
|
}
|
|
for (; i < len; i++) {
|
|
c = s[i];
|
|
if (c == '.') {
|
|
if (seen_dot) error("decimal: multiple '.' in \"" + s + "\".\n");
|
|
seen_dot = 1;
|
|
continue;
|
|
}
|
|
if (c == '_') continue; // digit-group separator
|
|
if (c < '0' || c > '9') error("decimal: bad character in \"" + s + "\".\n");
|
|
seen_digit = 1;
|
|
mant = _add_checked(_mul_checked(mant, 10), c - '0');
|
|
if (seen_dot && ++scale > DECIMAL_MAX_SCALE)
|
|
error("decimal: too many fractional digits in \"" + s + "\".\n");
|
|
}
|
|
if (!seen_digit) error("decimal: no digits in \"" + s + "\".\n");
|
|
return _make(sign < 0 ? -mant : mant, scale);
|
|
}
|
|
|
|
// Construct a decimal from a string ("12.34"), an int, a float (lossy),
|
|
// or an existing decimal (returns an independent copy).
|
|
int *to_decimal(mixed v) {
|
|
if (stringp(v)) return _from_string(v);
|
|
if (intp(v)) return _make(v, 0);
|
|
if (floatp(v)) return _from_string(sprintf("%.*f", DECIMAL_DIV_SCALE, v));
|
|
if (decimalp(v)) return _make(v[DEC_MANT], v[DEC_SCALE]);
|
|
error("decimal: cannot convert " + typeof(v) + " to decimal.\n");
|
|
}
|
|
|
|
// Align a and b to a common scale; returns ({ mant_a, mant_b, scale }).
|
|
private mixed *_align(int *a, int *b) {
|
|
int s = a[DEC_SCALE] > b[DEC_SCALE] ? a[DEC_SCALE] : b[DEC_SCALE];
|
|
return ({ _mul_checked(a[DEC_MANT], _pow10(s - a[DEC_SCALE])),
|
|
_mul_checked(b[DEC_MANT], _pow10(s - b[DEC_SCALE])), s });
|
|
}
|
|
|
|
int *decimal_add(int *a, int *b) {
|
|
mixed *x = _align(a, b);
|
|
return _make(_add_checked(x[0], x[1]), x[2]);
|
|
}
|
|
|
|
int *decimal_sub(int *a, int *b) {
|
|
mixed *x = _align(a, b);
|
|
return _make(_add_checked(x[0], -x[1]), x[2]);
|
|
}
|
|
|
|
int *decimal_mul(int *a, int *b) {
|
|
int m = _mul_checked(a[DEC_MANT], b[DEC_MANT]);
|
|
int s = a[DEC_SCALE] + b[DEC_SCALE];
|
|
while (s > DECIMAL_MAX_SCALE && m % 10 == 0) { m /= 10; s--; }
|
|
if (s > DECIMAL_MAX_SCALE) error("decimal: product scale exceeds maximum.\n");
|
|
return _make(m, s);
|
|
}
|
|
|
|
int *decimal_div(int *a, int *b) {
|
|
int rs, shift, num, q;
|
|
if (b[DEC_MANT] == 0) error("decimal: division by zero.\n");
|
|
rs = a[DEC_SCALE] > b[DEC_SCALE] ? a[DEC_SCALE] : b[DEC_SCALE];
|
|
if (rs < DECIMAL_DIV_SCALE) rs = DECIMAL_DIV_SCALE;
|
|
shift = rs + b[DEC_SCALE] - a[DEC_SCALE];
|
|
num = shift >= 0 ? _mul_checked(a[DEC_MANT], _pow10(shift))
|
|
: a[DEC_MANT] / _pow10(-shift);
|
|
q = num / b[DEC_MANT];
|
|
while (rs > 0 && q % 10 == 0) { q /= 10; rs--; }
|
|
return _make(q, rs);
|
|
}
|
|
|
|
int *decimal_mod(int *a, int *b) {
|
|
mixed *x;
|
|
if (b[DEC_MANT] == 0) error("decimal: modulo by zero.\n");
|
|
x = _align(a, b);
|
|
return _make(x[0] % x[1], x[2]);
|
|
}
|
|
|
|
int *decimal_neg(int *a) {
|
|
return _make(-a[DEC_MANT], a[DEC_SCALE]);
|
|
}
|
|
|
|
// -1 / 0 / 1
|
|
int decimal_cmp(int *a, int *b) {
|
|
mixed *x = _align(a, b);
|
|
return x[0] < x[1] ? -1 : (x[0] > x[1] ? 1 : 0);
|
|
}
|
|
|
|
int decimal_eq(int *a, int *b) { return decimal_cmp(a, b) == 0; }
|
|
int decimal_lt(int *a, int *b) { return decimal_cmp(a, b) < 0; }
|
|
int decimal_gt(int *a, int *b) { return decimal_cmp(a, b) > 0; }
|
|
|
|
string decimal_to_string(int *d) {
|
|
string digits, out;
|
|
int mant = d[DEC_MANT], scale = d[DEC_SCALE], neg = d[DEC_MANT] < 0, intlen;
|
|
// Unsigned magnitude digit string (peel digits to avoid negating the
|
|
// most-negative int).
|
|
digits = "";
|
|
if (mant == 0) {
|
|
digits = "0";
|
|
} else {
|
|
int t = mant, dg;
|
|
while (t != 0) {
|
|
dg = t % 10;
|
|
if (dg < 0) dg = -dg;
|
|
digits = sprintf("%c", '0' + dg) + digits;
|
|
t /= 10;
|
|
}
|
|
}
|
|
if (scale == 0) return (neg ? "-" : "") + digits;
|
|
intlen = strlen(digits) - scale;
|
|
if (intlen <= 0)
|
|
out = "0." + repeat_string("0", -intlen) + digits;
|
|
else
|
|
out = digits[0..intlen - 1] + "." + digits[intlen..];
|
|
return (neg ? "-" : "") + out;
|
|
}
|
|
|
|
int decimal_to_int(int *d) {
|
|
return d[DEC_MANT] / _pow10(d[DEC_SCALE]);
|
|
}
|
|
|
|
float decimal_to_float(int *d) {
|
|
return to_float(d[DEC_MANT]) / to_float(_pow10(d[DEC_SCALE]));
|
|
}
|