/*******************************************************************************
* Copyright 2013 Geoscience Australia
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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 au.gov.ga.earthsci.core.temporal;
import java.math.BigInteger;
import java.util.Date;
import au.gov.ga.earthsci.common.util.Validate;
/**
* Represents an immutable time instant with (potentially) nanosecond precision.
* Time is stored as an arbitrary precision integer as the number of nanoseconds
* since {@code 0:00:00:0000 January 1, 1970 UTC} (i.e. the standard Unix date
* epoch).
* <p/>
* All times are represented in UTC.
* <p/>
* While time instants can be represented to nanosecond precision, each
* {@link BigTime} instance includes a {@code resolution} in nanoseconds, which
* gives an indication of the scale at which the time instant can be considered
* useful. For example, a time instant obtained from a Java {@link Date} object
* might have a resolution of 1 second, while an instance representing a
* geological time period may have a resolution of 1 million years. It would be
* programmer error to use a {@link BigTime} instance at a resolution higher
* than it's inherent resolution.
* <p/>
* This class can be used in place of the standard {@link Date} class when
* either:
* <ul>
* <li>Precision higher than millisecond is required; or
* <li>Dates greater than ~292 Million Years (past or future) need to be
* represented (e.g. geological time scales)
* </ul>
* <p/>
* <b>Note:</b> The natural ordering of this class (implemented in
* {@link Comparable}) is inconsistent with {@code equals}. Two instances are
* considered equal via the {@link #equals(Object)} method if they have the same
* time instant AND the same resolution. The {@link #compareTo(BigTime)} method,
* on the other hand, compares two instances
* <em>at the lowest common resolution</em>. <br/>
* For example, a time of {@code [2010-01-01 12:00:00:000 resolution DAY]} is
* not equal to {@code [2010-01-01 12:05:01:000 resolution SECOND]} via
* {@link #equals(Object)}, but is via {@link #compareTo(BigTime)}.
*
* @author Michael de Hoog (michael.dehoog@ga.gov.au)
* @author James Navin (james.navin@ga.gov.au)
*
*/
public class BigTime implements Comparable<BigTime>
{
private static final BigInteger THOUSAND = BigInteger.valueOf(1000);
private static final BigInteger MILLION = BigInteger.valueOf(1000000);
private static final BigInteger SECONDS_IN_MINUTE = BigInteger.valueOf(60);
private static final BigInteger MINUTES_IN_HOUR = BigInteger.valueOf(60);
private static final BigInteger HOURS_IN_DAY = BigInteger.valueOf(24);
private static final double DAYS_IN_YEAR = 365.242196; // Approximation to the mean tropical year
public static final BigInteger NANOS_IN_MICROSECOND = THOUSAND;
public static final BigInteger NANOS_IN_MILLISECOND = NANOS_IN_MICROSECOND.multiply(THOUSAND);
public static final BigInteger NANOS_IN_SECOND = NANOS_IN_MILLISECOND.multiply(THOUSAND);
public static final BigInteger NANOS_IN_MINUTE = NANOS_IN_SECOND.multiply(SECONDS_IN_MINUTE);
public static final BigInteger NANOS_IN_HOUR = NANOS_IN_MINUTE.multiply(MINUTES_IN_HOUR);
public static final BigInteger NANOS_IN_DAY = NANOS_IN_HOUR.multiply(HOURS_IN_DAY);
public static final BigInteger NANOS_IN_YEAR = BigInteger
.valueOf((long) (NANOS_IN_DAY.doubleValue() * DAYS_IN_YEAR));
public static final BigInteger NANOSECOND_RESOLUTION = BigInteger.ONE;
public static final BigInteger MICROSECOND_RESOLUTION = NANOS_IN_MICROSECOND;
public static final BigInteger MILLISECOND_RESOLUTION = NANOS_IN_MILLISECOND;
public static final BigInteger SECOND_RESOLUTION = NANOS_IN_SECOND;
public static final BigInteger YEAR_RESOLUTION = NANOS_IN_YEAR;
public static final BigInteger MA_RESOLUTION = NANOS_IN_YEAR.multiply(MILLION);
public static final BigInteger BA_RESOLUTION = MA_RESOLUTION.multiply(THOUSAND);
public static final BigInteger EARLIEST_DATE_VALUE_IN_NANOS = BigInteger.valueOf(Long.MIN_VALUE).multiply(
NANOS_IN_MILLISECOND);
public static final BigInteger LARGEST_DATE_VALUE_IN_NANOS = BigInteger.valueOf(Long.MAX_VALUE).multiply(
NANOS_IN_MILLISECOND);
/**
* Create a new {@link BigTime} instance that represents the same time as
* the given {@link Date}.
*
* @return A new {@link BigTime} instance that represents the same time as
* the given {@link Date}, with millisecond resolution.
*/
public static BigTime fromDate(Date d)
{
if (d == null)
{
return null;
}
return new BigTime(BigInteger.valueOf(d.getTime()).multiply(NANOS_IN_MILLISECOND), MILLISECOND_RESOLUTION);
}
/**
* Create a new {@link BigTime} instance that represents the current time.
*
* @return a new {@link BigTime} instance representing the current instant
* in time (to millisecond resolution)
*/
public static BigTime now()
{
return fromDate(new Date());
}
// Choice of nanoseconds as the quanta of time is somewhat arbitrary, but is
// 1. Small enough for most (if not all) earth science simulations etc.; and
// 2. The smallest unit of time the Java runtime uses (see System.nanoTime()).
/**
* The number of nanoseconds since epoch (
* {@code 0:00:00:0000 January 1, 1970 UTC}) this instance represents.
*/
private BigInteger nansecondsSinceEpoch;
/**
* The resolution of this instance, in nanoseconds. This is an indication of
* the minimum scale at which this instance can be considered useful.
* <p/>
* See constants defined in this class for useful resolutions.
*/
private BigInteger resolution;
/**
* Create a new instance with the given nanosecond offset and resolution
*/
protected BigTime(long nanos, BigInteger resolution)
{
this(BigInteger.valueOf(nanos), resolution);
}
/**
* Create a new instance with the given nanosecond offset and nanosecond
* resolution
*/
public BigTime(BigInteger nanos)
{
this(nanos, NANOSECOND_RESOLUTION);
}
/**
* Create a new instance with the given nanosecond offset and resolution
*/
public BigTime(BigInteger nanos, BigInteger resolution)
{
Validate.notNull(nanos, "A valid nanosecond value is required"); //$NON-NLS-1$
this.nansecondsSinceEpoch = nanos;
this.resolution = resolution == null ? NANOSECOND_RESOLUTION : resolution;
}
@Override
public int compareTo(BigTime o)
{
BigInteger lowestResolution = resolution.max(o.resolution);
BigTime thisNormalised = changeResolution(lowestResolution).normalise();
BigTime otherNormalised = o.changeResolution(lowestResolution).normalise();
return thisNormalised.nansecondsSinceEpoch.compareTo(otherNormalised.nansecondsSinceEpoch);
}
@Override
public boolean equals(Object obj)
{
if (obj == this)
{
return true;
}
if (!(obj instanceof BigTime))
{
return false;
}
BigTime other = (BigTime) obj;
return other.nansecondsSinceEpoch.equals(this.nansecondsSinceEpoch) && other.resolution.equals(this.resolution);
}
@Override
public int hashCode()
{
return nansecondsSinceEpoch.hashCode() + resolution.hashCode();
}
/**
* Return the resolution of this instance, in nanoseconds.
*
* @return the resolution of this instance.
*
* @see #resolution
*/
public BigInteger getResolution()
{
return resolution;
}
/**
* @return the time this instance represents, in nanoseconds since epoch.
* Should always be used in conjunction with the {@link #resolution}
* .
*
* @see #nansecondsSinceEpoch
* @see #resolution
*/
public BigInteger getNansecondsSinceEpoch()
{
return nansecondsSinceEpoch;
}
/**
* Returns whether or not this BigTime instance can be represented as a
* {@link Date} object (to the resolution a {@link Date} allows).
*
* @return <code>true</code> if this instance is within the range of the
* {@link Date} class
*/
public boolean isInDateRange()
{
return this.nansecondsSinceEpoch.compareTo(EARLIEST_DATE_VALUE_IN_NANOS) >= 0
&& this.nansecondsSinceEpoch.compareTo(LARGEST_DATE_VALUE_IN_NANOS) <= 0;
}
/**
* Returns the {@link Date} representation of the time this instance
* represents, if it is possible.
*
* @return The {@link Date} representation of this instance; or
* <code>null</code> if this instance cannot be represented as a
* {@link Date}.
*
* @see #isInDateRange()
*/
public Date getDate()
{
if (!isInDateRange())
{
return null;
}
return new Date(this.nansecondsSinceEpoch.divide(NANOS_IN_MILLISECOND).longValue());
}
/**
* Return a new {@link BigTime} instance that represents the same time
* instant at the resolution of this instance, but with all information at
* higher resolutions removed.
*
* @return A new instance that represents the same time instant, but with
* all higher resolution information removed.
*/
public BigTime normalise()
{
return new BigTime(normaliseToResolution(nansecondsSinceEpoch, resolution), resolution);
}
/**
* Return a new {@link BigTime} instance that represents the same time
* instant at the provided resolution.
* <p/>
* Care should be taken if changing to a higher resolution as this may imply
* the time value is more precise than it may actually be.
*
* @param resolution
* The resolution to change to
*
* @return A new instance representing the same time at the provided
* resolution
*/
public BigTime changeResolution(BigInteger resolution)
{
return new BigTime(nansecondsSinceEpoch, resolution);
}
/**
* Calculate a normalised version of the provided nanosecond value to remove
* any information at resolutions higher than the provided value.
* <p/>
* For example, a value of 1021 normalised to microsecond resolution will
* return 1000.
*
* @return A normalised version of the given nanosecond value
*/
private static BigInteger normaliseToResolution(BigInteger nanos, BigInteger resolution)
{
// // Use as much precision as is available to reduce the large drift that occurs once the resolution
// // exceeds the range of a long value
// double decimalPart = resolution % 1f;
// double decimalPower = Math.pow(10, decimalPart);
// int count = (int)Math.floor(resolution) + 1;
// double d = decimalPower;
// while (d < Long.MAX_VALUE && count > 0)
// {
// count--;
// decimalPower = d;
// d *= 10;
// }
// BigInteger nanosInResolution = BigInteger.TEN.pow(count).multiply(BigInteger.valueOf((long)decimalPower));
return nanos.divide(resolution).multiply(resolution);
}
}