Skip to content
Closed
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
137 changes: 87 additions & 50 deletions core/src/main/java/org/jruby/RubyTime.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,13 @@
import org.jruby.exceptions.TypeError;
import org.jruby.java.proxies.JavaProxy;
import org.jruby.runtime.Block;
import org.jruby.runtime.CallSite;
import org.jruby.runtime.ClassIndex;
import org.jruby.runtime.Helpers;
import org.jruby.runtime.JavaSites.TimeSites;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.callsite.CachingCallSite;
import org.jruby.util.ArraySupport;
import org.jruby.util.ByteList;
import org.jruby.util.RubyDateFormatter;
import org.jruby.util.TypeConverter;
Expand All @@ -69,7 +67,7 @@
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.math.MathContext;
import java.time.*;
import java.time.Instant;
import java.time.LocalDateTime;
Expand All @@ -95,9 +93,16 @@
public class RubyTime extends RubyObject {
public static final String UTC = "UTC";

private static final BigDecimal ONE_MILLION_BD = BigDecimal.valueOf(1000000);
private static final BigDecimal ONE_BILLION_BD = BigDecimal.valueOf(1000000000);
public static final int TIME_SCALE = 1000000000;
private static final int ONE_MILLION = 1_000_000;
private static final int ONE_BILLION = 1_000_000_000;
private static final BigDecimal ONE_MILLION_BD = BigDecimal.valueOf(ONE_MILLION);
private static final BigDecimal ONE_BILLION_BD = BigDecimal.valueOf(ONE_BILLION);
private static final double ONE_THOUSAND_DOUBLE = 1_000.0;
private static final double ONE_BILLION_DOUBLE = 1_000_000_000.0;

public static final int TIME_SCALE = ONE_BILLION;
private static final double TIME_SCALE_DOUBLE = ONE_BILLION_DOUBLE;
private static final BigDecimal TIME_SCALE_BD = ONE_BILLION_BD;

private DateTime dt;
private long nsec;
Expand Down Expand Up @@ -492,8 +497,8 @@ public static RubyTime newTime(Ruby runtime, long milliseconds) {
}

public static RubyTime newTimeFromNanoseconds(Ruby runtime, long nanoseconds) {
long milliseconds = nanoseconds / 1000000;
long extraNanoseconds = nanoseconds % 1000000;
long milliseconds = nanoseconds / RubyTime.ONE_MILLION;
long extraNanoseconds = nanoseconds % RubyTime.ONE_MILLION;
return RubyTime.newTime(runtime, new DateTime(milliseconds, getLocalTimeZone(runtime)), extraNanoseconds);
}

Expand Down Expand Up @@ -764,11 +769,11 @@ private RubyTime opPlusMillis(final Ruby runtime, double adjustMillis) {
long currentMillis = getTimeInMillis();

long newMillisPart = currentMillis + (long) adjustMillis;
long adjustNanos = (long)((adjustMillis - Math.floor(adjustMillis)) * 1000000);
long adjustNanos = (long)((adjustMillis - Math.floor(adjustMillis)) * RubyTime.ONE_MILLION);
long newNanosPart = nsec + adjustNanos;

if (newNanosPart >= 1000000) {
newNanosPart -= 1000000;
if (newNanosPart >= RubyTime.ONE_MILLION) {
newNanosPart -= RubyTime.ONE_MILLION;
newMillisPart++;
}

Expand All @@ -782,10 +787,17 @@ private RubyTime opPlusMillis(final Ruby runtime, double adjustMillis) {
}

private RubyFloat opMinus(Ruby runtime, RubyTime other) {
long timeInMillis = getTimeInMillis() - other.getTimeInMillis();
double timeInSeconds = timeInMillis / 1000.0 + (getNSec() - other.getNSec()) / 1000000000.0;
if (nanosCanFitInLong() && other.nanosCanFitInLong()) {
long nanos1 = getTimeInMillis() * ONE_MILLION + getNSec();
long nanos2 = other.getTimeInMillis() * ONE_MILLION + other.getNSec();

return RubyFloat.newFloat(runtime, (nanos1 - nanos2) / ONE_BILLION_DOUBLE); // float number of seconds
} else {
long timeInMillis = getTimeInMillis() - other.getTimeInMillis();
double timeInSeconds = timeInMillis / ONE_THOUSAND_DOUBLE + (getNSec() - other.getNSec()) / ONE_BILLION_DOUBLE;

return RubyFloat.newFloat(runtime, timeInSeconds); // float number of seconds
return RubyFloat.newFloat(runtime, timeInSeconds); // float number of seconds
}
}

public IRubyObject op_minus(IRubyObject other) {
Expand All @@ -805,16 +817,16 @@ public IRubyObject op_minus19(ThreadContext context, IRubyObject other) {
}

private RubyTime opMinus(Ruby runtime, double other) {
long adjustmentInNanos = (long) (other * 1000000000);
long adjustmentInMillis = adjustmentInNanos / 1000000;
long adjustmentInNanosLeft = adjustmentInNanos % 1000000;
long adjustmentInNanos = (long) (other * TIME_SCALE);
long adjustmentInMillis = adjustmentInNanos / RubyTime.ONE_MILLION;
long adjustmentInNanosLeft = adjustmentInNanos % RubyTime.ONE_MILLION;

long time = getTimeInMillis() - adjustmentInMillis;

long nano;
if (nsec < adjustmentInNanosLeft) {
time--;
nano = 1000000 - (adjustmentInNanosLeft - nsec);
nano = RubyTime.ONE_MILLION - (adjustmentInNanosLeft - nsec);
} else {
nano = nsec - adjustmentInNanosLeft;
}
Expand Down Expand Up @@ -916,12 +928,37 @@ public RubyArray to_a(ThreadContext context) {

@JRubyMethod
public RubyFloat to_f() {
return RubyFloat.newFloat(getRuntime(), getTimeInSecsAsDouble());
}

// If we can fit the entire time as a single nanosecond resolution value in a long we can prevent doing
// multiple math operations and more closely match MRI. This range fits until about 2262AD.
private boolean nanosCanFitInLong() {
long millis = getTimeInMillis();
long nanos = nsec;
double secs = 0;
if (millis != 0) secs += (millis / 1000.0);
if (nanos != 0) secs += (nanos / 1000000000.0);
return RubyFloat.newFloat(getRuntime(), secs);

return millis >= 0 && millis < (Long.MAX_VALUE - nsec) / ONE_MILLION ||
millis < 0 && millis > -(Long.MAX_VALUE + nsec) / ONE_MILLION;
}

public double getTimeInSecsAsDouble() {

long millis = getTimeInMillis();

// Being able to do a single divide here has us match MRI rounding.
if (nanosCanFitInLong()) {
return BigDecimal.valueOf(millis)
.multiply(ONE_MILLION_BD)
.add(BigDecimal.valueOf(nsec))
.divide(TIME_SCALE_BD)
.doubleValue();
}

double secs;
secs = 0;
if (millis != 0) secs += (millis / ONE_THOUSAND_DOUBLE);
if (nsec != 0) secs += (nsec / TIME_SCALE_DOUBLE);

return secs;
}

@JRubyMethod(name = {"to_i", "tv_sec"})
Expand All @@ -944,7 +981,7 @@ public RubyInteger nsec() {

@JRubyMethod
public IRubyObject to_r(ThreadContext context) {
return RubyRational.newRationalCanonicalize(context, getTimeInMillis() * 1000000 + nsec, 1000000000);
return RubyRational.newRationalCanonicalize(context, getTimeInMillis() * RubyTime.ONE_MILLION + nsec, ONE_BILLION);
}

/**
Expand Down Expand Up @@ -1013,9 +1050,9 @@ public int getNanos() {
*/
public void setNanos(int nanos) {
long millis = getTimeInMillis();
millis = ( millis - (millis % 1000) ) + (nanos / 1_000_000);
millis = ( millis - (millis % 1000) ) + (nanos / ONE_MILLION);
dt = dt.withMillis(millis);
nsec = (nanos % 1_000_000);
nsec = (nanos % ONE_MILLION);
}

@JRubyMethod
Expand Down Expand Up @@ -1100,9 +1137,9 @@ public IRubyObject subsec() {

@JRubyMethod
public RubyNumeric subsec(final ThreadContext context) {
long nanosec = dt.getMillisOfSecond() * 1_000_000 + this.nsec;
long nanosec = dt.getMillisOfSecond() * ONE_MILLION + this.nsec;

RubyNumeric subsec = (RubyNumeric) RubyRational.newRationalCanonicalize(context, nanosec, 1_000_000_000);
RubyNumeric subsec = (RubyNumeric) RubyRational.newRationalCanonicalize(context, nanosec, ONE_BILLION);
return subsec.isZero() ? RubyFixnum.zero(context.runtime) : subsec;
}

Expand Down Expand Up @@ -1296,11 +1333,11 @@ public RubyTime round(ThreadContext context, IRubyObject[] args) {
throw context.getRuntime().newArgumentError("negative ndigits given");
}

int _nsec = this.dt.getMillisOfSecond() * 1000000 + (int) (this.nsec);
int _nsec = this.dt.getMillisOfSecond() * RubyTime.ONE_MILLION + (int) (this.nsec);
int pow = (int) Math.pow(10, 9 - ndigits);
int rounded = ((_nsec + pow/2) / pow) * pow;
DateTime _dt = this.dt.withMillisOfSecond(0).plusMillis(rounded / 1000000);
return newTime(context.runtime, _dt, rounded % 1000000);
DateTime _dt = this.dt.withMillisOfSecond(0).plusMillis(rounded / RubyTime.ONE_MILLION);
return newTime(context.runtime, _dt, rounded % RubyTime.ONE_MILLION);
}

/* Time class methods */
Expand Down Expand Up @@ -1416,22 +1453,22 @@ private static RubyTime at1(ThreadContext context, IRubyObject recv, IRubyObject
long seconds = RubyNumeric.float2long((RubyFloat) arg);
double dbl = ((RubyFloat) arg).value;

long nano = (long)((dbl - seconds) * 1000000000);
long nano = (long)((dbl - seconds) * ONE_BILLION);

if (dbl < 0 && nano != 0) {
nano += 1000000000;
nano += ONE_BILLION;
}

millisecs = seconds * 1000 + nano / 1000000;
nanosecs = nano % 1000000;
millisecs = seconds * 1000 + nano / RubyTime.ONE_MILLION;
nanosecs = nano % RubyTime.ONE_MILLION;
} else if (arg instanceof RubyRational) {
// use Rational numerator and denominator to calculate nanos
RubyRational rational = (RubyRational) arg;

BigInteger numerator = rational.getNumerator().getBigIntegerValue();
BigInteger denominator = rational.getDenominator().getBigIntegerValue();

BigDecimal nanosBD = new BigDecimal(numerator).divide(new BigDecimal(denominator), 50, RoundingMode.HALF_UP).multiply(ONE_BILLION_BD);
BigDecimal nanosBD = new BigDecimal(numerator).divide(new BigDecimal(denominator), MathContext.DECIMAL64).multiply(ONE_BILLION_BD);
BigInteger millis = nanosBD.divide(ONE_MILLION_BD).toBigInteger();
BigInteger nanos = nanosBD.remainder(ONE_MILLION_BD).toBigInteger();

Expand Down Expand Up @@ -1468,7 +1505,7 @@ private static RubyTime atMulti(ThreadContext context, RubyClass recv, IRubyObje
if (arg1 instanceof RubyFloat || arg1 instanceof RubyRational) {
double dbl = RubyNumeric.num2dbl(context, arg1);
millisecs = (long) (dbl * 1000);
nanosecs = ((long) (dbl * 1000000000)) % 1000000;
nanosecs = ((long) (dbl * ONE_BILLION)) % RubyTime.ONE_MILLION;
} else {
millisecs = RubyNumeric.num2long(arg1) * 1000;
}
Expand All @@ -1483,13 +1520,13 @@ private static RubyTime atMulti(ThreadContext context, RubyClass recv, IRubyObje
if (runtime.newSymbol("microsecond").eql(unit) || runtime.newSymbol("usec").eql(unit)) {
double micros = RubyNumeric.num2dbl(context, arg2);
double nanos = micros * 1000;
millisecs += (long) (nanos / 1000000);
nanosecs += (long) (nanos % 1000000);
millisecs += (long) (nanos / RubyTime.ONE_MILLION);
nanosecs += (long) (nanos % RubyTime.ONE_MILLION);
} else if (runtime.newSymbol("millisecond").eql(unit)) {
double millis = RubyNumeric.num2dbl(context, arg2);
double nanos = millis * 1000000;
millisecs += (long) (nanos / 1000000);
nanosecs += (long) (nanos % 1000000);
double nanos = millis * RubyTime.ONE_MILLION;
millisecs += (long) (nanos / RubyTime.ONE_MILLION);
nanosecs += (long) (nanos % RubyTime.ONE_MILLION);
} else if (runtime.newSymbol("nanosecond").eql(unit) || runtime.newSymbol("nsec").eql(unit)) {
nanosecs += RubyNumeric.num2long(arg2);
} else {
Expand All @@ -1499,23 +1536,23 @@ private static RubyTime atMulti(ThreadContext context, RubyClass recv, IRubyObje
if (runtime.newSymbol("microsecond").eql(unit) || runtime.newSymbol("usec").eql(unit)) {
long micros = RubyNumeric.num2long(arg2);
long nanos = micros * 1000;
millisecs += nanos / 1000000;
nanosecs += nanos % 1000000;
millisecs += nanos / RubyTime.ONE_MILLION;
nanosecs += nanos % RubyTime.ONE_MILLION;
} else if (runtime.newSymbol("millisecond").eql(unit)) {
double millis = RubyNumeric.num2long(arg2);
double nanos = millis * 1000000;
millisecs += nanos / 1000000;
nanosecs += nanos % 1000000;
double nanos = millis * RubyTime.ONE_MILLION;
millisecs += nanos / RubyTime.ONE_MILLION;
nanosecs += nanos % RubyTime.ONE_MILLION;
} else if (runtime.newSymbol("nanosecond").eql(unit) || runtime.newSymbol("nsec").eql(unit)) {
nanosecs += RubyNumeric.num2long(arg2);
} else {
throw context.runtime.newArgumentError("unexpected unit " + arg3);
}
}

long nanosecOverflow = (nanosecs / 1000000);
long nanosecOverflow = (nanosecs / RubyTime.ONE_MILLION);

time.setNSec(nanosecs % 1000000);
time.setNSec(nanosecs % RubyTime.ONE_MILLION);
time.dt = time.dt.withMillis(millisecs + nanosecOverflow);

if (!zone.isNil()) {
Expand Down Expand Up @@ -1852,7 +1889,7 @@ public java.time.Instant toInstant() {
// Keep this long cast to work on Java 8
long sec = Math.floorDiv(millis, (long) 1000);
// Keep this long cast to work on Java 8
long nanoAdj = getNSec() + (Math.floorMod(millis, (long) 1000) * 1_000_000);
long nanoAdj = getNSec() + (Math.floorMod(millis, (long) 1000) * ONE_MILLION);
return java.time.Instant.ofEpochSecond(sec, nanoAdj);
}

Expand Down