separate fraction digits from scientific notation display
This commit is contained in:
parent
ee33c679bc
commit
8da691d455
7 changed files with 173 additions and 72 deletions
|
|
@ -227,9 +227,10 @@ still required (FR-7.7): the mouse never becomes the only way to do something.
|
|||
### NFR-7: Number Display & Formatting
|
||||
|
||||
- Decimal numbers must use comma grouping for display (e.g., `4,294,967,295`).
|
||||
- **Inexact (f64) values**: scientific notation only when absolute value > 10^15 / < 10^-15. Never jump to scientific notation for values that fit in a readable decimal. This bound is not a readability preference: 10^15 is where f64 stops distinguishing consecutive integers (2^53 ~ 9.007 x 10^15), so printing a plain integer past it would assert precision the value does not have.
|
||||
- **Inexact (f64) values**: scientific notation only when absolute value > 10^15 / < 10^-15. Never jump to scientific notation for values that fit in a readable decimal. The upper bound is not a readability preference: 10^15 is where f64 stops distinguishing consecutive integers (2^53 ~ 9.007 x 10^15), so printing a plain integer past it would assert precision the value does not have. The lower bound is a readability choice, and it is therefore a frontend's to make: it is `Number.FormatOptions.scientific_below_exponent`, which both shipped frontends set to 15.
|
||||
- (An earlier draft also triggered scientific notation past 15 significant digits. That clause was never implemented and was wrong: read literally it renders `0.9999999999999998` as `9.999999999999998e-1`, which is worse.)
|
||||
- **Exact values**: the 10^15 bound above must NOT apply. It exists because of f64's precision cliff, and an exact value has no such cliff, so applying it would contradict NFR-9.1 - `9007199254740993` is ~9.007 x 10^15 and would render as `9.007199254740993e15`, which is precisely the bug NFR-9.1 forbids. Exact values instead have a readability cap on integer digits, above which the display abbreviates to scientific notation while the clipboard form retains every digit. The cap is not the engine's to choose: it is a field of `Number.FormatOptions`, which every frontend supplies (`max_integer_digits`, with `null` meaning never abbreviate, which is what the clipboard form asks for).
|
||||
- **Exact values**: the 10^15 upper bound above must NOT apply. It exists because of f64's precision cliff, and an exact value has no such cliff, so applying it would contradict NFR-9.1 - `9007199254740993` is ~9.007 x 10^15 and would render as `9.007199254740993e15`, which is precisely the bug NFR-9.1 forbids. Exact values instead have a readability cap on integer digits, above which the display abbreviates to scientific notation while the clipboard form retains every digit. The cap is not the engine's to choose: it is a field of `Number.FormatOptions`, which every frontend supplies (`max_integer_digits`, with `null` meaning never abbreviate, which is what the clipboard form asks for).
|
||||
- The 10^-15 **lower** bound does apply to exact values, and for the opposite reason to the upper one: at that magnitude a fixed rendering has spent its whole fractional budget on leading zeros and retains a handful of significant digits, where the scientific form retains seventeen. Abbreviating there loses less, not more. Both tiers therefore share one threshold, so the same magnitude reads the same way whichever tier produced it.
|
||||
- The cap must exceed the values the exact tier exists to serve: `9007199254740993` (16 digits) and `2^128` (39 digits). It exists at all because without any cap, `factorial(171)` renders 310 digits and `1.5e300 * 10` renders 301: accurate but unreadable.
|
||||
- **PROVISIONALLY 40 digits, pending review.** Chosen as the smallest round number above `2^128`. Not derived from any measured preference.
|
||||
- Programmer mode hex values display with space-separated bytes (e.g., `FF FF FF FF`).
|
||||
|
|
|
|||
|
|
@ -868,29 +868,42 @@ Each job went to the type that owns it:
|
|||
Two rules stayed in the engine because they are facts about values rather than
|
||||
preferences about screens: the 2^53 bound, past which an f64's fixed rendering would
|
||||
invent digits (so an inexact value goes scientific there no matter what the caller's
|
||||
budget says), and that a value needing more fractional digits than the budget allows
|
||||
abbreviates rather than being silently shortened. `fraction_digits` means the same
|
||||
thing to both tiers: an exact expansion is rounded there, and a float, whose shortest
|
||||
round-trip form is never rounded, goes scientific when its digits do not fit. That
|
||||
one rule replaced five thresholds, including `formatCompactFloat`'s separate
|
||||
`1e-4`/`1e16` pair.
|
||||
budget says), and that a value is never handed back rendered as zero.
|
||||
|
||||
The float arm renders onto a stack buffer sized by `std.fmt.float.bufferSize(.decimal,
|
||||
`FormatOptions` carries two fractional numbers because the callers ask two different
|
||||
questions, and collapsing them onto one is what left the tiers disagreeing (below):
|
||||
|
||||
- `fraction_digits` is where both tiers round. An exact expansion is divided out to
|
||||
that many places; a float whose shortest round-trip form is longer is rounded to
|
||||
the same place with `std.fmt.float.render`, trailing zeros trimmed, so a rounded
|
||||
float and a rounded rational of one value produce identical text.
|
||||
- `scientific_below_exponent` is where fixed notation stops being worth it, counted
|
||||
in leading fractional zeros. A result line wants 20 and 15: round at twenty
|
||||
places, abbreviate below 10^-15. The float view wants 17 and 6: show every digit
|
||||
an f64 has, but send a subnormal ULP to scientific rather than print 45 places.
|
||||
|
||||
That split is what let the abbreviation decision become a single function both arms
|
||||
call, so the tier a value came from cannot change the shape of the output.
|
||||
|
||||
The float arm renders onto stack buffers sized by `std.fmt.float.bufferSize(.decimal,
|
||||
f64)` (347 bytes) and allocates once, so a caller can hand it a
|
||||
`FixedBufferAllocator` sized for the text it asked for. `src/tui/float_view.zig` does
|
||||
exactly that: an 80-byte stack buffer, no heap in the draw path, "?" if it does not
|
||||
fit.
|
||||
|
||||
Behaviour changed in two bands, both deliberate, both making the tiers agree:
|
||||
Behaviour changed in three bands, all deliberate, all making the tiers agree:
|
||||
|
||||
- An inexact value in [1e15, 2^53) now prints in full instead of scientific. Those
|
||||
digits are real; the old `> 1e15` test was a display preference sitting in front
|
||||
of the representability bound that actually matters.
|
||||
- An inexact value whose shortest form fits the fractional budget now prints fixed
|
||||
instead of scientific, which is what the exact tier already did for the same
|
||||
magnitude: a 1e-16 result no longer reads differently depending on which tier
|
||||
produced it. One that does not fit (`pi * 1e-17` needs 33 digits) still
|
||||
abbreviates, as before.
|
||||
- An exact value below 10^-15 now abbreviates, where before it printed fixed text
|
||||
until the expansion rounded to literal zero at 10^-20. That was the band where the
|
||||
tiers visibly disagreed: `1/3000000000000000000` read as `0.00000000000000000033`,
|
||||
two significant digits out of twenty, while a float of the same size read as
|
||||
`3.3333333333333334e-19` and kept seventeen. The tier that knew more showed less.
|
||||
- An inexact value inside the window but longer than the budget is now rounded rather
|
||||
than abbreviated, which is what the exact tier always did. `pi * 1e-14` reads
|
||||
`0.00000000000003141593` from either tier.
|
||||
|
||||
Also found while checking callers: nothing consumes the `raw` form of an integer
|
||||
rendering, and there is no clipboard code in the TUI at all. The prefixed form is kept
|
||||
|
|
|
|||
|
|
@ -1613,6 +1613,7 @@ test "a grouped literal past 2^53 is still exact" {
|
|||
try testing.expect(value == .exact);
|
||||
const shown = try value.render(testing.allocator, .{
|
||||
.fraction_digits = 20,
|
||||
.scientific_below_exponent = 15,
|
||||
.max_integer_digits = null,
|
||||
.significant_digits = 17,
|
||||
.separators = false,
|
||||
|
|
|
|||
|
|
@ -1519,6 +1519,7 @@ test "money: groups the same way an ordinary result does" {
|
|||
var value = @import("number.zig").Number.fromFloat(231677);
|
||||
const as_value = try value.render(testing.allocator, .{
|
||||
.fraction_digits = 20,
|
||||
.scientific_below_exponent = 15,
|
||||
.max_integer_digits = 40,
|
||||
.significant_digits = 17,
|
||||
.separators = true,
|
||||
|
|
|
|||
|
|
@ -346,13 +346,23 @@ pub const Number = union(enum) {
|
|||
/// Deliberately without defaults: a frontend that forgets to decide gets a
|
||||
/// compile error instead of silently inheriting someone else's screen.
|
||||
pub const FormatOptions = struct {
|
||||
/// Fractional digits the text may use.
|
||||
///
|
||||
/// One rule for both tiers: an exact expansion is rounded here, and a
|
||||
/// float whose shortest round-trip form needs more than this abbreviates
|
||||
/// to scientific rather than being silently shortened. Exact arithmetic
|
||||
/// Fractional digits the text may use. Both tiers round here: an exact
|
||||
/// expansion is divided out to this many places, and a float whose
|
||||
/// shortest round-trip form is longer is rounded to it. Exact arithmetic
|
||||
/// can justify more digits than f64's ~17 significant ones.
|
||||
fraction_digits: usize,
|
||||
/// Where fixed notation stops being worth it, as a count of fractional
|
||||
/// places: 20 means a value whose first significant digit falls past the
|
||||
/// twentieth place abbreviates to scientific rather than spending the
|
||||
/// budget on leading zeros.
|
||||
///
|
||||
/// Separate from `fraction_digits` because they answer different
|
||||
/// questions. A result line wants both at 20: round at twenty places, and
|
||||
/// abbreviate only when a value is too small to show there at all. A view
|
||||
/// of an f64's own expansion wants a wide budget and a narrow window (17
|
||||
/// and 6), so it shows every digit the float has but sends a subnormal ULP
|
||||
/// to scientific instead of printing 45 places.
|
||||
scientific_below_exponent: usize,
|
||||
/// Integer digits shown in full before the text abbreviates to
|
||||
/// scientific notation. `null` never abbreviates, which is what a
|
||||
/// clipboard or a file wants (NFR-9.9, requirements.md line 237).
|
||||
|
|
@ -402,17 +412,7 @@ pub const Number = union(enum) {
|
|||
fn renderExact(r: Rational, allocator: Allocator, options: FormatOptions) Error!Rendered {
|
||||
const decimal = try r.toDecimalString(allocator, options.fraction_digits);
|
||||
|
||||
const abbreviate = blk: {
|
||||
if (options.max_integer_digits) |limit| {
|
||||
if (grouping.integerDigitCount(decimal.text) > limit) break :blk true;
|
||||
}
|
||||
// The other end of the same problem: a value below the fractional
|
||||
// budget renders as all zeros, which loses it completely rather than
|
||||
// merely rounding it (2^-70 as "0.00000000000000000000").
|
||||
break :blk isZeroText(decimal.text) and !r.isZero();
|
||||
};
|
||||
|
||||
if (abbreviate) {
|
||||
if (abbreviates(decimal.text, r.isZero(), options)) {
|
||||
allocator.free(decimal.text);
|
||||
return .{
|
||||
.text = try r.toScientificString(allocator, options.significant_digits),
|
||||
|
|
@ -431,34 +431,85 @@ pub const Number = union(enum) {
|
|||
// the stack and only the final text is allocated. That is what keeps a
|
||||
// caller's `FixedBufferAllocator` sized for the text it asked for rather
|
||||
// than for the widest thing an f64 can spell (347 bytes, for a subnormal).
|
||||
var stack: [std.fmt.float.bufferSize(.decimal, f64)]u8 = undefined;
|
||||
var shortest_buf: [std.fmt.float.bufferSize(.decimal, f64)]u8 = undefined;
|
||||
var rounded_buf: [std.fmt.float.bufferSize(.decimal, f64)]u8 = undefined;
|
||||
|
||||
// "inf", "-inf" and "nan" are the whole of what the value is.
|
||||
if (!std.math.isFinite(f)) {
|
||||
return dupeText(allocator, printFloat(&stack, "{d}", f), false);
|
||||
return dupeText(allocator, printFloat(&shortest_buf, "{d}", f), false);
|
||||
}
|
||||
if (@abs(f) >= f64_exact_integer_limit) {
|
||||
return dupeText(allocator, printFloat(&stack, "{e}", f), true);
|
||||
return dupeText(allocator, printFloat(&shortest_buf, "{e}", f), true);
|
||||
}
|
||||
|
||||
// Shortest round-trip: every digit of `fixed` is a digit the f64 has.
|
||||
const fixed = printFloat(&stack, "{d}", f);
|
||||
// Shortest round-trip: every digit of this is a digit the f64 has.
|
||||
const shortest = printFloat(&shortest_buf, "{d}", f);
|
||||
|
||||
const abbreviate = blk: {
|
||||
if (options.max_integer_digits) |limit| {
|
||||
if (grouping.integerDigitCount(fixed) > limit) break :blk true;
|
||||
}
|
||||
break :blk fractionDigitCount(fixed) > options.fraction_digits;
|
||||
};
|
||||
// Past the budget it is rounded, the same as an exact expansion would be.
|
||||
// Trailing zeros are dropped so a rounded float and a rounded rational of
|
||||
// the same value produce the same text.
|
||||
var fixed = shortest;
|
||||
var rounded = false;
|
||||
if (fractionDigitCount(shortest) > options.fraction_digits) {
|
||||
fixed = trimTrailingZeros(std.fmt.float.render(&rounded_buf, f, .{
|
||||
.mode = .decimal,
|
||||
.precision = options.fraction_digits,
|
||||
}) catch @panic("f64 text exceeded std.fmt.float.bufferSize"));
|
||||
rounded = true;
|
||||
}
|
||||
|
||||
if (abbreviate) {
|
||||
// Reuses `stack`, so `fixed` is dead from here. The scientific form is
|
||||
if (abbreviates(fixed, f == 0, options)) {
|
||||
// Reuses a buffer, so `fixed` is dead from here. The scientific form is
|
||||
// far shorter than the fixed one it replaces, so it still fits.
|
||||
return dupeText(allocator, printFloat(&stack, "{e}", f), true);
|
||||
return dupeText(allocator, printFloat(&shortest_buf, "{e}", f), true);
|
||||
}
|
||||
|
||||
if (!options.separators) return dupeText(allocator, fixed, false);
|
||||
return .{ .text = try groupText(allocator, fixed), .truncated = false };
|
||||
if (!options.separators) return dupeText(allocator, fixed, rounded);
|
||||
return .{ .text = try groupText(allocator, fixed), .truncated = rounded };
|
||||
}
|
||||
|
||||
/// Whether fixed `text` should give way to scientific notation. The one
|
||||
/// decision both arms make, so the tier a value came from cannot change the
|
||||
/// shape of the output.
|
||||
fn abbreviates(text: []const u8, is_zero: bool, options: FormatOptions) bool {
|
||||
if (options.max_integer_digits) |limit| {
|
||||
if (grouping.integerDigitCount(text) > limit) return true;
|
||||
}
|
||||
if (leadingFractionZeros(text)) |zeros| {
|
||||
if (zeros >= options.scientific_below_exponent) return true;
|
||||
}
|
||||
// Whatever the budgets say, never hand back a non-zero value rendered as
|
||||
// zero: 2^-70 as "0.00000000000000000000" loses it completely rather than
|
||||
// merely rounding it. Unreachable while `scientific_below_exponent` is at
|
||||
// or below `fraction_digits`, which is why it is a guard and not the rule.
|
||||
return !is_zero and isZeroText(text);
|
||||
}
|
||||
|
||||
/// Fractional places before the first significant digit: 0 for "0.5", 3 for
|
||||
/// "0.000123". How far out the value starts, which is what decides whether
|
||||
/// fixed notation can show it.
|
||||
///
|
||||
/// Null when a significant digit sits in the integer part, since such a value
|
||||
/// cannot be lost to leading zeros however narrow the window is.
|
||||
fn leadingFractionZeros(text: []const u8) ?usize {
|
||||
const dot = std.mem.indexOfScalar(u8, text, '.') orelse return null;
|
||||
if (!isZeroText(text[0..dot])) return null;
|
||||
var zeros: usize = 0;
|
||||
for (text[dot + 1 ..]) |ch| {
|
||||
if (ch != '0') break;
|
||||
zeros += 1;
|
||||
}
|
||||
return zeros;
|
||||
}
|
||||
|
||||
/// Drop the zeros a fixed precision pads with, and the point if nothing is left
|
||||
/// after it. An exact expansion never has them, so this keeps the two arms
|
||||
/// producing the same text for the same value.
|
||||
fn trimTrailingZeros(text: []const u8) []const u8 {
|
||||
if (std.mem.indexOfScalar(u8, text, '.') == null) return text;
|
||||
const trimmed = std.mem.trimEnd(u8, text, "0");
|
||||
if (std.mem.endsWith(u8, trimmed, ".")) return trimmed[0 .. trimmed.len - 1];
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/// Format into a buffer already known to be large enough for any f64.
|
||||
|
|
@ -497,15 +548,11 @@ pub const Number = union(enum) {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Fractional digits in decimal text, which is the budget the text spends.
|
||||
/// Fractional digits in decimal text.
|
||||
///
|
||||
/// The float arm's form of the check the exact arm makes with `isZeroText`:
|
||||
/// both ask "did this value need more fractional digits than the caller
|
||||
/// allows". The exact arm can ask after the fact because `toDecimalString`
|
||||
/// rounds at the budget and a value below it rounds to all zeros. A float's
|
||||
/// shortest round-trip text is never rounded, so its digits are counted
|
||||
/// instead, and a value needing more than the budget abbreviates rather than
|
||||
/// being silently shortened.
|
||||
/// The float arm asks this to decide whether the shortest round-trip form fits
|
||||
/// the caller's budget or has to be rounded to it. The exact arm never needs it:
|
||||
/// `toDecimalString` divides out to the budget and stops.
|
||||
fn fractionDigitCount(text: []const u8) usize {
|
||||
const dot = std.mem.indexOfScalar(u8, text, '.') orelse return 0;
|
||||
return text.len - dot - 1;
|
||||
|
|
@ -527,6 +574,7 @@ const alloc = testing.allocator;
|
|||
fn expectDecimal(expected: []const u8, expected_exact: bool, n: Number, digits: usize) !void {
|
||||
const shown = try n.render(alloc, .{
|
||||
.fraction_digits = digits,
|
||||
.scientific_below_exponent = digits,
|
||||
.max_integer_digits = null,
|
||||
.significant_digits = 17,
|
||||
.separators = false,
|
||||
|
|
@ -1155,6 +1203,7 @@ test "OOM safety: rendering" {
|
|||
/// integer digits. `src/main.zig` and `src/tui.zig` declare the same thing.
|
||||
const display_budget: Number.FormatOptions = .{
|
||||
.fraction_digits = 20,
|
||||
.scientific_below_exponent = 15,
|
||||
.max_integer_digits = 40,
|
||||
.significant_digits = 17,
|
||||
.separators = false,
|
||||
|
|
@ -1163,6 +1212,7 @@ const display_budget: Number.FormatOptions = .{
|
|||
/// The same, grouped, which is the form that reaches a screen.
|
||||
const grouped_budget: Number.FormatOptions = .{
|
||||
.fraction_digits = 20,
|
||||
.scientific_below_exponent = 15,
|
||||
.max_integer_digits = 40,
|
||||
.significant_digits = 17,
|
||||
.separators = true,
|
||||
|
|
@ -1172,16 +1222,18 @@ const grouped_budget: Number.FormatOptions = .{
|
|||
/// (requirements.md line 237).
|
||||
const clipboard_budget: Number.FormatOptions = .{
|
||||
.fraction_digits = 20,
|
||||
.scientific_below_exponent = 15,
|
||||
.max_integer_digits = null,
|
||||
.significant_digits = 17,
|
||||
.separators = false,
|
||||
};
|
||||
|
||||
/// What the float view asks for: single line, no separators, and 17 fractional
|
||||
/// digits, which is every digit an f64 has. A value needing more than that is one
|
||||
/// no row wants to show in full, such as a subnormal ULP.
|
||||
/// What the float view asks for: a wide digit budget and a narrow window, so a
|
||||
/// value shows every digit an f64 has, and one starting past the sixth fractional
|
||||
/// place goes scientific instead of printing leading zeros.
|
||||
const compact_budget: Number.FormatOptions = .{
|
||||
.fraction_digits = 17,
|
||||
.scientific_below_exponent = 6,
|
||||
.max_integer_digits = 16,
|
||||
.significant_digits = 17,
|
||||
.separators = false,
|
||||
|
|
@ -1235,8 +1287,9 @@ test "render: exact terminating fractions print in full" {
|
|||
try expectRenderExact("0.125", "0.125", grouped_budget);
|
||||
try expectRenderExact("1,234,567.25", "1234567.25", grouped_budget);
|
||||
try expectRenderExact("1,234,567.891", "1234567.891", grouped_budget);
|
||||
// The last magnitude a 20-digit budget can show.
|
||||
try expectRenderExact("0.00000000000000000001", "0.00000000000000000001", grouped_budget);
|
||||
// Inside the window: 14 leading zeros, so fixed notation still shows it. One
|
||||
// decade smaller and it abbreviates, whatever the fractional budget allows.
|
||||
try expectRenderExact("0.000000000000001", "0.000000000000001", grouped_budget);
|
||||
}
|
||||
|
||||
test "render: 0.1 + 0.2 renders as 0.3" {
|
||||
|
|
@ -1414,16 +1467,44 @@ test "render: an inexact value past the integer budget abbreviates" {
|
|||
try expectRender("-1e18", true, Number.fromFloat(-1e18), compact_budget);
|
||||
}
|
||||
|
||||
test "render: an inexact value needing more digits than the budget goes scientific" {
|
||||
// One rule for both arms: the text may spend at most `fraction_digits` on a
|
||||
// fraction. An exact 1/3 is rounded there; a float, whose shortest form is
|
||||
// never rounded, abbreviates instead of being silently shortened.
|
||||
test "render: past the window a value abbreviates, whichever tier it came from" {
|
||||
// The two budgets answer different questions, and this is the one the window
|
||||
// answers: a value whose first significant digit falls past the fifteenth
|
||||
// fractional place has no fixed form worth printing, so both tiers abbreviate
|
||||
// and the same magnitude reads the same way either way.
|
||||
var exact = try Number.parse(alloc, "0.00000000000000015");
|
||||
defer exact.deinit();
|
||||
try expectRender("1.5e-16", true, exact, grouped_budget);
|
||||
try expectRender("1.5e-16", true, Number.fromFloat(1.5e-16), grouped_budget);
|
||||
|
||||
// Before the window was separate from the budget, this band was where the
|
||||
// tiers disagreed: an exact value kept two significant digits in fixed text
|
||||
// while a float of the same size went scientific and kept seventeen.
|
||||
var band = try Number.parse(alloc, "0.00000000000000000033333333333333333");
|
||||
defer band.deinit();
|
||||
try expectRender("3.3333333333333333e-19", true, band, grouped_budget);
|
||||
// The nearest f64 to that decimal is a slightly different number, so its last
|
||||
// digit differs. The form is what this is about: both abbreviate.
|
||||
try expectRender("3.3333333333333334e-19", true, Number.fromFloat(3.3333333333333333e-19), grouped_budget);
|
||||
|
||||
try expectRender("1.5e-21", true, Number.fromFloat(1.5e-21), grouped_budget);
|
||||
// 33 fractional digits, past a 20-digit budget.
|
||||
try expectRender("3.1415926535897935e-17", true, Number.fromFloat(std.math.pi * 1e-17), grouped_budget);
|
||||
// 17, inside it, and the same text the exact tier gives for this magnitude.
|
||||
try expectRender("0.00000000000000015", false, Number.fromFloat(1.5e-16), grouped_budget);
|
||||
}
|
||||
|
||||
test "render: inside the window a value longer than the budget is rounded, not abbreviated" {
|
||||
// 13 leading zeros, so fixed notation applies; 30 fractional digits, so the
|
||||
// 20-digit budget rounds it. The float is rounded the same way an exact
|
||||
// expansion is, and both produce the same text.
|
||||
var exact = try Number.parse(alloc, "0.000000000000031415926535897932");
|
||||
defer exact.deinit();
|
||||
try expectRender("0.00000000000003141593", true, exact, grouped_budget);
|
||||
try expectRender("0.00000000000003141593", true, Number.fromFloat(std.math.pi * 1e-14), grouped_budget);
|
||||
}
|
||||
|
||||
test "render: a float shorter than the budget keeps every digit it has" {
|
||||
try expectRender("0.001", false, Number.fromFloat(0.001), grouped_budget);
|
||||
try expectRender("0.1", false, Number.fromFloat(0.1), grouped_budget);
|
||||
// 16 digits, inside a 20-digit budget, so nothing is rounded away.
|
||||
try expectRender("0.3333333333333333", false, Number.fromFloat(1.0 / 3.0), grouped_budget);
|
||||
}
|
||||
|
||||
test "render: scientific text is never grouped" {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ pub const Mode = enum { standard, programmer };
|
|||
/// precision"). These are the values the engine used to hold.
|
||||
const display_format: engine.Number.FormatOptions = .{
|
||||
.fraction_digits = 20,
|
||||
.scientific_below_exponent = 15,
|
||||
.max_integer_digits = 40,
|
||||
.significant_digits = 17,
|
||||
.separators = true,
|
||||
|
|
|
|||
11
src/tui.zig
11
src/tui.zig
|
|
@ -30,6 +30,7 @@ const Mode = enum { standard, programmer, financial, convert };
|
|||
/// the clipboard will ask for `clipboard_format` instead.
|
||||
pub const display_format: engine.Number.FormatOptions = .{
|
||||
.fraction_digits = 20,
|
||||
.scientific_below_exponent = 15,
|
||||
.max_integer_digits = 40,
|
||||
.significant_digits = 17,
|
||||
.separators = true,
|
||||
|
|
@ -38,12 +39,14 @@ pub const display_format: engine.Number.FormatOptions = .{
|
|||
/// A single line with no separators, for a value shown beside other text rather
|
||||
/// than as the result: the float view's rows and the convert view's factor.
|
||||
///
|
||||
/// 17 fractional digits is every digit an f64 actually has, so a value whose
|
||||
/// shortest form fits shows in full (an f32-rounded 0.1 reads
|
||||
/// "0.100000001490116"), and one that does not, such as a subnormal ULP, goes
|
||||
/// scientific instead of spending the row on 45 digits.
|
||||
/// A wide digit budget and a narrow window: 17 fractional digits is every digit an
|
||||
/// f64 has, so a value shows its own expansion in full (an f32-rounded 0.1 reads
|
||||
/// "0.100000001490116"), while anything starting past the sixth place goes
|
||||
/// scientific rather than spending the row on leading zeros. That is what keeps a
|
||||
/// subnormal ULP readable as "1.4e-45".
|
||||
pub const compact_format: engine.Number.FormatOptions = .{
|
||||
.fraction_digits = 17,
|
||||
.scientific_below_exponent = 6,
|
||||
.max_integer_digits = 16,
|
||||
.significant_digits = 17,
|
||||
.separators = false,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue