Skip to content

Commit 539085c

Browse files
Fix Time sub-second precision loss from floating-point arithmetic
Use integer arithmetic when computing millis/nanos from Rational Fixes Psych::Visitors::TestToRuby#test_time https://github.com/jruby/jruby/actions/runs/22045652593/job/63693879405
1 parent 51ace55 commit 539085c

1 file changed

Lines changed: 13 additions & 8 deletions

File tree

core/src/main/java/org/jruby/util/time/TimeArgs.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import org.jruby.runtime.ThreadContext;
1616
import org.jruby.runtime.builtin.IRubyObject;
1717

18+
import java.math.BigInteger;
19+
1820
import static org.jruby.RubyTime.TIME_SCALE;
1921
import static org.jruby.api.Convert.asFixnum;
2022
import static org.jruby.api.Convert.toDouble;
@@ -129,14 +131,17 @@ public void initializeTime(ThreadContext context, RubyTime time, DateTimeZone dt
129131
var numerator = subSecond.getNumerator().asLong(context);
130132
var denominator = subSecond.getDenominator().asLong(context);
131133
if (numerator >= denominator) {
132-
secondsInRational = (int) (numerator / (double) denominator);
134+
secondsInRational = (int) (numerator / denominator);
133135
numerator = numerator % denominator;
134-
subSecond = RubyRational.newRational(context.runtime, numerator, denominator);
135136
}
136-
var subSeconds = subSecond.asDouble(context) * TIME_SCALE;
137137

138-
millis = (long) subSeconds / 1_000_000;
139-
nanos = (long) subSeconds % 1_000_000;
138+
long subSeconds = BigInteger.valueOf(numerator)
139+
.multiply(BigInteger.valueOf(TIME_SCALE))
140+
.divide(BigInteger.valueOf(denominator))
141+
.longValue();
142+
143+
millis = subSeconds / 1_000_000;
144+
nanos = subSeconds % 1_000_000;
140145
} else {
141146
double secs = toDouble(context, secondObj);
142147

@@ -149,10 +154,10 @@ public void initializeTime(ThreadContext context, RubyTime time, DateTimeZone dt
149154
} else if (usecObj instanceof RubyRational subSecond) {
150155
if (subSecond.isNegativeNumber(context)) throw argumentError(context, "argument out of range");
151156

152-
var subSeconds = subSecond.asDouble(context) * 1_000;
157+
long subNanos = subSecond.getNumerator().asLong(context) * 1_000 / subSecond.getDenominator().asLong(context);
153158

154-
millis = (long) subSeconds / 1_000_000;
155-
nanos = (long) subSeconds % 1_000_000;
159+
millis = subNanos / 1_000_000;
160+
nanos = subNanos % 1_000_000;
156161
} else if (usecObj instanceof RubyFloat flo) {
157162
if (flo.isNegativeNumber(context)) throw argumentError(context, "argument out of range");
158163

0 commit comments

Comments
 (0)