Repository: calcite-avatica Updated Branches: refs/heads/branch-avatica-1.12 bd9c968e8 -> 0dd3d6ea1 (forced update)
[CALCITE-2303] In EXTRACT function, support MICROSECONDS, MILLISECONDS, EPOCH, ISODOW, ISOYEAR and DECADE time units (Sergey Nuyanzin) Also, fixed issue related to week extraction (wrong ISO-8601 week calculation in some cases, additional tests provided). Close apache/calcite-avatica#50 Project: http://git-wip-us.apache.org/repos/asf/calcite-avatica/repo Commit: http://git-wip-us.apache.org/repos/asf/calcite-avatica/commit/b8639882 Tree: http://git-wip-us.apache.org/repos/asf/calcite-avatica/tree/b8639882 Diff: http://git-wip-us.apache.org/repos/asf/calcite-avatica/diff/b8639882 Branch: refs/heads/branch-avatica-1.12 Commit: b863988291db6cd64b1517d238b49e98aaaf24d2 Parents: 4beeef4 Author: snuyanzin <snuyan...@gmail.com> Authored: Sun May 27 13:49:17 2018 +0300 Committer: Julian Hyde <jh...@apache.org> Committed: Fri Jun 1 10:52:03 2018 -0700 ---------------------------------------------------------------------- .../calcite/avatica/util/DateTimeUtils.java | 55 ++++- .../apache/calcite/avatica/util/TimeUnit.java | 11 +- .../calcite/avatica/util/TimeUnitRange.java | 2 + .../calcite/avatica/util/DateTimeUtilsTest.java | 208 ++++++++++++++++++- 4 files changed, 263 insertions(+), 13 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/b8639882/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java b/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java index e1f6999..b4148dc 100644 --- a/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java +++ b/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java @@ -87,6 +87,11 @@ public class DateTimeUtils { public static final long MILLIS_PER_DAY = 86400000; // = 24 * 60 * 60 * 1000 /** + * The number of seconds in a day. + */ + public static final long SECONDS_PER_DAY = 86_400; // = 24 * 60 * 60 + + /** * Calendar set to the epoch (1970-01-01 00:00:00 UTC). Useful for * initializing other values. Calendars are not immutable, so be careful not * to screw up this object for everyone else. @@ -729,7 +734,13 @@ public class DateTimeUtils { } public static long unixDateExtract(TimeUnitRange range, long date) { - return julianExtract(range, (int) date + EPOCH_JULIAN); + switch (range) { + case EPOCH: + // no need to extract year/month/day, just multiply + return date * SECONDS_PER_DAY; + default: + return julianExtract(range, (int) date + EPOCH_JULIAN); + } } private static int julianExtract(TimeUnitRange range, int julian) { @@ -758,6 +769,14 @@ public class DateTimeUtils { switch (range) { case YEAR: return year; + case ISOYEAR: + int weekNumber = getIso8601WeekNumber(julian, year, month, day); + if (weekNumber == 1 && month == 12) { + return year + 1; + } else if (month == 1 && weekNumber > 50) { + return year - 1; + } + return year; case QUARTER: return (month + 2) / 3; case MONTH: @@ -766,15 +785,15 @@ public class DateTimeUtils { return day; case DOW: return (int) floorMod(julian + 1, 7) + 1; // sun=1, sat=7 + case ISODOW: + return (int) floorMod(julian, 7) + 1; // mon=1, sun=7 case WEEK: - long fmofw = firstMondayOfFirstWeek(year); - if (julian < fmofw) { - fmofw = firstMondayOfFirstWeek(year - 1); - } - return (int) (julian - fmofw) / 7 + 1; + return getIso8601WeekNumber(julian, year, month, day); case DOY: final long janFirst = ymdToJulian(year, 1, 1); return (int) (julian - janFirst) + 1; + case DECADE: + return year / 10; case CENTURY: return year > 0 ? (year + 99) / 100 @@ -798,6 +817,30 @@ public class DateTimeUtils { return janFirst + (11 - janFirstDow) % 7 - 3; } + /** Returns the ISO-8601 week number based on year, month, day. + * Per ISO-8601 it is the Monday of the week that contains Jan 4, + * or equivalently, it is a Monday between Dec 29 and Jan 4. + * Sometimes it is in the year before the given year, sometimes after. */ + private static int getIso8601WeekNumber(int julian, int year, int month, int day) { + long fmofw = firstMondayOfFirstWeek(year); + if (month == 12 && day > 28) { + if (31 - day + 4 > 7 - ((int) floorMod(julian, 7) + 1) + && 31 - day + (int) (floorMod(julian, 7) + 1) >= 4) { + return (int) (julian - fmofw) / 7 + 1; + } else { + return 1; + } + } else if (month == 1 && day < 5) { + if (4 - day <= 7 - ((int) floorMod(julian, 7) + 1) + && day - ((int) (floorMod(julian, 7) + 1)) >= -3) { + return 1; + } else { + return (int) (julian - firstMondayOfFirstWeek(year - 1)) / 7 + 1; + } + } + return (int) (julian - fmofw) / 7 + 1; + } + /** Extracts a time unit from a UNIX date (milliseconds since epoch). */ public static int unixTimestampExtract(TimeUnitRange range, long timestamp) { http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/b8639882/core/src/main/java/org/apache/calcite/avatica/util/TimeUnit.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/util/TimeUnit.java b/core/src/main/java/org/apache/calcite/avatica/util/TimeUnit.java index 251c4cf..4516410 100644 --- a/core/src/main/java/org/apache/calcite/avatica/util/TimeUnit.java +++ b/core/src/main/java/org/apache/calcite/avatica/util/TimeUnit.java @@ -21,15 +21,16 @@ import java.math.BigDecimal; /** * Enumeration of time units used to construct an interval. * - * <p>Only {@link #YEAR}, {@link #YEAR}, {@link #MONTH}, {@link #DAY}, + * <p>Only {@link #YEAR}, {@link #MONTH}, {@link #DAY}, * {@link #HOUR}, {@link #MINUTE}, {@link #SECOND} can be the unit of a SQL * interval. * * <p>The others ({@link #QUARTER}, {@link #WEEK}, {@link #MILLISECOND}, * {@link #DOW}, {@link #DOY}, {@link #EPOCH}, {@link #DECADE}, {@link #CENTURY}, - * {@link #MILLENNIUM} and {@link #MICROSECOND}) are convenient to use internally, - * when converting to and from UNIX timestamps. And also may be arguments to the - * {@code EXTRACT}, {@code TIMESTAMPADD} and {@code TIMESTAMPDIFF} functions. + * {@link #MILLENNIUM}, {@link #MICROSECOND}, {@link #ISODOW} and {@link #ISOYEAR}) + * are convenient to use internally, when converting to and from UNIX timestamps. + * And also may be arguments to the {@code EXTRACT}, {@code TIMESTAMPADD} and + * {@code TIMESTAMPDIFF} functions. */ public enum TimeUnit { YEAR(true, ' ', BigDecimal.valueOf(12) /* months */, null), @@ -43,12 +44,14 @@ public enum TimeUnit { BigDecimal.valueOf(60)), QUARTER(true, '*', BigDecimal.valueOf(3) /* months */, BigDecimal.valueOf(4)), + ISOYEAR(true, ' ', BigDecimal.valueOf(12) /* months */, null), WEEK(false, '*', BigDecimal.valueOf(DateTimeUtils.MILLIS_PER_DAY * 7), BigDecimal.valueOf(53)), MILLISECOND(false, '.', BigDecimal.ONE, BigDecimal.valueOf(1000)), MICROSECOND(false, '.', BigDecimal.ONE.scaleByPowerOfTen(-3), BigDecimal.valueOf(1000000)), DOW(false, '-', null, null), + ISODOW(false, '-', null, null), DOY(false, '-', null, null), EPOCH(false, '*', null, null), DECADE(true, '*', BigDecimal.valueOf(120) /* months */, null), http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/b8639882/core/src/main/java/org/apache/calcite/avatica/util/TimeUnitRange.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/util/TimeUnitRange.java b/core/src/main/java/org/apache/calcite/avatica/util/TimeUnitRange.java index 42d44dc..2e65ed3 100644 --- a/core/src/main/java/org/apache/calcite/avatica/util/TimeUnitRange.java +++ b/core/src/main/java/org/apache/calcite/avatica/util/TimeUnitRange.java @@ -39,11 +39,13 @@ public enum TimeUnitRange { SECOND(TimeUnit.SECOND, null), // non-standard time units cannot participate in ranges + ISOYEAR(TimeUnit.ISOYEAR, null), QUARTER(TimeUnit.QUARTER, null), WEEK(TimeUnit.WEEK, null), MILLISECOND(TimeUnit.MILLISECOND, null), MICROSECOND(TimeUnit.MICROSECOND, null), DOW(TimeUnit.DOW, null), + ISODOW(TimeUnit.ISODOW, null), DOY(TimeUnit.DOY, null), EPOCH(TimeUnit.EPOCH, null), DECADE(TimeUnit.DECADE, null), http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/b8639882/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java b/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java index ea25349..9c9bc34 100644 --- a/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java +++ b/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java @@ -313,6 +313,15 @@ public class DateTimeUtilsTest { assertThat(unixDateExtract(TimeUnitRange.DOW, 365), is(6L)); assertThat(unixDateExtract(TimeUnitRange.DOW, 366), is(7L)); + // 1969/12/31 was a Wed (4) + assertThat(unixDateExtract(TimeUnitRange.ISODOW, -1), is(3L)); // wed + assertThat(unixDateExtract(TimeUnitRange.ISODOW, 0), is(4L)); // thu + assertThat(unixDateExtract(TimeUnitRange.ISODOW, 1), is(5L)); // fri + assertThat(unixDateExtract(TimeUnitRange.ISODOW, 2), is(6L)); // sat + assertThat(unixDateExtract(TimeUnitRange.ISODOW, 3), is(7L)); // sun + assertThat(unixDateExtract(TimeUnitRange.ISODOW, 365), is(5L)); + assertThat(unixDateExtract(TimeUnitRange.ISODOW, 366), is(6L)); + assertThat(unixDateExtract(TimeUnitRange.DOY, -1), is(365L)); assertThat(unixDateExtract(TimeUnitRange.DOY, 0), is(1L)); assertThat(unixDateExtract(TimeUnitRange.DOY, 1), is(2L)); @@ -342,14 +351,64 @@ public class DateTimeUtilsTest { is(1L)); // thu assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2005, 1, 1)), is(53L)); // sat + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2005, 1, 2)), + is(53L)); // sun + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2005, 12, 31)), + is(52L)); // sat assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2006, 1, 1)), is(52L)); // sun + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2006, 1, 2)), + is(1L)); // mon + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2006, 12, 31)), + is(52L)); // sun + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2007, 1, 1)), + is(1L)); // mon + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2007, 12, 30)), + is(52L)); // sun + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2007, 12, 31)), + is(1L)); // mon + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2008, 12, 28)), + is(52L)); // sun + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2008, 12, 29)), + is(1L)); // mon + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2008, 12, 30)), + is(1L)); // tue + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2008, 12, 31)), + is(1L)); // wen + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2009, 1, 1)), + is(1L)); // thu + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2009, 12, 31)), + is(53L)); // thu + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2010, 1, 1)), + is(53L)); // fri + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2010, 1, 2)), + is(53L)); // sat + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2010, 1, 3)), + is(53L)); // sun + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2010, 1, 4)), + is(1L)); // mon + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2012, 12, 30)), + is(52L)); // sun + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2012, 12, 31)), + is(1L)); // mon + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2014, 12, 30)), + is(1L)); // tue + assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(2014, 12, 31)), + is(1L)); // wen assertThat(unixDateExtract(TimeUnitRange.WEEK, ymdToUnixDate(1970, 1, 1)), is(1L)); // thu - assertThat(unixDateExtract(TimeUnitRange.WEEK, -1), is(53L)); // wed + // Based on the rule: The number of the ISO 8601 week-numbering week of the year. + // By definition, ISO weeks start on Mondays and the first week of a year contains + // January 4 of that year. In other words, the first Thursday of a year is in + // week 1 of that year. + // For that reason 1969-12-31, 1969-12-30 and 1969-12-29 are in the 1-st ISO week of 1970 + assertThat(unixDateExtract(TimeUnitRange.WEEK, -4), is(52L)); // sun + assertThat(unixDateExtract(TimeUnitRange.WEEK, -3), is(1L)); // mon + assertThat(unixDateExtract(TimeUnitRange.WEEK, -2), is(1L)); // tue + assertThat(unixDateExtract(TimeUnitRange.WEEK, -1), is(1L)); // wed assertThat(unixDateExtract(TimeUnitRange.WEEK, 0), is(1L)); // thu - assertThat(unixDateExtract(TimeUnitRange.WEEK, 1), is(1L)); // fru + assertThat(unixDateExtract(TimeUnitRange.WEEK, 1), is(1L)); // fri assertThat(unixDateExtract(TimeUnitRange.WEEK, 2), is(1L)); // sat assertThat(unixDateExtract(TimeUnitRange.WEEK, 3), is(1L)); // sun assertThat(unixDateExtract(TimeUnitRange.WEEK, 4), is(2L)); // mon @@ -428,6 +487,32 @@ public class DateTimeUtilsTest { unixDateExtract(TimeUnitRange.CENTURY, ymdToUnixDate(-2, 1, 1)), is(-1L)); + //The 201st decade started on 2010/01/01. A little bit different but based on + //https://www.postgresql.org/docs/9.1/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT + assertThat( + unixDateExtract(TimeUnitRange.DECADE, ymdToUnixDate(2010, 1, 1)), + is(201L)); + assertThat( + unixDateExtract(TimeUnitRange.DECADE, ymdToUnixDate(2000, 12, 31)), + is(200L)); + assertThat( + unixDateExtract(TimeUnitRange.DECADE, ymdToUnixDate(1852, 6, 7)), + is(185L)); + assertThat( + unixDateExtract(TimeUnitRange.DECADE, ymdToUnixDate(1, 2, 1)), + is(0L)); + // TODO: For a small time range around year 1, due to the Gregorian shift, + // we end up in the wrong decade. Should be 1. + assertThat( + unixDateExtract(TimeUnitRange.DECADE, ymdToUnixDate(1, 1, 1)), + is(0L)); + assertThat( + unixDateExtract(TimeUnitRange.DECADE, ymdToUnixDate(-2, 1, 1)), + is(0L)); + assertThat( + unixDateExtract(TimeUnitRange.DECADE, ymdToUnixDate(-20, 1, 1)), + is(-2L)); + // The 3rd millennium started on 2001/01/01 assertThat( unixDateExtract(TimeUnitRange.MILLENNIUM, ymdToUnixDate(2001, 1, 1)), @@ -447,6 +532,117 @@ public class DateTimeUtilsTest { assertThat( unixDateExtract(TimeUnitRange.MILLENNIUM, ymdToUnixDate(-2, 1, 1)), is(-1L)); + + // The ISO 8601 week-numbering year that the date falls in (not applicable + // to intervals). Each ISO 8601 week-numbering year begins with the Monday + // of the week containing the 4th of January, so in early January or late + // December the ISO year may be different from the Gregorian year. See the + // week field for more information. + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2003, 1, 1)), + is(2003L)); // wed + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2004, 1, 1)), + is(2004L)); // thu + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2005, 1, 1)), + is(2004L)); // sat + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2005, 1, 2)), + is(2004L)); // sun + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2005, 1, 3)), + is(2005L)); // mon + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2005, 12, 31)), + is(2005L)); // sat + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2006, 1, 1)), + is(2005L)); // sun + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2006, 1, 2)), + is(2006L)); // mon + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2006, 12, 31)), + is(2006L)); // sun + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2007, 1, 1)), + is(2007L)); // mon + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2007, 12, 30)), + is(2007L)); // sun + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2007, 12, 31)), + is(2008L)); // mon + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2008, 12, 28)), + is(2008L)); // sun + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2008, 12, 29)), + is(2009L)); // mon + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2008, 12, 30)), + is(2009L)); // tue + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2008, 12, 31)), + is(2009L)); // wen + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2009, 1, 1)), + is(2009L)); // thu + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2009, 12, 31)), + is(2009L)); // thu + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2010, 1, 1)), + is(2009L)); // fri + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2010, 1, 2)), + is(2009L)); // sat + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2010, 1, 3)), + is(2009L)); // sun + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2010, 1, 4)), + is(2010L)); // mon + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2012, 12, 29)), + is(2012L)); // sat + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2012, 12, 30)), + is(2012L)); // sun + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2012, 12, 31)), + is(2013L)); // mon + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2014, 12, 30)), + is(2015L)); // tue + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(2014, 12, 31)), + is(2015L)); // wen + assertThat( + unixDateExtract(TimeUnitRange.ISOYEAR, ymdToUnixDate(1970, 1, 1)), + is(1970L)); // thu + + // For date and timestamp values, the number of seconds since 1970-01-01 00:00:00 UTC + // (can be negative); for interval values, the total number of seconds in the interval + assertThat( + unixDateExtract(TimeUnitRange.EPOCH, ymdToUnixDate(2001, 1, 1)), + is(978_307_200L)); + assertThat( + unixDateExtract(TimeUnitRange.EPOCH, ymdToUnixDate(1969, 12, 31)), + is(-86_400L)); + assertThat( + unixDateExtract(TimeUnitRange.EPOCH, ymdToUnixDate(1970, 1, 1)), + is(0L)); + assertThat( + unixDateExtract(TimeUnitRange.EPOCH, ymdToUnixDate(1, 1, 1)), + is(-62_135_596_800L)); + assertThat( + unixDateExtract(TimeUnitRange.EPOCH, ymdToUnixDate(1, 2, 1)), + is(-62_132_918_400L)); + assertThat( + unixDateExtract(TimeUnitRange.EPOCH, ymdToUnixDate(-2, 1, 1)), + is(-62_230_291_200L)); } @Test public void testUnixDate() { @@ -522,14 +718,20 @@ public class DateTimeUtilsTest { is((long) month)); assertThat(unixDateExtract(TimeUnitRange.DAY, unixDate), is((long) day)); + final long isoYear = unixDateExtract(TimeUnitRange.ISOYEAR, unixDate); + assertTrue(isoYear >= year - 1 && isoYear <= year + 1); final long w = unixDateExtract(TimeUnitRange.WEEK, unixDate); assertTrue(w >= 1 && w <= 53); final long dow = unixDateExtract(TimeUnitRange.DOW, unixDate); assertTrue(dow >= 1 && dow <= 7); + final long iso_dow = unixDateExtract(TimeUnitRange.ISODOW, unixDate); + assertTrue(iso_dow >= 1 && iso_dow <= 7); final long doy = unixDateExtract(TimeUnitRange.DOY, unixDate); - assertTrue(doy >= 1 && dow <= 366); + assertTrue(doy >= 1 && doy <= 366); final long q = unixDateExtract(TimeUnitRange.QUARTER, unixDate); assertTrue(q >= 1 && q <= 4); + final long d = unixDateExtract(TimeUnitRange.DECADE, unixDate); + assertTrue(d == year / 10); final long c = unixDateExtract(TimeUnitRange.CENTURY, unixDate); assertTrue(c == (year > 0 ? (year + 99) / 100 : (year - 99) / 100)); final long m = unixDateExtract(TimeUnitRange.MILLENNIUM, unixDate);