/*
 **** BEGIN LICENSE BLOCK *****
 * Version: EPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Eclipse Public
 * License Version 1.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.eclipse.org/legal/epl-v20.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2017-2018 The JRuby Team
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the EPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the EPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/

package org.jruby.ext.date;

import org.jcodings.specific.USASCIIEncoding;
import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.Chronology;
import org.joda.time.Instant;
import org.joda.time.chrono.GJChronology;
import org.joda.time.chrono.GregorianChronology;
import org.joda.time.chrono.JulianChronology;

import org.joni.Regex;
import org.jruby.*;
import org.jruby.anno.JRubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.java.proxies.JavaProxy;
import org.jruby.javasupport.JavaUtil;
import org.jruby.runtime.*;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.*;
import org.jruby.util.log.Logger;
import org.jruby.util.log.LoggerFactory;

import java.io.Serializable;
import java.time.*;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import static org.jruby.RubyRegexp.*;
import static org.jruby.api.Access.*;
import static org.jruby.api.Convert.*;
import static org.jruby.api.Create.*;
import static org.jruby.api.Define.defineClass;
import static org.jruby.api.Error.argumentError;
import static org.jruby.api.Error.typeError;
import static org.jruby.ext.date.DateUtils.*;
import static org.jruby.util.Numeric.*;

/**
 * JRuby's <code>Date</code> implementation - 'native' parts.
 * In MRI, since 2.x, all of date.rb has been moved to native (C) code.
 *
 * NOTE: There's still date.rb, where this gets bootstrapped from.
 *
 * @since 9.2
 *
 * @author enebo
 * @author kares
 */
@JRubyClass(name = "Date")
public class RubyDate extends RubyObject {

    static final Logger LOG = LoggerFactory.getLogger(RubyDate.class);

    static final GJChronology CHRONO_ITALY_UTC = GJChronology.getInstance(DateTimeZone.UTC);

    // The Julian Day Number of the Day of Calendar Reform for Italy
    // and the Catholic countries.
    static final int ITALY = 2299161; // 1582-10-15

    // The Julian Day Number of the Day of Calendar Reform for England
    // and her Colonies.
    static final int ENGLAND = 2361222; // 1752-09-14

    // A constant used to indicate that a Date should always use the Julian calendar.
    private static final double JULIAN_INFINITY = Double.POSITIVE_INFINITY; // Infinity.new
    static final long JULIAN = (long) JULIAN_INFINITY; // Infinity.new

    // A constant used to indicate that a Date should always use the Gregorian calendar.
    private static final double GREGORIAN_INFINITY = Double.NEGATIVE_INFINITY; // -Infinity.new
    static final long GREGORIAN = (long) GREGORIAN_INFINITY; // -Infinity.new

    static final int REFORM_BEGIN_YEAR = 1582;
    static final int REFORM_END_YEAR = 1930;

    DateTime dt;
    int off; // @of (in seconds)
    long start = ITALY; // @sg
    long subMillisNum = 0, subMillisDen = 1; // @sub_millis

    static RubyClass createDateClass(ThreadContext context) {
        var Date = defineClass(context, "Date", objectClass(context), RubyDate::new).
                reifiedClass(RubyDate.class).
                include(context, comparableModule(context)).
                defineMethods(context, RubyDate.class).
                defineConstant(context, "ITALY", asFixnum(context, ITALY)).
                defineConstant(context, "ENGLAND", asFixnum(context, ENGLAND)).
                defineConstant(context, "VERSION", newString(context, "3.2.2"));

        var ArgumentError = argumentErrorClass(context);
        RubyClass dateError = Date.defineClassUnder(context, "Error", ArgumentError, ArgumentError.getAllocator());
        context.runtime.setDateError(dateError);

        return (RubyClass) Date;
    }

    // Julian Day Number day 0 ... `def self.civil(y=-4712, m=1, d=1, sg=ITALY)`
    static final DateTime defaultDateTime = new DateTime(-4712 - 1, 1, 1, 0, 0, CHRONO_ITALY_UTC);

    @Deprecated(since = "10.0")
    static RubyClass getDate(final Ruby runtime) {
        return getDate(runtime.getCurrentContext());
    }

    static RubyClass getDate(ThreadContext context) {
        return (RubyClass) objectClass(context).getConstantAt(context, "Date");
    }

    @Deprecated(since = "10.0")
    static RubyClass getDateTime(final Ruby runtime) {
        return getDateTime(runtime.getCurrentContext());
    }

    static RubyClass getDateTime(ThreadContext context) {
        return (RubyClass) objectClass(context).getConstantAt(context, "DateTime");
    }

    @Deprecated(since = "10.0")
    static boolean isDateTime(final Ruby runtime, final IRubyObject type) {
        return isDateTime(runtime.getCurrentContext(), type);
    }

    static boolean isDateTime(ThreadContext context, final IRubyObject type) {
        return ((RubyModule) type).hasAncestor(getDateTime(context));
    }

    protected RubyDate(Ruby runtime, RubyClass klass) {
        this(runtime, klass, defaultDateTime);
    }

    public RubyDate(Ruby runtime, RubyClass klass, DateTime dt) {
        super(runtime, klass);

        this.dt = dt; // assuming of = 0 (UTC)
    }

    @Deprecated(since = "10.0")
    public RubyDate(Ruby runtime, DateTime dt) {
        this(runtime, getDate(runtime.getCurrentContext()), dt);
    }

    RubyDate(Ruby runtime, RubyClass klass, DateTime dt, int off, long start) {
        super(runtime, klass);

        this.dt = dt;
        this.off = off; this.start = start;
    }

    RubyDate(Ruby runtime, RubyClass klass, DateTime dt, int off, long start, long subMillisNum, long subMillisDen) {
        super(runtime, klass);

        this.dt = dt;
        this.off = off; this.start = start;
        this.subMillisNum = subMillisNum; this.subMillisDen = subMillisDen;
    }

    @Deprecated(since = "10.0")
    public RubyDate(Ruby runtime, long millis, Chronology chronology) {
        this(runtime.getCurrentContext(), millis, chronology);
    }

    public RubyDate(ThreadContext context, long millis, Chronology chronology) {
        super(context.runtime, getDate(context));

        this.dt = new DateTime(millis, chronology);
    }

    RubyDate(ThreadContext context, RubyClass klass, IRubyObject ajd, Chronology chronology, int off) {
        this(context, klass, ajd, chronology, off, ITALY);
    }

    RubyDate(ThreadContext context, RubyClass klass, IRubyObject ajd, int off, long start) {
        this(context, klass, ajd, getChronology(context, start, off), off, start);
    }

    RubyDate(ThreadContext context, RubyClass klass, IRubyObject ajd, long[] rest, int off, long start) {
        this(context, klass, ajd, getChronology(context, start, off), off, start);

        if (rest[0] != 0) adjustWithDayFraction(context, this.dt, rest);
    }

    private RubyDate(ThreadContext context, RubyClass klass, IRubyObject ajd, Chronology chronology, int off, long start) {
        super(context.runtime, klass);

        this.dt = new DateTime(initMillis(context, ajd), chronology);
        this.off = off; this.start = start;
    }

    final RubyDate newInstance(final ThreadContext context, final DateTime dt, int off, long start) {
        return newInstance(context, dt, off, start, subMillisNum, subMillisDen);
    }

    // to be overriden by RubyDateTime
    RubyDate newInstance(final ThreadContext context, final DateTime dt, int off, long start, long subNum, long subDen) {
        return new RubyDate(context.runtime, getMetaClass(), dt, off, start, subNum, subDen);
    }

    /**
     * <p>Note: since <code>Date.new</code> is a <code>civil</code> alias, this won't ever get used</p>
     * @deprecated kept due AR-JDBC (uses RubyClass.newInstance(...) to 'fast' allocate a Date instance)
     */
    @Deprecated(since = "9.4-")
    @JRubyMethod(visibility = Visibility.PRIVATE)
    public RubyDate initialize(ThreadContext context, IRubyObject dt) {
        this.dt = JavaUtil.unwrapJavaValue(dt);
        return this;
    }

    @JRubyMethod(visibility = Visibility.PRIVATE)
    public RubyDate initialize(ThreadContext context, IRubyObject ajd, IRubyObject of) {
        initialize(context, ajd, of, ITALY);
        return this;
    }

    @JRubyMethod(visibility = Visibility.PRIVATE) // used by marshal_load
    public RubyDate initialize(ThreadContext context, IRubyObject ajd, IRubyObject of, IRubyObject sg) {
        initialize(context, ajd, of, val2sg(context, sg));
        return this;
    }

    private void initialize(final ThreadContext context, IRubyObject arg, IRubyObject of, final long start) {
        final int off = val2off(context, of);

        this.off = off; this.start = start;

        if (arg instanceof JavaProxy) { // backwards - compatibility with JRuby's date.rb
            this.dt = (DateTime) JavaUtil.unwrapJavaValue(arg);
            return;
        }
        this.dt = new DateTime(initMillis(context, arg), getChronology(context, start, off));
    }

    static final int DAY_IN_SECONDS = 86_400; // 24 * 60 * 60
    static final int DAY_MS = 86_400_000; // 24 * 60 * 60 * 1000
    private RubyFixnum DAY_MS_CACHE;

    private long initMillis(final ThreadContext context, IRubyObject ajd) {
        // cannot use DateTimeUtils.fromJulianDay since we need to keep ajd as a Rational for precision

        // millis, @sub_millis = ((ajd - UNIX_EPOCH_IN_AJD) * 86400000).divmod(1)

        IRubyObject val;
        if (ajd instanceof RubyFixnum fix) {
            val = fix.op_minus(context, 4881175 / 2);
            val = ((RubyFixnum) val).op_mul(context, DAY_MS);
            val = ((RubyInteger) val).op_plus(context, asFixnum(context, DAY_MS / 2)); // missing 1/2
        } else {
            RubyRational _UNIX_EPOCH_IN_AJD = RubyRational.newRational(context.runtime, -4881175, 2); // -(1970-01-01)
            val = _UNIX_EPOCH_IN_AJD.op_plus(context, ajd);
            val = DAY_MS(context).op_mul(context, val);
        }

        if (val instanceof RubyFixnum fix) return fix.getValue();

        // fallback
        val = ((RubyNumeric) val).divmod(context, RubyFixnum.one(context.runtime));
        IRubyObject millis = ((RubyArray) val).eltInternal(0);
        if (!(millis instanceof RubyFixnum ms)) { // > java.lang.Long::MAX_VALUE
            throw argumentError(context, "Date out of range: millis=" + millis + " (" + millis.getMetaClass() + ")");
        }

        var subMillis = (RubyNumeric) ((RubyArray) val).eltInternal(1);
        this.subMillisNum = toLong(context, subMillis.numerator(context));
        this.subMillisDen = toLong(context, subMillis.denominator(context));

        return ms.getValue();
    }

    private RubyFixnum DAY_MS(final ThreadContext context) {
        RubyFixnum v = DAY_MS_CACHE;
        if (v == null) v = DAY_MS_CACHE = asFixnum(context, DAY_MS);
        return v;
    }

    public IRubyObject initialize_copy(ThreadContext context, IRubyObject original) {
        final RubyDate from = (RubyDate) original;

        this.dt = from.dt; this.off = from.off; this.start = from.start;
        this.subMillisNum = from.subMillisNum; this.subMillisDen = from.subMillisDen;

        return this;
    }

    // Date.new!(dt_or_ajd=0, of=0, sg=ITALY, sub_millis=0)

    /**
     * @deprecated internal Date.new!
     */
    @JRubyMethod(name = "new!", meta = true, visibility = Visibility.PRIVATE)
    @Deprecated(since = "9.4-")
    public static RubyDate new_(ThreadContext context, IRubyObject self) {
        return isDateTime(context, self) ?
                new RubyDateTime(context.runtime, 0, CHRONO_ITALY_UTC) :
                new RubyDate(context, 0, CHRONO_ITALY_UTC);
    }

    /**
     * @deprecated internal Date.new!
     */
    @Deprecated(since = "9.4-")
    @JRubyMethod(name = "new!", meta = true, visibility = Visibility.PRIVATE)
    public static RubyDate new_(ThreadContext context, IRubyObject self, IRubyObject ajd) {
        var isDateTime = isDateTime(context, ajd);
        if (ajd instanceof JavaProxy) { // backwards - compatibility with JRuby's date.rb
            return isDateTime ?
                    new RubyDateTime(context.runtime, (RubyClass) self, JavaUtil.unwrapJavaValue(ajd)) :
                    new RubyDate(context.runtime, (RubyClass) self, JavaUtil.unwrapJavaValue(ajd));
        }
        return isDateTime ?
                new RubyDateTime(context, (RubyClass) self, ajd, CHRONO_ITALY_UTC, 0) :
                new RubyDate(context, (RubyClass) self, ajd, CHRONO_ITALY_UTC, 0);
    }

    /**
     * @deprecated internal Date.new!
     */
    @Deprecated(since = "9.4-")
    @JRubyMethod(name = "new!", meta = true, visibility = Visibility.PRIVATE)
    public static RubyDate new_(ThreadContext context, IRubyObject self, IRubyObject ajd, IRubyObject of) {
        return isDateTime(context, self) ?
                new RubyDateTime(context.runtime, (RubyClass) self).initialize(context, ajd, of) :
                new RubyDate(context.runtime, (RubyClass) self).initialize(context, ajd, of);
    }

    /**
     * @deprecated internal Date.new!
     */
    @Deprecated(since = "9.4-")
    @JRubyMethod(name = "new!", meta = true, visibility = Visibility.PRIVATE)
    public static RubyDate new_(ThreadContext context, IRubyObject self, IRubyObject ajd, IRubyObject of, IRubyObject sg) {
        return isDateTime(context, self) ?
                new RubyDateTime(context.runtime, (RubyClass) self).initialize(context, ajd, of, sg) :
                new RubyDate(context.runtime, (RubyClass) self).initialize(context, ajd, of, sg);
    }

    /**
     # Create a new Date object for the Civil Date specified by
     # year +y+, month +m+, and day-of-month +d+.
     #
     # +m+ and +d+ can be negative, in which case they count
     # backwards from the end of the year and the end of the
     # month respectively.  No wraparound is performed, however,
     # and invalid values cause an ArgumentError to be raised.
     # can be negative
     #
     # +y+ defaults to -4712, +m+ to 1, and +d+ to 1; this is
     # Julian Day Number day 0.
     #
     # +sg+ specifies the Day of Calendar Reform.
     **/
    // Date.civil([year=-4712[, month=1[, mday=1[, start=Date::ITALY]]]])
    // Date.new([year=-4712[, month=1[, mday=1[, start=Date::ITALY]]]])

    @JRubyMethod(name = "civil", alias = "new", meta = true)
    public static RubyDate civil(ThreadContext context, IRubyObject self) {
        return new RubyDate(context.runtime, (RubyClass) self, defaultDateTime);
    }

    @JRubyMethod(name = "civil", alias = "new", meta = true)
    public static RubyDate civil(ThreadContext context, IRubyObject self, IRubyObject year) {
        return new RubyDate(context.runtime, (RubyClass) self, civilImpl(context, year));
    }

    static RaiseException newDateError(ThreadContext context, String message) {
        throw context.runtime.newRaiseException(context.runtime.getDateError(), message);
    }

    static DateTime civilImpl(ThreadContext context, IRubyObject year) {
        int y = getYear(context, year);
        final DateTime dt;
        try {
            dt = defaultDateTime.withYear(y);
        } catch (IllegalArgumentException ex) {
            throw newDateError(context, "invalid date");
        }
        return dt;
    }

    @JRubyMethod(name = "civil", alias = "new", meta = true)
    public static RubyDate civil(ThreadContext context, IRubyObject self, IRubyObject year, IRubyObject month) {
        return new RubyDate(context.runtime, (RubyClass) self, civilImpl(context, year, month));
    }

    static DateTime civilImpl(ThreadContext context, IRubyObject year, IRubyObject month) {
        int y = getYear(context, year);
        int m = getMonth(context, month);
        final DateTime dt;
        final Chronology chronology = defaultDateTime.getChronology();
        long millis = defaultDateTime.getMillis();
        try {
            millis = chronology.year().set(millis, y);
            millis = chronology.monthOfYear().set(millis, m);
            dt = defaultDateTime.withMillis(millis);
        }
        catch (IllegalArgumentException ex) {
            throw newDateError(context, "invalid date");
        }
        return dt;
    }

    @JRubyMethod(name = "civil", alias = "new", meta = true)
    public static RubyDate civil(ThreadContext context, IRubyObject self, IRubyObject year, IRubyObject month, IRubyObject mday) {
        // return civil(context, self, new IRubyObject[] { year, month, mday, asFixnum(context, ITALY) });
        return civilImpl(context, (RubyClass) self, year, month, mday, ITALY);
    }

    private static RubyDate civilImpl(ThreadContext context, RubyClass klass,
                                      IRubyObject year, IRubyObject month, IRubyObject mday, final long sg) {
        final int y = (sg > 0) ? getYear(context, year) : toInt(context, year);
        final int m = getMonth(context, month);
        final long[] rest = new long[] { 0, 1 };
        final int d = (int) RubyDateTime.getDay(context, mday, rest);

        DateTime dt = civilDate(context, y, m ,d, getChronology(context, sg, 0));

        RubyDate date = new RubyDate(context.runtime, klass, dt, 0, sg);
        if (rest[0] != 0) date.adjustWithDayFraction(context, dt, rest);
        return date;
    }

    @JRubyMethod(name = "civil", alias = "new", meta = true, optional = 4, checkArity = false) // 4 args case
    public static RubyDate civil(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        // IRubyObject year, IRubyObject month, IRubyObject mday, IRubyObject start
        int argc = Arity.checkArgumentCount(context, args, 0, 4);

        switch (argc) {
            // NOTE: slightly annoying but send might route its Date.send(:civil, *args) here
            case 0: return civil(context, self);
            case 1: return civil(context, self, args[0]);
            case 2: return civil(context, self, args[0], args[1]);
            // besides the above note on send ^^
            // interpreter does not have a ThreeOperandArgNoBlockCallInstr thus routes 3 args as args[] here
            case 3: return civil(context, self, args[0], args[1], args[2]);
        }

        final long sg = val2sg(context, args[3]);

        return civilImpl(context, (RubyClass) self, args[0], args[1], args[2], sg);
    }

    public static DateTime civilDate(ThreadContext context, final int y, final int m, final int d, final Chronology chronology) {
        DateTime dt;
        try {
            if (d >= 0) { // let d == 0 fail (raise 'invalid date')
                dt = new DateTime(y, m, d, 0, 0, chronology);
            }
            else {
                dt = new DateTime(y, m, 1, 0, 0, chronology);
                long ms = dt.getMillis();
                int last = chronology.dayOfMonth().getMaximumValue(ms);
                ms = chronology.dayOfMonth().set(ms, last + d + 1); // d < 0 (d == -1 -> d == 31)
                dt = dt.withMillis(ms);
            }
        }
        catch (IllegalArgumentException ex) {
            debug(context, "invalid date", ex);
            throw newDateError(context, "invalid date");
        }
        return dt;
    }

    @Deprecated(since = "10.0")
    static int getYear(IRubyObject year) {
        return getYear(((RubyBasicObject) year).getCurrentContext(), year);
    }

    // NOTE: no Bignum special care since JODA does not support 'huge' years anyway
    static int getYear(ThreadContext context, IRubyObject year) {
        int y = toInt(context, year); // handles Rational(x, y)
        return (y <= 0) ? --y : y; // due julian date calc -> see adjustJodaYear
    }

    @Deprecated(since = "10.0")
    static int getMonth(IRubyObject month) {
        return getYear(((RubyBasicObject) month).getCurrentContext(), month);
    }

    static int getMonth(ThreadContext context, IRubyObject month) {
        int m = toInt(context, month); // handles Rational(x, y)
        return (m < 0) ? m + 13 : m;
    }

    @JRubyMethod(name = "valid_civil?", alias = "valid_date?", meta = true, required = 3, optional = 1, checkArity = false)
    public static IRubyObject valid_civil_p(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        int argc = Arity.checkArgumentCount(context, args, 3, 4);

        final long sg = argc > 3 ? val2sg(context, args[3]) : ITALY;
        final Long jd = validCivilImpl(context, args[0], args[1], args[2], sg);
        return jd == null ? context.fals : context.tru;
    }

    static Long validCivilImpl(ThreadContext context, IRubyObject year, IRubyObject month, IRubyObject day, final long sg) {
        final int y = toInt(context, year);
        final int m = getMonth(context, month);
        final int d = toInt(context, day);

        return DateUtils._valid_civil_p(y, m, d, sg);
    }

    // Do hour +h+, minute +min+, and second +s+ constitute a valid time?
    // If they do, returns their value as a fraction of a day.  If not, returns nil.
    @JRubyMethod(name = "_valid_time?", meta = true, visibility = Visibility.PRIVATE)
    public static IRubyObject _valid_time_p(ThreadContext context, IRubyObject self,
                                            IRubyObject h, IRubyObject m, IRubyObject s) {

        long hour = normIntValue(context, h, 24);
        long min = normIntValue(context, m, 60);
        long sec = normIntValue(context, s, 60);

        if (valid_time_p(hour, min, sec)) {
            return timeToDayFraction(context, (int) hour, (int) min, (int) sec);
        }
        return context.nil;
    }

    private static long normIntValue(ThreadContext context, IRubyObject val, final int negOffset) {
        long v = val instanceof RubyFixnum fixnum ?
                fixnum.getValue() : toLong(context, val);

        return (v < 0) ? v + negOffset : v;
    }

    // Rational(h * 3600 + min * 60 + s, 86400)
    static RubyNumeric timeToDayFraction(ThreadContext context, int hour, int min, int sec) {
        return (RubyNumeric) RubyRational.newRationalCanonicalize(context, hour * 3600 + min * 60 + sec, DAY_IN_SECONDS);
    }

    /**
     * Create a new Date object from a Julian Day Number.
     *
     * +jd+ is the Julian Day Number; if not specified, it defaults to 0.
     * +sg+ specifies the Day of Calendar Reform.
     */

    @JRubyMethod(name = "jd", meta = true)
    public static RubyDate jd(ThreadContext context, IRubyObject self) { // jd = 0, sg = ITALY
        return new RubyDate(context.runtime, (RubyClass) self, defaultDateTime);
    }

    @JRubyMethod(name = "jd", meta = true)
    public static RubyDate jd(ThreadContext context, IRubyObject self, IRubyObject jd) { // sg = ITALY
        return jdImpl(context, self, jd, ITALY);
    }

    @JRubyMethod(name = "jd", meta = true)
    public static RubyDate jd(ThreadContext context, IRubyObject self, IRubyObject jd, IRubyObject sg) {
        return jdImpl(context, self, jd, val2sg(context, sg));
    }

    private static RubyDate jdImpl(ThreadContext context, IRubyObject self, IRubyObject jd, final long sg) {
        final long[] rest = new long[] { 0, 1 };
        long jdi = RubyDateTime.getDay(context, jd, rest);
        RubyNumeric ajd = jd_to_ajd(context, jdi);

        return new RubyDate(context, (RubyClass) self, ajd, rest, 0, sg);
    }

    private void adjustWithDayFraction(ThreadContext context, DateTime dt, final long[] rest) {
        final RubyFixnum zero = RubyFixnum.zero(context.runtime);
        int ival;

        ival = RubyDateTime.getHour(context, zero, rest);
        dt = dt.plusHours(ival);

        if (rest[0] != 0) {
            ival = RubyDateTime.getMinute(context, zero, rest);
            dt = dt.plusMinutes(ival);

            if (rest[0] != 0) {
                ival = RubyDateTime.getSecond(context, zero, rest);
                dt = dt.plusSeconds(ival);

                final long r0 = rest[0], r1 = rest[1];
                if (r0 != 0) {
                    long millis = ( 1000 * r0 ) / r1;
                    dt = dt.plusMillis((int) millis);

                    subMillisNum = ((1000 * r0) - (millis * r1));
                    subMillisDen = r1;
                    normalizeSubMillis();
                }
            }
        }

        this.dt = dt;
    }

    final RubyDate normalizeSubMillis() {
        long subNum = subMillisNum;
        long subDen = subMillisDen;
        if (subNum == 0) subMillisDen = 1;
        else {
            long gcd = i_gcd(subNum, subDen);
            subMillisNum = subNum / gcd;
            subMillisDen = subDen / gcd;
        }
        return this;
    }

    @JRubyMethod(name = "valid_jd?", meta = true)
    public static IRubyObject valid_jd_p(ThreadContext context, IRubyObject self, IRubyObject jd) {
        return validJDArg(context, jd) ? context.tru : context.fals; // @see _valid_jd_p
    }

    @JRubyMethod(name = "valid_jd?", meta = true)
    public static IRubyObject valid_jd_p(ThreadContext context, IRubyObject self, IRubyObject jd, IRubyObject sg) {
        return validJDArg(context, jd) ? context.tru : context.fals; // @see _valid_jd_p
    }

    private static boolean validJDArg(ThreadContext context, IRubyObject jd) {
        return jd != context.nil && jd != context.fals && !(jd instanceof RubySymbol);
    }

    // Is +jd+ a valid Julian Day Number?
    //
    // If it is, returns it.  In fact, any value is treated as a valid Julian Day Number.

    @JRubyMethod(name = "_valid_jd?", meta = true, visibility = Visibility.PRIVATE)
    public static IRubyObject _valid_jd_p(IRubyObject self, IRubyObject jd) {
        return jd;
    }

    @JRubyMethod(name = "_valid_jd?", meta = true, visibility = Visibility.PRIVATE)
    public static IRubyObject _valid_jd_p(IRubyObject self, IRubyObject jd, IRubyObject sg) {
        return jd;
    }

    @JRubyMethod(name = "ordinal", meta = true, optional = 3, checkArity = false)
    public static RubyDate ordinal(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        // ordinal(y=-4712, d=1, sg=ITALY)
        int argc = Arity.checkArgumentCount(context, args, 0, 3);

        final long sg = argc > 2 ? val2sg(context, args[2]) : ITALY;
        IRubyObject year = (argc > 0) ? args[0] : asFixnum(context, -4712);
        IRubyObject day = (argc > 1) ? args[1] : asFixnum(context, 1);

        final long[] rest = new long[] { 0, 1 };
        final int d = (int) RubyDateTime.getDay(context, day, rest);
        Long jd = validOrdinalImpl(context, year, d, sg);

        if (jd == null) throw newDateError(context, "invalid date");

        return new RubyDate(context, (RubyClass) self, jd_to_ajd(context, jd), rest, 0, sg);
    }

    @JRubyMethod(name = "valid_ordinal?", meta = true, required = 2, optional = 1, checkArity = false)
    public static IRubyObject valid_ordinal_p(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        int argc = Arity.checkArgumentCount(context, args, 2, 3);

        final long sg = argc > 2 ? val2sg(context, args[2]) : ITALY;
        final Long jd = validOrdinalImpl(context, args[0], args[1], sg);
        return jd == null ? context.fals : context.tru;
    }

    static Long validOrdinalImpl(ThreadContext context, IRubyObject year, IRubyObject day, final long sg) {
        return validOrdinalImpl(context, year, toInt(context, day), sg);
    }

    private static Long validOrdinalImpl(ThreadContext context, IRubyObject year, int day, final long sg) {
        final int y = toInt(context, year);
        return DateUtils._valid_ordinal_p(y, day, sg);
    }

    @Deprecated // NOTE: should go away once no date.rb is using it
    @JRubyMethod(name = "_valid_ordinal?", meta = true, required = 2, optional = 1, checkArity = false, visibility = Visibility.PRIVATE)
    public static IRubyObject _valid_ordinal_p(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        int argc = Arity.checkArgumentCount(context, args, 2, 3);

        final long sg = argc > 2 ? val2sg(context, args[2]) : GREGORIAN;
        final Long jd = validOrdinalImpl(context, args[0], args[1], sg);
        return jd == null ? context.nil : asFixnum(context, jd);
    }

    @Deprecated // NOTE: should go away once no date.rb is using it
    @JRubyMethod(name = "_valid_ordinal?", required = 2, optional = 1, checkArity = false, visibility = Visibility.PRIVATE)
    public IRubyObject _valid_ordinal_p(ThreadContext context, IRubyObject[] args) {
        return RubyDate._valid_ordinal_p(context, null, args);
    }

    @JRubyMethod(name = "commercial", meta = true, optional = 4, checkArity = false)
    public static RubyDate commercial(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        // commercial(y=-4712, w=1, d=1, sg=ITALY)
        int argc = Arity.checkArgumentCount(context, args, 0, 4);

        final long sg = argc > 3 ? val2sg(context, args[3]) : ITALY;
        IRubyObject year = (argc > 0) ? args[0] : asFixnum(context, -4712);
        IRubyObject week = (argc > 1) ? args[1] : asFixnum(context, 1);
        IRubyObject day = (argc > 2) ? args[2] : asFixnum(context, 1);

        Long jd = validCommercialImpl(context, year, week, day, sg);
        if (jd == null) throw newDateError(context, "invalid date");

        return new RubyDate(context, (RubyClass) self, jd_to_ajd(context, jd), 0, sg);
    }

    @JRubyMethod(name = "valid_commercial?", meta = true, required = 3, optional = 1, checkArity = false)
    public static IRubyObject valid_commercial_p(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        int argc = Arity.checkArgumentCount(context, args, 3, 4);

        final long sg = argc > 3 ? val2sg(context, args[3]) : ITALY;
        final Long jd = validCommercialImpl(context, args[0], args[1], args[2], sg);
        return jd == null ? context.fals : context.tru;
    }

    static Long validCommercialImpl(ThreadContext context, IRubyObject year, IRubyObject week, IRubyObject day, final long sg) {
        int y = toInt(context, year);
        int w = toInt(context, week);
        int d = toInt(context, day);

        return DateUtils._valid_commercial_p(y, w, d, sg);
    }

    @Deprecated // NOTE: should go away once no date.rb is using it
    @JRubyMethod(name = "_valid_commercial?", meta = true, required = 3, optional = 1, checkArity = false, visibility = Visibility.PRIVATE)
    public static IRubyObject _valid_commercial_p(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        int argc = Arity.checkArgumentCount(context, args, 3, 4);

        final long sg = argc > 3 ? val2sg(context, args[3]) : GREGORIAN;
        final Long jd = validCommercialImpl(context, args[0], args[1], args[2], sg);
        return jd == null ? context.nil : asFixnum(context, jd);
    }

    @Deprecated // NOTE: should go away once no date.rb is using it
    @JRubyMethod(name = "_valid_weeknum?", meta = true, required = 4, optional = 1, checkArity = false, visibility = Visibility.PRIVATE)
    public static IRubyObject _valid_weeknum_p(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        int argc = Arity.checkArgumentCount(context, args, 4, 5);

        final long sg = argc > 4 ? val2sg(context, args[4]) : GREGORIAN;
        final int y = toInt(context, args[0]);
        final int w = toInt(context, args[1]);
        final int d = toInt(context, args[2]);
        final int f = toInt(context, args[3]);
        final Long jd = DateUtils._valid_weeknum_p(y, w, d, f, sg);
        return jd == null ? context.nil : asFixnum(context, jd);
    }

    /**
     # Create a new Date object representing today.
     #
     # +sg+ specifies the Day of Calendar Reform.
     **/

    @JRubyMethod(meta = true)
    public static RubyDate today(ThreadContext context, IRubyObject self) { // sg=ITALY
        return new RubyDate(context.runtime, (RubyClass) self, todayDate(context, CHRONO_ITALY_UTC));
    }

    @JRubyMethod(meta = true)
    public static RubyDate today(ThreadContext context, IRubyObject self, IRubyObject sg) {
        final long start = val2sg(context, sg);
        final Chronology chrono = getChronology(context, start, 0);
        return new RubyDate(context.runtime, (RubyClass) self, todayDate(context, chrono), 0, start);
    }

    private static DateTime todayDate(final ThreadContext context, final Chronology chrono) {
        org.joda.time.LocalDate today = new org.joda.time.LocalDate(RubyTime.getLocalTimeZone(context));
        return new DateTime(today.getYear(), today.getMonthOfYear(), today.getDayOfMonth(), 0, 0, chrono);
    }

    @JRubyMethod(name = "_valid_civil?", meta = true, required = 3, optional = 1, checkArity = false)
    public static IRubyObject _valid_civil_p(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        int argc = Arity.checkArgumentCount(context, args, 3, 4);

        final long sg = argc > 3 ? val2sg(context, args[3]) : GREGORIAN;
        final Long jd = validCivilImpl(context, args[0], args[1], args[2], sg);
        return jd == null ? context.nil : asFixnum(context, jd);
    }

    @Deprecated // NOTE: should go away once no date.rb is using it
    @JRubyMethod(name = "_valid_civil?", required = 3, optional = 1, checkArity = false, visibility = Visibility.PRIVATE)
    public IRubyObject _valid_civil_p(ThreadContext context, IRubyObject[] args) {
        return RubyDate._valid_civil_p(context, null, args);
    }

    public DateTime getDateTime() { return dt; }

    @Override
    public boolean equals(Object other) {
        return other instanceof RubyDate date ? equals(date) : false;
    }

    public final boolean equals(RubyDate that) {
        return this.start == that.start && this.dt.equals(that.dt) &&
               this.subMillisNum == that.subMillisNum && this.subMillisDen == that.subMillisDen;
    }

    @Override
    @JRubyMethod(name = "eql?")
    public IRubyObject eql_p(ThreadContext context, IRubyObject other) {
        return equals(other) ? context.tru : context.fals;
    }

    /**
     * The relationship operator for Date.
     *
     * Compares dates by Julian Day Number.  When comparing two DateTime instances, or a DateTime with a Date,
     * the instances will be regarded as equivalent if they fall on the same date in local time.
     * @param context
     * @param other
     * @return true/false/nil
     */
    @Override
    @JRubyMethod(name = "===")
    public IRubyObject op_eqq(ThreadContext context, IRubyObject other) {
        if (other instanceof RubyDate date) return f_equal(context, jd(context), date.jd(context));
        if (other instanceof RubyNumeric) return f_equal(context, jd(context), other);
        return fallback_eqq(context, other); // rb_num_coerce_cmp(self, other, "==")
    }

    private IRubyObject fallback_eqq(ThreadContext context, IRubyObject other) {
        RubyArray res;
        final IRubyObject $ex = context.getErrorInfo();
        try {
            res = (RubyArray) other.callMethod(context, "coerce", this);
        } catch (RaiseException ex) {
            context.setErrorInfo($ex);
            if (ex.getException() instanceof RubyNoMethodError) return context.nil;
            throw ex;
        }
        return f_equal(context, res.eltInternal(0), res.eltInternal(1));
    }

    @Override
    @JRubyMethod(name = "<=>")
    public IRubyObject op_cmp(ThreadContext context, IRubyObject other) {
        if (other instanceof RubyDate date) return asFixnum(context, cmp(context, date));

        // other (Numeric) - interpreted as an Astronomical Julian Day Number.

        // Comparison is by Astronomical Julian Day Number, including
        // fractional days.  This means that both the time and the
        // timezone offset are taken into account when comparing
        // two DateTime instances.  When comparing a DateTime instance
        // with a Date instance, the time of the latter will be
        // considered as falling on midnight UTC.

        if (other instanceof RubyNumeric) return f_cmp(context, ajd(context), other);

        return fallback_cmp(context, other);
    }

    private int cmp(ThreadContext context, final RubyDate that) {
        int cmp = this.dt.compareTo(that.dt); // 0, +1, -1

        if (cmp == 0) {
            if (this.subMillisDen == 1 && that.subMillisDen == 1) {
                long subNum1 = this.subMillisNum;
                long subNum2 = that.subMillisNum;
                if (subNum1 < subNum2) return -1;
                if (subNum1 > subNum2) return +1;
                return 0;
            }
            return cmpSubMillis(context, that);
        }

        return cmp;
    }

    private int cmpSubMillis(ThreadContext context, final RubyDate that) {
        RubyNumeric diff = subMillisDiff(context, that);
        return diff.isZero(context) ? 0 : ( Numeric.f_negative_p(context, diff) ? -1 : +1 );
    }

    private IRubyObject fallback_cmp(ThreadContext context, IRubyObject other) {
        RubyArray res;
        final IRubyObject $ex = context.getErrorInfo();
        try {
            res = (RubyArray) other.callMethod(context, "coerce", this);
        } catch (RaiseException ex) {
            context.setErrorInfo($ex);
            if (ex.getException() instanceof RubyNoMethodError) return context.nil;
            throw ex;
        }
        return f_cmp(context, res.eltInternal(0), res.eltInternal(1));
    }

    @Override
    public int hashCode() {
        return (int) (dt.getMillis() ^ dt.getMillis() >>> 32);
    }

    @JRubyMethod
    public RubyFixnum hash(ThreadContext context) {
        return asFixnum(context, dt.getMillis());
    }

    @JRubyMethod // Get the date as a Julian Day Number.
    public RubyFixnum jd(ThreadContext context) {
        return asFixnum(context, getJulianDayNumber());
    }

    public final long getJulianDayNumber() {
        return DateTimeUtils.toJulianDayNumber(dt.getMillis() + off * 1000);
    }

    @JRubyMethod(name = "julian?")
    public RubyBoolean julian_p(ThreadContext context) {
        return asBoolean(context, isJulian());
    }

    @JRubyMethod(name = "gregorian?")
    public RubyBoolean gregorian_p(ThreadContext context) {
        return asBoolean(context, ! isJulian());
    }

    public final boolean isJulian() {
        // JULIAN.<=>(numeric)     => +1
        if (start == JULIAN) return true;
        // GREGORIAN.<=>(numeric)  => -1
        if (start == GREGORIAN) return false;
        return getJulianDayNumber() < start;
    }

    // Get the date as an Astronomical Julian Day Number.
    @JRubyMethod
    public IRubyObject ajd(ThreadContext context) {
        long num = 210_866_760_000_000l + dt.getMillis();
        // + subMillis :
        if (subMillisDen == 1) {
            num += subMillisNum;
            return RubyRational.newInstance(context, asFixnum(context, num), DAY_MS(context));
        }

        RubyNumeric val = (RubyNumeric) asFixnum(context, num).op_plus(context, subMillis(context));
        return RubyRational.newRationalConvert(context, val, DAY_MS(context));
    }

    // Get the date as an Astronomical Modified Julian Day Number.
    @JRubyMethod
    public IRubyObject amjd(ThreadContext context) { // ajd - MJD_EPOCH_IN_AJD
        final RubyRational _MJD_EPOCH_IN_AJD = RubyRational.newRational(context.runtime, -4800001, 2); // 1858-11-17
        return _MJD_EPOCH_IN_AJD.op_plus(context, ajd(context));
    }

    // When is the Day of Calendar Reform for this Date object?
    @JRubyMethod
    public IRubyObject start(ThreadContext context) {
        Chronology chrono = dt.getChronology();
        if (chrono instanceof GregorianChronology) {
            return getMetaClass().getConstant(context, "GREGORIAN"); // Date::GREGORIAN (-Date::Infinity)
        }
        if (chrono instanceof JulianChronology) {
            return getMetaClass().getConstant(context, "JULIAN"); // Date::JULIAN (+Date::Infinity)
        }
        long cutover = DateTimeUtils.toJulianDayNumber(((GJChronology) chrono).getGregorianCutover().getMillis());
        return new RubyFixnum(context.runtime, cutover);
    }

    final int adjustJodaYear(int year) {
        if (year < 0 && isJulian()) {
            // Joda-time returns -x for year x BC in JulianChronology (so there is no year 0),
            // while date.rb returns -x+1, following astronomical year numbering (with year 0)
            return ++year;
        }
        return year;
    }

    @JRubyMethod(name = "year")
    public RubyInteger year(ThreadContext context) {
        return asFixnum(context, adjustJodaYear(dt.getYear()));
    }

    @JRubyMethod(name = "yday")
    public RubyInteger yday(ThreadContext context) {
        return asFixnum(context, dt.getDayOfYear());
    }

    @JRubyMethod(name = "mon", alias = "month")
    public RubyInteger mon(ThreadContext context) {
        return asFixnum(context, dt.getMonthOfYear());
    }

    @JRubyMethod(name = "mday", alias = "day")
    public RubyInteger mday(ThreadContext context) {
        return asFixnum(context, dt.getDayOfMonth());
    }

    // Get any fractional day part of the date.
    @JRubyMethod(name = "day_fraction")
    public RubyNumeric day_fraction(ThreadContext context) { // Rational(millis, 86_400_000)
        long ms = dt.getSecondOfDay() * 1000L + dt.getMillisOfSecond();
        if (subMillisDen == 1) {
            return (RubyNumeric) RubyRational.newRationalCanonicalize(context, ms + subMillisNum, DAY_MS);
        }
        RubyNumeric sum = RubyRational.newRational(context.runtime, ms, 1).op_plus(context, subMillis(context));
        return sum.convertToRational(context).op_div(context, asFixnum(context, DAY_MS));
    }

    @JRubyMethod(name = "hour", visibility = Visibility.PRIVATE)
    public RubyInteger hour(ThreadContext context) {
        return asFixnum(context, dt.getHourOfDay());
    }

    @JRubyMethod(name = "min", alias = "minute", visibility = Visibility.PRIVATE)
    public RubyInteger minute(ThreadContext context) {
        return asFixnum(context, dt.getMinuteOfHour());
    }

    @JRubyMethod(name = "sec", alias = "second", visibility = Visibility.PRIVATE)
    public RubyInteger second(ThreadContext context) {
        return asFixnum(context, dt.getSecondOfMinute());
    }

    @JRubyMethod(name = "sec_fraction", alias = "second_fraction", visibility = Visibility.PRIVATE)
    public RubyNumeric sec_fraction(ThreadContext context) {
        long ms = dt.getMillisOfSecond();
        if (subMillisDen == 1) {
            return (RubyNumeric) RubyRational.newRationalCanonicalize(context, ms + subMillisNum, 1000);
        }
        RubyNumeric sum = RubyRational.newRational(context.runtime, ms, 1).op_plus(context, subMillis(context));
        return sum.convertToRational(context).op_div(context, asFixnum(context, 1000));
    }

    @JRubyMethod
    public RubyInteger cwyear(ThreadContext context) {
        return asFixnum(context, adjustJodaYear(dt.getWeekyear()));
    }

    @JRubyMethod
    public RubyInteger cweek(ThreadContext context) {
        return asFixnum(context, dt.getWeekOfWeekyear());
    }

    @JRubyMethod
    public RubyInteger cwday(ThreadContext context) {
        // Monday is commercial day-of-week 1; Sunday is commercial day-of-week 7.
        return asFixnum(context, dt.getDayOfWeek());
    }

    @JRubyMethod
    public RubyInteger wday(ThreadContext context) {
        // Sunday is day-of-week 0; Saturday is day-of-week 6.
        return asFixnum(context, dt.getDayOfWeek() % 7);
    }

    private static final ByteList UTC_ZONE = new ByteList(new byte[] { '+','0','0',':','0','0' }, false);

    @JRubyMethod(visibility = Visibility.PRIVATE)
    public RubyString zone(ThreadContext context) {
        // MRI: m_zone
        if (this.off == 0) {
            return RubyString.newUsAsciiStringShared(context.runtime, UTC_ZONE);
        }
        return RubyString.newUsAsciiStringNoCopy(context.runtime, of2str(this.off));
    }

    private static final int HOUR_IN_SECONDS = 60 * 60;
    private static final int MINUTE_IN_SECONDS = 60;

    private static ByteList of2str(final int of) {
        // MRI: decode_offset
        byte s = (byte) ((of < 0) ? '-' : '+');
        int a = (of < 0) ? -of : of;
        int h = a / HOUR_IN_SECONDS;
        int m = a % HOUR_IN_SECONDS / MINUTE_IN_SECONDS;

        // format "%c%02d:%02d", s, h, m
        String digs;
        ByteList str = new ByteList(6);
        str.append(s);

        digs = Integer.toString(h);
        if (digs.length() == 1) {
            str.append('0').append(digs.charAt(0));
        }
        else if (digs.length() == 2) {
            str.append(digs.charAt(0)).append(digs.charAt(1));
        }
        else {
            append(str, digs);
        }

        str.append(':');

        digs = Integer.toString(m);
        if (digs.length() == 1) {
            str.append('0').append(digs.charAt(0));
        }
        else if (digs.length() == 2) {
            str.append(digs.charAt(0)).append(digs.charAt(1));
        }
        else {
            append(str, digs);
        }

        return str;
    }

    private static void append(final ByteList out, String val) {
        for (int i=0; i<val.length(); i++) out.append(val.charAt(i));
    }

    // def zone() strftime('%:z') end

    //

    private static final int MJD_EPOCH_IN_CJD = 2400001;
    //private static final int UNIX_EPOCH_IN_CJD = 2440588;
    private static final int LD_EPOCH_IN_CJD = 2299160;

    // Get the date as a Modified Julian Day Number.
    @JRubyMethod
    public IRubyObject mjd(ThreadContext context) {
        return asFixnum(context, getJulianDayNumber() - MJD_EPOCH_IN_CJD);
    }

    // Get the date as the number of days since the Day of Calendar
    // Reform (in Italy and the Catholic countries).
    @JRubyMethod
    public IRubyObject ld(ThreadContext context) {
        return asFixnum(context, getJulianDayNumber() - LD_EPOCH_IN_CJD);
    }

    //

    @JRubyMethod(visibility = Visibility.PRIVATE)
    public IRubyObject offset(ThreadContext context) {
        final int offset = dt.getChronology().getZone().getOffset(dt);
        return RubyRational.newRationalCanonicalize(context, offset, DAY_MS);
    }

    @JRubyMethod(optional = 1, checkArity = false, visibility = Visibility.PRIVATE)
    public IRubyObject new_offset(ThreadContext context, IRubyObject[] args) {
        int argc = Arity.checkArgumentCount(context, args, 0, 1);

        IRubyObject of = argc > 0 ? args[0] : RubyFixnum.zero(context.runtime);

        final int off = val2off(context, of);
        DateTime dt = this.dt.withChronology(getChronology(context, start, off));
        return newInstance(context, dt, off, start);
    }

    @JRubyMethod
    public IRubyObject new_start(ThreadContext context) {
        return newStart(context, ITALY);
    }

    // Create a copy of this Date object using a new Day of Calendar Reform.
    @JRubyMethod
    public IRubyObject new_start(ThreadContext context, IRubyObject sg) {
        return newStart(context, val2sg(context, sg));
    }

    private RubyDate newStart(ThreadContext context, final long start) {
        DateTime dt = this.dt.withChronology(getChronology(context, start, off));
        return newInstance(context, dt, off, start, subMillisNum, subMillisDen);
    }

    @JRubyMethod
    public IRubyObject italy(ThreadContext context) { return newStart(context, ITALY); }

    @JRubyMethod
    public IRubyObject england(ThreadContext context) { return newStart(context, ENGLAND); }

    @JRubyMethod
    public IRubyObject julian(ThreadContext context) { return newStart(context, JULIAN); }

    @JRubyMethod
    public IRubyObject gregorian(ThreadContext context) { return newStart(context, GREGORIAN); }

    @JRubyMethod(name = "julian_leap?", meta = true)
    public static IRubyObject julian_leap_p(ThreadContext context, IRubyObject self, IRubyObject year) {
        return asBoolean(context, isJulianLeap(toLong(context, year)));
    }

    @JRubyMethod(name = "gregorian_leap?", alias = "leap?", meta = true)
    public static IRubyObject gregorian_leap_p(ThreadContext context, IRubyObject self, IRubyObject year) {
        return asBoolean(context, isGregorianLeap(toLong(context, year)));
    }

    // All years divisible by 4 are leap years in the Julian calendar.
    private static boolean isJulianLeap(final long year) {
        return year % 4 == 0;
    }

    // All years divisible by 4 are leap years in the Gregorian calendar,
    // except for years divisible by 100 and not by 400.
    private static boolean isGregorianLeap(final long year) {
        long uy = (year >= 0) ? year : -year;
        if ((uy & 3) != 0)
            return false;

        long century = uy / 100;
        if (uy != century * 100)
            return true;
        return (century & 3) == 0;
    }

    @JRubyMethod(name = "leap?")
    public IRubyObject leap_p(ThreadContext context) {
        final long year = dt.getYear();
        return asBoolean(context,  isJulian() ? isJulianLeap(year) : isGregorianLeap(year) );
    }

    //

    @JRubyMethod(name = "+")
    public IRubyObject op_plus(ThreadContext context, IRubyObject n) {
        return n instanceof RubyFixnum fixnum ?
                newInstance(context, dt.plusDays(+fixnum.asInt(context)), off, start) :
                op_plus_numeric(context, castAsNumeric(context, n, "expected numeric"));
    }

    RubyDate op_plus_numeric(ThreadContext context, RubyNumeric n) {
        // ms, sub = (n * 86_400_000).divmod(1)
        // sub = 0 if sub == 0 # avoid Rational(0, 1)
        // sub_millis = @sub_millis + sub
        // if sub_millis >= 1
        //   sub_millis -= 1
        //   ms += 1
        // end
        RubyNumeric val = (RubyNumeric) asFixnum(context, DAY_MS).op_mul(context, n);

        var res = (RubyArray<?>) val.divmod(context, asFixnum(context, 1));
        long ms = ((RubyInteger) res.eltInternal(0)).asLong(context);
        RubyNumeric sub = (RubyNumeric) res.eltInternal(1);

        RubyNumeric sub_millis = subMillis(context);

        if ( sub.isZero(context) ) ; // done - noop
        else if ( sub instanceof RubyFloat flote) {
            sub = roundToPrecision(context, flote, SUB_MS_PRECISION);
            sub_millis = (RubyNumeric) sub_millis.op_plus(context, sub);
        } else {
            sub_millis = (RubyNumeric) sub_millis.op_plus(context, sub);
        }

        long subNum = toLong(context, sub_millis.numerator(context));
        long subDen = toLong(context, sub_millis.denominator(context));
        if (subNum / subDen >= 1) { // sub_millis >= 1
            subNum -= subDen; ms += 1; // sub_millis -= 1
        }
        return newInstance(context, dt.plus(ms), off, start, subNum, subDen).normalizeSubMillis();
    }

    static final int SUB_MS_PRECISION = 1_000_000_000;

    static RubyNumeric roundToPrecision(ThreadContext context, RubyFloat sub, final long precision) {
        long s = Math.round(sub.asDouble(context) * precision);
        return (RubyNumeric) RubyRational.newRationalCanonicalize(context, s, precision);
    }

    @JRubyMethod(name = "-")
    public IRubyObject op_minus(ThreadContext context, IRubyObject n) {
        return switch (n) {
            case RubyFixnum fixnum -> newInstance(context, dt.plusDays(-fixnum.asInt(context)), off, start);
            case RubyNumeric numeric -> op_plus_numeric(context, (RubyNumeric) numeric.op_uminus(context));
            case RubyDate date -> op_minus_date(context, date);
            default -> throw typeError(context, "expected numeric or date");
        };
    }

    private RubyNumeric op_minus_date(ThreadContext context, final RubyDate that) {
        long diff = this.dt.getMillis() - that.dt.getMillis();
        RubyNumeric diffMillis = (RubyNumeric) RubyRational.newRationalCanonicalize(context, diff, DAY_MS);
        RubyNumeric subDiff = subMillisDiff(context, that);

        if (!subDiff.isZero(context)) { // diff += diff_sub;
            subDiff = subDiff.convertToRational(context).op_div(context, asFixnum(context, DAY_MS));  // #5493
            return (RubyNumeric) diffMillis.op_plus(context, subDiff);
        }
        return diffMillis;
    }

    private RubyNumeric subMillisDiff(final ThreadContext context, final RubyDate that) {
        long subNum1 = this.subMillisNum;
        long subNum2 = that.subMillisNum;
        if (subNum2 == 0) return RubyRational.newRational(context.runtime, +subNum1, this.subMillisDen);
        if (subNum1 == 0) return RubyRational.newRational(context.runtime, -subNum2, that.subMillisDen);

        return this.subMillisDen == 1 && that.subMillisDen == 1 ?
                (RubyInteger) asFixnum(context, subNum1).op_minus(context, subNum2) :
                subMillis(context).op_minus(context, that.subMillis(context));
    }

    final RubyRational subMillis(final ThreadContext context) {
        return RubyRational.newRational(context.runtime, subMillisNum, subMillisDen);
    }

    // Return a new Date one day after this one.
    @JRubyMethod(name = "next", alias = "succ")
    public IRubyObject next(ThreadContext context) {
        return next_day(context);
    }

    @JRubyMethod
    public IRubyObject next_day(ThreadContext context) {
        return newInstance(context, dt.plusDays(+1), off, start);
    }

    @JRubyMethod
    public IRubyObject next_day(ThreadContext context, IRubyObject n) {
        return newInstance(context, dt.plusDays(+simpleIntDiff(context, n)), off, start);
    }

    @JRubyMethod
    public IRubyObject prev_day(ThreadContext context) {
        return newInstance(context, dt.plusDays(-1), off, start);
    }

    @JRubyMethod
    public IRubyObject prev_day(ThreadContext context, IRubyObject n) {
        return newInstance(context, dt.plusDays(-simpleIntDiff(context, n)), off, start);
    }

    @JRubyMethod
    public IRubyObject next_month(ThreadContext context) {
        return newInstance(context, dt.plusMonths(+1), off, start);
    }

    @JRubyMethod
    public IRubyObject next_month(ThreadContext context, IRubyObject n) {
        return newInstance(context, dt.plusMonths(+simpleIntDiff(context, n)), off, start);
    }

    @JRubyMethod
    public IRubyObject prev_month(ThreadContext context) {
        return newInstance(context, dt.plusMonths(-1), off, start);
    }

    @JRubyMethod
    public IRubyObject prev_month(ThreadContext context, IRubyObject n) {
        return newInstance(context, dt.plusMonths(-simpleIntDiff(context, n)), off, start);
    }

    private static int simpleIntDiff(ThreadContext context, IRubyObject n) {
        final int days = toInt(context, n);
        return n instanceof RubyRational rat && rat.getDenominator().asLong(context) != 1 ?
                days + 1 : // MRI rulez: 1/2 -> 1 (but 0.5 -> 0)
                days;
    }

    @JRubyMethod(name = ">>")
    public IRubyObject shift_fw(ThreadContext context, IRubyObject n) {
        return next_month(context, n);
    }

    @JRubyMethod(name = "<<")
    public IRubyObject shift_bw(ThreadContext context, IRubyObject n) {
        return prev_month(context, n);
    }

    @JRubyMethod
    public IRubyObject next_year(ThreadContext context) {
        return newInstance(context, dt.plusYears(+1), off, start);
    }

    @JRubyMethod
    public IRubyObject next_year(ThreadContext context, IRubyObject n) {
        return prevNextYear(context, n, false);
    }

    @JRubyMethod
    public IRubyObject prev_year(ThreadContext context) {
        return newInstance(context, dt.plusYears(-1), off, start);
    }

    @JRubyMethod
    public IRubyObject prev_year(ThreadContext context, IRubyObject n) {
        return prevNextYear(context, n, true);
    }

    private RubyDate prevNextYear(ThreadContext context, IRubyObject n, final boolean negate) {
        long months = timesIntDiff(context, n, 12);
        if (negate) months = -months; // prev_year
        final int years = RubyNumeric.checkInt(context.runtime, months / 12);
        return newInstance(context, this.dt.plusYears(years).plusMonths((int) (months % 12)), off, start);
    }

    static long timesIntDiff(final ThreadContext context, IRubyObject n, final int times) {
        var mul = (RubyNumeric) asFixnum(context, times).op_mul(context, n);
        return toLong(context, mul.round(context));
    }

    @JRubyMethod // [ ajd, @of, @sg ]
    public IRubyObject marshal_dump(ThreadContext context) {
        return newArrayNoCopy(context, ajd(context), RubyRational.newRationalCanonicalize(context, off, DAY_IN_SECONDS),
                asFixnum(context, start));
    }

    @JRubyMethod(meta = true)
    public static RubyDate _load(ThreadContext context, IRubyObject klass, IRubyObject str) {
        IRubyObject a = RubyMarshal.load(context, null, new IRubyObject[] { str }, null);
        RubyDate obj = (RubyDate) ((RubyClass) klass).allocate(context);
        return obj.marshal_load(context, a);
    }

    @JRubyMethod
    public RubyDate marshal_load(ThreadContext context, IRubyObject a) {
        checkFrozen();

        final RubyArray ary = castAsArray(context, a, "expected an array");

        IRubyObject ajd, of, sg;

        switch (ary.size()) {
            case 2: /* 1.6.x */
                ajd = valMinusOneHalf(context, ary.eltInternal(0));
                of = RubyFixnum.zero(context.runtime);
                sg = ary.eltInternal(1);
                if (!k_numeric_p(sg)) {
                    sg = RubyFloat.newFloat(context.runtime, sg.isTrue() ? GREGORIAN_INFINITY : JULIAN_INFINITY);
                }
                break;
            case 3: /* 1.8.x, 1.9.2 */ // ajd, of, sg = a
                ajd = ary.eltInternal(0);
                of = ary.eltInternal(1);
                sg = ary.eltInternal(2);
                break;
            case 6: // _, jd, df, sf, of, sg = a
                IRubyObject jd = ary.eltInternal(1);
                IRubyObject df = ary.eltInternal(2);
                IRubyObject sf = ary.eltInternal(3);
                of = ary.eltInternal(4);
                sg = ary.eltInternal(5);
                of = newRationalConvert(context, of, DAY_IN_SECONDS);
                ajd = marshal_load_6(context, jd, df, sf);
                break;
            default:
                throw typeError(context, "invalid size: " + ary.size());
        }

        return initialize(context, ajd, of, sg);
    }

    private IRubyObject marshal_load_6(ThreadContext context, IRubyObject jd, IRubyObject df, IRubyObject sf) {
        IRubyObject ajd = valMinusOneHalf(context, jd);
        if (!((RubyNumeric) df).isZero(context)) {
            ajd = newRationalConvert(context, df, DAY_IN_SECONDS).op_plus(context, ajd);
        }
        if (!((RubyNumeric) sf).isZero(context)) {
            ajd = newRationalConvert(context, sf, DAY_IN_SECONDS * 1_000_000_000L).op_plus(context, ajd);
        }
        return ajd;
    }

    private static IRubyObject valMinusOneHalf(ThreadContext context, IRubyObject val) {
        return RubyRational.newRational(context.runtime, -1, 2).op_plus(context, val);
    }

    static RubyRational newRationalConvert(ThreadContext context, IRubyObject num, long den) {
        return (RubyRational) RubyRational.newRationalConvert(context, num, asFixnum(context, den));
    }

    // def jd_to_ajd(jd, fr, of=0) jd + fr - of - Rational(1, 2) end
    private static double jd_to_ajd(long jd) { return jd - 0.5; }

    static RubyNumeric jd_to_ajd(ThreadContext context, long jd) {
        return RubyRational.newRational(context.runtime, (jd * 2) - 1, 2);
    }

    static RubyNumeric jd_to_ajd(ThreadContext context, long jd, RubyNumeric fr, int of_sec) {
        return jd_to_ajd(context, asFixnum(context, jd), fr, of_sec);
    }

    static RubyNumeric jd_to_ajd(ThreadContext context, RubyNumeric jd, RubyNumeric fr, int of_sec) {
        RubyNumeric tmp = jd; // jd - of :
        if (of_sec != 0) {
            tmp = (RubyNumeric) tmp.op_plus(context, RubyRational.newRationalCanonicalize(context, -of_sec, DAY_IN_SECONDS));
        }
        final RubyRational MINUS_HALF = RubyRational.newRational(context.runtime, -1, 2);
        return (RubyNumeric) ((RubyNumeric) tmp.op_plus(context, fr)).op_plus(context, MINUS_HALF);
    }

    @JRubyMethod(meta = true, required = 2, optional = 1, checkArity = false, visibility = Visibility.PRIVATE)
    public static RubyNumeric jd_to_ajd(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        int argc = Arity.checkArgumentCount(context, args, 2, 3);

        RubyNumeric jd = (RubyNumeric) args[0];
        RubyNumeric fr = (RubyNumeric) args[1];
        int of_sec = 0;
        if (argc > 2 && ! ((RubyNumeric) args[2]).isZero(context)) {
            RubyNumeric of = (RubyNumeric) f_mul(context, args[2], asFixnum(context, DAY_IN_SECONDS));
            of_sec = of.asInt(context);
        }
        return jd_to_ajd(context, jd, fr, of_sec);
    }

    public static Chronology getChronology(ThreadContext context, final long sg, final int off) {
        final DateTimeZone zone;
        if (off == 0) {
            if (sg == ITALY) return CHRONO_ITALY_UTC;
            zone = DateTimeZone.UTC;
        } else {
            try {
                zone = DateTimeZone.forOffsetMillis(off * 1000); // off in seconds
            } // NOTE: JODA only allows 'valid': -23:59:59.999 to +23:59:59.999
            catch (IllegalArgumentException ex) { // while MRI handles 25/24 fine
                debug(context, "invalid offset", ex);
                throw argumentError(context, "invalid offset: " + off);
            }
        }
        return getChronology(context, sg, zone);
    }

    static Chronology getChronology(ThreadContext context, final long sg, final DateTimeZone zone) {
        if (sg == ITALY) return GJChronology.getInstance(zone);
        if (sg == JULIAN) return JulianChronology.getInstance(zone);
        if (sg == GREGORIAN) return GregorianChronology.getInstance(zone);

        Instant cutover = new Instant(DateTimeUtils.fromJulianDay(jd_to_ajd(sg)));
        try {
            return GJChronology.getInstance(zone, cutover);
        } // java.lang.IllegalArgumentException: Cutover too early. Must be on or after 0001-01-01.
        catch (IllegalArgumentException ex) {
            debug(context, "invalid date", ex);
            throw newDateError(context, "invalid date");
        }
    }

    // MRI: #define val2sg(vsg,dsg)
    static long val2sg(ThreadContext context, IRubyObject sg) {
        return getValidStart(context, sg.convertToFloat().asDouble(context), ITALY);
    }

    @Deprecated(since = "10.0")
    static long valid_sg(ThreadContext context, IRubyObject sg) {
        return getValidStart(context, sg.convertToFloat().asDouble(context), 0);
    }

    // MRI: #define valid_sg(sg)
    static long getValidStart(final ThreadContext context, final double sg, final int DEFAULT_SG) {
        // MRI: c_valid_start_p(double sg)

        if (sg == Double.NEGATIVE_INFINITY || sg == Double.POSITIVE_INFINITY) return (long) sg;

        if (Double.isNaN(sg) || sg < REFORM_BEGIN_JD && sg > REFORM_END_JD) {
            RubyKernel.warn(context, null, newString(context, "invalid start is ignored"));
            return DEFAULT_SG;
        }
        ;
        return (long) sg;
    }

    private static final int REFORM_BEGIN_JD = 2298874; /* ns 1582-01-01 */
    private static final int REFORM_END_JD = 2426355; /* os 1930-12-31 */

    // MRI: #define val2off(vof,iof)
    static int val2off(ThreadContext context, IRubyObject of) {
        final int off = offset_to_sec(context, of);
        if (off == INVALID_OFFSET) {
            RubyKernel.warn(context, null, newString(context, "invalid offset is ignored"));
            return 0;
        }
        return off;
    }

    static void debug(ThreadContext context, final String msg, Exception ex) {
        if (LOG.isDebugEnabled()) LOG.debug(msg, ex);
        else if (context.runtime.isDebug()) LOG.info(msg, ex);
    }

    @JRubyMethod
    public RubyString inspect(ThreadContext context) {
        int off = this.off;
        int s = (dt.getHourOfDay() * 60 + dt.getMinuteOfHour()) * 60 + dt.getSecondOfMinute() - off;
        long ns = (dt.getMillisOfSecond() * 1_000_000) + (subMillisNum * 1_000_000) / subMillisDen;
        ByteList str = new ByteList(54); // e.g. #<Date: 2018-01-15 ((2458134j,0s,0n),+0s,2299161j)>
        str.append('#').append('<');
        str.append(((RubyString) getMetaClass().to_s(context)).getByteList());
        str.append(':').append(' ');
        str.append(to_s(context).getByteList()); // to_s
        str.append(' ').append('(').append('(');
        str.append(ConvertBytes.longToByteListCached(getJulianDayNumber()));
        str.append('j').append(',');
        str.append(ConvertBytes.longToByteListCached(s));
        str.append('s').append(',');
        str.append(ConvertBytes.longToByteListCached(ns));
        str.append('n').append(')');
        str.append(',');
        if (off >= 0) str.append('+');
        str.append(ConvertBytes.longToByteListCached(off));
        str.append('s').append(',');
        if (start == GREGORIAN) {
            str.append('-').append('I').append('n').append('f');
        }
        else if (start == JULIAN) {
            str.append('I').append('n').append('f');
        }
        else {
            str.append(ConvertBytes.longToByteListCached(start));
        }
        str.append('j').append(')').append('>');

        return RubyString.newUsAsciiStringNoCopy(context.runtime, str);
    }

    private static final ByteList TO_S_FORMAT = new ByteList(ByteList.plain("%.4d-%02d-%02d"), false);
    static { TO_S_FORMAT.setEncoding(USASCIIEncoding.INSTANCE); }

    @Override
    @JRubyMethod
    public RubyString to_s(ThreadContext context) { // format('%.4d-%02d-%02d', year, mon, mday)
        return format(context, TO_S_FORMAT, year(context), mon(context), mday(context));
    }

    static RubyString format(ThreadContext context, ByteList fmt, IRubyObject... args) {
        final RubyString str = RubyString.newStringLight(context.runtime, fmt);
        return str.op_format(context, RubyArray.newArrayNoCopy(context.runtime, args));
    }

    @JRubyMethod
    public RubyDate to_date() { return this; }

    @JRubyMethod
    public RubyDateTime to_datetime(ThreadContext context) {
        return new RubyDateTime(context.runtime, getDateTime(context), dt.withTimeAtStartOfDay(), off, start);
    }

    @JRubyMethod // Time.local(year, mon, mday)
    public RubyTime to_time(ThreadContext context) {
        DateTime dt = this.dt;

        dt = new DateTime(adjustJodaYear(dt.getYear()), dt.getMonthOfYear(), dt.getDayOfMonth(),
                0, 0, 0, RubyTime.getLocalTimeZone(context));
        return new RubyTime(context.runtime, timeClass(context), dt);
    }

    // date/format.rb

    @JRubyMethod // def strftime(fmt='%F')
    public RubyString strftime(ThreadContext context) {
        return strftime(context, RubyString.newStringLight(context.runtime, DEFAULT_FORMAT_BYTES));
    }

    @JRubyMethod // alias_method :format, :strftime
    public RubyString strftime(ThreadContext context, IRubyObject fmt) {
        RubyRational subMillis = this.subMillisNum == 0 ? null :
                RubyRational.newRational(context.runtime, this.subMillisNum, this.subMillisDen);
        RubyString format = context.getRubyDateFormatter().compileAndFormat(
                fmt.convertToString().getByteList(), true, this.dt, 0, subMillis
        );
        return format;
    }

    private static final String DEFAULT_FORMAT = "%F";
    private static final ByteList DEFAULT_FORMAT_BYTES = ByteList.create(DEFAULT_FORMAT);
    static { DEFAULT_FORMAT_BYTES.setEncoding(USASCIIEncoding.INSTANCE); }

    @JRubyMethod(meta = true)
    public static IRubyObject _strptime(ThreadContext context, IRubyObject self, IRubyObject string) {
        return parse(context, string, DEFAULT_FORMAT);
    }

    @JRubyMethod(meta = true)
    public static IRubyObject _strptime(ThreadContext context, IRubyObject self, IRubyObject string, IRubyObject format) {
        string = string.convertToString();
        format = format.convertToString();
        return parse(context, string, ((RubyString) format).decodeString());
    }

    static IRubyObject parse(ThreadContext context, IRubyObject string, String format) {
        string = TypeConverter.checkStringType(context.runtime, string);

        return new RubyDateParser().parse(context, format, (RubyString) string);
    }

    // @Deprecated
    public static IRubyObject _strptime(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        return switch (args.length) {
            case 1 -> _strptime(context, self, args[0]);
            case 2 -> _strptime(context, self, args[0], args[1]);
            default -> throw argumentError(context, args.length, 1);
        };
    }

    @JRubyMethod(name = "zone_to_diff", meta = true, visibility = Visibility.PRIVATE)
    public static IRubyObject zone_to_diff(ThreadContext context, IRubyObject self, IRubyObject zone) {
        final int offset = TimeZoneConverter.dateZoneToDiff(zone.asJavaString());
        return offset == TimeZoneConverter.INVALID_ZONE ? context.nil : asFixnum(context, offset);
    }

    @JRubyMethod(name = "i", meta = true, visibility = Visibility.PRIVATE)
    public static RubyInteger _i(ThreadContext context, IRubyObject self, IRubyObject val) {
        return (RubyInteger) TypeConverter.convertToInteger(context, val, 10); // Integer(str, 10)
    }

    @JRubyMethod(name = "comp_year69", meta = true, visibility = Visibility.PRIVATE)
    public static RubyInteger _comp_year69(ThreadContext context, IRubyObject self, IRubyObject year) {
        RubyInteger y = _i(context, self, year);
        if (((RubyString) year).strLength() < 4) {
            final long yi = y.asLong(context);
            return asFixnum(context, yi >= 69 ? yi + 1900 : yi + 2000);
        }
        return y;
    }

    private static final ByteList[] ABBR_DAYS = new ByteList[] {
            new ByteList(new byte[] { 's','u','n' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'm','o','n' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 't','u','e' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'w','e','d' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 't','h','u' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'f','r','i' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 's','a','t' }, USASCIIEncoding.INSTANCE),
    };

    private static int day_num(RubyString s) {
        ByteList sb = s.getByteList();
        int i;
        for (i=0; i<ABBR_DAYS.length; i++) {
            if (sb.caseInsensitiveCmp(ABBR_DAYS[i]) == 0) return i;
        }
        return -1;
    }

    private static final ByteList _parse_time, _parse_time2;
    static {
        _parse_time = ByteList.create(
                "(" +
                  "(?:" +
                    "\\d+\\s*:\\s*\\d+" +
                    "(?:" +
                      "\\s*:\\s*\\d+(?:[,.]\\d+)?" +
                    ")?" +
                  "|" +
                    "\\d+\\s*h(?:\\s*\\d+m?(?:\\s*\\d+s?)?)?" +
                  ")" +
                  "(?:" +
                    "\\s*" +
                    "[ap](?:m\\b|\\.m\\.)" +
                  ")?" +
                  "|" +
                    "\\d+\\s*[ap](?:m\\b|\\.m\\.)" +
                  ")" +
                  "(?:" +
                    "\\s*" +
                    "(" +
                      "(?:gmt|utc?)?[-+]\\d+(?:[,.:]\\d+(?::\\d+)?)?" +
                    "|" +
                      "(?-i:[[:alpha:].\\s]+)(?:standard|daylight)\\stime\\b" +
                    "|" +
                      "(?-i:[[:alpha:]]+)(?:\\sdst)?\\b" +
                    ")" +
                ")?"
        );
        _parse_time.setEncoding(USASCIIEncoding.INSTANCE);

        _parse_time2 = ByteList.create(
                "\\A(\\d+)h?" +
                "(?:\\s*:?\\s*(\\d+)m?" +
                  "(?:" +
                    "\\s*:?\\s*(\\d+)(?:[,.](\\d+))?s?" +
                  ")?" +
                ")?" +
                "(?:\\s*([ap])(?:m\\b|\\.m\\.))?"
        );
        _parse_time2.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static IRubyObject _parse_time(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {

        RubyRegexp re = newRegexpFromCache(context, _parse_time, RE_OPTION_IGNORECASE | RE_OPTION_EXTENDED);
        IRubyObject sub = subSpace(context, str, re);
        if (!(sub instanceof RubyMatchData match)) return context.nil;

        final RubyString s1 = (RubyString) match.at(context, 1);
        final RubyString s2 = matchOrNull(context, match, 2);

        if (s2 != null) hash.fastASet(asSymbol(context, "zone"), s2);

        re = newRegexpFromCache(context, _parse_time2, RE_OPTION_IGNORECASE | RE_OPTION_EXTENDED);
        sub = re.match_m(context, s1, false);
        if (sub instanceof RubyMatchData match2) {
            RubyInteger hour;
            RubyString m = (RubyString) match2.at(context, 1);
            hash.fastASet(asSymbol(context, "hour"), hour = (RubyInteger) m.to_i(context));
            m = matchOrNull(context, match2, 2);
            if (m != null) hash.fastASet(asSymbol(context, "min"), m.to_i(context));
            m = matchOrNull(context, match2, 3);
            if (m != null) hash.fastASet(asSymbol(context, "sec"), m.to_i(context));
            m = matchOrNull(context, match2, 4);
            if (m != null) {
                RubyInteger den = (RubyInteger) asFixnum(context, 10).op_pow(context, m.length());
                hash.fastASet(asSymbol(context, "sec_fraction"), RubyRational.newInstance(context, (RubyInteger) m.to_i(context), den));
            }
            m = matchOrNull(context, match2, 5);
            if (m != null) {
                hour = (RubyInteger) hour.op_mod(context, 12);
                if (m.length() == 1 && strPtr(m, 'p') || strPtr(m, 'P')) {
                    hour = (RubyInteger) hour.op_plus(context, 12);
                }
                hash.fastASet(asSymbol(context, "hour"), hour);
            }
        } else {
            hash.fastASet(asSymbol(context, "hour"), asFixnum(context, 0));
        }

        return context.tru;
    }

    private static final ByteList[] ABBR_MONTHS = new ByteList[] {
            new ByteList(new byte[] { 'j','a','n' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'f','e','b' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'm','a','r' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'a','p','r' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'm','a','y' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'j','u','n' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'j','u','l' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'a','u','g' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 's','e','p' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'o','c','t' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'n','o','v' }, USASCIIEncoding.INSTANCE),
            new ByteList(new byte[] { 'd','e','c' }, USASCIIEncoding.INSTANCE),
    };

    private static int mon_num(RubyString s) {
        ByteList sb = s.getByteList();
        int i;
        for (i=0; i<ABBR_MONTHS.length; i++) {
            if (sb.caseInsensitiveCmp(ABBR_MONTHS[i]) == 0) return i + 1;
        }
        return -1;
    }

    private static final ByteList _parse_day;
    static {
        _parse_day = ByteList.create("\\b(sun|mon|tue|wed|thu|fri|sat)[^-\\d\\s]*");
        _parse_day.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static IRubyObject _parse_day(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {
        RubyRegexp re = newRegexpFromCache(context, _parse_day, RE_OPTION_IGNORECASE);
        IRubyObject sub = subSpace(context, str, re);
        if (!(sub instanceof RubyMatchData match)) return context.nil;

        int day = day_num((RubyString) match.at(context, 1));
        hash.fastASet(asSymbol(context, "wday"), asFixnum(context, day));
        return context.tru;
    }

    private static final ByteList _parse_mon;
    static {
        _parse_mon = ByteList.create("\\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\\S*");
        _parse_mon.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static IRubyObject _parse_mon(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {
        RubyRegexp re = newRegexpFromCache(context, _parse_mon, RE_OPTION_IGNORECASE);
        IRubyObject sub = subSpace(context, str, re);
        if (!(sub instanceof RubyMatchData match)) return context.nil;

        int mon = mon_num((RubyString) match.at(context, 1));
        hash.fastASet(asSymbol(context, "mon"), asFixnum(context, mon));
        return context.tru;
    }

    private static final ByteList _parse_year;
    static {
        _parse_year = ByteList.create("'(\\d+)\\b");
        _parse_year.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static IRubyObject _parse_year(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {
        RubyRegexp re = RubyRegexp.newRegexp(context.runtime, _parse_year);
        IRubyObject sub = subSpace(context, str, re);
        if (!(sub instanceof RubyMatchData match)) return context.nil;

        hash.fastASet(asSymbol(context, "year"), ((RubyString) match.at(context, 1)).to_i(context));
        return context.tru;
    }

    private static final ByteList _parse_mday;
    static {
        _parse_mday = ByteList.create("(\\d+)(st|nd|rd|th)\\b");
        _parse_mday.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static IRubyObject _parse_mday(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {
        RubyRegexp re = newRegexpFromCache(context, _parse_mday, RE_OPTION_IGNORECASE);
        IRubyObject sub = subSpace(context, str, re);
        if (!(sub instanceof RubyMatchData match)) return context.nil;

        hash.fastASet(asSymbol(context, "mday"), ((RubyString) match.at(context, 1)).to_i(context));
        return context.tru;
    }

    private static final ByteList _parse_eu;
    static {
        _parse_eu = ByteList.create(
                "('?\\d+)[^-\\d\\s]*" +
                "\\s*" +
                "(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[^-\\d\\s']*" +
                "(?:" +
                  "\\s*" +
                  "(c(?:e|\\.e\\.)|b(?:ce|\\.c\\.e\\.)|a(?:d|\\.d\\.)|b(?:c|\\.c\\.))?" +
                  "\\s*" +
                  "('?-?\\d+(?:(?:st|nd|rd|th)\\b)?)" +
                ")?"
        );
        _parse_eu.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static IRubyObject _parse_eu(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {
        RubyRegexp re = newRegexpFromCache(context, _parse_eu, RE_OPTION_IGNORECASE);
        IRubyObject sub = subSpace(context, str, re);
        if (!(sub instanceof RubyMatchData match)) return context.nil;

        RubyString d = (RubyString) match.at(context, 1);
        RubyString mon = (RubyString) match.at(context, 2);
        mon = newSharedString(context, ConvertBytes.byteToSharedByteList((short) mon_num(mon)));
        RubyString b = matchOrNull(context, match, 3);
        RubyString y = matchOrNull(context, match, 4);

        s3e(context, hash, y, mon, d, b != null && b.length() > 1 && (b.charAt(0) == 'B' || b.charAt(0) == 'b'));

        return context.tru;
    }

    private static final ByteList _parse_us;
    static {
        _parse_us = ByteList.create(
                "\\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[^-\\d\\s']*" +
                "\\s*" +
                "('?\\d+)[^-\\d\\s']*" +
                "(?:" +
                  "\\s*,?" +
                  "\\s*" +
                  "(c(?:e|\\.e\\.)|b(?:ce|\\.c\\.e\\.)|a(?:d|\\.d\\.)|b(?:c|\\.c\\.))?" +
                  "\\s*" +
                  "('?-?\\d+)" +
                ")?"
        );
        _parse_us.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static IRubyObject _parse_us(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {
        RubyRegexp re = newRegexpFromCache(context, _parse_us, RE_OPTION_IGNORECASE);
        IRubyObject sub = subSpace(context, str, re);
        if (!(sub instanceof RubyMatchData match)) return context.nil;

        RubyString mon = (RubyString) match.at(context, 1);
        mon = newSharedString(context, ConvertBytes.byteToSharedByteList((short) mon_num(mon)));
        RubyString d = (RubyString) match.at(context, 2);
        RubyString b = matchOrNull(context, match, 3);
        RubyString y = matchOrNull(context, match, 4);

        s3e(context, hash, y, mon, d, b != null && b.length() > 1 && (b.charAt(0) == 'B' || b.charAt(0) == 'b'));

        return context.tru;
    }

    // NOTE: without this things get slower than the .rb version of _parse_eu/_parse_us etc.
    private static RubyRegexp newRegexpFromCache(ThreadContext context, ByteList str, int opts) {
        RegexpOptions options = RegexpOptions.fromEmbeddedOptions(opts);
        Regex pattern = getRegexpFromCache(context.runtime, str, str.getEncoding(), options);
        return new RubyRegexp(context.runtime, pattern, str, options);
    }

    private static RubyString matchOrNull(ThreadContext context, final RubyMatchData match, int i) {
        IRubyObject val = match.at(context, i);
        return val == context.nil ? null : (RubyString) val;
    }

    private static final ByteList _parse_iso;
    static {
        _parse_iso = ByteList.create("('?[-+]?\\d+)-(\\d+)-('?-?\\d+)");
        _parse_iso.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static IRubyObject _parse_iso(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {
        RubyRegexp re = RubyRegexp.newRegexp(context.runtime, _parse_iso);
        IRubyObject sub = subSpace(context, str, re);
        if (!(sub instanceof RubyMatchData match)) return context.nil;

        s3e(context, hash, (RubyString) match.at(context, 1), (RubyString) match.at(context, 2),
                (RubyString) match.at(context, 3), false);
        return context.tru;
    }

    private static final ByteList _parse_sla;
    static {
        _parse_sla = ByteList.create("('?-?\\d+)/\\s*('?\\d+)(?:\\D\\s*('?-?\\d+))?");
        _parse_sla.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static IRubyObject _parse_sla(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {
        return parse_sla_dot(context, _parse_sla, str, hash);
    }

    private static final ByteList _parse_dot;
    static {
        _parse_dot = ByteList.create("('?-?\\d+)\\.\\s*('?\\d+)\\.\\s*('?-?\\d+)");
        _parse_dot.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static IRubyObject _parse_dot(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {
        return parse_sla_dot(context, _parse_dot, str, hash);
    }

    private static IRubyObject parse_sla_dot(ThreadContext context, ByteList pattern, RubyString str, RubyHash hash) {
        final Ruby runtime = context.runtime;
        RubyRegexp re = RubyRegexp.newRegexp(runtime, pattern);
        IRubyObject sub = subSpace(context, str, re);
        if (sub != context.nil) {
            final RubyMatchData match = (RubyMatchData) sub;
            RubyString y = matchOrNull(context, match, 1);
            RubyString mon = matchOrNull(context, match, 2);
            RubyString d = matchOrNull(context, match, 3);

            s3e(context, hash, y, mon, d, false);
            return context.tru;
        }
        return sub; // nil
    }

    private static final ByteList _parse_bc;
    static {
        _parse_bc = ByteList.create("\\b(bc\\b|bce\\b|b\\.c\\.|b\\.c\\.e\\.)");
        _parse_bc.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static void parse_bc(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {
        RubyRegexp re = newRegexpFromCache(context, _parse_bc, RE_OPTION_IGNORECASE);
        IRubyObject sub = subSpace(context, (RubyString) str, re);
        if (sub != context.nil) {
            //set_hash(context, (RubyHash) h, "_bc", context.tru);
        }

        boolean bc = sub != context.nil;
        if (bc || hashGetTest(context, hash, "_bc")) { // if (RTEST(ref_hash("_bc"))) part from _parse
            RubyInteger y;

            y = (RubyInteger) hashGet(context, hash, "year");
            if (y != null) set_hash(context, hash, "year", y.negate(context).op_plus(context, 1));

            y = (RubyInteger) hashGet(context, hash, "cwyear");
            if (y != null) set_hash(context, hash, "cwyear", y.negate(context).op_plus(context, 1));
        }
    }

    private static final ByteList _parse_frag;
    static {
        _parse_frag = ByteList.create("\\A\\s*(\\d{1,2})\\s*\\z");
        _parse_frag.setEncoding(USASCIIEncoding.INSTANCE);
    }

    static void parse_frag(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash) {
        IRubyObject sub = null;

        if (hashGet(context, hash, "hour") != null && hashGet(context, hash, "mday") == null) {
            RubyRegexp re = newRegexpFromCache(context, _parse_frag, RE_OPTION_IGNORECASE);
            sub = subSpace(context, str, re);
            if (sub instanceof RubyMatchData match) {
                RubyInteger v = (RubyInteger) ((RubyString) match.at(context, 1)).to_i(context);
                long vi = v.asLong(context);
                if (1 <= vi && vi <= 31) hash.fastASet(asSymbol(context, "mday"), v);
            }
        }

        if (hashGet(context, hash, "mday") != null && hashGet(context, hash, "hour") == null) {
            if (sub == null) {
                RubyRegexp re = newRegexpFromCache(context, _parse_frag, RE_OPTION_IGNORECASE);
                sub = subSpace(context, str, re);
            }
            if (sub instanceof RubyMatchData match) {
                RubyInteger v = (RubyInteger) ((RubyString) match.at(context, 1)).to_i(context);
                long vi = v.asLong(context);
                if (0 <= vi && vi <= 24) hash.fastASet(asSymbol(context, "hour"), v);
            }
        }
    }

    private static IRubyObject hashGet(final ThreadContext context, final RubyHash hash, final String key) {
        IRubyObject val = hash.fastARef(asSymbol(context, key));
        return val == null || val == context.nil ? null : val;
    }

    private static boolean hashGetTest(final ThreadContext context, final RubyHash hash, final String key) {
        IRubyObject val = hash.fastARef(asSymbol(context, key));
        return val == null || val == context.nil ? false : val.isTrue();
    }

    private static final ByteList SPACE = new ByteList(new byte[] { ' ' }, false);

    private static IRubyObject subSpace(ThreadContext context, RubyString str, RubyRegexp reg) {
        return str.subBangFast(context, reg, newSharedString(context, SPACE));
    }

    // NOTE: still in .rb
    public static IRubyObject _parse_jis(ThreadContext context, IRubyObject self, IRubyObject str, IRubyObject h) {
        return Helpers.invoke(context, self, "_parse_jis", str, h);
    }

    // NOTE: still in .rb
    public static IRubyObject _parse_vms(ThreadContext context, IRubyObject self, IRubyObject str, IRubyObject h) {
        return Helpers.invoke(context, self, "_parse_vms", str, h);
    }

    // NOTE: still in .rb
    public static IRubyObject _parse_iso2(ThreadContext context, IRubyObject self, IRubyObject str, IRubyObject h) {
        return Helpers.invoke(context, self, "_parse_iso2", str, h);
    }

    // NOTE: still in .rb
    public static IRubyObject _parse_ddd(ThreadContext context, IRubyObject self, IRubyObject str, IRubyObject h) {
        return Helpers.invoke(context, self, "_parse_ddd", str, h);
    }

    private static final ByteList _parse_impl;
    static {
        _parse_impl = ByteList.create("[^-+',.\\/:@[:alnum:]\\[\\]]+");
        _parse_impl.setEncoding(USASCIIEncoding.INSTANCE);
    }

    @JRubyMethod(name = "_parse_impl", meta = true, visibility = Visibility.PRIVATE)
    public static IRubyObject _parse_impl(ThreadContext context, IRubyObject self, IRubyObject s, IRubyObject h) {
        RubyString str = (RubyString) s; RubyHash hash = (RubyHash) h;

        str = str.gsubFast(context, newRegexp(context.runtime, _parse_impl), newSharedString(context, SPACE), Block.NULL_BLOCK);

        int flags = check_class(str);
        if ((flags & HAVE_ALPHA) == HAVE_ALPHA) {
            _parse_day(context, self, str, hash);
        }
        if ((flags & HAVE_DIGIT) == HAVE_DIGIT
            && ((flags & (HAVE_COLON|HAVE_M_m|HAVE_H_h|HAVE_S_s)) != 0)) { // JRuby opt
            _parse_time(context, self, str, hash);
        }

        do_parse(context, self, str, hash, flags);

        // ok:
        if ((flags & HAVE_B_b) == HAVE_B_b) { // JRuby opt - instead of HAVE_ALPHA
            parse_bc(context, self, str, hash);
        }
        if ((flags & HAVE_DIGIT) == HAVE_DIGIT) { // NOTE: MRI re-loops string
            parse_frag(context, self, str, hash);
        }

        if (hashGetTest(context, hash, "_comp")) {
            RubyInteger y;

            y = (RubyInteger) hashGet(context, hash, "cwyear");
            if (y != null) {
                long yi = y.asLong(context);
                if (yi >= 0 && yi <= 99) {
                    set_hash(context, hash, "cwyear", y.op_plus(context, yi >= 69 ? 1900 : 2000));
                }
            }
            y = (RubyInteger) hashGet(context, hash, "year");
            if (y != null) {
                long yi = y.asLong(context);
                if (yi >= 0 && yi <= 99) {
                    set_hash(context, hash, "year", y.op_plus(context, yi >= 69 ? 1900 : 2000));
                }
            }
        }

        IRubyObject zone;
        if (hashGet(context, hash, "offset") == null && (zone = hashGet(context, hash, "zone")) != null) {
            set_hash(context, hash, "offset", zone_to_diff(context, self, zone));
        }

        hash.fastDelete(asSymbol(context, "_bc"));
        hash.fastDelete(asSymbol(context, "_comp"));

        return hash;
    }

    private static void do_parse(ThreadContext context, IRubyObject self, RubyString str, RubyHash hash, final int flags) {
        //#ifdef TIGHT_PARSER
        // if (HAVE_ELEM_P(HAVE_ALPHA)) parse_era(str, hash);
        //#endif

        IRubyObject res;

        if ((flags & (HAVE_ALPHA|HAVE_DIGIT)) == (HAVE_ALPHA|HAVE_DIGIT)) {
            res = _parse_eu(context, self, str, hash);
            if (res != context.nil) return;
            res = _parse_us(context, self, str, hash);
            if (res != context.nil) return;
        }
        if ((flags & (HAVE_DIGIT|HAVE_DASH)) == (HAVE_DIGIT|HAVE_DASH)) {
            res = _parse_iso(context, self, str, hash);
            if (res != context.nil) return;
        }
        if ((flags & (HAVE_DIGIT|HAVE_DOT)) == (HAVE_DIGIT|HAVE_DOT)) {
            res = _parse_jis(context, self, str, hash);
            if (res != context.nil) return;
        }
        if ((flags & (HAVE_ALPHA|HAVE_DIGIT|HAVE_DASH)) == (HAVE_ALPHA|HAVE_DIGIT|HAVE_DASH)) {
            res = _parse_vms(context, self, str, hash);
            if (res != context.nil) return;
        }
        if ((flags & (HAVE_DIGIT|HAVE_SLASH)) == (HAVE_DIGIT|HAVE_SLASH)) {
            res = _parse_sla(context, self, str, hash);
            if (res != context.nil) return;
        }
        if ((flags & (HAVE_DIGIT|HAVE_DOT)) == (HAVE_DIGIT|HAVE_DOT)) {
            res = _parse_dot(context, self, str, hash);
            if (res != context.nil) return;
        }
        if ((flags & HAVE_DIGIT) == HAVE_DIGIT) {
            res = _parse_iso2(context, self, str, hash);
            if (res != context.nil) return;
        }
        if ((flags & HAVE_DIGIT) == HAVE_DIGIT) {
            res = _parse_year(context, self, str, hash);
            if (res != context.nil) return;
        }
        if ((flags & HAVE_ALPHA) == HAVE_ALPHA) {
            res = _parse_mon(context, self, str, hash);
            if (res != context.nil) return;
        }
        if ((flags & HAVE_DIGIT) == HAVE_DIGIT) {
            res = _parse_mday(context, self, str, hash);
            if (res != context.nil) return;
        }
        if ((flags & HAVE_DIGIT) == HAVE_DIGIT) {
            res = _parse_ddd(context, self, str, hash);
            if (res != context.nil) return;
        }

        // MRI does an ERROR here ...
    }

    private static final int HAVE_ALPHA = (1<<0);
    private static final int HAVE_DIGIT = (1<<1);
    private static final int HAVE_DASH  = (1<<2);
    private static final int HAVE_DOT   = (1<<3);
    private static final int HAVE_SLASH = (1<<4);
    // custom, not in MRI :
    private static final int HAVE_COLON = (1<<6);
    private static final int HAVE_M_m   = (1<<7); // am|pm 3m
    private static final int HAVE_H_h   = (1<<8); // 9h
    private static final int HAVE_S_s   = (1<<9); // 3s
    private static final int HAVE_B_b   = (1<<10); // bc

    private static int check_class(RubyString s) { // TODO: we could assume single-byte like MRI, right?
        int flags = 0;
        for (int i=0; i<s.length(); i++) {
            final char c = s.charAt(i);
            switch (c) {
                case '-': flags |= HAVE_DASH; break;
                case '.': flags |= HAVE_DOT;  break;
                case '/': flags |= HAVE_SLASH; break;
                case ':': flags |= HAVE_COLON; break;
                case 'b': case 'B':
                    flags |= HAVE_ALPHA|HAVE_B_b;
                    break;
                case 'm': case 'M':
                    flags |= HAVE_ALPHA|HAVE_M_m;
                    break;
                case 'h': case 'H':
                    flags |= HAVE_ALPHA|HAVE_H_h;
                    break;
                case 's': case 'S':
                    flags |= HAVE_ALPHA|HAVE_S_s;
                    break;
                default:
                    if (isDigit(c)) flags |= HAVE_DIGIT;
                    else if (isAlpha(c)) flags |= HAVE_ALPHA;
            }
        }
        return flags;
    }

    // str.sub! /reg/, ' ' (without $~)
    @JRubyMethod(name = "subs", meta = true, visibility = Visibility.PRIVATE)
    public static IRubyObject _subs(ThreadContext context, IRubyObject self, IRubyObject str, IRubyObject reg) {
        return subSpace(context, (RubyString) str, (RubyRegexp) reg);
    }

    // /re/.match str (without $~)
    @JRubyMethod(name = "match", meta = true, visibility = Visibility.PRIVATE)
    public static IRubyObject _match(ThreadContext context, IRubyObject self, IRubyObject reg, IRubyObject str) {
        return ((RubyRegexp) reg).match_m(context, str, false);
    }

    @JRubyMethod(name = "s3e", meta = true, required = 4, optional = 1, checkArity = false, visibility = Visibility.PRIVATE)
    public static IRubyObject _s3e(ThreadContext context, IRubyObject self, IRubyObject[] args) {
        int argc = Arity.checkArgumentCount(context, args, 4, 5);

        final IRubyObject nil = context.nil;

        RubyString y = args[1] == nil ? null : (RubyString) args[1];
        RubyString m = args[2] == nil ? null : (RubyString) args[2];
        RubyString d = args[3] == nil ? null : (RubyString) args[3];

        return s3e(context, (RubyHash) args[0], y, m, d, argc > 4 ? args[4].isTrue() : false);
    }

    private static IRubyObject s3e(ThreadContext context, final RubyHash hash,
                                   RubyString y, RubyString m, RubyString d, boolean bc) {

        Boolean comp = null; RubyString oy, om, od;

        if (d == null && y != null && m != null) {
            oy = y; om = m; od = d;
            y = od; m = oy; d = om;
        }

        if (y == null) {
            if (d != null && d.strLength() > 2) {
                y = d; d = null;
            } else if (d != null && strPtr(d, '\'')) {
                y = d; d = null;
            }
        }


        if (y != null) {
            int s = skipNonDigitsAndSign(y);
            int bp = s;
            char c = s < y.strLength() ? y.charAt(s) : '\0';
            if (c == '+' || c == '-') s++;
            int ep = skipDigits(y, s);
            if (ep != y.strLength()) {
                oy = y; y = d;
                d = (RubyString) oy.substrEnc(context, bp, ep - bp);
            }
        }

        if (m != null) {
            if (strPtr(m, '\'') || m.strLength() > 2) {
                /* us -> be */
                oy = y; om = m; od = d;
                y = om; m = od; d = oy;
            }
        }

        if (d != null) {
            if (strPtr(d, '\'') || d.strLength() > 2) {
                oy = y; od = d;
                y = od; d = oy;
            }
        }

        if (y != null) {
            boolean sign = false;

            int s = skipNonDigitsAndSign(y);

            int bp = s;
            char c = s < y.strLength() ? y.charAt(s) : '\0';
            if (c == '+' || c == '-') {
                s++; sign = true;
            }
            if (sign) comp = false;
            int ep = skipDigits(y, s);
            if (ep - s > 2) comp = false;

            RubyInteger iy = cstr2num(context.runtime, y, bp, ep);
            if (bc) iy = (RubyInteger) iy.negate(context).op_plus(context, 1);
            set_hash(context, hash, "year", iy);
        }

        //if (bc) set_hash("_bc", Qtrue);

        if (m != null) {
            int s = skipNonDigitsAndSign(m);

            int bp = s;
            int ep = skipDigits(m, s);
            set_hash(context, hash, "mon", cstr2num(context.runtime, m, bp, ep));
        }

        if (d != null) {
            int s = skipNonDigitsAndSign(d);

            int bp = s;
            int ep = skipDigits(d, s);
            set_hash(context, hash, "mday", cstr2num(context.runtime, d, bp, ep));
        }

        if (comp != null) set_hash(context, hash, "_comp", asBoolean(context, comp));

        return hash;
    }

    private static void set_hash(final ThreadContext context, RubyHash hash, String key, IRubyObject val) {
        hash.fastASet(asSymbol(context, key), val);
    }

    private static RubyInteger cstr2num(Ruby runtime, RubyString str, int bp, int ep) {
        if (bp == ep) return RubyFixnum.zero(runtime);
        return ConvertBytes.byteListToInum(runtime, str.getByteList(), bp, ep, 10, true);
    }

    private static boolean strPtr(RubyString str, char c) {
        return str.strLength() > 0 && str.charAt(0) == c;
    }

    private static boolean isDigit(char c) {
        switch (c) {
            case '0': case '1': case '2': case '3': case '4': return true;
            case '5': case '6': case '7': case '8': case '9': return true;
            default: return false;
        }
    }

    private static boolean isAlpha(char c) {
        return Character.isLetter(c);
    }

    private static int skipNonDigitsAndSign(RubyString str) {
        int s = 0;
        while (s < str.length()) {
            char c = str.charAt(s);
            if (isDigit(c) || (c == '+' || c == '-')) break;
            s++;
        }
        return s;
    }

    private static int skipDigits(RubyString str, int off) {
        int i = off;
        for (; i < str.length(); i++) {
            if (!isDigit(str.charAt(i))) return i;
        }
        return i;
    }

    // Java API

    /**
     * @return year
     */
    public int getYear() { return dt.getYear(); }

    /**
     * @return month-of-year (1..12)
     */
    public int getMonth() { return dt.getMonthOfYear(); }

    /**
     * @return day-of-month
     */
    public int getDay() { return dt.getDayOfMonth(); }

    /**
     * @return hour-of-day (0..23)
     */
    public int getHour() { return dt.getHourOfDay(); }

    /**
     * @return minute-of-hour
     */
    public int getMinute() { return dt.getMinuteOfHour(); }

    /**
     * @return second-of-minute
     */
    public int getSecond() { return dt.getSecondOfMinute(); }

    /**
     * @return the nano second part (only) of time
     */
    public int getNanos() {
        final ThreadContext context = getRuntime().getCurrentContext();
        RubyNumeric usec = (RubyNumeric) subMillis(context).op_mul(context, asFixnum(context, 1_000_000));
        return usec.asInt(context);
    }

    public Date toDate() {
        return this.dt.toDate();
    }

    /**
     * @return an instant
     */
    public java.time.Instant toInstant() {
        return java.time.Instant.ofEpochMilli(dt.getMillis()).plusNanos(getNanos());
    }

    /**
     * @return a (local) date
     */
    public LocalDate toLocalDate() {
        return LocalDate.of(getYear(), getMonth(), getDay());
    }

    @Override
    public Class getJavaClass() {
        return Date.class; // for compatibility with RubyTime
    }

    @Override
    public <T> T toJava(Class<T> target) {
        // retain compatibility with RubyTime (`target.isAssignableFrom(Date.class)`)
        if (target == Date.class || target == Comparable.class || target == Object.class) {
            return target.cast(toDate());
        }
        if (target == Calendar.class || target == GregorianCalendar.class) {
            return target.cast(dt.toGregorianCalendar());
        }

        // target == Comparable.class and target == Object.class already handled above
        if (target.isAssignableFrom(DateTime.class) && target != Serializable.class) {
            return target.cast(this.dt);
        }

        // SQL
        if (target == java.sql.Date.class) {
            return target.cast(new java.sql.Date(dt.getMillis()));
        }
        if (target == java.sql.Time.class) {
            return target.cast(new java.sql.Time(dt.getMillis()));
        }
        if (target == java.sql.Timestamp.class) {
            java.sql.Timestamp timestamp = new java.sql.Timestamp(dt.getMillis());
            timestamp.setNanos(getNanos());
            return target.cast(timestamp);
        }

        // Java 8
        if (target != Serializable.class) {
            if (target.isAssignableFrom(java.time.Instant.class)) { // covers Temporal/TemporalAdjuster
                return (T) toInstant();
            }
            if (target.isAssignableFrom(LocalDate.class)) { // java.time.chrono.ChronoLocalDate.class
                return (T) toLocalDate();
            }
        }

        return super.toJava(target);
    }

}
