Skip to content

Commit 4be7406

Browse files
committed
double-str: render decimal-only (Carp lexer rejects scientific notation)
1 parent 1030595 commit 4be7406

2 files changed

Lines changed: 45 additions & 13 deletions

File tree

carp-reader.carp

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,23 +126,48 @@ strings round-trip readably.")
126126
(Maybe.Just d2) (Double.= d2 d)
127127
(Maybe.Nothing) false))
128128

129+
(private decimal-only?)
130+
(hidden decimal-only?)
131+
(defn decimal-only? [s]
132+
(not (or (String.contains? s \e) (String.contains? s \E))))
133+
134+
(private strip-trailing-zeros)
135+
(hidden strip-trailing-zeros)
136+
(doc strip-trailing-zeros "for a decimal string containing `.`, drops
137+
trailing `0` digits but keeps at least one digit after the decimal point
138+
so the result remains lexically a `Double` (e.g. `\"10.000\"` becomes
139+
`\"10.0\"`, not `\"10.\"`).")
140+
(defn strip-trailing-zeros [s]
141+
(if (not (String.contains? s \.))
142+
@s
143+
(let-do [bs (String.chars s)
144+
n (Array.length &bs)
145+
end n]
146+
(while-do (and (Int.> end 2)
147+
(Char.= @(Array.unsafe-nth &bs (Int.dec end)) \0))
148+
(set! end (Int.dec end)))
149+
(when (Char.= @(Array.unsafe-nth &bs (Int.dec end)) \.)
150+
(set! end (Int.inc end)))
151+
(String.from-chars &(Array.prefix &bs end)))))
152+
129153
(private double-str)
130154
(hidden double-str)
131-
(doc double-str "renders a `Double` so it round-trips and stays
132-
lexically a double. `Double.str` uses `%g` (6 sig figs by default),
133-
which loses precision for values like `3.14159265358979`. We try that
134-
first; if it doesn't parse back to the same `Double`, fall back to
135-
`%.17g` (17 sig figs guarantees exact round-trip for IEEE 754).
136-
Finally, append `.0` when the result has no decimal point or exponent,
137-
so the source reads as a double instead of an int.")
155+
(doc double-str "renders a `Double` as a decimal literal that
156+
round-trips and parses with Carp's lexer (which does NOT accept
157+
scientific notation: only `digits.digits`).
158+
159+
Tries `Double.str` (`%g`, 6 sig figs) first, which is short and works
160+
for values the user wrote concisely. Falls back to `%.17g` (full
161+
round-trip precision). If either uses an exponent, expands to fixed
162+
decimal with `%.30f` and strips trailing zeros. Appends `.0` when the
163+
result has no decimal point so it parses as `Double`, not `Int`.")
138164
(defn double-str [d]
139165
(let-do [s (Double.str d)]
140-
(unless (double-round-trips? &s d) (set! s (Double.format "%.17g" d)))
141-
(if (or
142-
(String.contains? &s \.)
143-
(or (String.contains? &s \e) (String.contains? &s \E)))
144-
s
145-
(String.append &s ".0"))))
166+
(when (or (not (decimal-only? &s)) (not (double-round-trips? &s d)))
167+
(set! s (Double.format "%.17g" d)))
168+
(when (not (decimal-only? &s))
169+
(set! s (strip-trailing-zeros &(Double.format "%.30f" d))))
170+
(if (String.contains? &s \.) s (String.append &s ".0"))))
146171

147172
(defn str [a]
148173
(match-ref a

test/carp-reader.carp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@
168168
"3.14159265358979"
169169
&(Located.str &(first-form "3.14159265358979"))
170170
"high-precision Double round-trips bit-exact")
171+
; Carp's lexer doesn't accept scientific notation (`5e-15`), so very
172+
; small doubles get rendered as plain decimal even at the cost of a
173+
; long output.
174+
(assert-equal t
175+
"0.000000000000005633189227582935"
176+
&(Located.str &(first-form "0.000000000000005633189227582935"))
177+
"tiny Double renders in decimal, not scientific notation")
171178

172179
; --- end positions track the byte after the form ---
173180
(assert-equal t

0 commit comments

Comments
 (0)