arjdbc.util.DateTimeUtils.java Source code

Java tutorial

Introduction

Here is the source code for arjdbc.util.DateTimeUtils.java

Source

/*
 * The MIT License
 *
 * Copyright 2014 Karol Bucek.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package arjdbc.util;

import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.TimeZone;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyFloat;
import org.jruby.RubyString;
import org.jruby.RubyTime;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;

import static arjdbc.jdbc.RubyJdbcConnection.getBase;
import static arjdbc.util.StringHelper.decByte;

/**
 * Utilities for handling/converting dates and times.
 * @author kares
 */
public abstract class DateTimeUtils {

    @SuppressWarnings("deprecation")
    public static ByteList timeToString(final Time time) {
        final ByteList str = new ByteList(8); // hh:mm:ss

        int hours = time.getHours();
        int minutes = time.getMinutes();
        int seconds = time.getSeconds();

        str.append(decByte(hours / 10));
        str.append(decByte(hours % 10));

        str.append(':');

        str.append(decByte(minutes / 10));
        str.append(decByte(minutes % 10));

        str.append(':');

        str.append(decByte(seconds / 10));
        str.append(decByte(seconds % 10));

        return str;
    }

    @SuppressWarnings("deprecation")
    public static ByteList dateToString(final Date date) {
        final ByteList str = new ByteList(10); // "2000-00-00"

        int year = date.getYear() + 1900;
        int month = date.getMonth() + 1;
        int day = date.getDate();

        str.append(decByte((year / 1000) % 10));
        str.append(decByte((year / 100) % 10));
        str.append(decByte((year / 10) % 10));
        str.append(decByte(year % 10));

        str.append('-');

        str.append(decByte(month / 10));
        str.append(decByte(month % 10));

        str.append('-');

        str.append(decByte(day / 10));
        str.append(decByte(day % 10));

        return str;
    }

    @SuppressWarnings("deprecation")
    public static ByteList timestampToString(final Timestamp timestamp) {
        final ByteList str = new ByteList(29); // yyyy-mm-dd hh:mm:ss.fffffffff

        int year = timestamp.getYear() + 1900;
        int month = timestamp.getMonth() + 1;
        int day = timestamp.getDate();
        int hours = timestamp.getHours();
        int minutes = timestamp.getMinutes();
        int seconds = timestamp.getSeconds();
        int nanos = timestamp.getNanos();

        str.append(decByte((year / 1000) % 10));
        str.append(decByte((year / 100) % 10));
        str.append(decByte((year / 10) % 10));
        str.append(decByte(year % 10));

        str.append('-');

        str.append(decByte(month / 10));
        str.append(decByte(month % 10));

        str.append('-');

        str.append(decByte(day / 10));
        str.append(decByte(day % 10));

        if (hours != 0 || minutes != 0 || seconds != 0 || nanos != 0) {
            str.append(' ');

            str.append(decByte(hours / 10));
            str.append(decByte(hours % 10));

            str.append(':');

            str.append(decByte(minutes / 10));
            str.append(decByte(minutes % 10));

            str.append(':');

            str.append(decByte(seconds / 10));
            str.append(decByte(seconds % 10));

            if (nanos != 0) {
                str.append('.');

                int pow = 100000000; // nanos <= 999999999
                for (int i = 0; i < 8; i++) {
                    final int b = nanos / pow;
                    if (b == 0)
                        break; // done (no trailing zeros)
                    str.append(decByte(b % 10));
                    pow = pow / 10;
                }
            }
        }

        return str;
    }

    @SuppressWarnings("deprecation")
    public static IRubyObject newTime(final ThreadContext context, final Time time) {
        //if ( time == null ) return context.nil;
        final int hours = time.getHours();
        final int minutes = time.getMinutes();
        final int seconds = time.getSeconds();
        //final int offset = time.getTimezoneOffset();

        final DateTime dateTime;
        if (isDefaultTimeZoneUTC(context)) {
            dateTime = new DateTime(2000, 1, 1, hours, minutes, seconds, 0, DateTimeZone.UTC);
        } else {
            dateTime = new DateTime(2000, 1, 1, hours, minutes, seconds, 0);
        }
        return RubyTime.newTime(context.runtime, dateTime);
    }

    @SuppressWarnings("deprecation")
    public static IRubyObject newTime(final ThreadContext context, final Timestamp timestamp) {
        //if ( time == null ) return context.nil;

        final int year = timestamp.getYear() + 1900;
        final int month = timestamp.getMonth() + 1;
        final int day = timestamp.getDate();
        final int hours = timestamp.getHours();
        final int minutes = timestamp.getMinutes();
        final int seconds = timestamp.getSeconds();
        final int nanos = timestamp.getNanos(); // max 999-999-999

        final DateTime dateTime;
        if (isDefaultTimeZoneUTC(context)) {
            dateTime = new DateTime(year, month, day, hours, minutes, seconds, 0, DateTimeZone.UTC);
        } else {
            dateTime = new DateTime(year, month, day, hours, minutes, seconds, 0);
        }
        return RubyTime.newTime(context.runtime, dateTime, nanos);
    }

    @SuppressWarnings("deprecation")
    public static IRubyObject newTime(final ThreadContext context, final Date date) {
        //if ( time == null ) return context.nil;

        final int year = date.getYear() + 1900;
        final int month = date.getMonth() + 1;
        final int day = date.getDate();

        DateTime dateTime = new DateTime(year, month, day, 0, 0, 0, 0);
        return RubyTime.newTime(context.runtime, dateTime);
    }

    public static Timestamp convertToTimestamp(final RubyFloat value) {
        final Timestamp timestamp = new Timestamp(value.getLongValue() * 1000); // millis

        // for usec we shall not use: ((long) floatValue * 1000000) % 1000
        // if ( usec >= 0 ) timestamp.setNanos( timestamp.getNanos() + usec * 1000 );
        // due doubles inaccurate precision it's better to parse to_s :
        final ByteList strValue = ((RubyString) value.to_s()).getByteList();
        final int dot1 = strValue.lastIndexOf('.') + 1, dot4 = dot1 + 3;
        final int len = strValue.getRealSize() - strValue.getBegin();
        if (dot1 > 0 && dot4 < len) { // skip .123 but handle .1234
            final int end = Math.min(len - dot4, 3);
            CharSequence usecSeq = strValue.subSequence(dot4, end);
            final int usec = Integer.parseInt(usecSeq.toString());
            if (usec < 10) { // 0.1234 ~> 4
                timestamp.setNanos(timestamp.getNanos() + usec * 100);
            } else if (usec < 100) { // 0.12345 ~> 45
                timestamp.setNanos(timestamp.getNanos() + usec * 10);
            } else { // if ( usec < 1000 ) { // 0.123456 ~> 456
                timestamp.setNanos(timestamp.getNanos() + usec);
            }
        }

        return timestamp;
    }

    public static IRubyObject getTimeInDefaultTimeZone(final ThreadContext context, IRubyObject value) {
        if (value.respondsTo("to_time")) {
            value = value.callMethod(context, "to_time");
        }
        final String method = isDefaultTimeZoneUTC(context) ? "getutc" : "getlocal";
        if (value.respondsTo(method)) {
            value = value.callMethod(context, method);
        }
        return value;
    }

    public static boolean isDefaultTimeZoneUTC(final ThreadContext context) {
        final String defaultTimeZone = getDefaultTimeZone(context);
        if (defaultTimeZone.length() != 3)
            return false;
        return "utc".equalsIgnoreCase(defaultTimeZone);
    }

    private static String defaultTimeZone;

    public static String getDefaultTimeZone(final ThreadContext context) {
        String default_timezone = defaultTimeZone;
        if (default_timezone == null) {
            final RubyClass base = getBase(context.runtime);
            default_timezone = base.callMethod(context, "default_timezone").toString(); // :utc
            //synchronized (DateTimeUtils.class) { defaultTimeZone = default_timezone; }
        }
        return default_timezone;
    }

    public static double adjustTimeFromDefaultZone(final IRubyObject value) {
        // Time's to_f is : ( millis * 1000 + usec ) / 1_000_000.0
        final double time = value.convertToFloat().getDoubleValue(); // to_f
        // NOTE: MySQL assumes default TZ thus need to adjust to match :
        final int offset = TimeZone.getDefault().getOffset((long) time * 1000);
        // Time's to_f is : ( millis * 1000 + usec ) / 1_000_000.0
        return time - (offset / 1000.0);
    }

    public static IRubyObject parseDate(final ThreadContext context, final String str)
            throws IllegalArgumentException {
        final int len = str.length();

        int year;
        int month;
        int day;

        int start = nonSpaceIndex(str, 0, len); // Skip leading whitespace
        int end = nonDigitIndex(str, start, len);

        if (end >= len) {
            throw new IllegalArgumentException("unexpected date value: '" + str + "'");
        }

        // year
        year = extractIntValue(str, start, end);
        start = end + 1; // Skip '-'

        // month
        end = nonDigitIndex(str, start, len);
        month = extractIntValue(str, start, end);

        //sep = str.charAt(end);
        //if ( sep != '-' ) {
        //    throw new NumberFormatException("expected date to be dash-separated, got '" + sep + "'");
        //}

        start = end + 1; // Skip '-'

        // day of month
        end = nonDigitIndex(str, start, len);
        day = extractIntValue(str, start, end);

        final Ruby runtime = context.runtime;
        return runtime.getClass("Date").callMethod(context, "new",
                new IRubyObject[] { RubyFixnum.newFixnum(runtime, year), RubyFixnum.newFixnum(runtime, month),
                        RubyFixnum.newFixnum(runtime, day) });
    }

    public static RubyTime parseDateTime(final ThreadContext context, final String str)
            throws IllegalArgumentException {

        boolean hasDate = false;
        int year = 2000;
        int month = 1;
        int day = 1;
        boolean hasTime = false;
        int minute = 0;
        int hour = 0;
        int second = 0;
        int millis = 0;
        long nanos = 0;

        DateTimeZone zone = null;
        boolean bcEra = false;

        // We try to parse these fields in order; all are optional
        // (but some combinations don't make sense, e.g. if you have
        //  both date and time then they must be whitespace-separated).
        // At least one of date and time must be present.

        //   leading whitespace
        //   yyyy-mm-dd
        //   whitespace
        //   hh:mm:ss
        //   whitespace
        //   timezone in one of the formats:  +hh, -hh, +hh:mm, -hh:mm
        //   whitespace
        //   if date is present, an era specifier: AD or BC
        //   trailing whitespace

        final int len = str.length();

        int start = nonSpaceIndex(str, 0, len); // Skip leading whitespace
        int end = nonDigitIndex(str, start, len);

        // Possibly read date.
        if (end < len && str.charAt(end) == '-') {
            hasDate = true;

            // year
            year = extractIntValue(str, start, end);
            start = end + 1; // Skip '-'

            // month
            end = nonDigitIndex(str, start, len);
            month = extractIntValue(str, start, end);

            char sep = str.charAt(end);
            if (sep != '-') {
                throw new IllegalArgumentException("expected date to be dash-separated, got '" + sep + "'");
            }

            start = end + 1; // Skip '-'

            // day of month
            end = nonDigitIndex(str, start, len);
            day = extractIntValue(str, start, end);

            start = nonSpaceIndex(str, end, len); // Skip trailing whitespace
        }

        // Possibly read time.
        if (start < len && Character.isDigit(str.charAt(start))) {
            hasTime = true;

            // hours
            end = nonDigitIndex(str, start, len);
            hour = extractIntValue(str, start, end);

            //sep = str.charAt(end);
            //if ( sep != ':' ) {
            //    throw new IllegalArgumentException("expected time to be colon-separated, got '" + sep + "'");
            //}

            start = end + 1; // Skip ':'

            // minutes
            end = nonDigitIndex(str, start, len);
            minute = extractIntValue(str, start, end);

            //sep = str.charAt(end);
            //if ( sep != ':' ) {
            //    throw new IllegalArgumentException("expected time to be colon-separated, got '" + sep + "'");
            //}

            start = end + 1; // Skip ':'

            // seconds
            end = nonDigitIndex(str, start, len);
            second = extractIntValue(str, start, end);
            start = end;

            // Fractional seconds.
            if (start < len && str.charAt(start) == '.') {
                end = nonDigitIndex(str, start + 1, len); // Skip '.'
                int numlen = end - (start + 1);
                if (numlen <= 3) {
                    millis = extractIntValue(str, start + 1, end);
                    for (; numlen < 3; ++numlen)
                        millis *= 10;
                } else {
                    nanos = extractIntValue(str, start + 1, end);
                    for (; numlen < 9; ++numlen)
                        nanos *= 10;
                }

                start = end;
            }

            start = nonSpaceIndex(str, start, len); // Skip trailing whitespace
        }

        // Possibly read timezone.
        char sep = start < len ? str.charAt(start) : '\0';
        if (sep == '+' || sep == '-') {
            int zoneSign = (sep == '-') ? -1 : 1;
            int hoursOffset, minutesOffset, secondsOffset;

            end = nonDigitIndex(str, start + 1, len); // Skip +/-
            hoursOffset = extractIntValue(str, start + 1, end);
            start = end;

            if (start < len && str.charAt(start) == ':') {
                end = nonDigitIndex(str, start + 1, len); // Skip ':'
                minutesOffset = extractIntValue(str, start + 1, end);
                start = end;
            } else {
                minutesOffset = 0;
            }

            secondsOffset = 0;
            if (start < len && str.charAt(start) == ':') {
                end = nonDigitIndex(str, start + 1, len); // Skip ':'
                secondsOffset = extractIntValue(str, start + 1, end);
                start = end;
            }

            // Setting offset does not seem to work correctly in all
            // cases.. So get a fresh calendar for a synthetic timezone
            // instead

            int offset = zoneSign * hoursOffset * 60;
            if (offset < 0) {
                offset = offset - Math.abs(minutesOffset);
            } else {
                offset = offset + minutesOffset;
            }
            offset = (offset * 60 + secondsOffset) * 1000;
            zone = DateTimeZone.forOffsetMillis(offset);

            start = nonSpaceIndex(str, start, len); // Skip trailing whitespace
        }

        if (hasDate && start < len) {
            final char e1 = str.charAt(start);
            if (e1 == 'A' && str.charAt(start + 1) == 'D') {
                bcEra = false;
                start += 2;
            } else if (e1 == 'B' && str.charAt(start + 1) == 'C') {
                bcEra = true;
                start += 2;
            }
        }

        if (start < len) {
            throw new IllegalArgumentException(
                    "trailing junk: '" + str.substring(start, len - start) + "' on '" + str + "'");
        }
        if (!hasTime && !hasDate) {
            throw new IllegalArgumentException("'" + str + "' has neither date nor time");
        }

        if (bcEra)
            year = -1 * year;

        if (zone == null) {
            zone = isDefaultTimeZoneUTC(context) ? DateTimeZone.UTC : DateTimeZone.getDefault();
        }

        DateTime dateTime = new DateTime(year, month, day, hour, minute, second, millis, zone);
        return RubyTime.newTime(context.runtime, dateTime, nanos);
    }

    @SuppressWarnings("deprecation")
    private static int nonSpaceIndex(final String str, int beg, int len) {
        for (int i = beg; i < len; i++) {
            if (!Character.isSpace(str.charAt(i)))
                return i;
        }
        return len;
    }

    private static int nonDigitIndex(final String str, int beg, int len) {
        for (int i = beg; i < len; i++) {
            if (!Character.isDigit(str.charAt(i)))
                return i;
        }
        return len;
    }

    private static int extractIntValue(final String str, int beg, int end) {
        int n = 0;
        for (int i = beg; i < end; i++) {
            n = 10 * n + (str.charAt(i) - '0');
        }
        return n;
    }

}