All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jruby.util.time.TimeArgs Maven / Gradle / Ivy

package org.jruby.util.time;

import org.joda.time.Chronology;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.IllegalFieldValueException;
import org.jruby.Ruby;
import org.jruby.RubyBoolean;
import org.jruby.RubyFloat;
import org.jruby.RubyNumeric;
import org.jruby.RubyRational;
import org.jruby.RubyString;
import org.jruby.RubyStruct;
import org.jruby.RubyTime;
import org.jruby.runtime.JavaSites;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;

import java.util.function.Function;

public class TimeArgs {
    private final int year;
    private final int month;
    private final int day;
    private final int hour;
    private final int minute;
    private final int second;
    private final IRubyObject secondObj;
    private final IRubyObject usecObj;
    private final boolean dst;

    public TimeArgs(ThreadContext context, IRubyObject year, IRubyObject month, IRubyObject day, IRubyObject hour, IRubyObject minute, IRubyObject second, IRubyObject usec, boolean dst) {
        this.year = parseYear(context, year);
        this.month = parseMonth(context, month);
        this.day = parseIntOrDefault(context, day, 1);
        this.hour = parseIntOrDefault(context, hour, 0);
        this.minute = parseIntOrDefault(context, minute, 0);

        this.secondObj = parseIntArg(context, second);
        this.second = parseIntOrDefault(context, secondObj, 0);

        validateDayHourMinuteSecond(context);

        this.usecObj = usec;
        this.dst = dst;
    }

    public TimeArgs(ThreadContext context, IRubyObject[] args) {
        IRubyObject nil = context.nil;

        IRubyObject year, month, day, hour, minute, second, usec;

        month = day = hour = minute = second = usec = nil;

        boolean dst = false;

        switch (args.length) {
            default: // zero args or more than 10 args
                throw new RuntimeException("BUG: should not get here");

            case 10:
                // smhDMY format
                if (args[8] instanceof RubyBoolean) dst = args[8].isTrue();

                year = args[5];
                month = args[4];
                day = args[3];
                hour = args[2];
                minute = args[1];
                second = args[0];
                usec = nil;
                break;

            // YMDhms formats
            case 8:
                // Ignores usec if 8 args (for compatibility with parse-date) or if not supplied.
                args[6] = nil;

            case 9:

            case 7: usec = args[6];
            case 6: second = args[5];
            case 5: minute = args[4];
            case 4: hour = args[3];
            case 3: day = args[2];
            case 2: month = args[1];
            case 1: year = args[0];
        }

        this.year = parseYear(context, year);
        this.month = parseMonth(context, month);
        this.day = parseIntOrDefault(context, day, 1);
        this.hour = parseIntOrDefault(context, hour, 0);
        this.minute = parseIntOrDefault(context, minute, 0);

        this.secondObj = parseIntArg(context, second);
        this.second = parseIntOrDefault(context, secondObj, 0);

        validateDayHourMinuteSecond(context);

        this.usecObj = usec;
        this.dst = dst;
    }

    public void initializeTime(ThreadContext context, RubyTime time, DateTimeZone dtz) {
        Ruby runtime = context.runtime;

        // set up with min values and then add to allow rolling over
        DateTime dt = new DateTime(year, month, 1, 0, 0, 0, 0, DateTimeZone.UTC);
        long instant = dt.getMillis();

        final Chronology chrono = dt.getChronology();

        instant = chrono.days().add(instant, this.day - 1);
        instant = chrono.hours().add(instant, this.hour);
        instant = chrono.minutes().add(instant, this.minute);
        instant = chrono.seconds().add(instant, this.second);

        IRubyObject usecObj = this.usecObj;
        IRubyObject secondObj = this.secondObj;

        long millis = 0;
        long nanos = 0;

        if (usecObj.isNil()) {
            if (!secondObj.isNil()) {
                if (secondObj instanceof RubyRational) {
                    RubyRational rat = (RubyRational) secondObj;

                    if (rat.isNegative()) {
                        throw runtime.newArgumentError("argument out of range.");
                    }

                    RubyRational nsec = (RubyRational) rat.op_mul(context, runtime.newFixnum(1_000_000_000));

                    long fullNanos = nsec.getLongValue();
                    long fullMillis = fullNanos / 1_000_000;

                    nanos = fullNanos - fullMillis * 1_000_000;
                    millis = fullMillis % 1000;
                } else {
                    double secs = RubyFloat.num2dbl(context, secondObj);

                    if (secs < 0 || secs >= RubyTime.TIME_SCALE) {
                        throw runtime.newArgumentError("argument out of range.");
                    }

                    millis = (int) (secs * 1000) % 1000;
                    nanos = ((long) (secs * 1000000000) % 1000000);
                }
            }
        } else if (usecObj instanceof RubyRational) {
            RubyRational rat = (RubyRational) usecObj;

            if (rat.isNegative()) {
                throw runtime.newArgumentError("argument out of range.");
            }

            RubyRational nsec = (RubyRational) rat.op_mul(context, runtime.newFixnum(1000));

            long tmpNanos = (long) nsec.getDoubleValue(context);

            millis = tmpNanos / 1_000_000;
            nanos = tmpNanos % 1_000_000;
        } else if (usecObj instanceof RubyFloat) {
            RubyFloat flo = (RubyFloat) usecObj;

            if (flo.isNegative()) {
                throw runtime.newArgumentError("argument out of range.");
            }

            double micros = flo.getDoubleValue();

            millis = (long) (micros / 1000);
            nanos = (long) Math.rint((micros * 1000) % 1_000_000);
        } else {
            int usec = parseIntArg(context, usecObj).isNil() ? 0 : RubyNumeric.num2int(usecObj);

            if (usec < 0 || usec >= RubyTime.TIME_SCALE / 1000) {
                throw runtime.newArgumentError("argument out of range.");
            }

            int usecPart = usec % 1000;
            int msecPart = usec / 1000;

            if (usec < 0) {
                msecPart -= 1;
                usecPart += 1000;
            }

            nanos = 1000 * usecPart;
            millis = msecPart;
        }

        instant = chrono.millis().add(instant, millis);

        try {
            dt = dt.withMillis(instant);
            dt = dt.withZoneRetainFields(dtz);
            dt = adjustZoneOffset(dtz, dt, dst);
        } catch (IllegalFieldValueException e) {
            throw runtime.newArgumentError("time out of range");
        }

        time.setDateTime(dt);
        time.setNSec(nanos);
    }

    private static int parseYear(ThreadContext context, IRubyObject _year) {
        if (_year instanceof RubyString) {
            _year = RubyNumeric.str2inum(context.runtime, (RubyString) _year, 10, false);
        }

        return RubyNumeric.num2int(_year);
    }

    private static int parseMonth(ThreadContext context, IRubyObject _month) {
        if (_month.isNil()) return 1;

        int month;
        IRubyObject tmp = _month.checkStringType();

        if (!tmp.isNil()) {
            String monthStr = tmp.toString().toLowerCase();

            if (monthStr.length() == 3) {
                switch (monthStr) {
                    case "jan" : return  1;
                    case "feb" : return  2;
                    case "mar" : return  3;
                    case "apr" : return  4;
                    case "may" : return  5;
                    case "jun" : return  6;
                    case "jul" : return  7;
                    case "aug" : return  8;
                    case "sep" : return  9;
                    case "oct" : return 10;
                    case "nov" : return 11;
                    case "dec" : return 12;
                }
            }

            try {
                month = Integer.parseInt(monthStr);
            } catch (NumberFormatException ex) {
                throw context.runtime.newArgumentError("Argument out of range.");
            }
        } else {
            month = RubyNumeric.num2int(_month);
        }

        if (month < 1 || month > 12) {
            throw context.runtime.newArgumentError("Argument out of range: for month: " + month);
        }

        return month;
    }

    private static IRubyObject parseIntArg(ThreadContext context, IRubyObject arg) {
        if (arg == context.nil) return arg;

        if (arg instanceof RubyNumeric) return arg;

        final JavaSites.TimeSites sites = sites(context);

        if (sites.respond_to_to_int.respondsTo(context, arg, arg)) {
            return sites.to_int.call(context, arg, arg);
        }

        return sites.to_i.call(context, arg, arg);
    }

    private void validateDayHourMinuteSecond(ThreadContext context) {
        if (day < 1 || day > 31) throw context.runtime.newArgumentError("argument out of range for day");

        if (hour < 0 || hour > 24) throw context.runtime.newArgumentError("argument out of range for hour");

        if ((minute < 0 || minute > 59) || (hour == 24 && minute > 0)) {
            throw context.runtime.newArgumentError("argument out of range for minute");
        }

        if ((second < 0 || second > 60) || (hour == 24 && second > 0)) {
            throw context.runtime.newArgumentError("argument out of range for second");
        }
    }

    private static int parseIntOrDefault(ThreadContext context, IRubyObject obj, int def) {
        return (obj = parseIntArg(context, obj)).isNil() ? def : RubyNumeric.num2int(obj);
    }

    private static DateTime adjustZoneOffset(DateTimeZone dtz, DateTime dt, boolean dst) {
        // If we're at a DST boundary, we need to choose the correct side of the boundary
        final DateTime beforeDstBoundary = dt.withEarlierOffsetAtOverlap();
        final DateTime afterDstBoundary = dt.withLaterOffsetAtOverlap();

        final int offsetBeforeBoundary = dtz.getOffset(beforeDstBoundary);
        final int offsetAfterBoundary = dtz.getOffset(afterDstBoundary);

        if (dst) {
            // If the time is during DST, we need to pick the time with the highest offset
            dt = offsetBeforeBoundary > offsetAfterBoundary ? beforeDstBoundary : afterDstBoundary;
        }
        else {
            dt = offsetBeforeBoundary > offsetAfterBoundary ? afterDstBoundary : beforeDstBoundary;
        }
        return dt;
    }

    private static JavaSites.TimeSites sites(ThreadContext context) {
        return context.sites.Time;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy