tech.tablesaw.columns.dates.PackedLocalDate Maven / Gradle / Ivy
/*
* 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 tech.tablesaw.columns.dates;
import static java.time.DayOfWeek.FRIDAY;
import static java.time.DayOfWeek.MONDAY;
import static java.time.DayOfWeek.SATURDAY;
import static java.time.DayOfWeek.SUNDAY;
import static java.time.DayOfWeek.THURSDAY;
import static java.time.DayOfWeek.TUESDAY;
import static java.time.DayOfWeek.WEDNESDAY;
import static java.time.Month.APRIL;
import static java.time.Month.AUGUST;
import static java.time.Month.DECEMBER;
import static java.time.Month.FEBRUARY;
import static java.time.Month.JANUARY;
import static java.time.Month.JULY;
import static java.time.Month.JUNE;
import static java.time.Month.MARCH;
import static java.time.Month.MAY;
import static java.time.Month.NOVEMBER;
import static java.time.Month.OCTOBER;
import static java.time.Month.SEPTEMBER;
import static java.time.temporal.ChronoField.EPOCH_DAY;
import static java.time.temporal.ChronoField.YEAR;
import static tech.tablesaw.columns.DateAndTimePredicates.isEqualTo;
import static tech.tablesaw.columns.DateAndTimePredicates.isGreaterThanOrEqualTo;
import static tech.tablesaw.columns.DateAndTimePredicates.isLessThan;
import static tech.tablesaw.columns.DateAndTimePredicates.isLessThanOrEqualTo;
import com.google.common.base.Strings;
import com.google.common.primitives.Ints;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Month;
import java.time.chrono.IsoChronology;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalField;
import java.time.temporal.WeekFields;
import java.util.Locale;
import tech.tablesaw.columns.numbers.IntColumnType;
/**
* A short localdate packed into a single int value. It uses a short for year so the range is about
* +-30,000 years
*
* The bytes are packed into the int as: First two bytes: short (year) next byte (month of year)
* last byte (day of month)
*/
public class PackedLocalDate {
/** The number of days in a 400 year cycle. */
private static final int DAYS_PER_CYCLE = 146097;
/**
* The number of days from year zero to year 1970. There are five 400 year cycles from year zero
* to 2000. There are 7 leap years from 1970 to 2000.
*/
private static final long DAYS_0000_TO_1970 = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L);
public static byte getDayOfMonth(int date) {
return (byte) date; // last byte
}
public static short getYear(int date) {
// get first two bytes, then convert to a short
byte byte1 = (byte) (date >> 24);
byte byte2 = (byte) (date >> 16);
return (short) ((byte1 << 8) + (byte2 & 0xFF));
}
public static LocalDate asLocalDate(int date) {
if (date == IntColumnType.missingValueIndicator()) {
return null;
}
return LocalDate.of(getYear(date), getMonthValue(date), getDayOfMonth(date));
}
public static byte getMonthValue(int date) {
// get the third byte
return (byte) (date >> 8);
}
public static int pack(LocalDate date) {
if (date == null) {
return DateColumnType.missingValueIndicator();
}
return pack((short) date.getYear(), (byte) date.getMonthValue(), (byte) date.getDayOfMonth());
}
public static int pack(short yr, byte m, byte d) {
byte byte1 = (byte) ((yr >> 8) & 0xff);
byte byte2 = (byte) yr;
return Ints.fromBytes(byte1, byte2, m, d);
}
public static int pack(int yr, int m, int d) {
byte byte1 = (byte) ((yr >> 8) & 0xff);
byte byte2 = (byte) yr;
return Ints.fromBytes(byte1, byte2, (byte) m, (byte) d);
}
public static String toDateString(int date) {
if (date == Integer.MIN_VALUE) {
return "";
}
return getYear(date)
+ "-"
+ Strings.padStart(Byte.toString(getMonthValue(date)), 2, '0')
+ "-"
+ Strings.padStart(Byte.toString(getDayOfMonth(date)), 2, '0');
}
public static int getDayOfYear(int packedDate) {
return getMonth(packedDate).firstDayOfYear(isLeapYear(packedDate))
+ getDayOfMonth(packedDate)
- 1;
}
public static boolean isLeapYear(int packedDate) {
return IsoChronology.INSTANCE.isLeapYear(getYear(packedDate));
}
public static Month getMonth(int packedDate) {
return Month.of(getMonthValue(packedDate));
}
public static int lengthOfMonth(int packedDate) {
switch (getMonthValue(packedDate)) {
case 2:
return (isLeapYear(packedDate) ? 29 : 28);
case 4:
case 6:
case 9:
case 11:
return 30;
default:
return 31;
}
}
/** Returns the epoch day in a form consistent with the java standard */
public static long toEpochDay(int packedDate) {
long y = getYear(packedDate);
long m = getMonthValue(packedDate);
long total = 0;
total += 365 * y;
if (y >= 0) {
total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400;
} else {
total -= y / -4 - y / -100 + y / -400;
}
total += ((367 * m - 362) / 12);
total += getDayOfMonth(packedDate) - 1;
if (m > 2) {
total--;
if (!isLeapYear(packedDate)) {
total--;
}
}
return total - DAYS_0000_TO_1970;
}
public static DayOfWeek getDayOfWeek(int packedDate) {
int dow0 = Math.floorMod((int) toEpochDay(packedDate) + 3, 7);
return DayOfWeek.of(dow0 + 1);
}
/**
* Returns the quarter of the year of the given date as an int from 1 to 4, or -1, if the argument
* is the MISSING_VALUE for DateColumn
*/
public static int getQuarter(int packedDate) {
if (packedDate == DateColumnType.missingValueIndicator()) {
return -1;
}
Month month = getMonth(packedDate);
switch (month) {
case JANUARY:
case FEBRUARY:
case MARCH:
return 1;
case APRIL:
case MAY:
case JUNE:
return 2;
case JULY:
case AUGUST:
case SEPTEMBER:
return 3;
case OCTOBER:
case NOVEMBER:
default: // must be december
return 4;
}
}
public static boolean isInQ1(int packedDate) {
return getQuarter(packedDate) == 1;
}
public static boolean isInQ2(int packedDate) {
return getQuarter(packedDate) == 2;
}
public static boolean isInQ3(int packedDate) {
return getQuarter(packedDate) == 3;
}
public static boolean isInQ4(int packedDate) {
return getQuarter(packedDate) == 4;
}
public static boolean isAfter(int packedDate, int value) {
return packedDate > value;
}
public static boolean isEqualTo(int packedDate, int value) {
return isEqualTo.test(packedDate, value);
}
public static boolean isBefore(int packedDate, int value) {
return isLessThan.test(packedDate, value);
}
public static boolean isOnOrBefore(int packedDate, int value) {
return isLessThanOrEqualTo.test(packedDate, value);
}
public static boolean isOnOrAfter(int packedDate, int value) {
return isGreaterThanOrEqualTo.test(packedDate, value);
}
public static boolean isDayOfWeek(int packedDate, DayOfWeek dayOfWeek) {
DayOfWeek dow = getDayOfWeek(packedDate);
return dayOfWeek == dow;
}
public static boolean isSunday(int packedDate) {
return isDayOfWeek(packedDate, SUNDAY);
}
public static boolean isMonday(int packedDate) {
return isDayOfWeek(packedDate, MONDAY);
}
public static boolean isTuesday(int packedDate) {
return isDayOfWeek(packedDate, TUESDAY);
}
public static boolean isWednesday(int packedDate) {
return isDayOfWeek(packedDate, WEDNESDAY);
}
public static boolean isThursday(int packedDate) {
return isDayOfWeek(packedDate, THURSDAY);
}
public static boolean isFriday(int packedDate) {
return isDayOfWeek(packedDate, FRIDAY);
}
public static boolean isSaturday(int packedDate) {
return isDayOfWeek(packedDate, SATURDAY);
}
public static boolean isFirstDayOfMonth(int packedDate) {
return getDayOfMonth(packedDate) == 1;
}
public static boolean isInJanuary(int packedDate) {
return getMonth(packedDate) == JANUARY;
}
public static boolean isInFebruary(int packedDate) {
return getMonth(packedDate) == FEBRUARY;
}
public static boolean isInMarch(int packedDate) {
return getMonth(packedDate) == MARCH;
}
public static boolean isInApril(int packedDate) {
return getMonth(packedDate) == APRIL;
}
public static boolean isInMay(int packedDate) {
return getMonth(packedDate) == MAY;
}
public static boolean isInJune(int packedDate) {
return getMonth(packedDate) == JUNE;
}
public static boolean isInJuly(int packedDate) {
return getMonth(packedDate) == JULY;
}
public static boolean isInAugust(int packedDate) {
return getMonth(packedDate) == AUGUST;
}
public static boolean isInSeptember(int packedDate) {
return getMonth(packedDate) == SEPTEMBER;
}
public static boolean isInOctober(int packedDate) {
return getMonth(packedDate) == OCTOBER;
}
public static boolean isInNovember(int packedDate) {
return getMonth(packedDate) == NOVEMBER;
}
public static boolean isInDecember(int packedDate) {
return getMonth(packedDate) == DECEMBER;
}
public static boolean isLastDayOfMonth(int packedDate) {
return getDayOfMonth(packedDate) == lengthOfMonth(packedDate);
}
public static int withDayOfMonth(int dayOfMonth, int packedDate) {
byte d = (byte) dayOfMonth;
byte m = getMonthValue(packedDate);
short y = getYear(packedDate);
return pack(y, m, d);
}
public static int withMonth(int month, int packedDate) {
byte day = getDayOfMonth(packedDate);
byte _month = (byte) month;
short year = getYear(packedDate);
return pack(year, _month, day);
}
public static int withYear(int year, int packedDate) {
byte day = getDayOfMonth(packedDate);
byte month = getMonthValue(packedDate);
short _year = (short) year;
return pack(_year, month, day);
}
public static int plusYears(int yearsToAdd, int packedDate) {
if (yearsToAdd == 0) {
return packedDate;
}
byte d = getDayOfMonth(packedDate);
byte m = getMonthValue(packedDate);
short y = getYear(packedDate);
int newYear = YEAR.checkValidIntValue(yearsToAdd + y);
return resolvePreviousValid(newYear, m, d);
}
public static int minusYears(int years, int packedDate) {
return plusYears(-years, packedDate);
}
public static int plusMonths(int months, int packedDate) {
if (months == 0) {
return packedDate;
}
byte d = getDayOfMonth(packedDate);
byte m = getMonthValue(packedDate);
short y = getYear(packedDate);
long monthCount = y * 12L + (m - 1);
long calcMonths = monthCount + months;
int newYear = YEAR.checkValidIntValue(Math.floorDiv((int) calcMonths, 12));
int newMonth = Math.floorMod((int) calcMonths, 12) + 1;
return resolvePreviousValid(newYear, newMonth, d);
}
public static int minusMonths(int months, int packedDate) {
return plusMonths(-months, packedDate);
}
public static int plusWeeks(int valueToAdd, int packedDate) {
return plusDays(valueToAdd * 7, packedDate);
}
public static int minusWeeks(int valueToSubtract, int packedDate) {
return minusDays(valueToSubtract * 7, packedDate);
}
public static int plusDays(int days, int packedDate) {
if (days == 0) {
return packedDate;
}
byte d = getDayOfMonth(packedDate);
byte m = getMonthValue(packedDate);
short y = getYear(packedDate);
int dom = days + d;
if (dom > 0) {
if (dom <= 28) {
return pack(y, m, (byte) dom);
} else if (dom <= 59) { // 59th Jan is 28th Feb, 59th Feb is 31st Mar
long monthLen = lengthOfMonth(packedDate);
if (dom <= monthLen) {
return pack(y, m, (byte) dom);
} else if (m < 12) {
return pack(y, (byte) (m + 1), (byte) (dom - monthLen));
} else {
YEAR.checkValidValue((long) y + 1);
return pack((short) (y + 1), (byte) 1, (byte) (dom - monthLen));
}
}
}
long mjDay = Math.addExact(toEpochDay(packedDate), days);
return ofEpochDay(mjDay);
}
public static int minusDays(int days, int packedDate) {
return plusDays(-days, packedDate);
}
public static boolean isInYear(int next, int year) {
return getYear(next) == year;
}
public static int lengthOfYear(int packedDate) {
return (isLeapYear(packedDate) ? 366 : 365);
}
private static int resolvePreviousValid(int year, int month, int day) {
int dayResult = day;
switch (month) {
case 2:
dayResult = Math.min(day, IsoChronology.INSTANCE.isLeapYear(year) ? 29 : 28);
break;
case 4:
case 6:
case 9:
case 11:
dayResult = Math.min(day, 30);
break;
}
return pack((short) year, (byte) month, (byte) dayResult);
}
public static int getWeekOfYear(int packedDateTime) {
LocalDate date = asLocalDate(packedDateTime);
if (date == null) {
throw new IllegalArgumentException("Cannot get week of year for missing value");
}
TemporalField woy = WeekFields.of(Locale.getDefault()).weekOfWeekBasedYear();
return date.get(woy);
}
private static int ofEpochDay(long epochDay) {
EPOCH_DAY.checkValidValue(epochDay);
long zeroDay = epochDay + DAYS_0000_TO_1970;
// find the march-based year
zeroDay -= 60; // adjust to 0000-03-01 so leap day is at end of four year cycle
long adjust = 0;
if (zeroDay < 0) {
// adjust negative years to positive for calculation
long adjustCycles = (zeroDay + 1) / DAYS_PER_CYCLE - 1;
adjust = adjustCycles * 400;
zeroDay += -adjustCycles * DAYS_PER_CYCLE;
}
long yearEst = (400 * zeroDay + 591) / DAYS_PER_CYCLE;
long doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);
if (doyEst < 0) {
// fix estimate
yearEst--;
doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);
}
yearEst += adjust; // reset any negative year
int marchDoy0 = (int) doyEst;
// convert march-based values back to january-based
int marchMonth0 = (marchDoy0 * 5 + 2) / 153;
int month = (marchMonth0 + 2) % 12 + 1;
int dom = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1;
yearEst += marchMonth0 / 10;
// check year now we are certain it is correct
int year = YEAR.checkValidIntValue(yearEst);
return pack((short) year, (byte) month, (byte) dom);
}
public static int plus(int valueToAdd, ChronoUnit unit, int packedDate) {
switch (unit) {
case YEARS:
return plusYears(valueToAdd, packedDate);
case MONTHS:
return plusMonths(valueToAdd, packedDate);
case WEEKS:
return plusWeeks(valueToAdd, packedDate);
case DAYS:
return plusDays(valueToAdd, packedDate);
default:
throw new IllegalArgumentException("Unsupported Temporal Unit");
}
}
public static int minus(int valueToAdd, ChronoUnit unit, int packedDate) {
return plus(-valueToAdd, unit, packedDate);
}
public static int daysUntil(int packedDateEnd, int packedDateStart) {
return (int) (toEpochDay(packedDateEnd) - toEpochDay(packedDateStart));
}
public static int weeksUntil(int packedDateEnd, int packedDateStart) {
return (int) (toEpochDay(packedDateEnd) - toEpochDay(packedDateStart)) / 7;
}
public static int monthsUntil(int packedDateEnd, int packedDateStart) {
int start = getMonthInternal(packedDateStart) * 32 + getDayOfMonth(packedDateStart);
int end = getMonthInternal(packedDateEnd) * 32 + getDayOfMonth(packedDateEnd);
return (end - start) / 32;
}
public static int yearsUntil(int packedDateEnd, int packedDateStart) {
return monthsUntil(packedDateEnd, packedDateStart) / 12;
}
private static int getMonthInternal(int packedDate) {
return (getYear(packedDate) * 12 + getMonthValue(packedDate) - 1);
}
}