Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions internal/object/float_tos_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package object

import (
"math"
"testing"
)

// TestFloatToSMatchesMRI pins Float#to_s against MRI's flo_to_s (numeric.c):
// the shortest round-tripping digits are laid out in fixed notation while the
// decimal point falls within -3..DBL_DIG (15), and in exponential notation
// outside that, where the mantissa always carries a fractional digit.
//
// Every expected value is what `ruby` 4.0.5 prints for the same literal.
func TestFloatToSMatchesMRI(t *testing.T) {
cases := []struct {
in float64
want string
}{
// Fixed notation, including the "1.0 not 1" rule.
{0, "0.0"},
{math.Copysign(0, -1), "-0.0"},
{1, "1.0"},
{3.14, "3.14"},
{-2.5, "-2.5"},
{0.1, "0.1"},
{100, "100.0"},
{1e6, "1000000.0"}, // Go's 'g' verb gives "1e+06" here
// The upper boundary: decpt 15 stays fixed, 16 goes exponential.
{1e14, "100000000000000.0"},
{123456789012345, "123456789012345.0"},
{1e15, "1.0e+15"},
{1234567890123456, "1.234567890123456e+15"},
// The lower boundary: decpt -3 stays fixed, -4 goes exponential.
{1e-4, "0.0001"},
{1e-5, "1.0e-05"},
// Exponential, with and without a fraction of its own.
{1e-6, "1.0e-06"},
{1.5e-6, "1.5e-06"},
{-1e-6, "-1.0e-06"},
{1e20, "1.0e+20"},
{1e100, "1.0e+100"},
{5e-324, "5.0e-324"},
{math.MaxFloat64, "1.7976931348623157e+308"},
// The non-finite values.
{math.Inf(1), "Infinity"},
{math.Inf(-1), "-Infinity"},
{math.NaN(), "NaN"},
}
for _, c := range cases {
if got := Float(c.in).ToS(); got != c.want {
t.Errorf("Float(%v).ToS() = %q, want %q", c.in, got, c.want)
}
if got := Float(c.in).Inspect(); got != c.want {
t.Errorf("Float(%v).Inspect() = %q, want %q", c.in, got, c.want)
}
}
}
33 changes: 27 additions & 6 deletions internal/object/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,39 @@ func BigOf(v Value) (*big.Int, bool) {
type Float float64

func (f Float) ToS() string {
if math.IsInf(float64(f), 1) {
v := float64(f)
if math.IsInf(v, 1) {
return "Infinity"
}
if math.IsInf(float64(f), -1) {
if math.IsInf(v, -1) {
return "-Infinity"
}
if math.IsNaN(float64(f)) {
if math.IsNaN(v) {
return "NaN"
}
// Ruby always shows a decimal point for floats (1.0 not 1).
s := strconv.FormatFloat(float64(f), 'g', -1, 64)
if !strings.ContainsAny(s, ".eE") {
// MRI's flo_to_s (numeric.c) lays the shortest round-tripping digits out in
// fixed notation when the decimal point falls within -3..DBL_DIG (15), and in
// exponential notation otherwise. Go's 'g' verb decides differently — it
// compares the exponent against the number of significant digits — so 1e6
// rendered as "1e+06" where Ruby prints "1000000.0", and 1e15 as
// "1e+15" where Ruby prints "1.0e+15".
//
// 'e' with precision -1 gives the shortest digits plus the exponent, which is
// what decides the layout. Atoi cannot fail on an exponent Go just produced.
e := strconv.FormatFloat(v, 'e', -1, 64)
at := strings.IndexByte(e, 'e')
exp, _ := strconv.Atoi(e[at+1:])
if decpt := exp + 1; decpt < -3 || decpt > 15 {
// Ruby always shows a fractional digit in the mantissa: 1.0e-06, not 1e-06.
mant := e[:at]
if !strings.ContainsRune(mant, '.') {
mant += ".0"
}
return mant + e[at:]
}
s := strconv.FormatFloat(v, 'f', -1, 64)
// And a decimal point in fixed notation: 1.0, not 1.
if !strings.ContainsRune(s, '.') {
s += ".0"
}
return s
Expand Down
9 changes: 4 additions & 5 deletions internal/vm/process_limits_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,11 +384,10 @@ p Process.times.utime > u`, "true\n"},
{`p Process.clock_getres(:TIME_BASED_CLOCK_REALTIME, :nanosecond)`, "1000000000\n"},
{`p Process.clock_getres(:GETRUSAGE_BASED_CLOCK_PROCESS_CPUTIME_ID, :nanosecond)`, "1000\n"},
{`p Process.clock_getres(Process::CLOCK_REALTIME, :nanosecond)`, "1000\n"},
// 1e-06 second, the same value MRI reports; the spelling differs only
// because this VM's Float#inspect drops the ".0" before the exponent
// (`p 0.000001` prints 1e-06 here and 1.0e-06 in MRI) — a core
// Float-formatting divergence, not a clock one.
{`p Process.clock_getres(Process::CLOCK_MONOTONIC)`, "1e-06\n"},
// 1e-06 second, the same value and now the same spelling as MRI: the
// Float#inspect divergence this case used to document (1e-06 here,
// 1.0e-06 in MRI) is fixed in internal/object.
{`p Process.clock_getres(Process::CLOCK_MONOTONIC)`, "1.0e-06\n"},
// argv0 is frozen and is the same object every time.
{`p [Process.argv0.is_a?(String), Process.argv0.frozen?, Process.argv0.equal?(Process.argv0)]`,
"[true, true, true]\n"},
Expand Down
Loading