Skip to content

Commit 148f885

Browse files
authored
Merge pull request jruby#9261 from evaniainbrooks/test-time-precision-error
Fix Time subsecond precision loss from floating point arithmetic
2 parents 2b64eab + 0ff29d1 commit 148f885

2 files changed

Lines changed: 14 additions & 8 deletions

File tree

core/src/main/java/org/jruby/RubyTime.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ public class RubyTime extends RubyObject {
113113
private static final BigDecimal ONE_MILLION_BD = BigDecimal.valueOf(1000000);
114114
private static final BigDecimal ONE_BILLION_BD = BigDecimal.valueOf(1000000000);
115115
public static final int TIME_SCALE = 1_000_000_000;
116+
public static final BigInteger TIME_SCALE_BI = BigInteger.valueOf(TIME_SCALE);
116117
public static final int TIME_SCALE_DIGITS = 9;
117118

118119
private DateTime dt;

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(RubyTime.TIME_SCALE_BI)
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)