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

com.rt.storage.api.client.util.DateTime Maven / Gradle / Ivy

package com.rt.storage.api.client.util;

import com.google.common.base.Strings;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Objects;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Immutable representation of a date with an optional time and an optional time zone based on RFC 3339.
 *
 * 

Implementation is immutable and therefore thread-safe. * * @since 1.0 * @author Yaniv Inbar */ public final class DateTime implements Serializable { private static final long serialVersionUID = 1L; private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); /** Regular expression for parsing RFC3339 date/times. */ private static final String RFC3339_REGEX = "(\\d{4})-(\\d{2})-(\\d{2})" // yyyy-MM-dd + "([Tt](\\d{2}):(\\d{2}):(\\d{2})(\\.\\d{1,9})?)?" // 'T'HH:mm:ss.nanoseconds + "([Zz]|([+-])(\\d{2}):(\\d{2}))?"; // 'Z' or time zone shift HH:mm following '+' or '-' private static final Pattern RFC3339_PATTERN = Pattern.compile(RFC3339_REGEX); /** * Date/time value expressed as the number of ms since the Unix epoch. * *

If the time zone is specified, this value is normalized to UTC, so to format this date/time * value, the time zone shift has to be applied. */ private final long value; /** Specifies whether this is a date-only value. */ private final boolean dateOnly; /** Time zone shift from UTC in minutes or {@code 0} for date-only value. */ private final int tzShift; /** * Instantiates {@link DateTime} from a {@link Date} and {@link TimeZone}. * * @param date date and time * @param zone time zone; if {@code null}, it is interpreted as {@code TimeZone.getDefault()}. */ public DateTime(Date date, TimeZone zone) { this(false, date.getTime(), zone == null ? null : zone.getOffset(date.getTime()) / 60000); } /** * Instantiates {@link DateTime} from the number of milliseconds since the Unix epoch. * *

The time zone is interpreted as {@code TimeZone.getDefault()}, which may vary with * implementation. * * @param value number of milliseconds since the Unix epoch (January 1, 1970, 00:00:00 GMT) */ public DateTime(long value) { this(false, value, null); } /** * Instantiates {@link DateTime} from a {@link Date}. * *

The time zone is interpreted as {@code TimeZone.getDefault()}, which may vary with * implementation. * * @param value date and time */ public DateTime(Date value) { this(value.getTime()); } /** * Instantiates {@link DateTime} from the number of milliseconds since the Unix epoch, and a shift * from UTC in minutes. * * @param value number of milliseconds since the Unix epoch (January 1, 1970, 00:00:00 GMT) * @param tzShift time zone, represented by the number of minutes off of UTC. */ public DateTime(long value, int tzShift) { this(false, value, tzShift); } /** * Instantiates {@link DateTime}, which may represent a date-only value, from the number of * milliseconds since the Unix epoch, and a shift from UTC in minutes. * * @param dateOnly specifies if this should represent a date-only value * @param value number of milliseconds since the Unix epoch (January 1, 1970, 00:00:00 GMT) * @param tzShift time zone, represented by the number of minutes off of UTC, or {@code null} for * {@code TimeZone.getDefault()}. */ public DateTime(boolean dateOnly, long value, Integer tzShift) { this.dateOnly = dateOnly; this.value = value; this.tzShift = dateOnly ? 0 : tzShift == null ? TimeZone.getDefault().getOffset(value) / 60000 : tzShift; } /** * Instantiates {@link DateTime} from an RFC 3339 * date/time value. * *

Upgrade warning: in prior version 1.17, this method required milliseconds to be exactly 3 * digits (if included), and did not throw an exception for all types of invalid input values, but * starting in version 1.18, the parsing done by this method has become more strict to enforce * that only valid RFC3339 strings are entered, and if not, it throws a {@link * NumberFormatException}. Also, in accordance with the RFC3339 standard, any number of * milliseconds digits is now allowed. * * @param value an RFC 3339 date/time value. * @since 1.11 */ public DateTime(String value) { // Note, the following refactoring is being considered: Move the implementation of parseRfc3339 // into this constructor. Implementation of parseRfc3339 can then do // "return new DateTime(str);". DateTime dateTime = parseRfc3339(value); this.dateOnly = dateTime.dateOnly; this.value = dateTime.value; this.tzShift = dateTime.tzShift; } /** * Returns the date/time value expressed as the number of milliseconds since the Unix epoch. * *

If the time zone is specified, this value is normalized to UTC, so to format this date/time * value, the time zone shift has to be applied. * * @since 1.5 */ public long getValue() { return value; } /** * Returns whether this is a date-only value. * * @since 1.5 */ public boolean isDateOnly() { return dateOnly; } /** * Returns the time zone shift from UTC in minutes or {@code 0} for date-only value. * * @since 1.5 */ public int getTimeZoneShift() { return tzShift; } /** Formats the value as an RFC 3339 date/time string. */ public String toStringRfc3339() { StringBuilder sb = new StringBuilder(); Calendar dateTime = new GregorianCalendar(GMT); long localTime = value + (tzShift * 60000L); dateTime.setTimeInMillis(localTime); // date appendInt(sb, dateTime.get(Calendar.YEAR), 4); sb.append('-'); appendInt(sb, dateTime.get(Calendar.MONTH) + 1, 2); sb.append('-'); appendInt(sb, dateTime.get(Calendar.DAY_OF_MONTH), 2); if (!dateOnly) { // time sb.append('T'); appendInt(sb, dateTime.get(Calendar.HOUR_OF_DAY), 2); sb.append(':'); appendInt(sb, dateTime.get(Calendar.MINUTE), 2); sb.append(':'); appendInt(sb, dateTime.get(Calendar.SECOND), 2); if (dateTime.isSet(Calendar.MILLISECOND)) { sb.append('.'); appendInt(sb, dateTime.get(Calendar.MILLISECOND), 3); } // time zone if (tzShift == 0) { sb.append('Z'); } else { int absTzShift = tzShift; if (tzShift > 0) { sb.append('+'); } else { sb.append('-'); absTzShift = -absTzShift; } int tzHours = absTzShift / 60; int tzMinutes = absTzShift % 60; appendInt(sb, tzHours, 2); sb.append(':'); appendInt(sb, tzMinutes, 2); } } return sb.toString(); } @Override public String toString() { return toStringRfc3339(); } /** * {@inheritDoc} * *

A check is added that the time zone is the same. If you ONLY want to check equality of time * value, check equality on the {@link #getValue()}. */ @Override public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof DateTime)) { return false; } DateTime other = (DateTime) o; return dateOnly == other.dateOnly && value == other.value && tzShift == other.tzShift; } @Override public int hashCode() { return Arrays.hashCode(new long[] {value, dateOnly ? 1 : 0, tzShift}); } /** * Parses an RFC3339 date/time value. * *

Upgrade warning: in prior version 1.17, this method required milliseconds to be exactly 3 * digits (if included), and did not throw an exception for all types of invalid input values, but * starting in version 1.18, the parsing done by this method has become more strict to enforce * that only valid RFC3339 strings are entered, and if not, it throws a {@link * NumberFormatException}. Also, in accordance with the RFC3339 standard, any number of * milliseconds digits is now allowed. * *

Any time information beyond millisecond precision is truncated. * *

For the date-only case, the time zone is ignored and the hourOfDay, minute, second, and * millisecond parameters are set to zero. * * @param str Date/time string in RFC3339 format * @throws NumberFormatException if {@code str} doesn't match the RFC3339 standard format; an * exception is thrown if {@code str} doesn't match {@code RFC3339_REGEX} or if it contains a * time zone shift but no time. */ public static DateTime parseRfc3339(String str) { return parseRfc3339WithNanoSeconds(str).toDateTime(); } /** * Parses an RFC3339 timestamp to a pair of seconds and nanoseconds since Unix Epoch. * * @param str Date/time string in RFC3339 format * @throws IllegalArgumentException if {@code str} doesn't match the RFC3339 standard format; an * exception is thrown if {@code str} doesn't match {@code RFC3339_REGEX} or if it contains a * time zone shift but no time. */ public static SecondsAndNanos parseRfc3339ToSecondsAndNanos(String str) { Rfc3339ParseResult time = parseRfc3339WithNanoSeconds(str); return time.toSecondsAndNanos(); } /** A timestamp represented as the number of seconds and nanoseconds since Epoch. */ public static final class SecondsAndNanos implements Serializable { private final long seconds; private final int nanos; public static SecondsAndNanos ofSecondsAndNanos(long seconds, int nanos) { return new SecondsAndNanos(seconds, nanos); } private SecondsAndNanos(long seconds, int nanos) { this.seconds = seconds; this.nanos = nanos; } public long getSeconds() { return seconds; } public int getNanos() { return nanos; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } SecondsAndNanos that = (SecondsAndNanos) o; return seconds == that.seconds && nanos == that.nanos; } @Override public int hashCode() { return Objects.hash(seconds, nanos); } @Override public String toString() { return String.format("Seconds: %d, Nanos: %d", seconds, nanos); } } /** Result of parsing an RFC 3339 string. */ private static class Rfc3339ParseResult implements Serializable { private final long seconds; private final int nanos; private final boolean timeGiven; private final Integer tzShift; private Rfc3339ParseResult(long seconds, int nanos, boolean timeGiven, Integer tzShift) { this.seconds = seconds; this.nanos = nanos; this.timeGiven = timeGiven; this.tzShift = tzShift; } /** * Convert this {@link Rfc3339ParseResult} to a {@link DateTime} with millisecond precision. Any * fraction of a millisecond will be truncated. */ private DateTime toDateTime() { long seconds = TimeUnit.SECONDS.toMillis(this.seconds); long nanos = TimeUnit.NANOSECONDS.toMillis(this.nanos); return new DateTime(!timeGiven, seconds + nanos, tzShift); } private SecondsAndNanos toSecondsAndNanos() { return new SecondsAndNanos(seconds, nanos); } } private static Rfc3339ParseResult parseRfc3339WithNanoSeconds(String str) throws NumberFormatException { Matcher matcher = RFC3339_PATTERN.matcher(str); if (!matcher.matches()) { throw new NumberFormatException("Invalid date/time format: " + str); } int year = Integer.parseInt(matcher.group(1)); // yyyy int month = Integer.parseInt(matcher.group(2)) - 1; // MM int day = Integer.parseInt(matcher.group(3)); // dd boolean isTimeGiven = matcher.group(4) != null; // 'T'HH:mm:ss.milliseconds String tzShiftRegexGroup = matcher.group(9); // 'Z', or time zone shift HH:mm following '+'/'-' boolean isTzShiftGiven = tzShiftRegexGroup != null; int hourOfDay = 0; int minute = 0; int second = 0; int nanoseconds = 0; Integer tzShiftInteger = null; if (isTzShiftGiven && !isTimeGiven) { throw new NumberFormatException( "Invalid date/time format, cannot specify time zone shift" + " without specifying time: " + str); } if (isTimeGiven) { hourOfDay = Integer.parseInt(matcher.group(5)); // HH minute = Integer.parseInt(matcher.group(6)); // mm second = Integer.parseInt(matcher.group(7)); // ss if (matcher.group(8) != null) { // contains .nanoseconds? String fraction = Strings.padEnd(matcher.group(8).substring(1), 9, '0'); nanoseconds = Integer.parseInt(fraction); } } Calendar dateTime = new GregorianCalendar(GMT); dateTime.clear(); dateTime.set(year, month, day, hourOfDay, minute, second); long value = dateTime.getTimeInMillis(); if (isTimeGiven && isTzShiftGiven) { if (Character.toUpperCase(tzShiftRegexGroup.charAt(0)) != 'Z') { int tzShift = Integer.parseInt(matcher.group(11)) * 60 // time zone shift HH + Integer.parseInt(matcher.group(12)); // time zone shift mm if (matcher.group(10).charAt(0) == '-') { // time zone shift + or - tzShift = -tzShift; } value -= tzShift * 60000L; // e.g. if 1 hour ahead of UTC, subtract an hour to get UTC time tzShiftInteger = tzShift; } else { tzShiftInteger = 0; } } // convert to seconds and nanoseconds long secondsSinceEpoch = value / 1000L; return new Rfc3339ParseResult(secondsSinceEpoch, nanoseconds, isTimeGiven, tzShiftInteger); } /** Appends a zero-padded number to a string builder. */ private static void appendInt(StringBuilder sb, int num, int numDigits) { if (num < 0) { sb.append('-'); num = -num; } int x = num; while (x > 0) { x /= 10; numDigits--; } for (int i = 0; i < numDigits; i++) { sb.append('0'); } if (num != 0) { sb.append(num); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy