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

software.amazon.ion.Timestamp Maven / Gradle / Ivy

There is a newer version: 1.5.1
Show newest version
/*
 * Copyright 2008-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at:
 *
 *     http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 */

package software.amazon.ion;

import static software.amazon.ion.impl.PrivateUtils.safeEquals;
import static software.amazon.ion.util.IonTextUtils.printCodePointAsString;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import software.amazon.ion.impl.PrivateUtils;
import software.amazon.ion.util.IonTextUtils;

/**
 * An immutable representation of a point in time. Ion defines a simple
 * representation of time based on Coordinated Universal Time (UTC).
 * In practice the use of time could be more accurately described as
 * UTC-SLS (UTC Smoothed Leap Seconds) as there is no representation for the
 * leap second discontinuities that UTC has added.
 * 

* Timestamps preserve precision, meaning the fields that are included, and the * significant digits of any fractional second. Only common break * points in the values are supported. Any unspecified fields are handled * as the start of the new year/month/day. * * *

Equality and Comparison

* * As with {@link IonValue} classes, the {@link #equals equals} methods on this class * perform a strict equivalence that observes the precision and local offset * of each timestamp. * This means that it's possible to have two {@link Timestamp} instances that * represent the same point in time but are not {@code equals}. *

* On the other hand, the {@link #compareTo} methods perform point in time * comparison, ignoring precision and local offset. * Thus the natural comparison method of this class is not * consistent with equals. See the documentation of {@link Comparable} for * further discussion. *

* To illustrate this distinction, consider the following timestamps. None are * {@link #equals} to each other, but any pair will return a zero result from * {@link #compareTo}. *

    *
  • {@code 2009T}
  • *
  • {@code 2009-01T}
  • *
  • {@code 2009-01-01T}
  • *
  • {@code 2009-01-01T00:00Z}
  • *
  • {@code 2009-01-01T00:00:00Z}
  • *
  • {@code 2009-01-01T00:00:00.0Z}
  • *
  • {@code 2009-01-01T00:00:00.00Z}
  • *
  • {@code 2009-01-01T00:00:00.000Z} etc.
  • *
* * @see #equals(Timestamp) * @see #compareTo(Timestamp) */ public final class Timestamp implements Comparable, Cloneable { private static final boolean APPLY_OFFSET_YES = true; private static final boolean APPLY_OFFSET_NO = false; private static final int NO_MONTH = 0; private static final int NO_DAY = 0; private static final int NO_HOURS = 0; private static final int NO_MINUTES = 0; private static final int NO_SECONDS = 0; private static final BigDecimal NO_FRACTIONAL_SECONDS = null; /** * Unknown local offset from UTC. */ public static final Integer UNKNOWN_OFFSET = null; /** * Local offset of zero hours from UTC. */ public static final Integer UTC_OFFSET = Integer.valueOf(0); private static final int FLAG_YEAR = 0x01; private static final int FLAG_MONTH = 0x02; private static final int FLAG_DAY = 0x04; private static final int FLAG_MINUTE = 0x08; private static final int FLAG_SECOND = 0x10; /** * The precision of the Timestamp. */ public static enum Precision { YEAR (FLAG_YEAR), MONTH (FLAG_YEAR | FLAG_MONTH), DAY (FLAG_YEAR | FLAG_MONTH | FLAG_DAY), // HOUR is not a supported precision per https://www.w3.org/TR/NOTE-datetime MINUTE (FLAG_YEAR | FLAG_MONTH | FLAG_DAY | FLAG_MINUTE), SECOND (FLAG_YEAR | FLAG_MONTH | FLAG_DAY | FLAG_MINUTE | FLAG_SECOND); /** Bit flags for the precision. */ private final int flags; private Precision(int flags) { this.flags = flags; } private boolean alwaysUnknownOffset() { return this.ordinal() <= DAY.ordinal(); } public boolean includes(Precision isIncluded) { switch (isIncluded) { case SECOND: return (flags & FLAG_SECOND) != 0; case MINUTE: return (flags & FLAG_MINUTE) != 0; case DAY: return (flags & FLAG_DAY) != 0; case MONTH: return (flags & FLAG_MONTH) != 0; case YEAR: return (flags & FLAG_YEAR) != 0; default: break; } throw new IllegalStateException("unrecognized precision" + isIncluded); } } private static final int HASH_SIGNATURE = "INTERNAL TIMESTAMP".hashCode(); /** * The precision of the Timestamp. The fractional seconds component is * defined by a BigDecimal. *

* During construction of all Timestamps, they will have a * date value (i.e. Year, Month, Day) but with reduced precision they may * exclude any time values that are more precise than the precision that is * being defined. */ private Precision _precision; // These are the time field values for the Timestamp. // _month and _day are 1-based (0 is an invalid value for // these in a non-null Timestamp). // TODO amzn/ion-java#28 - Represent internal time field values in its local time, // instead of UTC. This makes it much less confusing. private short _year; private byte _month = 1; // Initialized to valid default private byte _day = 1; // Initialized to valid default private byte _hour; private byte _minute; private byte _second; private BigDecimal _fraction; // fractional seconds, must be within range [0, 1) /** * Minutes offset from UTC; zero means UTC proper, * null means that the offset is unknown. */ private Integer _offset; // jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec // the first 0 is to make these arrays 1 based (since month values are 1-12) private static final int[] LEAP_DAYS_IN_MONTH = { 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; private static final int[] NORMAL_DAYS_IN_MONTH = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; private static int last_day_in_month(int year, int month) { boolean is_leap; if ((year % 4) == 0) { // divisible by 4 (lower 2 bits are zero) - may be a leap year if ((year % 100) == 0) { // and divisible by 100 - not a leap year if ((year % 400) == 0) { // but divisible by 400 - then it is a leap year is_leap = true; } else { is_leap = false; } } else { is_leap = true; } } else { is_leap = false; } return is_leap ? LEAP_DAYS_IN_MONTH[month] : NORMAL_DAYS_IN_MONTH[month]; } /** * Applies the local time zone offset from UTC to the applicable time field * values. Depending on the local time zone offset, adjustments * (i.e. rollover) will be made to the Year, Day, Hour, Minute time field * values. * * @param offset the local offset, in minutes from UTC. */ private void apply_offset(int offset) { if (offset == 0) return; if (offset < -24*60 || offset > 24*60) { throw new IllegalArgumentException("bad offset " + offset); } // To convert _to_ UTC you must SUBTRACT the local offset offset = -offset; int hour_offset = offset / 60; int min_offset = offset - (hour_offset * 60); if (offset < 0) { _minute += min_offset; // lower the minute value by adding a negative offset _hour += hour_offset; if (_minute < 0) { _minute += 60; _hour -= 1; } if (_hour >= 0) return; // hour is 0-23 _hour += 24; _day -= 1; if (_day >= 1) return; // day is 1-31 // we can't do this until we've figured out the month and year: _day += last_day_in_month(_year, _month); _month -= 1; if (_month >= 1) { _day += last_day_in_month(_year, _month); // now we know (when the year doesn't change assert(_day == last_day_in_month(_year, _month)); return; // 1-12 } _month += 12; _year -= 1; if (_year < 1) throw new IllegalArgumentException("year is less than 1"); _day += last_day_in_month(_year, _month); // and now we know, even if the year did change assert(_day == last_day_in_month(_year, _month)); } else { _minute += min_offset; // lower the minute value by adding a negative offset _hour += hour_offset; if (_minute > 59) { _minute -= 60; _hour += 1; } if (_hour < 24) return; // hour is 0-23 _hour -= 24; _day += 1; if (_day <= last_day_in_month(_year, _month)) return; // day is 1-31 // we can't do this until we figure out the final month and year: _day -= last_day_in_month(_year, _month); _day = 1; // this is always the case _month += 1; if (_month <= 12) { return; // 1-12 } _month -= 12; _year += 1; if (_year > 9999) throw new IllegalArgumentException("year exceeds 9999"); } } // 0001-01-01T00:00:00.0Z in millis private static final long MINIMUM_ALLOWED_TIMESTAMP_IN_MILLIS = -62135769600000L; /** * This method uses deprecated methods from {@link java.util.Date} * instead of {@link Calendar} so that this code can be used (more easily) * on the mobile Java platform (which has Date but does not have Calendar). */ @SuppressWarnings("deprecation") private void set_fields_from_millis(long millis) { if(millis < MINIMUM_ALLOWED_TIMESTAMP_IN_MILLIS){ throw new IllegalArgumentException("year is less than 1"); } Date date = new Date(millis); // These fields are in the system timezone! this._year = checkAndCastYear(date.getYear() + 1900); this._month = checkAndCastMonth(date.getMonth() + 1); // calendar months are 0 based, timestamp months are 1 based this._day = checkAndCastDay(date.getDate(), _year, _month); this._hour = checkAndCastHour(date.getHours()); this._minute = checkAndCastMinute(date.getMinutes()); this._second = checkAndCastSecond(date.getSeconds()); // this is done because the y-m-d values are in the local timezone // so this adjusts the value back to zulu time (UTC) // Note that the sign on this is opposite of Ion (and Calendar) offset. // Example: PST = 480 here but Ion/Calendar use -480 = -08:00 = UTC-8 int offset = date.getTimezoneOffset(); this.apply_offset(-offset); } /** * Copies data from a {@link Calendar} into this timestamp. * Must only be called during construction due to timestamp immutabliity. * * @param cal must have at least one field set. * * @throws IllegalArgumentException if the calendar has no fields set. */ private void set_fields_from_calendar(Calendar cal, Precision precision, boolean setLocalOffset) { _precision = precision; _offset = UNKNOWN_OFFSET; boolean dayPrecision = false; boolean calendarHasMilliseconds = cal.isSet(Calendar.MILLISECOND); switch (this._precision) { case SECOND: this._second = checkAndCastSecond(cal.get(Calendar.SECOND)); if (calendarHasMilliseconds) { BigDecimal millis = BigDecimal.valueOf(cal.get(Calendar.MILLISECOND)); this._fraction = millis.movePointLeft(3); // convert to fraction } case MINUTE: { this._hour = checkAndCastHour(cal.get(Calendar.HOUR_OF_DAY)); this._minute = checkAndCastMinute(cal.get(Calendar.MINUTE)); // If this test is made before calling get(), it will return // false even when Calendar.setTimeZone() was called. if (setLocalOffset && cal.isSet(Calendar.ZONE_OFFSET)) { int offset = cal.get(Calendar.ZONE_OFFSET); if (cal.isSet(Calendar.DST_OFFSET)) { offset += cal.get(Calendar.DST_OFFSET); } // convert ms to minutes _offset = offset / (1000*60); } } case DAY: dayPrecision = true; case MONTH: // Calendar months are 0 based, Timestamp months are 1 based this._month = checkAndCastMonth((cal.get(Calendar.MONTH) + 1)); case YEAR: int year; if(cal.get(Calendar.ERA) == GregorianCalendar.AD) { year = cal.get(Calendar.YEAR); } else { year = -cal.get(Calendar.YEAR); } this._year = checkAndCastYear(year); } if (dayPrecision) { this._day = checkAndCastDay(cal.get(Calendar.DAY_OF_MONTH), _year, _month); } if (_offset != UNKNOWN_OFFSET) { // Transform our members from local time to Zulu this.apply_offset(_offset); } } /** * Creates a new Timestamp, precise to the year, with unknown local offset. *

* This is equivalent to the corresponding Ion value {@code YYYYT}. */ private Timestamp(int zyear) { this(Precision.YEAR, zyear, NO_MONTH, NO_DAY, NO_HOURS, NO_MINUTES, NO_SECONDS, NO_FRACTIONAL_SECONDS, UNKNOWN_OFFSET, APPLY_OFFSET_NO); } /** * Creates a new Timestamp, precise to the month, with unknown local offset. *

* This is equivalent to the corresponding Ion value {@code YYYY-MMT}. */ private Timestamp(int zyear, int zmonth) { this(Precision.MONTH, zyear, zmonth, NO_DAY, NO_HOURS, NO_MINUTES, NO_SECONDS, NO_FRACTIONAL_SECONDS, UNKNOWN_OFFSET, APPLY_OFFSET_NO); } /** * Creates a new Timestamp, precise to the day, with unknown local offset. *

* This is equivalent to the corresponding Ion value {@code YYYY-MM-DD}. */ @Deprecated private Timestamp(int zyear, int zmonth, int zday) { this(Precision.DAY, zyear, zmonth, zday, NO_HOURS, NO_MINUTES, NO_SECONDS, NO_FRACTIONAL_SECONDS, UNKNOWN_OFFSET, APPLY_OFFSET_NO); } /** * Creates a new Timestamp, precise to the minute, with a given local * offset. *

* This is equivalent to the corresponding Ion value * {@code YYYY-MM-DDThh:mm+-oo:oo}, where {@code oo:oo} represents the * hour and minutes of the local offset from UTC. * * @param offset * the local offset from UTC, measured in minutes; * may be {@code null} to represent an unknown local offset */ @Deprecated private Timestamp(int year, int month, int day, int hour, int minute, Integer offset) { this(Precision.MINUTE, year, month, day, hour, minute, NO_SECONDS, NO_FRACTIONAL_SECONDS, offset, APPLY_OFFSET_YES); } /** * Creates a new Timestamp, precise to the second, with a given local * offset. *

* This is equivalent to the corresponding Ion value * {@code YYYY-MM-DDThh:mm:ss+-oo:oo}, where {@code oo:oo} represents the * hour and minutes of the local offset from UTC. * * @param offset * the local offset from UTC, measured in minutes; * may be {@code null} to represent an unknown local offset. */ @Deprecated private Timestamp(int year, int month, int day, int hour, int minute, int second, Integer offset) { this(Precision.SECOND, year, month, day, hour, minute, second, NO_FRACTIONAL_SECONDS, offset, APPLY_OFFSET_YES); } /** * Creates a new Timestamp from the individual time components. The * individual time components are expected to be in UTC, * with the local offset from UTC (i.e. {@code offset}) already * applied to the time components. *

* Any time component that is more precise * than the precision parameter {@code p} will be excluded from the * calculation of the resulting Timestamp's point in time. * * @param frac must be >= 0 and < 1 * * @param offset * the local offset from UTC, measured in minutes; * may be {@code null} to represent an unknown local offset * * @see #createFromUtcFields(Precision, int, int, int, int, int, int, BigDecimal, Integer) */ private Timestamp(Precision p, int zyear, int zmonth, int zday, int zhour, int zminute, int zsecond, BigDecimal frac, Integer offset, boolean shouldApplyOffset) { boolean dayPrecision = false; switch (p) { default: throw new IllegalArgumentException("invalid Precision passed to constructor"); case SECOND: if (frac == null || frac.equals(BigDecimal.ZERO)) { _fraction = null; } else { _fraction = frac.abs(); } _second = checkAndCastSecond(zsecond); case MINUTE: _minute = checkAndCastMinute(zminute); _hour = checkAndCastHour(zhour); _offset = offset; // offset must be null for years/months/days case DAY: dayPrecision = true; case MONTH: _month = checkAndCastMonth(zmonth); case YEAR: _year = checkAndCastYear(zyear); } if (dayPrecision) { _day = checkAndCastDay(zday, zyear, zmonth); } _precision = checkFraction(p, _fraction); if (shouldApplyOffset && offset != null) { apply_offset(offset); } } /** * Creates a new Timestamp from the individual time components. The * individual time components are expected to be in UTC, * with the local offset from UTC (i.e. {@code offset}) already * applied to the time components. * As such, if the given {@code offset} is non-null or zero, the resulting * Timestamp will have time values that DO NOT match the time * parameters. This method also has a behavior of precision "narrowing", * detailed in the sub-section below. * *

* For example, the following method calls will return Timestamps with * values (in its local time) respectively: *

     * createFromUtcFields(Precision.FRACTION, 2012, 2, 3, 4, 5, 6, 0.007, null)    will return 2012-02-03T04:05:06.007-00:00 (match)
     * createFromUtcFields(Precision.FRACTION, 2012, 2, 3, 4, 5, 6, 0.007, 0)       will return 2012-02-03T04:05:06.007+00:00 (match)
     * createFromUtcFields(Precision.FRACTION, 2012, 2, 3, 4, 5, 6, 0.007, 480)     will return 2012-02-03T12:05:06.007+08:00 (do not match)
     * createFromUtcFields(Precision.FRACTION, 2012, 2, 3, 4, 5, 6, 0.007, -480)    will return 2012-02-02T20:05:06.007-08:00 (do not match)
     * createFromUtcFields(Precision.FRACTION, 2012, 2, 3, 4, 5, 6, 0.007, 720)     will return 2012-02-03T16:05:06.007+12:00 (do not match)
     * createFromUtcFields(Precision.FRACTION, 2012, 2, 3, 4, 5, 6, 0.007, -720)    will return 2012-02-02T16:05:06.007-12:00 (do not match)
     *
* Note: All of these resulting Timestamps have the similar value (in UTC) 2012-02-03T04:05:06.007Z. * *

Precision "Narrowing"

* *

* Any time component that is more precise * than the precision parameter {@code p} will be excluded from the * calculation of the resulting Timestamp's point in time. *

* For example, the following method calls will return Timestamps with * values respectively: *

     * createFromUtcFields(Precision.YEAR    , 2012, 2, 3, 4, 5, 6, 0.007, 0)    will return 2012T
     * createFromUtcFields(Precision.MONTH   , 2012, 2, 3, 4, 5, 6, 0.007, 0)    will return 2012-02T
     * createFromUtcFields(Precision.DAY     , 2012, 2, 3, 4, 5, 6, 0.007, 0)    will return 2012-02-03T
     * createFromUtcFields(Precision.MINUTE  , 2012, 2, 3, 4, 5, 6, 0.007, 0)    will return 2012-02-03T04:05Z
     * createFromUtcFields(Precision.SECOND  , 2012, 2, 3, 4, 5, 6, 0.007, 0)    will return 2012-02-03T04:05:06Z
     * createFromUtcFields(Precision.FRACTION, 2012, 2, 3, 4, 5, 6, 0.007, 0)    will return 2012-02-03T04:05:06.007Z
     *
* * @param p the desired timestamp precision. The result may have a * different precision if the input data isn't precise enough. * * @param offset * the local offset from UTC, measured in minutes; * may be {@code null} to represent an unknown local offset. * * @deprecated This is an internal API that is subject to change without notice. */ @Deprecated public static Timestamp createFromUtcFields(Precision p, int zyear, int zmonth, int zday, int zhour, int zminute, int zsecond, BigDecimal frac, Integer offset) { return new Timestamp(p, zyear, zmonth, zday, zhour, zminute, zsecond, frac, offset, APPLY_OFFSET_NO); } /** * Creates a new Timestamp from a {@link Calendar}, preserving the * {@link Calendar}'s precision and local offset from UTC. *

* The most precise calendar field of {@code cal} will be used to determine * the precision of the resulting Timestamp. * * For example, the calendar field will have a Timestamp precision accordingly: *

    *
  • {@link Calendar#YEAR} - year precision, unknown local offset
  • *
  • {@link Calendar#MONTH} - month precision, unknown local offset
  • *
  • {@link Calendar#DAY_OF_MONTH} - day precision, unknown local offset
  • *
  • {@link Calendar#HOUR_OF_DAY} or {@link Calendar#MINUTE} - minute precision
  • *
  • {@link Calendar#SECOND} - second precision
  • *
  • {@link Calendar#MILLISECOND} - fractional second precision
  • *
* * @throws IllegalArgumentException * if {@code cal} has no appropriate calendar fields set. */ @Deprecated private Timestamp(Calendar cal) { Precision precision; if (cal.isSet(Calendar.MILLISECOND) || cal.isSet(Calendar.SECOND)) { precision = Precision.SECOND; } else if (cal.isSet(Calendar.HOUR_OF_DAY) || cal.isSet(Calendar.MINUTE)) { precision = Precision.MINUTE; } else if (cal.isSet(Calendar.DAY_OF_MONTH)) { precision = Precision.DAY; } else if (cal.isSet(Calendar.MONTH)) { precision = Precision.MONTH; } else if (cal.isSet(Calendar.YEAR)) { precision = Precision.YEAR; } else { throw new IllegalArgumentException("Calendar has no fields set"); } set_fields_from_calendar(cal, precision, true); } private Timestamp(Calendar cal, Precision precision, BigDecimal fraction, Integer offset) { set_fields_from_calendar(cal, precision, false); _fraction = fraction; if (offset != null) { _offset = offset; apply_offset(offset); } } private Timestamp(BigDecimal millis, Precision precision, Integer localOffset) { long ms = millis.longValue(); set_fields_from_millis(ms); switch (precision) { case YEAR: _month = 1; case MONTH: _day = 1; case DAY: _hour = 0; _minute = 0; case MINUTE: _second = 0; _fraction = null; break; case SECOND: BigDecimal secs = millis.movePointLeft(3); BigDecimal secsDown = secs.setScale(0, RoundingMode.FLOOR); _fraction = secs.subtract(secsDown); } _precision = checkFraction(precision, _fraction); _offset = localOffset; } /** * Creates a new Timestamp that represents the point in time that is * {@code millis} milliseconds (including any fractional * milliseconds) from the epoch, with a given local offset. * *

* The resulting Timestamp will be precise to the second if {@code millis} * doesn't contain information that is more granular than seconds. * For example, a {@code BigDecimal} of * value 132541995e4 (132541995 × 104) * will return a Timestamp of {@code 2012-01-01T12:12:30Z}, * precise to the second. * *

* The resulting Timestamp will be precise to the fractional second if * {@code millis} contains information that is at least granular to * milliseconds. * For example, a {@code BigDecimal} of * value 1325419950555 * will return a Timestamp of {@code 2012-01-01T12:12:30.555Z}, * precise to the fractional second. * * @param millis * number of milliseconds (including any fractional * milliseconds) from the epoch (1970-01-01T00:00:00.000Z); * must not be {@code null} * @param localOffset * the local offset from UTC, measured in minutes; * may be {@code null} to represent an unknown local offset * * @throws NullPointerException if {@code millis} is {@code null} */ @Deprecated private Timestamp(BigDecimal millis, Integer localOffset) { if (millis == null) throw new NullPointerException("millis is null"); long ms = millis.longValue(); set_fields_from_millis(ms); this._precision = Precision.SECOND; int scale = millis.scale(); if (scale <= -3) { this._fraction = null; } else { BigDecimal secs = millis.movePointLeft(3); BigDecimal secsDown = secs.setScale(0, RoundingMode.FLOOR); this._fraction = secs.subtract(secsDown); } this._offset = localOffset; } /** * Creates a new Timestamp that represents the point in time that is * {@code millis} milliseconds from the epoch, with a given local offset. *

* The resulting Timestamp will be precise to the fractional second. * * @param millis * number of milliseconds from the epoch (1970-01-01T00:00:00.000Z) * @param localOffset * the local offset from UTC, measured in minutes; * may be {@code null} to represent an unknown local offset. */ @Deprecated private Timestamp(long millis, Integer localOffset) { this.set_fields_from_millis(millis); // fractional seconds portion BigDecimal secs = BigDecimal.valueOf(millis).movePointLeft(3); BigDecimal secsDown = secs.setScale(0, RoundingMode.FLOOR); this._fraction = secs.subtract(secsDown); this._precision = checkFraction(Precision.SECOND, _fraction); this._offset = localOffset; } private static IllegalArgumentException fail(CharSequence input, String reason) { input = IonTextUtils.printString(input); return new IllegalArgumentException("invalid timestamp: " + reason + ": " + input); } private static IllegalArgumentException fail(CharSequence input) { input = IonTextUtils.printString(input); return new IllegalArgumentException("invalid timestamp: " + input); } static final String NULL_TIMESTAMP_IMAGE = "null.timestamp"; static final int LEN_OF_NULL_IMAGE = NULL_TIMESTAMP_IMAGE.length(); static final int END_OF_YEAR = 4; // 1234T static final int END_OF_MONTH = 7; // 1234-67T static final int END_OF_DAY = 10; // 1234-67-90T static final int END_OF_MINUTES = 16; static final int END_OF_SECONDS = 19; /** * Returns a new Timestamp that represents the point in time, precision * and local offset defined in Ion format by the {@link CharSequence}. * * @param ionFormattedTimestamp * a sequence of characters that is the Ion representation of a * Timestamp * * @throws IllegalArgumentException * if the {@code CharSequence} is an invalid Ion representation * of a Timestamp; * or if the {@code CharSequence} has excess characters which * are not one of the following valid thirteen numeric-stop * characters (escaped accordingly for readability): * {}[](),\"\'\ \t\n\r} * * @return * {@code null} if the {@code CharSequence} is "null.timestamp" * * @see Ion Timestamp Page * @see W3C Note on Date and Time Formats */ public static Timestamp valueOf(CharSequence ionFormattedTimestamp) { final CharSequence in = ionFormattedTimestamp; int pos; final int length = in.length(); if (length == 0) { throw fail(in); } // check for 'null.timestamp' if (in.charAt(0) == 'n') { if (length >= LEN_OF_NULL_IMAGE && NULL_TIMESTAMP_IMAGE.contentEquals(in.subSequence(0, LEN_OF_NULL_IMAGE))) { if (length > LEN_OF_NULL_IMAGE) { if (!isValidFollowChar(in.charAt(LEN_OF_NULL_IMAGE))) { throw fail(in); } } return null; } throw fail(in); } int year = 1; int month = 1; int day = 1; int hour = 0; int minute = 0; int seconds = 0; BigDecimal fraction = null; Precision precision; // fake label to turn goto's into a break so Java is happy :) enjoy do { // otherwise we expect yyyy-mm-ddThh:mm:ss.ssss+hh:mm if (length < END_OF_YEAR + 1) { // +1 for the "T" throw fail(in, "year is too short (must be at least yyyyT)"); } pos = END_OF_YEAR; precision = Precision.YEAR; year = read_digits(in, 0, 4, -1, "year"); char c = in.charAt(END_OF_YEAR); if (c == 'T') break; if (c != '-') { throw fail(in, "expected \"-\" between year and month, found " + printCodePointAsString(c)); } if (length < END_OF_MONTH + 1) { // +1 for the "T" throw fail(in, "month is too short (must be yyyy-mmT)"); } pos = END_OF_MONTH; precision = Precision.MONTH; month = read_digits(in, END_OF_YEAR + 1, 2, -1, "month"); c = in.charAt(END_OF_MONTH); if (c == 'T') break; if (c != '-') { throw fail(in, "expected \"-\" between month and day, found " + printCodePointAsString(c)); } if (length < END_OF_DAY) { throw fail(in, "too short for yyyy-mm-dd"); } pos = END_OF_DAY; precision = Precision.DAY; day = read_digits(in, END_OF_MONTH + 1, 2, -1, "day"); if (length == END_OF_DAY) break; c = in.charAt(END_OF_DAY); if (c != 'T') { throw fail(in, "expected \"T\" after day, found " + printCodePointAsString(c)); } if (length == END_OF_DAY + 1) break; // now lets see if we have a time value if (length < END_OF_MINUTES) { throw fail(in, "too short for yyyy-mm-ddThh:mm"); } hour = read_digits(in, 11, 2, ':', "hour"); minute = read_digits(in, 14, 2, -1, "minutes"); pos = END_OF_MINUTES; precision = Precision.MINUTE; // we may have seconds if (length <= END_OF_MINUTES || in.charAt(END_OF_MINUTES) != ':') { break; } if (length < END_OF_SECONDS) { throw fail(in, "too short for yyyy-mm-ddThh:mm:ss"); } seconds = read_digits(in, 17, 2, -1, "seconds"); pos = END_OF_SECONDS; precision = Precision.SECOND; if (length <= END_OF_SECONDS || in.charAt(END_OF_SECONDS) != '.') { break; } precision = Precision.SECOND; pos = END_OF_SECONDS + 1; while (length > pos && Character.isDigit(in.charAt(pos))) { pos++; } if (pos <= END_OF_SECONDS + 1) { throw fail(in, "must have at least one digit after decimal point"); } fraction = new BigDecimal(in.subSequence(19, pos).toString()); } while (false); Integer offset; // now see if they included a timezone offset char timezone_start = pos < length ? in.charAt(pos) : '\n'; if (timezone_start == 'Z') { offset = 0; pos++; } else if (timezone_start == '+' || timezone_start == '-') { if (length < pos + 5) { throw fail(in, "local offset too short"); } // +/- hh:mm pos++; int tzdHours = read_digits(in, pos, 2, ':', "local offset hours"); if (tzdHours < 0 || tzdHours > 23) { throw fail(in, "local offset hours must be between 0 and 23 inclusive"); } pos += 3; int tzdMinutes = read_digits(in, pos, 2, -1, "local offset minutes"); if (tzdMinutes > 59) { throw fail(in, "local offset minutes must be between 0 and 59 inclusive"); } pos += 2; int temp = tzdHours * 60 + tzdMinutes; if (timezone_start == '-') { temp = -temp; } if (temp == 0 && timezone_start == '-') { // int doesn't do negative zero very elegantly offset = null; } else { offset = temp; } } else { switch (precision) { case YEAR: case MONTH: case DAY: break; default: throw fail(in, "missing local offset"); } offset = null; } if (length > (pos + 1) && !isValidFollowChar(in.charAt(pos + 1))) { throw fail(in, "invalid excess characters"); } Timestamp ts = new Timestamp(precision, year, month, day, hour, minute, seconds, fraction, offset, APPLY_OFFSET_YES); return ts; } private static int read_digits(CharSequence in, int start, int length, int terminator, String field) { int ii, value = 0; int end = start + length; if (in.length() < end) { throw fail(in, field + " requires " + length + " digits"); } for (ii=start; ii= in.length() || in.charAt(ii) != terminator) { throw fail(in, field + " should end with " + printCodePointAsString(terminator)); } } // Otherwise make sure we don't have too many digits. else if (ii < in.length() && Character.isDigit(in.charAt(ii))) { throw fail(in, field + " requires " + length + " digits but has more"); } return value; } private static boolean isValidFollowChar(char c) { switch (c) { default: return false; case '{': case '}': case '[': case ']': case '(': case ')': case ',': case '\"': case '\'': case '\\': case '\t': case '\n': case '\r': return true; } } /** * Creates a copy of this Timestamp. The resulting Timestamp will * represent the same point in time and has the same precision and local * offset. *

* {@inheritDoc} */ @Override public Timestamp clone() { // The Copy-Constructor we're using here already expects the time field // values to be in UTC, and that is already what we have for this // Timestamp -- no adjustment necessary to make it local time. return new Timestamp(_precision, _year, _month, _day, _hour, _minute, _second, _fraction, _offset, APPLY_OFFSET_NO); } /** * Applies the local offset from UTC to each of the applicable time field * values and returns the new Timestamp. In short, this makes the Timestamp * represent local time. * * @return a new Timestamp in its local time */ private Timestamp make_localtime() { int offset = _offset != null ? _offset.intValue() : 0; // We use a Copy-Constructor that expects the time parameters to be in // UTC, as that's what we're supposed to have. // As this Copy-Constructor doesn't apply local offset to the time // field values (it assumes that the local offset is already applied to // them), we explicitly apply the local offset to the time field values // after we obtain the new Timestamp instance. Timestamp localtime = new Timestamp(_precision, _year, _month, _day, _hour, _minute, _second, _fraction, _offset, APPLY_OFFSET_NO); // explicitly apply the local offset to the time field values localtime.apply_offset(-offset); assert localtime._offset == _offset; return localtime; } /** * Returns a Timestamp, precise to the year, with unknown local offset. *

* This is equivalent to the corresponding Ion value {@code YYYYT}. */ public static Timestamp forYear(int yearZ) { return new Timestamp(yearZ); } /** * Returns a Timestamp, precise to the month, with unknown local offset. *

* This is equivalent to the corresponding Ion value {@code YYYY-MMT}. */ public static Timestamp forMonth(int yearZ, int monthZ) { return new Timestamp(yearZ, monthZ); } /** * Returns a Timestamp, precise to the day, with unknown local offset. *

* This is equivalent to the corresponding Ion value {@code YYYY-MM-DD}. * */ public static Timestamp forDay(int yearZ, int monthZ, int dayZ) { return new Timestamp(yearZ, monthZ, dayZ); } /** * Returns a Timestamp, precise to the minute, with a given local * offset. *

* This is equivalent to the corresponding Ion value * {@code YYYY-MM-DDThh:mm+-oo:oo}, where {@code oo:oo} represents the * hour and minutes of the local offset from UTC. * * @param offset * the local offset from UTC, measured in minutes; * may be {@code null} to represent an unknown local offset * */ public static Timestamp forMinute(int year, int month, int day, int hour, int minute, Integer offset) { return new Timestamp(year, month, day, hour, minute, offset); } /** * Returns a Timestamp, precise to the second, with a given local offset. *

* This is equivalent to the corresponding Ion value * {@code YYYY-MM-DDThh:mm:ss+-oo:oo}, where {@code oo:oo} represents the * hour and minutes of the local offset from UTC. * * @param offset * the local offset from UTC, measured in minutes; * may be {@code null} to represent an unknown local offset * */ public static Timestamp forSecond(int year, int month, int day, int hour, int minute, int second, Integer offset) { return new Timestamp(year, month, day, hour, minute, second, offset); } /** * Returns a Timestamp, precise to the second, with a given local offset. *

* This is equivalent to the corresponding Ion value * {@code YYYY-MM-DDThh:mm:ss.sss+-oo:oo}, where {@code oo:oo} represents * the hour and minutes of the local offset from UTC. * * @param second must be at least zero and less than 60. * Must not be null. * * @param offset * the local offset from UTC, measured in minutes; * may be {@code null} to represent an unknown local offset * */ public static Timestamp forSecond(int year, int month, int day, int hour, int minute, BigDecimal second, Integer offset) { // Tease apart the whole and fractional seconds. // Storing them separately is silly. int s = second.intValue(); BigDecimal frac = second.subtract(BigDecimal.valueOf(s)); return new Timestamp(Precision.SECOND, year, month, day, hour, minute, s, frac, offset, APPLY_OFFSET_YES); } /** * Returns a Timestamp that represents the point in time that is * {@code millis} milliseconds from the epoch, with a given local offset. *

* The resulting Timestamp will be precise to the millisecond. * * @param millis * the number of milliseconds from the epoch (1970-01-01T00:00:00.000Z). * @param localOffset * the local offset from UTC, measured in minutes; * may be {@code null} to represent an unknown local offset. * */ public static Timestamp forMillis(long millis, Integer localOffset) { return new Timestamp(millis, localOffset); } /** * Returns a Timestamp that represents the point in time that is * {@code millis} milliseconds (including any fractional * milliseconds) from the epoch, with a given local offset. * *

* The resulting Timestamp will be precise to the second if {@code millis} * doesn't contain information that is more granular than seconds. * For example, a {@code BigDecimal} of * value 132541995e4 (132541995 × 104) * will return a Timestamp of {@code 2012-01-01T12:12:30Z}, * precise to the second. * *

* The resulting Timestamp will be precise to the fractional second if * {@code millis} contains information that is at least granular to * milliseconds. * For example, a {@code BigDecimal} of * value 1325419950555 * will return a Timestamp of {@code 2012-01-01T12:12:30.555Z}, * precise to the fractional second. * * @param millis * number of milliseconds (including any fractional * milliseconds) from the epoch (1970-01-01T00:00:00.000Z); * must not be {@code null} * @param localOffset * the local offset from UTC, measured in minutes; * may be {@code null} to represent an unknown local offset * * @throws NullPointerException if {@code millis} is {@code null} * */ public static Timestamp forMillis(BigDecimal millis, Integer localOffset) { return new Timestamp(millis, localOffset); } /** * Converts a {@link Calendar} to a Timestamp, preserving the calendar's * time zone as the equivalent local offset when it has at least minutes * precision. * * @return a Timestamp instance, with precision determined by the smallest * field set in the {@code Calendar}; * or {@code null} if {@code calendar} is {@code null} * */ public static Timestamp forCalendar(Calendar calendar) { if (calendar == null) return null; return new Timestamp(calendar); } /** * Converts a {@link Date} to a Timestamp in UTC representing the same * point in time. *

* The resulting Timestamp will be precise to the millisecond. * * @return * a new Timestamp instance, in UTC, precise to the millisecond; * {@code null} if {@code date} is {@code null} * */ public static Timestamp forDateZ(Date date) { if (date == null) return null; long millis = date.getTime(); return new Timestamp(millis, UTC_OFFSET); } /** * Converts a {@link java.sql.Timestamp} to a Timestamp in UTC representing * the same point in time. *

* The resulting Timestamp will be precise to the nanosecond. * * @param sqlTimestamp assumed to have nanoseconds precision * * @return * a new Timestamp instance, in UTC, precise to the * nanosecond * {@code null} if {@code sqlTimestamp} is {@code null} * */ public static Timestamp forSqlTimestampZ(java.sql.Timestamp sqlTimestamp) { if (sqlTimestamp == null) return null; long millis = sqlTimestamp.getTime(); Timestamp ts = new Timestamp(millis, UTC_OFFSET); int nanos = sqlTimestamp.getNanos(); BigDecimal frac = BigDecimal.valueOf(nanos).movePointLeft(9); ts._fraction = frac; return ts; } /** * Returns a Timestamp representing the current time (based on the JVM * clock), with an unknown local offset. *

* The resulting Timestamp will be precise to the millisecond. * * @return * a new Timestamp instance representing the current time. */ public static Timestamp now() { long millis = System.currentTimeMillis(); return new Timestamp(millis, UNKNOWN_OFFSET); } /** * Returns a Timestamp in UTC representing the current time (based on the * the JVM clock). *

* The resulting Timestamp will be precise to the millisecond. * * @return * a new Timestamp instance, in UTC, representing the current * time. * */ public static Timestamp nowZ() { long millis = System.currentTimeMillis(); return new Timestamp(millis, UTC_OFFSET); } /** * Converts the value of this Timestamp into a {@link Date}, * representing the time in UTC. *

* This method will return the same result for all Timestamps representing * the same point in time, regardless of the local offset. *

* Because {@link Date} instances are mutable, this method returns a * new instance from each call. * * @return a new {@code Date} instance, in UTC */ public Date dateValue() { long millis = getMillis(); return new Date(millis); } /** * Converts the value of this Timestamp as a {@link Calendar}, in its * local time. *

* Because {@link Calendar} instances are mutable, this method returns a * new instance from each call. * * @return a new {@code Calendar} instance, in its local time. * */ public Calendar calendarValue() { Calendar cal = new GregorianCalendar(PrivateUtils.UTC); long millis = getMillis(); Integer offset = _offset; if (offset != null && offset != 0) { int offsetMillis = offset * 60 * 1000; millis += offsetMillis; cal.setTimeInMillis(millis); // Resets the offset! cal.set(Calendar.ZONE_OFFSET, offsetMillis); } else { cal.setTimeInMillis(millis); } return cal; } /** * Returns a number representing the Timestamp's point in time that is * the number of milliseconds (ignoring any fractional milliseconds) * from the epoch. *

* This method will return the same result for all Timestamps representing * the same point in time, regardless of the local offset. * * @return * number of milliseconds (ignoring any fractional * milliseconds) from the epoch (1970-01-01T00:00:00.000Z) */ @SuppressWarnings("deprecation") public long getMillis() { // month is 0 based for Date long millis = Date.UTC(this._year - 1900, this._month - 1, this._day, this._hour, this._minute, this._second); if (this._fraction != null) { int frac = this._fraction.movePointRight(3).intValue(); millis += frac; } return millis; } /** * Returns a BigDecimal representing the Timestamp's point in time that is * the number of milliseconds (including any fractional milliseconds) * from the epoch. *

* This method will return the same result for all Timestamps representing * the same point in time, regardless of the local offset. * * @return * number of milliseconds (including any fractional * milliseconds) from the epoch (1970-01-01T00:00:00.000Z) */ @SuppressWarnings("deprecation") public BigDecimal getDecimalMillis() { long millis; BigDecimal dec; switch (this._precision) { case YEAR: case MONTH: case DAY: case MINUTE: case SECOND: millis = Date.UTC(this._year - 1900, this._month - 1, this._day, this._hour, this._minute, this._second); dec = BigDecimal.valueOf(millis); if (_fraction != null) { dec = dec.add(this._fraction.movePointRight(3)); } return dec; } throw new IllegalArgumentException(); } /** * Returns the precision of this Timestamp. */ public Precision getPrecision() { return this._precision; } /** * Returns the offset of this Timestamp, measured in minutes, for the local * timezone in UTC. *

* For example, calling this method on Timestamps of: *

    *
  • {@code 1969-02-23T07:00+07:00} will return {@code 420}
  • *
  • {@code 1969-02-22T22:45:00.00-01:15} will return {@code -75}
  • *
  • {@code 1969-02-23} (by Ion's definition, equivalent to * {@code 1969-02-23T00:00-00:00}) will return {@code null}
  • *
* * @return * {@code null} if the local offset is unknown * (i.e. {@code -00:00}) */ public Integer getLocalOffset() { return _offset; } /** * Returns the year of this Timestamp, in its local time. * * @return * a number within the range [1, 9999], in its local time */ public int getYear() { Timestamp adjusted = this; if (this._offset != null) { if (this._offset.intValue() != 0) { adjusted = make_localtime(); } } return adjusted._year; } /** * Returns the month of this Timestamp, in its local time. * * @return * a number within the range [1, 12], whereby 1 refers to January * and 12 refers to December, in its local time; * 1 is returned if the Timestamp isn't precise to * the month */ public int getMonth() { Timestamp adjusted = this; if (this._offset != null) { if (this._offset.intValue() != 0) { adjusted = make_localtime(); } } return adjusted._month; } /** * Returns the day (within the month) of this Timestamp, in its local time. * * @return * a number within the range [1, 31], in its local time; * 1 is returned if the Timestamp isn't * precise to the day */ public int getDay() { Timestamp adjusted = this; if (this._offset != null) { if (this._offset.intValue() != 0) { adjusted = make_localtime(); } } return adjusted._day; } /** * Returns the hour of this Timestamp, in its local time. * * @return * a number within the range [0, 23], in its local time; * 0 is returned if the Timestamp isn't * precise to the hour */ public int getHour() { Timestamp adjusted = this; if (this._offset != null) { if (this._offset.intValue() != 0) { adjusted = make_localtime(); } } return adjusted._hour; } /** * Returns the minute of this Timestamp, in its local time. * * @return * a number within the range [0, 59], in its local time; * 0 is returned if the Timestamp isn't * precise to the minute */ public int getMinute() { Timestamp adjusted = this; if (this._offset != null) { if (this._offset.intValue() != 0) { adjusted = make_localtime(); } } return adjusted._minute; } /** * Returns the seconds of this Timestamp, truncated to an integer. *

* Seconds are not affected by local offsets. * As such, this method produces the same output as {@link #getZSecond()}. * * @return * a number within the range [0, 59]; * 0 is returned if the Timestamp isn't precise to the second * * @see #getZSecond() */ public int getSecond() { return this._second; } /** * Returns the seconds of this Timestamp. *

* Seconds are not affected by local offsets. * As such, this method produces the same output as * {@link #getZDecimalSecond()}. * * @return * a number within the range [0, 60); * 0 is returned if the Timestamp isn't precise to the second * * @see #getZDecimalSecond() */ public BigDecimal getDecimalSecond() { BigDecimal sec = BigDecimal.valueOf(_second); if (_fraction != null) { sec = sec.add(_fraction); } return sec; } /** * Returns the year of this Timestamp, in UTC. * * @return * a number within the range [1, 9999], in UTC */ public int getZYear() { return this._year; } /** * Returns the month of this Timestamp, in UTC. * * @return * a number within the range [1, 12], whereby 1 refers to January * and 12 refers to December, in UTC; * 1 is returned if the Timestamp isn't precise to * the month */ public int getZMonth() { return this._month; } /** * Returns the day of this Timestamp, in UTC. * * @return * a number within the range [1, 31], in UTC; * 1 is returned if the Timestamp isn't * precise to the day */ public int getZDay() { return this._day; } /** * Returns the hour of this Timestamp, in UTC. * * @return * a number within the range [0, 23], in UTC; * 0 is returned if the Timestamp isn't * precise to the hour */ public int getZHour() { return this._hour; } /** * Returns the minute of this Timestamp, in UTC. * * @return * a number within the range [0, 59], in UTC; * 0 is returned if the Timestamp isn't * precise to the minute */ public int getZMinute() { return this._minute; } /** * Returns the second of this Timestamp. *

* Seconds are not affected by local offsets. * As such, this method produces the same output as {@link #getSecond()}. * * @return * a number within the range [0, 59]; * 0 is returned if the Timestamp isn't precise to the second * * @see #getSecond() */ public int getZSecond() { return this._second; } /** * Returns the seconds of this Timestamp. *

* Seconds are not affected by local offsets. * As such, this method produces the same output as * {@link #getDecimalSecond()}. * * @return * a number within the range [0, 60); * 0 is returned if the Timestamp isn't precise to the second * * @see #getDecimalSecond() */ public BigDecimal getZDecimalSecond() { return getDecimalSecond(); } /** * Returns the fractional second of this Timestamp. *

* Fractional seconds are not affected by local offsets. * * @return * a BigDecimal within the range [0, 1); * {@code null} is returned if the Timestamp isn't * precise to the fractional second * * @deprecated This is an internal API that is subject to change without notice. */ @Deprecated public BigDecimal getZFractionalSecond() { return this._fraction; } //========================================================================= // Modification methods /** * Returns a timestamp at the same point in time, but with the given local * offset. If this timestamp has precision coarser than minutes, then it * is returned unchanged since such timestamps always have an unknown * offset. */ public Timestamp withLocalOffset(Integer offset) { Precision precision = getPrecision(); if (precision.alwaysUnknownOffset() || safeEquals(offset, getLocalOffset())) { return this; } Timestamp ts = createFromUtcFields(precision, getZYear(), getZMonth(), getZDay(), getZHour(), getZMinute(), getZSecond(), getZFractionalSecond(), offset); return ts; } //========================================================================= /** * Returns the string representation (in Ion format) of this Timestamp in * its local time. * * @see #toZString() * @see #print(Appendable) */ @Override public String toString() { StringBuilder buffer = new StringBuilder(32); try { print(buffer); } catch (IOException e) { throw new RuntimeException("Exception printing to StringBuilder", e); } return buffer.toString(); } /** * Returns the string representation (in Ion format) of this Timestamp * in UTC. * * @see #toString() * @see #printZ(Appendable) */ public String toZString() { StringBuilder buffer = new StringBuilder(32); try { printZ(buffer); } catch (IOException e) { throw new RuntimeException("Exception printing to StringBuilder", e); } return buffer.toString(); } /** * Prints to an {@code Appendable} the string representation (in Ion format) * of this Timestamp in its local time. *

* This method produces the same output as {@link #toString()}. * * @param out not {@code null} * * @throws IOException propagated when the {@link Appendable} throws it * * @see #printZ(Appendable) */ public void print(Appendable out) throws IOException { // we have to make a copy to preserve the "immutable" contract // on Timestamp and we don't want someone reading the calendar // member while we've shifted it around. Timestamp adjusted = this; // Adjust UTC time back to local time if (this._offset != null && this._offset.intValue() != 0) { adjusted = make_localtime(); } print(out, adjusted); } /** * Prints to an {@code Appendable} the string representation (in Ion format) * of this Timestamp in UTC. *

* This method produces the same output as {@link #toZString()}. * * @param out not {@code null} * * @throws IOException propagated when the {@code Appendable} throws it. * * @see #print(Appendable) */ public void printZ(Appendable out) throws IOException { switch (_precision) { case YEAR: case MONTH: case DAY: { assert _offset == UNKNOWN_OFFSET; // No need to adjust offset, we won't be using it. print(out); break; } case MINUTE: case SECOND: { Timestamp ztime = this.clone(); ztime._offset = UTC_OFFSET; ztime.print(out); break; } } } /** * helper for print(out) and printZ(out) so that printZ can create * a zulu time and pass it directly and print can apply the local * offset and adjust the various fields (without breaking the * contract to be immutable). * @param out destination for the text image of the value * @param adjusted the time value with the fields adjusted to match the desired text output * @throws IOException */ private static void print(Appendable out, Timestamp adjusted) throws IOException { // null is our first "guess" to get it out of the way if (adjusted == null) { out.append("null.timestamp"); return; } // so we have a real value - we'll start with the date portion // which we always have print_digits(out, adjusted._year, 4); if (adjusted._precision == Precision.YEAR) { assert adjusted._offset == UNKNOWN_OFFSET; out.append("T"); return; } out.append("-"); print_digits(out, adjusted._month, 2); // convert calendar months to a base 1 value if (adjusted._precision == Precision.MONTH) { assert adjusted._offset == UNKNOWN_OFFSET; out.append("T"); return; } out.append("-"); print_digits(out, adjusted._day, 2); if (adjusted._precision == Precision.DAY) { assert adjusted._offset == UNKNOWN_OFFSET; // out.append("T"); return; } out.append("T"); print_digits(out, adjusted._hour, 2); out.append(":"); print_digits(out, adjusted._minute, 2); // ok, so how much time do we have ? if (adjusted._precision == Precision.SECOND) { out.append(":"); print_digits(out, adjusted._second, 2); if (adjusted._fraction != null) { print_fractional_digits(out, adjusted._fraction); } } if (adjusted._offset != UNKNOWN_OFFSET) { int min, hour; min = adjusted._offset; if (min == 0) { out.append('Z'); } else { if (min < 0) { min = -min; out.append('-'); } else { out.append('+'); } hour = min / 60; min = min - hour*60; print_digits(out, hour, 2); out.append(":"); print_digits(out, min, 2); } } else { out.append("-00:00"); } } private static void print_digits(Appendable out, int value, int length) throws IOException { char temp[] = new char[length]; while (length > 0) { length--; int next = value / 10; temp[length] = (char)('0' + (value - next*10)); value = next; } while (length > 0) { length--; temp[length] = '0'; } for (char c : temp) { out.append(c); } } private static void print_fractional_digits(Appendable out, BigDecimal value) throws IOException { String temp = value.toPlainString(); // crude, but it works if (temp.charAt(0) == '0') { // this should always be true temp = temp.substring(1); } out.append(temp); } //========================================================================= // Timestamp arithmetic /** * Returns a timestamp relative to this one by the given number of * milliseconds. * * @param amount a (positive or negative) number of milliseconds. */ public final Timestamp addMillis(long amount) { if (amount == 0) return this; // This strips off the local offset, expressing our fields as if they // were UTC. BigDecimal millis = make_localtime().getDecimalMillis(); millis = millis.add(BigDecimal.valueOf(amount)); Timestamp ts = new Timestamp(millis, _precision, _offset); // Anything with courser-than-millis precision will have been extended // to 3 decimal places due to use of getDecimalMillis(). Fix that. ts._fraction = _fraction; if (_offset != null && _offset != 0) { ts.apply_offset(_offset); } return ts; } /** * Returns a timestamp relative to this one by the given number of seconds. * * @param amount a (positive or negative) number of seconds. */ public final Timestamp addSecond(int amount) { long delta = (long) amount * 1000; return addMillis(delta); } /** * Returns a timestamp relative to this one by the given number of minutes. * * @param amount a (positive or negative) number of minutes. */ public final Timestamp addMinute(int amount) { long delta = (long) amount * 60 * 1000; return addMillis(delta); } /** * Returns a timestamp relative to this one by the given number of hours. * * @param amount a (positive or negative) number of hours. */ public final Timestamp addHour(int amount) { long delta = (long) amount * 60 * 60 * 1000; return addMillis(delta); } /** * Returns a timestamp relative to this one by the given number of days. * * @param amount a (positive or negative) number of hours. */ public final Timestamp addDay(int amount) { long delta = (long) amount * 24 * 60 * 60 * 1000; return addMillis(delta); } // Shifting month and year are more complicated since the length of a month // varies and we want the day-of-month to stay the same when possible. // We rely on Calendar for the logic. /** * Returns a timestamp relative to this one by the given number of months. * The day field may be adjusted to account for different month length and * leap days. For example, adding one month to {@code 2011-01-31} * results in {@code 2011-02-28}. * * @param amount a (positive or negative) number of hours. */ public final Timestamp addMonth(int amount) { if (amount == 0) return this; Calendar cal = calendarValue(); cal.add(Calendar.MONTH, amount); return new Timestamp(cal, _precision, _fraction, _offset); } /** * Returns a timestamp relative to this one by the given number of years. * The day field may be adjusted to account for leap days. For example, * adding one year to {@code 2012-02-29} results in {@code 2013-02-28}. * * @param amount a (positive or negative) number of hours. */ public final Timestamp addYear(int amount) { if (amount == 0) return this; Calendar cal = calendarValue(); cal.add(Calendar.YEAR, amount); return new Timestamp(cal, _precision, _fraction, _offset); } //========================================================================= /** * Returns a hash code consistent with {@link #equals(Object)}. *

* {@inheritDoc} */ @Override public int hashCode() { // Performs a Shift-Add-XOR-Rotate hash. Rotating at each step to // produce an "Avalanche" effect for timestamps with small deltas, which // is found to be a common input data set. final int prime = 8191; int result = HASH_SIGNATURE; result = prime * result + (_fraction != null ? _fraction.hashCode() : 0); result ^= (result << 19) ^ (result >> 13); result = prime * result + this._year; result = prime * result + this._month; result = prime * result + this._day; result = prime * result + this._hour; result = prime * result + this._minute; result = prime * result + this._second; result ^= (result << 19) ^ (result >> 13); result = prime * result + this._precision.toString().hashCode(); result ^= (result << 19) ^ (result >> 13); result = prime * result + (_offset == null ? 0 : _offset.hashCode()); result ^= (result << 19) ^ (result >> 13); return result; } /** * Performs a comparison of the two points in time represented by two * Timestamps. * If the point in time represented by this Timestamp precedes that of * {@code t}, then {@code -1} is returned. * If {@code t} precedes this Timestamp then {@code 1} is returned. * If the Timestamps represent the same point in time, then * {@code 0} is returned. * Note that a {@code 0} result does not imply that the two Timestamps are * {@link #equals}, as the local offset or precision of the two Timestamps * may be different. * *

* This method is provided in preference to individual methods for each of * the six boolean comparison operators (<, ==, >, >=, !=, <=). * The suggested idiom for performing these comparisons is: * {@code (x.compareTo(y)}<op>{@code 0)}, * where <op> is one of the six comparison operators. * *

* For example, the pairs below will return a {@code 0} result: *

    *
  • {@code 2009T}
  • *
  • {@code 2009-01T}
  • *
  • {@code 2009-01-01T}
  • *
  • {@code 2009-01-01T00:00Z}
  • *
  • {@code 2009-01-01T00:00:00Z}
  • *
  • {@code 2009-01-01T00:00:00.0Z}
  • *
  • {@code 2009-01-01T00:00:00.00Z}
  • * *
  • {@code 2008-12-31T16:00-08:00}
  • *
  • {@code 2008-12-31T12:00-12:00}
  • *
  • {@code 2009-01-01T12:00+12:00}
  • *
* *

* Use the {@link #equals(Timestamp)} method to compare the point * in time, including precision and local offset. * * @param t * the other {@code Timestamp} to compare this {@code Timestamp} to * * @return * -1, 0, or 1 if this {@code Timestamp} * is less than, equal to, or greater than {@code t} respectively * * @throws NullPointerException if {@code t} is null. * * @see #equals(Timestamp) */ public int compareTo(Timestamp t) { // Test at millisecond precision first. long this_millis = this.getMillis(); long arg_millis = t.getMillis(); if (this_millis != arg_millis) { return (this_millis < arg_millis) ? -1 : 1; } // Values are equivalent at millisecond precision, so compare fraction BigDecimal this_fraction = ((this._fraction == null) ? BigDecimal.ZERO : this._fraction); BigDecimal arg_fraction = (( t._fraction == null) ? BigDecimal.ZERO : t._fraction); return this_fraction.compareTo(arg_fraction); } /** * Compares this {@link Timestamp} to the specified Object. * The result is {@code true} if and only if the parameter is a * {@link Timestamp} object that represents the same point in time, * precision and local offset as this Timestamp. *

* Use the {@link #compareTo(Timestamp)} method to compare only the point * in time, ignoring precision and local offset. * * @see #equals(Timestamp) * @see #compareTo(Timestamp) */ @Override public boolean equals(Object t) { if (!(t instanceof Timestamp)) return false; return equals((Timestamp)t); } /** * Compares this {@link Timestamp} to another {@link Timestamp} object. * The result is {@code true} if and only if the parameter * represents the same point in time and has * the same precision and local offset as this object. *

* These pairs are {@link #equals} to each other, as they * represent the same points in time, precision and local offset: * *

    *
  • {@code 2001-01-01T11:22+00:00} (minute precision, in UTC)
  • *
  • {@code 2001-01-01T11:22Z} (minute precision, in UTC)
  • *
* *

* On the other hand, none of these pairs are {@link #equals} to each other, * they represent the same points in time, but with different precisions * and/or local offsets: * *

    *
  • {@code 2001T} (year precision, unknown local offset)
  • *
  • {@code 2001-01T} (month precision, unknown local offset)
  • *
  • {@code 2001-01-01T} (day precision, unknown local offset)
  • * *
  • {@code 2001-01-01T00:00-00:00} (second precision, unknown local offset)
  • *
  • {@code 2001-01-01T00:00+00:00} (second precision, in UTC)
  • * *
  • {@code 2001-01-01T00:00.000-00:00} (millisecond precision, unknown local offset)
  • *
  • {@code 2001-01-01T00:00.000+00:00} (millisecond precision, in UTC)
  • *
* *

* Use the {@link #compareTo(Timestamp)} method to compare only the point * in time, ignoring precision and local offset. * * @see #compareTo(Timestamp) */ public boolean equals(Timestamp t) { if (this == t) return true; if (t == null) return false; // if the precisions are not the same the values are not // precision doesn't matter WRT equality if (this._precision != t._precision) return false; // if the local offset are not the same the values are not if (this._offset == null) { if (t._offset != null) return false; } else { if (t._offset == null) return false; } // so now we check the actual time value if (this._year != t._year) return false; if (this._month != t._month) return false; if (this._day != t._day) return false; if (this._hour != t._hour) return false; if (this._minute != t._minute) return false; if (this._second != t._second) return false; // and if we have a local offset, check the value here if (this._offset != null) { if (this._offset.intValue() != t._offset.intValue()) return false; } // we only look at the fraction if we know that it's actually there if ((this._fraction != null && t._fraction == null) || (this._fraction == null && t._fraction != null)) { // one of the fractions are null return false; } if (this._fraction == null && t._fraction == null) { // both are null return true; } return this._fraction.equals(t._fraction); } private static short checkAndCastYear(int year) { if (year < 1 || year > 9999) { throw new IllegalArgumentException(String.format("Year %s must be between 1 and 9999 inclusive", year)); } return (short) year; } private static byte checkAndCastMonth(int month) { if (month < 1 || month > 12) { throw new IllegalArgumentException(String.format("Month %s must be between 1 and 12 inclusive", month)); } return (byte) month; } private static byte checkAndCastDay(int day, int year, int month) { int lastDayInMonth = last_day_in_month(year, month); if (day < 1 || day > lastDayInMonth) { throw new IllegalArgumentException(String.format("Day %s for year %s and month %s must be between 1 and %s inclusive", day, year, month, lastDayInMonth)); } return (byte) day; } private static byte checkAndCastHour(int hour) { if (hour < 0 || hour > 23) { throw new IllegalArgumentException(String.format("Hour %s must be between 0 and 23 inclusive", hour)); } return (byte) hour; } private static byte checkAndCastMinute(int minute) { if (minute < 0 || minute > 59) { throw new IllegalArgumentException(String.format("Minute %s must be between between 0 and 59 inclusive", minute)); } return (byte) minute; } private static byte checkAndCastSecond(int second) { if (second < 0 || second > 59) { throw new IllegalArgumentException(String.format("Second %s must be between between 0 and 59 inclusive", second)); } return (byte) second; } private static Precision checkFraction(Precision precision, BigDecimal fraction) { if (precision == Precision.SECOND) { if (fraction != null && (fraction.signum() == -1 || BigDecimal.ONE.compareTo(fraction) != 1)) { throw new IllegalArgumentException(String.format("Fractional seconds %s must be greater than or equal to 0 and less than 1", fraction)); } } else { if (fraction != null) { throw new IllegalArgumentException("Fraction must be null for non-second precision: " + fraction); } } return precision; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy