/*
* 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.DateTimeZone;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TimeZone;
/**
* Tests {@link TimeInterval}
*/
public class TestTimeInterval {
// Constant offset time zones
private static final DateTimeZone UTC = DateTimeZone.forTimeZone(
TimeZone.getTimeZone(
"GMT"
));
private static final DateTimeZone EST = DateTimeZone.forTimeZone(
TimeZone.getTimeZone(
"GMT-5"
));
private static final DateTimeZone PST = DateTimeZone.forTimeZone(
TimeZone.getTimeZone(
"GMT-8"
));
private static final DateTimeZone IST = DateTimeZone.forTimeZone(
TimeZone.getTimeZone(
"GMT+5:30"
));
// Daylight savings time zone
private static final DateTimeZone PDT = DateTimeZone.forTimeZone(
TimeZone.getTimeZone(
"America/Los_Angeles"
));
private static List<DateTimeZone> getTestTimeZones() {
return Arrays.asList(UTC, EST, PST, IST, PDT);
}
@DataProvider(name = "durationParams")
public Object[][] durationParams() throws Exception {
// Use timezones with constant offsets for testing
final int day = 1000 * 60 * 60 * 24;
final int hour = 1000 * 60 * 60;
final int minute = 1000 * 60;
final int minute5 = 1000 * 60 * 5;
final DateTime time = new DateTime(2011, 11, 3, 20, 21, 10, 14, PST);
return new Object[][]{
// day intervals
{time, day, PST, new DateTime(2011, 11, 3, 0, 0, 0, 0, PST )},
{time, day, EST, new DateTime( 2011, 11, 3, 0, 0, 0, 0, EST)},
{time, day, UTC, new DateTime(2011, 11, 4, 0, 0, 0, 0, UTC )},
{time, day, IST, new DateTime( 2011, 11, 4, 0, 0, 0, 0, IST)},
// hour intervals
{time, hour, PST, new DateTime(2011, 11, 3, 20, 0, 0, 0, PST )},
{time, hour, EST, new DateTime( 2011, 11, 3, 23, 0, 0, 0, EST)},
{time, hour, UTC, new DateTime(2011, 11, 4, 4, 0, 0, 0, UTC )},
{time, hour, IST, new DateTime( 2011, 11, 4, 9, 0, 0, 0, IST)},
// minute intervals
{time, minute, PST, new DateTime(2011, 11, 3, 20, 21, 0, 0, PST )},
{time, minute, EST, new DateTime( 2011, 11, 3, 23, 21, 0, 0, EST)},
{time, minute, UTC, new DateTime(2011, 11, 4, 4, 21, 0, 0, UTC )},
{time, minute, IST, new DateTime( 2011, 11, 4, 9, 51, 0, 0, IST)},
// minute5 intervals
{time, minute5, PST, new DateTime(2011, 11, 3, 20, 20, 0, 0, PST )},
{time, minute5, EST, new DateTime( 2011, 11, 3, 23, 20, 0, 0, EST)},
{time, minute5, UTC, new DateTime(2011, 11, 4, 4, 20, 0, 0, UTC )},
{time, minute5, IST, new DateTime( 2011, 11, 4, 9, 50, 0, 0, IST)},
// does not handle PDT correctly
{new DateTime(1310579420000L, PDT), day, PDT, new DateTime(2011, 7, 13, 1, 0, 0, 0, PDT)},
// handles INFINITE interval correctly
{time, 0, PST, new DateTime(1970, 1, 1, 0, 0, 0, 0, PST)},
// handles ZERO interval correctly
{time, -1, PST, time}
};
}
@Test(groups = "fast", dataProvider = "durationParams")
public void testDurationStartOfInterval(
DateTime eventTime,
int intervalMillis,
DateTimeZone tz,
DateTime expectedValue) throws Exception {
TimeInterval timeInterval = intervalMillis == 0
? TimeInterval.INFINITE
: intervalMillis == -1 ? TimeInterval.ZERO : TimeInterval.withMillis(intervalMillis);
Assert.assertEquals(
timeInterval.getIntervalStart(eventTime.withZone(tz)),
expectedValue
);
Assert.assertFalse(timeInterval.isPeriod());
Assert.assertEquals(timeInterval.getLength(), intervalMillis);
Assert.assertEquals(timeInterval.toApproxMillis(), intervalMillis);
Assert.assertNull(timeInterval.getType());
}
@DataProvider(name = "intervalParams")
public Object[][] intervalparams() {
List<Object[]> params = new ArrayList<Object[]>();
for (DateTimeZone timeZone: getTestTimeZones()) {
DateTime time = new DateTime(2011, 8, 9, 14, 35, 12, 17, timeZone);
params.addAll(Arrays.asList(new Object[][] {
{time, TimeIntervalType.SECOND, 5, new DateTime(2011 ,8, 9, 14, 35, 10, 0, timeZone)},
{time, TimeIntervalType.SECOND, 6, new DateTime(2011 ,8, 9, 14, 35, 12, 0, timeZone)},
{time, TimeIntervalType.SECOND, 7, new DateTime(2011 ,8, 9, 14, 35, 7, 0, timeZone)},
{time, TimeIntervalType.SECOND, 30, new DateTime(2011 ,8, 9, 14, 35, 0, 0, timeZone)},
{time, TimeIntervalType.MINUTE, 6, new DateTime(2011 ,8, 9, 14, 30, 0, 0, timeZone)},
{time, TimeIntervalType.HOUR, 1, new DateTime(2011 ,8, 9, 14, 0, 0, 0, timeZone)},
{time, TimeIntervalType.HOUR, 6, new DateTime(2011 ,8, 9, 12, 0, 0, 0, timeZone)},
{time, TimeIntervalType.DAY, 1, new DateTime(2011 ,8, 9, 0, 0, 0, 0, timeZone)},
{time, TimeIntervalType.DAY, 7, new DateTime(2011 ,8, 8, 0, 0, 0, 0, timeZone)},
// Note saturday is the start of week because the 2011-1-1 was a Saturday
{time, TimeIntervalType.WEEK, 1, new DateTime(2011 ,8, 6, 0, 0, 0, 0, timeZone)},
{time, TimeIntervalType.MONTH, 1, new DateTime(2011 ,8, 1, 0, 0, 0, 0, timeZone)},
{time, TimeIntervalType.YEAR, 1, new DateTime(2011 ,1, 1, 0, 0, 0, 0, timeZone)},
{time, TimeIntervalType.YEAR, 3, new DateTime(2009 ,1, 1, 0, 0, 0, 0, timeZone)},
}));
time = new DateTime(2012, 11, 4, 23, 35, 12, 17, timeZone);
params.addAll(Arrays.asList(new Object[][] {
{time, TimeIntervalType.SECOND, 5, new DateTime(2012, 11, 4, 23, 35, 10, 0, timeZone)},
{time, TimeIntervalType.SECOND, 6, new DateTime(2012, 11, 4, 23, 35, 12, 0, timeZone)},
{time, TimeIntervalType.SECOND, 7, new DateTime(2012, 11, 4, 23, 35, 7, 0, timeZone)},
{time, TimeIntervalType.SECOND, 30, new DateTime(2012, 11, 4, 23, 35, 0, 0, timeZone)},
{time, TimeIntervalType.MINUTE, 6, new DateTime(2012, 11, 4, 23, 30, 0, 0, timeZone)},
{time, TimeIntervalType.HOUR, 1, new DateTime(2012, 11, 4, 23, 0, 0, 0, timeZone)},
{time, TimeIntervalType.HOUR, 6, PDT.equals(timeZone)
? new DateTime(2012, 11, 4, 23, 0, 0, 0, timeZone)
: new DateTime(2012, 11, 4, 18, 0, 0, 0, timeZone)},
{time, TimeIntervalType.DAY, 1, new DateTime(2012, 11, 4, 0, 0, 0, 0, timeZone)},
{time, TimeIntervalType.DAY, 3, new DateTime(2012, 11, 4, 0, 0, 0, 0, timeZone)},
{time, TimeIntervalType.DAY, 7, new DateTime(2012, 11, 1, 0, 0, 0, 0, timeZone)},
// Note sunday is the start of week because the 2012-1-1 was a sunday
{time, TimeIntervalType.WEEK, 1, new DateTime(2012, 11, 4, 0, 0, 0, 0, timeZone)},
{time, TimeIntervalType.MONTH, 1, new DateTime(2012, 11, 1, 0, 0, 0, 0, timeZone)},
{time, TimeIntervalType.YEAR, 1, new DateTime(2012, 1, 1, 0, 0, 0, 0, timeZone)},
{time, TimeIntervalType.YEAR, 3, new DateTime(2012, 1, 1, 0, 0, 0, 0, timeZone)},
}));
}
return params.toArray(new Object[params.size()][]);
}
@Test(groups = "fast", dataProvider = "intervalParams")
public void testPeriodStartOfInterval(
DateTime eventTime,
TimeIntervalType intervalType,
int length,
DateTime expectedValue) throws Exception {
TimeInterval timeInterval = TimeInterval.withTypeAndLength(intervalType, length);
Assert.assertEquals(timeInterval.getIntervalStart(eventTime), expectedValue);
Assert.assertTrue(timeInterval.isPeriod());
Assert.assertEquals(timeInterval.getLength(), length);
Assert.assertEquals(timeInterval.getType(), intervalType);
}
@DataProvider(name = "intervalLengthFail")
public Object[][] intervalLengthFail() {
List<Object[]> params = new ArrayList<Object[]>();
for (DateTimeZone timeZone: getTestTimeZones()) {
params.addAll(Arrays.asList(new Object[][] {
{TimeIntervalType.MILLIS, timeZone, 0},
{TimeIntervalType.MILLIS, timeZone, 1000},
{TimeIntervalType.SECOND, timeZone, 0},
{TimeIntervalType.SECOND, timeZone, 60},
{TimeIntervalType.MINUTE, timeZone, 0},
{TimeIntervalType.MINUTE, timeZone, 60},
{TimeIntervalType.HOUR, timeZone, 0},
{TimeIntervalType.HOUR, timeZone, 24},
{TimeIntervalType.DAY, timeZone, 0},
{TimeIntervalType.DAY, timeZone, 32},
{TimeIntervalType.WEEK, timeZone, 0},
{TimeIntervalType.WEEK, timeZone, 54},
{TimeIntervalType.MONTH, timeZone, 0},
{TimeIntervalType.MONTH, timeZone, 13},
{TimeIntervalType.YEAR, timeZone, 0},
}));
}
return params.toArray(new Object[params.size()][]);
}
@Test(groups = "fast",
dataProvider = "intervalLengthFail",
expectedExceptions = {IllegalArgumentException.class},
expectedExceptionsMessageRegExp = ".+is out of bounds for .+")
public void testIntervalLengthOutOfBounds(
TimeIntervalType intervalType,
DateTimeZone timeZone,
long length) {
intervalType.validateValue(timeZone, length);
}
@DataProvider(name = "intervalsValid")
public Object[][] intervalsValid() {
List<Object[]> params = new ArrayList<Object[]>();
// millis in a second
final long oneSec = 1000L;
final long oneDay = 24 * 60 * 60 * oneSec;
final long oneYear = 365 * oneDay + (5 * 3600 + 2952) * oneSec;
for (DateTimeZone timeZone: getTestTimeZones()) {
params.addAll(Arrays.asList(new Object[][] {
{TimeIntervalType.MILLIS, timeZone, 1, 1L},
{TimeIntervalType.MILLIS, timeZone, 999, 999L},
{TimeIntervalType.SECOND, timeZone, 1, oneSec},
{TimeIntervalType.SECOND, timeZone, 59, 59 * oneSec},
{TimeIntervalType.MINUTE, timeZone, 1, 60 * oneSec},
{TimeIntervalType.MINUTE, timeZone, 59, 59 * 60 * oneSec},
{TimeIntervalType.HOUR, timeZone, 1, 60 * 60 * oneSec},
{TimeIntervalType.HOUR, timeZone, 23, 23 * 60 * 60 * oneSec},
{TimeIntervalType.DAY, timeZone, 1, oneDay},
{TimeIntervalType.DAY, timeZone, 31, 31 * oneDay},
{TimeIntervalType.WEEK, timeZone, 1, 7 * oneDay},
{TimeIntervalType.WEEK, timeZone, 53, 53 * 7 * oneDay},
// Gives you a non intuitive value, Beware!
{TimeIntervalType.MONTH, timeZone, 1, 30 * oneDay + 37746 * oneSec},
// Gives you a non intuitive value, Beware!
{TimeIntervalType.MONTH, timeZone, 12, oneYear},
// Gives you a non intuitive value, Beware!
{TimeIntervalType.YEAR, timeZone, 1, oneYear},
}));
}
return params.toArray(new Object[params.size()][]);
}
@Test(groups = "fast", dataProvider = "intervalsValid")
public void testValidIntervalLength(
TimeIntervalType intervalType,
DateTimeZone timeZone,
int length,
long millisValue
) {
intervalType.validateValue(timeZone, length);
Assert.assertEquals(
TimeInterval.withTypeAndLength(intervalType, length).toApproxMillis(),
millisValue);
}
@DataProvider(name = "plusMinus")
public Object[][] plusMinus() {
// 2011-3-13T00:03:00 is DST start
final DateTime testTime = new DateTime(2011, 3, 13, 0, 29, 10, 101, PDT);
return new Object[][] {
{testTime, TimeInterval.withTypeAndLength(TimeIntervalType.MILLIS, 1), new DateTime(2011, 3, 13, 0, 29, 10, 102, PDT), 1},
{testTime, TimeInterval.withTypeAndLength(TimeIntervalType.SECOND, 1), new DateTime(2011, 3, 13, 0, 29, 11, 101, PDT), 1},
{testTime, TimeInterval.withTypeAndLength(TimeIntervalType.MINUTE, 5), new DateTime(2011, 3, 13, 0, 34, 10, 101, PDT), 1},
{testTime, TimeInterval.withTypeAndLength(TimeIntervalType.MINUTE, 121), new DateTime(2011, 3, 13, 3, 30, 10, 101, PDT), 1},
{testTime, TimeInterval.withTypeAndLength(TimeIntervalType.HOUR, 2), new DateTime(2011, 3, 13, 3, 29, 10, 101, PDT), 1},
{testTime, TimeInterval.withTypeAndLength(TimeIntervalType.DAY, 1), new DateTime(2011, 3, 14, 0, 29, 10, 101, PDT), 1},
{testTime, TimeInterval.withTypeAndLength(TimeIntervalType.MONTH, 3), new DateTime(2011, 6, 13, 0, 29, 10, 101, PDT), 1},
{testTime, TimeInterval.withTypeAndLength(TimeIntervalType.WEEK, 7), new DateTime(2011, 5, 1, 0, 29, 10, 101, PDT), 1},
// Tests out leap years too
{testTime, TimeInterval.withTypeAndLength(TimeIntervalType.YEAR, 5), new DateTime(2016, 3, 13, 0, 29, 10, 101, PDT), 1},
// Test multiple > 1
{testTime, TimeInterval.withTypeAndLength(TimeIntervalType.MINUTE, 60), new DateTime(2011, 3, 13, 3, 29, 10, 101, PDT), 2},
// Test exact instant of transition
{new DateTime(2011, 3, 13, 1, 59, 59, 999, PDT), TimeInterval.withTypeAndLength(TimeIntervalType.MILLIS, 1), new DateTime(2011, 3, 13, 3, 0, 0, 0, PDT), 1},
// Test duration based interval
{new DateTime(2011, 3, 13, 1, 59, 59, 999, PDT), TimeInterval.withMillis(1), new DateTime(2011, 3, 13, 3, 0, 0, 0, PDT), 1},
{testTime, TimeInterval.withMillis(24 * 60 * 60 * 1000), new DateTime(2011, 3, 14, 1, 29, 10, 101, PDT), 1},
{testTime, TimeInterval.ZERO, testTime, 1}
};
}
@Test(groups = "fast", dataProvider = "plusMinus")
public void testIntervalPlusMinus(
DateTime before,
TimeInterval interval,
DateTime after,
int multiple) {
Assert.assertEquals(interval.plus(before, multiple), after);
Assert.assertEquals(interval.minus(after, multiple), before);
}
@Test(groups = "fast")
public void testEqualsAndHashcode() {
TimeInterval []intervals = {
TimeInterval.ZERO,
TimeInterval.INFINITE,
TimeInterval.withMillis(1),
TimeInterval.withMillis(2),
TimeInterval.withTypeAndLength(TimeIntervalType.DAY, 1),
TimeInterval.withTypeAndLength(TimeIntervalType.DAY, 2),
TimeInterval.withTypeAndLength(TimeIntervalType.HOUR, 1),
};
for (int i = 0; i < intervals.length; i++) {
TimeInterval interval1 = intervals[i];
for (int j = 0; j < intervals.length; j++) {
TimeInterval interval2 = intervals[j];
if (i == j) {
Assert.assertEquals(interval1, interval2);
Assert.assertEquals(interval1.hashCode(), interval2.hashCode());
} else {
Assert.assertFalse(interval1.equals(interval2));
// TimeInterval.ZERO and TimeInterval.INFINITE happens to have the
// same hash code:
// 11111111111111111111111111111111 XOR 11111111111111111111111111111111 = 0
// 00000000000000000000000000000000 XOR 00000000000000000000000000000000 = 0
// Since the hash code function maps a long (length) to an int, it's bound
// to have *some* collisions, and according to Anshul the likelyhood of these
// being used as keys is low. Also equals() could tell the difference of
// TimeInterval.ZERO and TimeInterval.INFINITE, it's not a correctness issue.
// No big deal but a little special handling is needed in unit testing. :-)
Assert.assertTrue(interval1.hashCode() != interval2.hashCode() ||
interval1 == TimeInterval.ZERO && interval2 == TimeInterval.INFINITE ||
interval1 == TimeInterval.INFINITE && interval2 == TimeInterval.ZERO);
}
}
}
Assert.assertFalse(TimeInterval.INFINITE.equals(null));
Assert.assertFalse(TimeInterval.INFINITE.equals(new Object()));
Assert.assertFalse(TimeInterval.ZERO.equals(null));
Assert.assertFalse(TimeInterval.ZERO.equals(new Object()));
Assert.assertFalse(TimeInterval.ZERO.equals(TimeInterval.INFINITE));
}
@Test(groups = "fast",
expectedExceptions = {IllegalArgumentException.class},
expectedExceptionsMessageRegExp = "length cannot be less than one: 0")
public void testInvalidMillis() {
TimeInterval.withMillis(0);
}
@Test(groups = "fast",
expectedExceptions = {IllegalArgumentException.class},
expectedExceptionsMessageRegExp = "type cannot be null")
public void testNullType() {
TimeInterval.withTypeAndLength(null, 1);
}
@Test(groups = "fast",
expectedExceptions = {IllegalArgumentException.class},
expectedExceptionsMessageRegExp = "length cannot be less than one: 0")
public void testInvalidLength() {
TimeInterval.withTypeAndLength(TimeIntervalType.DAY, 0);
}
@Test(groups = "fast",
expectedExceptions = {IllegalArgumentException.class},
expectedExceptionsMessageRegExp = "Multiple cannot be less that 0 : -1")
public void testInvalidMultiple() {
TimeInterval.withMillis(10).plus(new DateTime(1000), -1);
}
@Test(groups = "fast", expectedExceptions = {IllegalStateException.class})
public void testInfinitePlus() {
TimeInterval.INFINITE.plus(new DateTime(1000), 1);
}
@Test(groups = "fast", expectedExceptions = {IllegalStateException.class})
public void testInfiniteMinus() {
DateTime testTimePDT = new DateTime(2011, 3, 13, 0, 29, 10, 101, PDT);
TimeInterval.INFINITE.minus(testTimePDT, 1);
}
}