/*
* 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 org.joda.time.DateTime;
import org.joda.time.DateTimeField;
import org.joda.time.DateTimeFieldType;
import org.joda.time.DateTimeZone;
import org.joda.time.DurationField;
import org.joda.time.Period;
import org.joda.time.PeriodType;
import org.joda.time.chrono.ISOChronology;
import org.joda.time.field.FieldUtils;
import static org.joda.time.DateTimeFieldType.centuryOfEra;
import static org.joda.time.DateTimeFieldType.dayOfMonth;
import static org.joda.time.DateTimeFieldType.hourOfDay;
import static org.joda.time.DateTimeFieldType.millisOfSecond;
import static org.joda.time.DateTimeFieldType.minuteOfHour;
import static org.joda.time.DateTimeFieldType.monthOfYear;
import static org.joda.time.DateTimeFieldType.secondOfMinute;
import static org.joda.time.DateTimeFieldType.weekOfWeekyear;
import static org.joda.time.DateTimeFieldType.yearOfCentury;
/**
* Represents a time interval type when specifying a time period. Instances of
* this class provide means to calculate the start instant of the interval
* containing a given time instant.
*
* <p>
* Each interval type computes the time interval assuming that they start
* from it's minimum value. For example, MINUTE represents the time interval
* starting from the 0th minute in the containing hour. So given a time instant
* 2011-10-09T13:11:49, and a time interval type of MINUTE and length 3, the
* start instant of the time interval containing that instant will be
* 2011-10-09T13:09:00.
* </p>
*/
public enum TimeIntervalType {
MILLIS(PeriodType.millis(), millisOfSecond(), secondOfMinute()) {
@Override
public Period toPeriod(int length) {
return Period.millis(length);
}
},
SECOND(PeriodType.seconds(), secondOfMinute(), minuteOfHour()) {
@Override
public Period toPeriod(int length) {
return Period.seconds(length);
}
},
MINUTE(PeriodType.minutes(), minuteOfHour(), hourOfDay()) {
@Override
public Period toPeriod(int length) {
return Period.minutes(length);
}
},
HOUR(PeriodType.hours(), hourOfDay(), dayOfMonth()) {
@Override
public Period toPeriod(int length) {
return Period.hours(length);
}
},
DAY(PeriodType.days(), dayOfMonth(), monthOfYear()) {
@Override
public Period toPeriod(int length) {
return Period.days(length);
}
},
/**
* Week interval is currently non-intuitive. It assumes that the weeks
* start on the same day as the first day of the year. Don't use weeks
* as a time interval type, until we can have the weeks starting on the
* correct day (Sunday/Monday) per the locale.
*/
WEEK(PeriodType.weeks(), weekOfWeekyear(), yearOfCentury()) {
@Override
public Period toPeriod(int length) {
return Period.weeks(length);
}
},
MONTH(PeriodType.months(), monthOfYear(), yearOfCentury()) {
@Override
public Period toPeriod(int length) {
return Period.months(length);
}
},
YEAR(PeriodType.years(), yearOfCentury(), centuryOfEra()) {
@Override
public Period toPeriod(int length) {
return Period.years(length);
}
};
private final PeriodType periodType;
private final DateTimeFieldType fieldType;
private final DateTimeFieldType truncateFieldType;
TimeIntervalType(
PeriodType periodType, DateTimeFieldType fieldType, DateTimeFieldType truncateFieldType
) {
this.periodType = periodType;
this.fieldType = fieldType;
this.truncateFieldType = truncateFieldType;
}
/**
* Returns a period representing this interval type.
*
* @param length the multiple of the base period unit
* @return the period value
*/
public abstract Period toPeriod(int length);
/**
* Validates that the specified interval value is valid for this
* interval type in the supplied time zone.
*
* @param timeZone the time zone
* @param intervalLength the interval length
*/
public void validateValue(DateTimeZone timeZone, long intervalLength) {
final DateTimeField field = fieldType.getField(TimeUtil.getChronology(timeZone.getID()));
if (intervalLength < 1
|| intervalLength > field.getMaximumValue()) {
throw new IllegalArgumentException(
"Supplied value " + intervalLength + " is out of bounds for " + name()
);
}
}
/**
* Gets the start instant given the event instant, interval length
* and the time zone for this interval type.
*
* @param instant the event time instant.
* @param length the interval length
*
* @return the start instant of the interval of given length that contains
* the supplied time instant in the supplied time zone
*/
public DateTime getTimeIntervalStart(DateTime instant, long length) {
validateValue(instant.getZone(), length);
// Reset all the fields
DateTime periodStart = instant.property(truncateFieldType)
.roundFloorCopy();
// figure out the which time interval does the instant lie in
Period period = new Period(periodStart, instant, periodType);
DurationField durationField = fieldType.getField(instant.getChronology()).getDurationField();
int diff = period.get(durationField.getType());
long startDelta = (diff / length) * length;
return periodStart.withFieldAdded(durationField.getType(), FieldUtils.safeToInt(startDelta));
}
/**
* Returns the number of milliseconds per unit of this interval type.
*
* @return the number of milliseconds per unit of this interval type.
*/
public long toDurationMillis() {
return fieldType.getDurationType().getField(
// Assume that durations are always in UTC
ISOChronology.getInstance(DateTimeZone.UTC)).getUnitMillis();
}
}