diff --git a/.kiro/specs/calculator/design.md b/.kiro/specs/calculator/design.md index 0b6123a..6db8b56 100644 --- a/.kiro/specs/calculator/design.md +++ b/.kiro/specs/calculator/design.md @@ -688,6 +688,9 @@ IEEE 754 float interpretation: - Significand: another color (e.g. green) - Display below the bit grid shows: - Decimal value (exact) + - Value and ULP use scientific notation for very small/large + magnitudes (e.g. a subnormal ULP shows as `1.4e-45`), fixed-point + otherwise, so each row stays on one line - Formula: (-1)^s * 2^(e-bias) * 1.mantissa - Classification: normal, denormal, zero, infinity, NaN - ULP: delta to next representable value diff --git a/engine/src/formatter.zig b/engine/src/formatter.zig index 8e00858..444fff1 100644 --- a/engine/src/formatter.zig +++ b/engine/src/formatter.zig @@ -55,6 +55,24 @@ pub fn formatFloat(buf: []u8, value: f64) FormattedValue { return .{ .display = buf[0..raw_len], .raw = buf[0..raw_len] }; } +/// Format a float for compact single-line display (used by the float view). +/// Uses shortest round-trip fixed-point for normal magnitudes, and scientific +/// notation for very small or very large magnitudes so rows stay readable +/// (e.g. a subnormal ULP prints as "1.4e-45" instead of 45 decimal digits). +/// Non-finite values render as "inf", "-inf", or "nan". +pub fn formatCompactFloat(buf: []u8, value: f64) []const u8 { + if (std.math.isNan(value)) return "nan"; + if (std.math.isPositiveInf(value)) return "inf"; + if (std.math.isNegativeInf(value)) return "-inf"; + + const abs = @abs(value); + const use_scientific = abs != 0 and (abs < 1e-4 or abs >= 1e16); + if (use_scientific) { + return std.fmt.bufPrint(buf, "{e}", .{value}) catch return "ERR"; + } + return std.fmt.bufPrint(buf, "{d}", .{value}) catch return "ERR"; +} + /// Format an integer for programmer mode hex display. /// Display: "FF FF FF FF" (space per byte), byte order per `endian`. /// Raw: "0xFFFFFFFF" (no separators, canonical MSB-first value regardless of @@ -569,3 +587,59 @@ test "formatAscii: 0x7F is non-printable" { const result = formatAscii(&buf, 0x7F, .bits8, .big); try testing.expectEqualStrings(".", result.raw); } + +fn hasChar(s: []const u8, c: u8) bool { + return std.mem.indexOfScalar(u8, s, c) != null; +} + +test "formatCompactFloat: normal magnitudes use fixed-point" { + var buf: [64]u8 = undefined; + try testing.expect(!hasChar(formatCompactFloat(&buf, 1.0), 'e')); + try testing.expect(!hasChar(formatCompactFloat(&buf, 3.14), 'e')); + try testing.expect(!hasChar(formatCompactFloat(&buf, 0.5), 'e')); + try testing.expect(!hasChar(formatCompactFloat(&buf, -2.0), 'e')); + // Just above the small-magnitude threshold stays fixed-point + try testing.expect(!hasChar(formatCompactFloat(&buf, 0.001), 'e')); +} + +test "formatCompactFloat: 3.14 renders exactly" { + var buf: [64]u8 = undefined; + try testing.expectEqualStrings("3.14", formatCompactFloat(&buf, 3.14)); +} + +test "formatCompactFloat: preserves round-trip precision for normal magnitude" { + var buf: [64]u8 = undefined; + // 0.1 stored as f32 then widened: normal magnitude, so fixed-point and full + const v: f64 = @floatCast(@as(f32, 0.1)); + const s = formatCompactFloat(&buf, v); + try testing.expect(!hasChar(s, 'e')); + try testing.expect(std.mem.startsWith(u8, s, "0.100000001")); +} + +test "formatCompactFloat: very small magnitudes use scientific" { + var buf: [64]u8 = undefined; + // smallest f32 subnormal ~ 1.4e-45 + try testing.expect(hasChar(formatCompactFloat(&buf, std.math.ldexp(@as(f64, 1.0), -149)), 'e')); + // f32 ULP of 1.0 = 2^-23 ~ 1.19e-7 + try testing.expect(hasChar(formatCompactFloat(&buf, std.math.ldexp(@as(f64, 1.0), -23)), 'e')); + // f64 ULP of 1.0 = 2^-52 ~ 2.2e-16 + try testing.expect(hasChar(formatCompactFloat(&buf, std.math.ldexp(@as(f64, 1.0), -52)), 'e')); +} + +test "formatCompactFloat: very large magnitudes use scientific" { + var buf: [64]u8 = undefined; + try testing.expect(hasChar(formatCompactFloat(&buf, 1e20), 'e')); + try testing.expect(hasChar(formatCompactFloat(&buf, -1e18), 'e')); +} + +test "formatCompactFloat: zero is fixed-point, not scientific" { + var buf: [64]u8 = undefined; + try testing.expectEqualStrings("0", formatCompactFloat(&buf, 0.0)); +} + +test "formatCompactFloat: non-finite values" { + var buf: [64]u8 = undefined; + try testing.expectEqualStrings("inf", formatCompactFloat(&buf, std.math.inf(f64))); + try testing.expectEqualStrings("-inf", formatCompactFloat(&buf, -std.math.inf(f64))); + try testing.expectEqualStrings("nan", formatCompactFloat(&buf, std.math.nan(f64))); +}