/*
* Copyright (C) 2012 Facebook, Inc.
*
* 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 com.facebook.util;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.joda.time.DateTime;
import org.joda.time.field.FieldUtils;
/**
* Represents Time intervals either as durations or periods and abstracts out
* operations on time intervals in the System.
* <p/>
* Durations represent a fixed period of time regardless of when
* they start or end. ie. 1 day will always be 86400 seconds. Instances
* that represent duration are constructed via {@link #withMillis(long)}.
* <p/>
* Periods represent a period of time but the actual time will depend
* on when the period starts. For example 1 day will be 23 hours on the first
* day of DST transition and will be 25 hours on the last day of DST transition.
* Instances that represent periods are constructed via
* {@link #withTypeAndLength(TimeIntervalType, int)}.
* <p>
* The main operations abstracted out are the computation of start of
* an interval and addition / subtraction of the interval from a time instant.
* </p>
*/
public class TimeInterval {
/**
* An infinite time interval has a length of 0 and always returns the
* interval start as the start of unix time epoch.
*/
public static final TimeInterval INFINITE = new TimeInterval(null, 0);
public static final TimeInterval ZERO = new TimeInterval(null, -1);
private final long length;
private final TimeIntervalType type;
private TimeInterval(TimeIntervalType type, long length) {
this.type = type;
this.length = length;
}
/**
* Creates a time interval having a fixed duration of time.
*
* @param millis the duration for the interval in milliseconds
* @return the time interval instance.
* @throws IllegalArgumentException if millis is less than 1.
*/
public static TimeInterval withMillis(long millis) {
validateLength(millis);
return new TimeInterval(null, millis);
}
/**
* Creates a time interval period based on the supplied type. The actual duration
* of the period will vary depending on the time instant. The period
* will take into account DST, varying number of days in a month,
* leap years, etc.
* <p>
* Note that if the interval length doesn't divide the maximum value of the
* interval type equally, the last interval will be of a smaller length
* than the previous ones. For example if you specify the interval as
* 40 seconds, the first interval will have the first 40 seconds in a minute
* and the second interval will have the remaining 20 seconds in the minute.
* </p>
* @param type the time interval type, cannot be null.
* @param length the length of the interval
* @return the time interval instance.
* @throws IllegalArgumentException if length is less than 1.
*/
public static TimeInterval withTypeAndLength(TimeIntervalType type, int length) {
if (type == null) {
throw new IllegalArgumentException("type cannot be null");
}
validateLength(length);
return new TimeInterval(type, length);
}
/**
* Used by jackson for serde
*/
@JsonCreator
private static TimeInterval fromJson(
@JsonProperty("type") TimeIntervalType type,
@JsonProperty("length") long length
) {
if (type == null) {
if (length == 0) {
return INFINITE;
} else if (length == -1) {
return ZERO;
}
}
validateLength(length);
return new TimeInterval(type, length);
}
/**
* Gets the start instant of the time interval that will contain the
* supplied time instant. Note that the time zone of the supplied instant
* plays a significant role in computation of the interval.
*
* @param instant the time instant
*
* @return the start instant of the time interval that will contain the
* instant in the time zone of the supplied instant. If the TimeInterval is INFINITE
* unix epoch for the timezone is returned.
*/
public DateTime getIntervalStart(DateTime instant) {
// special handling for ZERO and INFINITE
if (this == ZERO) {
return instant;
} else if (this == INFINITE) {
return new DateTime(1970, 1, 1, 0, 0, 0, 0, instant.getZone());
}
if (type == null) {
// unix epoch for the timezone.
DateTime startOfTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, instant.getZone());
long intervalStart = ((instant.getMillis() - startOfTime.getMillis()) / length) * length;
return startOfTime.plus(intervalStart);
} else {
return type.getTimeIntervalStart(
instant,
length
);
}
}
/**
* Adds supplied multiples of this interval to the supplied instant.
*
* @param instant the instant that needs to be added to.
* @param multiple the multiple value
* @throws IllegalArgumentException if multiple is less than one.
* @throws UnsupportedOperationException if the function is invoked on an {@link #INFINITE}
* object
*
*/
public DateTime plus(DateTime instant, int multiple) {
if (this == INFINITE) {
throw new IllegalStateException(
"plus() function is not supported on an infinite TimeInterval"
);
} else if (this == ZERO) {
return instant;
}
validateMultiple(multiple);
if (type == null) {
return instant.plus(multiple * getLength());
} else {
return instant.plus(
type.toPeriod(
FieldUtils.safeMultiplyToInt(multiple, getLength())
)
);
}
}
/**
* Subtracts the supplied multiples of this interval from the supplied instant. If the
* TimeInterval is {@link #INFINITE} the epoch in the timezone of {@code instant} is returned
*
* @param instant the instant to subtract from
* @param multiple the multiple value
* @throws IllegalArgumentException if multiple is less than one.
*/
public DateTime minus(DateTime instant, int multiple) {
if (this == INFINITE) {
throw new IllegalStateException(
"minus() function is not supported on an infinite TimeInterval"
);
} else if (this == ZERO) {
return instant;
}
validateMultiple(multiple);
if (type == null) {
return instant.minus(multiple * getLength());
} else {
return instant.minus(type.toPeriod(
FieldUtils.safeMultiplyToInt(multiple, getLength())));
}
}
/**
* If this interval is of type period. Note that for {@link #INFINITE} & {@link #ZERO} time
* intervals, this method will return false.
*/
public boolean isPeriod() {
return type != null;
}
/**
* Returns the length value.
*
* @return the length value
*/
@JsonProperty("length")
public long getLength() {
return length;
}
/**
* Returns the interval type. Interval type is null if {@link #isPeriod()} is false.
*
* @return the interval type
*/
@JsonProperty("type")
public TimeIntervalType getType() {
return type;
}
/**
* Returns the length of the interval in milliseconds.
*
* Note that the length is approximate if the interval was constructed
* via {@link #withTypeAndLength(TimeIntervalType, int)}.
*
* Also note that this method returns zero if the TimeInterval is
* {@link #INFINITE}, -1 if the TimeInterval is {@link #ZERO}.
*
* @return the length in millis
* @deprecated Usage of this method is not encouraged because this only
* works if the TimeInterval represents a duration. If the time interval
* is period, this might return unexpected values.
*/
@Deprecated
public long toApproxMillis() {
if (type == null) {
return length;
} else {
return type.toDurationMillis() * length;
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final TimeInterval that = (TimeInterval) o;
if (length != that.length) {
return false;
}
return type == that.type;
}
@Override
public int hashCode() {
int result = (int) (length ^ (length >>> 32));
result = 31 * result + (type != null ? type.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "TimeInterval{" +
"length=" + length +
", type=" + type +
'}';
}
private static void validateMultiple(int multiple) {
if (multiple < 0) {
throw new IllegalArgumentException("Multiple cannot be less that 0 : " + multiple);
}
}
private static void validateLength(long length) {
if (length < 1) {
throw new IllegalArgumentException("length cannot be less than one: " + length);
}
}
}