Skip to content

Commit 99acdd2

Browse files
committed
feat(bigdecimal): .toPlainString — always-plain rendering shared with toString's plain arm
Smell-audited: 1: the plain-digit body was extracted from writeBigDecimalDigits into a shared writePlainDigits (+ the always-plain writeBigDecimalPlain entry) instead of a second copy — F-011 commonization; no other smell. D-564's cheapest named residual: (BigDecimal. "1E+3") .toPlainString → "1000" while .toString keeps "1E+3" (JVM-exact); negative-scale zero append + sub-1 zero padding covered. Corpus bigdecimal_plain.txt (6/6 clj-oracle matched); compat_tiers BigDecimal row updated (the compat_members gate enforces it); D-564 narrowed.
1 parent a7c95e6 commit 99acdd2

5 files changed

Lines changed: 79 additions & 19 deletions

File tree

‎.dev/debt.yaml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ active:
4040
class_name.zig / host_class_resolve / rt.types keys / compat rows
4141
from a single table — prerequisite: ADR-0174, landed). Named member
4242
residuals the D9 refresh surfaced (removed from compat_tiers as rot,
43-
not implemented this campaign): BigDecimal .toPlainString (commonly
44-
used; siblings all exist — cheap wire), ZonedDateTime full build-out
43+
not implemented this campaign): BigDecimal .toPlainString DISCHARGED
44+
2026-07-17 (shared print.writeBigDecimalPlain body + corpus
45+
bigdecimal_plain.txt, 6/6 oracle), ZonedDateTime full build-out
4546
(= D-105), Socket/MessageDigest build-out (= D-106), and the
4647
instance-method fills judged out of ADR-0174 scope (StringBuilder
4748
insert/delete/substring set beyond the landed 8, File

‎data/compat_tiers.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ tiers:
608608
files:
609609
surface: runtime/java/math/BigDecimal.zig
610610
impl: runtime/numeric/big_decimal.zig
611-
methods: [abs, add, compareTo, divide, divideAndRemainder, divideToIntegralValue, doubleValue, intValue, longValue, max, min, movePointLeft, movePointRight, multiply, negate, pow, precision, remainder, round, scale, scaleByPowerOfTen, setScale, signum, stripTrailingZeros, subtract, toBigInteger, ulp, unscaledValue, valueOf]
611+
methods: [abs, add, compareTo, divide, divideAndRemainder, divideToIntegralValue, doubleValue, intValue, longValue, max, min, movePointLeft, movePointRight, multiply, negate, pow, precision, remainder, round, scale, scaleByPowerOfTen, setScale, signum, stripTrailingZeros, subtract, toBigInteger, toPlainString, ulp, unscaledValue, valueOf]
612612
static_fields: ["ONE", "ROUND_CEILING", "ROUND_DOWN", "ROUND_FLOOR", "ROUND_HALF_DOWN", "ROUND_HALF_EVEN", "ROUND_HALF_UP", "ROUND_UNNECESSARY", "ROUND_UP", "TEN", "TWO", "ZERO"]
613613
clojure_peer_vars: [clojure.core/bigdec]
614614
# java.math.RoundingMode — the 8 enum constants (UP..UNNECESSARY) as

‎src/runtime/java/math/BigDecimal.zig‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ fn scaleFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) an
4747
return Value.initInteger(big_decimal.asScale(args[0]));
4848
}
4949

50+
/// `(.toPlainString bd)` — the value WITHOUT exponent notation, whatever the
51+
/// scale (`(BigDecimal. "1E+3")` → "1000"; `.toString` keeps "1E+3").
52+
/// JVM `BigDecimal.toPlainString()`. Rendering shared with the toString
53+
/// plain arm via `print.writeBigDecimalPlain` (D-564 residual).
54+
fn toPlainStringFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
55+
_ = env;
56+
try error_catalog.checkArity("toPlainString", args, 1, loc);
57+
try requireBd(args[0], "toPlainString", loc);
58+
var aw: std.Io.Writer.Allocating = .init(rt.gpa);
59+
defer aw.deinit();
60+
try print_mod.writeBigDecimalPlain(&aw.writer, args[0]);
61+
return string_collection.alloc(rt, aw.writer.buffered());
62+
}
63+
5064
/// `(.signum bd)` — -1 / 0 / 1 by the sign of the value. JVM `BigDecimal.signum()`.
5165
fn signumFn(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
5266
_ = rt;
@@ -494,6 +508,7 @@ pub fn installNativeMethods(rt: *Runtime) !void {
494508
const specs = .{
495509
.{ "setScale", &setScale },
496510
.{ "scale", &scaleFn },
511+
.{ "toPlainString", &toPlainStringFn },
497512
.{ "signum", &signumFn },
498513
.{ "unscaledValue", &unscaledValueFn },
499514
.{ "precision", &precisionFn },

‎src/runtime/print.zig‎

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,53 @@ fn printBigDecimal(w: *Writer, v: Value) anyerror!void {
10851085
try w.writeByte('M');
10861086
}
10871087

1088+
/// Plain (no-exponent) rendering of unsigned `digits` at `scale` — the body
1089+
/// both `BigDecimal.toString`'s plain arm and `.toPlainString` share.
1090+
/// scale < 0 appends zeros (unscaled × 10^-scale); scale > 0 places the
1091+
/// decimal point, zero-padding a sub-1 magnitude.
1092+
fn writePlainDigits(w: *Writer, digits: []const u8, scale: i32) anyerror!void {
1093+
if (scale == 0) {
1094+
try w.writeAll(digits);
1095+
} else if (scale < 0) {
1096+
try w.writeAll(digits);
1097+
const zeros: usize = @intCast(-@as(i64, scale));
1098+
for (0..zeros) |_| try w.writeByte('0');
1099+
} else {
1100+
const scale_u: usize = @intCast(scale);
1101+
if (scale_u >= digits.len) {
1102+
try w.writeAll("0.");
1103+
for (0..scale_u - digits.len) |_| try w.writeByte('0');
1104+
try w.writeAll(digits);
1105+
} else {
1106+
const dot_pos = digits.len - scale_u;
1107+
try w.writeAll(digits[0..dot_pos]);
1108+
try w.writeByte('.');
1109+
try w.writeAll(digits[dot_pos..]);
1110+
}
1111+
}
1112+
}
1113+
1114+
/// JVM `BigDecimal.toPlainString` — ALWAYS plain notation, whatever the
1115+
/// scale/adjusted exponent (`1E+3M` renders "1000", `1E-10M` renders
1116+
/// "0.0000000001"). The `.toPlainString` surface method writes through this;
1117+
/// `writeBigDecimalDigits` (= toString) uses the same digit body only in its
1118+
/// plain arm.
1119+
pub fn writeBigDecimalPlain(w: *Writer, v: Value) anyerror!void {
1120+
const bd = v.decodePtr(*const big_decimal_mod.BigDecimal);
1121+
var buf: [1024]u8 = undefined;
1122+
var sw: std.Io.Writer = .fixed(&buf);
1123+
sw.print("{f}", .{bd.unscaled.m}) catch {
1124+
// Unscaled wider than the buffer — lossy digits-only fallback,
1125+
// mirroring writeBigDecimalDigits' stance.
1126+
return w.print("{f}", .{bd.unscaled.m});
1127+
};
1128+
const written = buf[0..sw.end];
1129+
const neg = written.len > 0 and written[0] == '-';
1130+
const digits = if (neg) written[1..] else written;
1131+
if (neg) try w.writeByte('-');
1132+
try writePlainDigits(w, digits, bd.scale);
1133+
}
1134+
10881135
pub fn writeBigDecimalDigits(w: *Writer, v: Value) anyerror!void {
10891136
// value = unscaled * 10^(-scale). Reproduces JVM `BigDecimal.toString`:
10901137
// plain notation when `scale >= 0` AND the adjusted exponent
@@ -1107,22 +1154,7 @@ pub fn writeBigDecimalDigits(w: *Writer, v: Value) anyerror!void {
11071154

11081155
if (neg) try w.writeByte('-');
11091156
if (bd.scale >= 0 and adj_exp >= -6) {
1110-
// Plain notation (toPlainString).
1111-
if (bd.scale == 0) {
1112-
try w.writeAll(digits);
1113-
} else {
1114-
const scale_u: usize = @intCast(bd.scale);
1115-
if (scale_u >= digits.len) {
1116-
try w.writeAll("0.");
1117-
for (0..scale_u - digits.len) |_| try w.writeByte('0');
1118-
try w.writeAll(digits);
1119-
} else {
1120-
const dot_pos = digits.len - scale_u;
1121-
try w.writeAll(digits[0..dot_pos]);
1122-
try w.writeByte('.');
1123-
try w.writeAll(digits[dot_pos..]);
1124-
}
1125-
}
1157+
try writePlainDigits(w, digits, bd.scale);
11261158
} else {
11271159
// Scientific: one digit, optional fraction, `E`, signed exponent.
11281160
try w.writeByte(digits[0]);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
(.toPlainString 1.5M)
2+
;;=> "1.5"
3+
(.toPlainString (java.math.BigDecimal. "1E+3"))
4+
;;=> "1000"
5+
(.toPlainString 0.00001M)
6+
;;=> "0.00001"
7+
(.toPlainString -2.50M)
8+
;;=> "-2.50"
9+
(.toPlainString 123M)
10+
;;=> "123"
11+
(.toPlainString (java.math.BigDecimal. "1E-10"))
12+
;;=> "0.0000000001"

0 commit comments

Comments
 (0)