/** * Copyright 2011-2017 Asakusa Framework Team. * * 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.asakusafw.runtime.value; import java.util.Calendar; /** * Utilities about date and time. * @since 0.1.0 * @version 0.7.0 */ public final class DateUtil { private static final ThreadLocal<Calendar> CALENDAR_CACHE = ThreadLocal.withInitial(Calendar::getInstance); private static final int DAYS_YEAR = 365; private static final int DAYS_JANUARY = 31; private static final int DAYS_FEBRUARY = DAYS_JANUARY + 28; private static final int DAYS_MARCH = DAYS_FEBRUARY + 31; private static final int DAYS_APRIL = DAYS_MARCH + 30; private static final int DAYS_MAY = DAYS_APRIL + 31; private static final int DAYS_JUNE = DAYS_MAY + 30; private static final int DAYS_JULY = DAYS_JUNE + 31; private static final int DAYS_AUGUST = DAYS_JULY + 31; private static final int DAYS_SEPTEMBER = DAYS_AUGUST + 30; private static final int DAYS_OCTOBER = DAYS_SEPTEMBER + 31; private static final int DAYS_NOVEMBER = DAYS_OCTOBER + 30; private static final int[] DAYS_MONTH = { 0, DAYS_JANUARY, DAYS_FEBRUARY, DAYS_MARCH, DAYS_APRIL, DAYS_MAY, DAYS_JUNE, DAYS_JULY, DAYS_AUGUST, DAYS_SEPTEMBER, DAYS_OCTOBER, DAYS_NOVEMBER, }; private static final int YEARS_LEAP_CYCLE = 400; private static final int DAYS_LEAP_CYCLE = DAYS_YEAR * YEARS_LEAP_CYCLE + (YEARS_LEAP_CYCLE / 4) - (YEARS_LEAP_CYCLE / 100) + (YEARS_LEAP_CYCLE / 400); private static final int YEARS_CENTURY = 100; private static final int DAYS_CENTURY = DAYS_YEAR * YEARS_CENTURY + (YEARS_CENTURY / 4) - (YEARS_CENTURY / 100) + (YEARS_CENTURY / 400); private static final int YEARS_LEAP = 4; private static final int DAYS_LEAP = DAYS_YEAR * YEARS_LEAP + (YEARS_LEAP / 4) - (YEARS_LEAP / 100) + (YEARS_LEAP / 400); /** * Returns a date as the number of elapsed days from {@code 0001/01/01 (YYYY/MM/DD)}. * @param year year * @param month month (1-12) * @param day day (1-31) * @return the number of elapsed days (0-origin) */ public static int getDayFromDate(int year, int month, int day) { int result = 0; result += getDayFromYear(year); result += DAYS_MONTH[month - 1]; result += day - 1; if (month >= 3 && isLeap(year)) { result += 1; } return result; } /** * Returns a date as the number of elapsed days from {@code 0001/01/01 (YYYY/MM/DD)}. * @param calendar the target calendar object * @return the number of elapsed days (0-origin) * @since 0.2.2 */ public static int getDayFromCalendar(Calendar calendar) { int year = calendar.get(Calendar.YEAR); int month = calendar.get(Calendar.MONTH) + 1; int day = calendar.get(Calendar.DAY_OF_MONTH); return getDayFromDate(year, month, day); } /** * Returns a date as the number of elapsed days from {@code 0001/01/01 (YYYY/MM/DD)}. * @param date the target date object * @return the number of elapsed days (0-origin) * @since 0.7.0 */ public static int getDayFromDate(java.util.Date date) { return getDayFromCalendar(toCachedCalendar(date)); } /** * Converts the number of elapsed days from {@code 0001/01/01 (YYYY/MM/DD)} to the corresponded date, * and set it to the target calendar object. * Note that, the hour, minute, second and millisecond fields will be set to {@code 0}. * @param days the number of elapsed days from {@code 0001/01/01 (YYYY/MM/DD)} * @param calendar the target calendar object * @since 0.2.2 */ public static void setDayToCalendar(int days, Calendar calendar) { int year = getYearFromDay(days); int daysInYear = days - getDayFromYear(year); boolean leap = isLeap(year); int month = getMonthOfYear(daysInYear, leap); int day = getDayOfMonth(daysInYear, leap); calendar.set(year, month - 1, day, 0, 0, 0); calendar.set(Calendar.MILLISECOND, 0); } /** * Converts the time as the number of elapsed seconds from {@code 00:00:00}. * @param hour hour * @param minute minute * @param second second * @return the number of elapsed seconds (0-origin) */ public static int getSecondFromTime(int hour, int minute, int second) { int result = 0; result += hour * 60 * 60; result += minute * 60; result += second; return result; } /** * Converts the date and time as the number of elapsed seconds from {@code 0001/01/01 (YYYY/MM/DD) 00:00:00}. * @param calendar the target calendar object * @return the number of elapsed seconds (0-origin) * @since 0.2.2 */ public static long getSecondFromCalendar(Calendar calendar) { int days = getDayFromCalendar(calendar); long result = (long) days * 86400; result += calendar.get(Calendar.HOUR_OF_DAY) * 60 * 60; result += calendar.get(Calendar.MINUTE) * 60; result += calendar.get(Calendar.SECOND); return result; } /** * Converts the date and time as the number of elapsed seconds from {@code 0001/01/01 (YYYY/MM/DD) 00:00:00}. * @param date the target date object * @return the number of elapsed seconds (0-origin) * @since 0.7.0 */ public static long getSecondFromDate(java.util.Date date) { return getSecondFromCalendar(toCachedCalendar(date)); } private static Calendar toCachedCalendar(java.util.Date date) { Calendar work = CALENDAR_CACHE.get(); work.setTime(date); return work; } /** * Converts the number of elapsed seconds from {@code 0001/01/01 (YYYY/MM/DD) 00:00:00} to the corresponded date, * and set it to the target calendar object. * Note that, the millisecond fields will be set to {@code 0}. * @param seconds the number of elapsed seconds * @param calendar the target calendar object * @since 0.2.2 */ public static void setSecondToCalendar(long seconds, Calendar calendar) { int days = getDayFromSeconds(seconds); int year = getYearFromDay(days); int daysInYear = days - getDayFromYear(year); boolean leap = isLeap(year); int month = getMonthOfYear(daysInYear, leap); int day = getDayOfMonth(daysInYear, leap); int rest = getSecondOfDay(seconds); int hour = rest / (60 * 60); int minute = rest / 60 % 60; int second = rest % 60; calendar.set(year, month - 1, day, hour, minute, second); calendar.set(Calendar.MILLISECOND, 0); } /** * Converts the number of elapsed days from {@code 0001/01/01 (YYYY/MM/DD)} to the corresponding year. * @param days the number of elapsed days * @return the corresponded year */ public static int getYearFromDay(int days) { // the number of leap year cycles (400years) int cycles = days / DAYS_LEAP_CYCLE; int cycleRest = days % DAYS_LEAP_CYCLE; // the century offset in the current leap year cycle (0-3) int centInCycle = cycleRest / DAYS_CENTURY; int centRest = cycleRest % DAYS_CENTURY; centRest += DAYS_CENTURY * (centInCycle / (YEARS_LEAP_CYCLE / YEARS_CENTURY)); centInCycle -= (centInCycle / (YEARS_LEAP_CYCLE / YEARS_CENTURY)); // the leap year offset in the current century (0-24) int leapInCent = centRest / DAYS_LEAP; int leapRest = centRest % DAYS_LEAP; // the year offset since the last leap year (0-3) int yearInLeap = leapRest / DAYS_YEAR; yearInLeap -= (yearInLeap / YEARS_LEAP); // compute the year int year = YEARS_LEAP_CYCLE * cycles + YEARS_CENTURY * centInCycle + YEARS_LEAP * leapInCent + yearInLeap + 1; return year; } /** * Whether the target year is leap year or not. * @param year the target year * @return {@code true} if the target year is leap year, otherwise {@code false} */ public static boolean isLeap(int year) { if (year % 4 != 0) { return false; } return (year % 100) != 0 || (year % 400) == 0; } /** * Converts the year to the number of elapsed days from {@code 0001/01/01 (YYYY/MM/DD)} about * the first day of the specified year. * For example, {@code getDayFromYear(1)} returns just {@code 0}. * @param year the target year * @return the number of elapsed days about the first day of the specified year */ public static int getDayFromYear(int year) { int y = year - 1; return DAYS_YEAR * y + (y / 4) - (y / 100) + (y / 400); } /** * Converts the number of elapsed days from beginning of the year to the corresponding month. * @param dayOfYear the number of elapsed days from beginning of the year * @param leap whether the target year is leap year or not * @return the month including the target day (<em>1-origin</em>) */ public static int getMonthOfYear(int dayOfYear, boolean leap) { int d = dayOfYear; if (d < DAYS_JANUARY) { return 1; } if (leap) { d--; } if (d < DAYS_FEBRUARY) { return 2; } if (d < DAYS_MARCH) { return 3; } if (d < DAYS_APRIL) { return 4; } if (d < DAYS_MAY) { return 5; } if (d < DAYS_JUNE) { return 6; } if (d < DAYS_JULY) { return 7; } if (d < DAYS_AUGUST) { return 8; } if (d < DAYS_SEPTEMBER) { return 9; } if (d < DAYS_OCTOBER) { return 10; } if (d < DAYS_NOVEMBER) { return 11; } return 12; } /** * Converts the number of elapsed days from beginning of the year to the date in the current month. * @param dayOfYear the number of elapsed days from beginning of the year * @param leap whether the target year is leap year or not * @return the date in the current month (1-origin) */ public static int getDayOfMonth(int dayOfYear, boolean leap) { int d = dayOfYear; if (d < DAYS_JANUARY) { return d + 1; } if (d < DAYS_FEBRUARY) { return d - (DAYS_JANUARY - 1); } if (leap) { if (d == DAYS_FEBRUARY) { return 29; } d--; } if (d < DAYS_MARCH) { return d - (DAYS_FEBRUARY - 1); } if (d < DAYS_APRIL) { return d - (DAYS_MARCH - 1); } if (d < DAYS_MAY) { return d - (DAYS_APRIL - 1); } if (d < DAYS_JUNE) { return d - (DAYS_MAY - 1); } if (d < DAYS_JULY) { return d - (DAYS_JUNE - 1); } if (d < DAYS_AUGUST) { return d - (DAYS_JULY - 1); } if (d < DAYS_SEPTEMBER) { return d - (DAYS_AUGUST - 1); } if (d < DAYS_OCTOBER) { return d - (DAYS_SEPTEMBER - 1); } if (d < DAYS_NOVEMBER) { return d - (DAYS_OCTOBER - 1); } return d - (DAYS_NOVEMBER - 1); } /** * Converts the number of elapsed seconds from {@code 0001/01/01 (YYYY/MM/DD) 00:00:00} to * the corresponded date as the number of elapsed days. * @param seconds the number of elapsed seconds (0-origin) * @return the corresponded number of elapsed days (0-origin) * @see #getSecondOfDay(long) */ public static int getDayFromSeconds(long seconds) { return (int) (seconds / 86400); } /** * Converts the number of elapsed seconds from {@code 0001/01/01 (YYYY/MM/DD) 00:00:00} to * the number of elapsed seconds from beginning of the corresponding date. * @param seconds the number of elapsed seconds (0-origin) * @return the number of elapsed seconds of the day (0-origin) * @see #getDayFromSeconds(long) */ public static int getSecondOfDay(long seconds) { return (int) (seconds % 86400); } private static final int COL_YEAR_BEGIN = 0; private static final int COL_YEAR_END = COL_YEAR_BEGIN + 4; private static final int COL_MONTH_BEGIN = COL_YEAR_END + 1; private static final int COL_MONTH_END = COL_MONTH_BEGIN + 2; private static final int COL_DAY_BEGIN = COL_MONTH_END + 1; private static final int COL_DAY_END = COL_DAY_BEGIN + 2; private static final int COL_HOUR_BEGIN = COL_DAY_END + 1; private static final int COL_HOUR_END = COL_HOUR_BEGIN + 2; private static final int COL_MINUTE_BEGIN = COL_HOUR_END + 1; private static final int COL_MINUTE_END = COL_MINUTE_BEGIN + 2; private static final int COL_SECOND_BEGIN = COL_MINUTE_END + 1; private static final int COL_SECOND_END = COL_SECOND_BEGIN + 2; /** * Parses a {@code Date} value. * @param value the date value * @param dateSegmentSeparator the separator char between each date segment * @return the days for {@link Date} object * @since 0.7.0 */ public static int parseDate( CharSequence value, char dateSegmentSeparator) { int length = value.length(); if (length == COL_DAY_END && is(value, COL_YEAR_END, dateSegmentSeparator) && is(value, COL_MONTH_END, dateSegmentSeparator)) { int year = parse(value, COL_YEAR_BEGIN, COL_YEAR_END); int month = parse(value, COL_MONTH_BEGIN, COL_MONTH_END); int day = parse(value, COL_DAY_BEGIN, COL_DAY_END); if (year > 0 && month > 0 && day > 0) { return DateUtil.getDayFromDate(year, month, day); } } return -1; } /** * Parses a {@code DateTime} value. * @param value the date-time value * @param dateSegmentSeparator the separator char between each date segment * @param dateTimeSeparator the separator char between date and time * @param timeSegmentSeparator the separator char between each time segment * @return the seconds for {@link DateTime} object * @since 0.7.0 */ public static long parseDateTime( CharSequence value, char dateSegmentSeparator, char dateTimeSeparator, char timeSegmentSeparator) { int length = value.length(); if (length >= COL_SECOND_END && is(value, COL_YEAR_END, dateSegmentSeparator) && is(value, COL_MONTH_END, dateSegmentSeparator) && is(value, COL_DAY_END, dateTimeSeparator) && is(value, COL_HOUR_END, timeSegmentSeparator) && is(value, COL_MINUTE_END, timeSegmentSeparator)) { int year = parse(value, COL_YEAR_BEGIN, COL_YEAR_END); int month = parse(value, COL_MONTH_BEGIN, COL_MONTH_END); int day = parse(value, COL_DAY_BEGIN, COL_DAY_END); int hour = parse(value, COL_HOUR_BEGIN, COL_HOUR_END); int minute = parse(value, COL_MINUTE_BEGIN, COL_MINUTE_END); int second = parse(value, COL_SECOND_BEGIN, COL_SECOND_END); if (year > 0 && month > 0 && day > 0 && hour >= 0 && minute >= 0 && second >= 0) { long result = DateUtil.getDayFromDate(year, month, day) * 86400L; result += DateUtil.getSecondFromTime(hour, minute, second); return result; } } return -1; } private static boolean is(CharSequence string, int column, char value) { return string.charAt(column) == value; } private static int parse(CharSequence string, int begin, int end) { int result = 0; for (int i = begin; i < end; i++) { char c = string.charAt(i); if (c < '0' || '9' < c) { return -1; } result = (result * 10) + (c - '0'); } return result; } /** * Appends a string representation of {@link Date}. * @param elapsedDays the number of elapsed days from 0001/01/01 * @param dateSegmentSeparator the separator char between each date segment * @param target the target buffer * @since 0.7.0 */ public static void toDateString( int elapsedDays, char dateSegmentSeparator, StringBuilder target) { appendDateString(target, elapsedDays, dateSegmentSeparator); } /** * Appends a string representation of {@link DateTime}. * @param elapsedSeconds the number of elapsed seconds from 0001/01/01 00:00:00 * @param dateSegmentSeparator the separator char between each date segment * @param dateTimeSeparator the separator char between date and time * @param timeSegmentSeparator the separator char between each time segment * @param target the target buffer * @since 0.7.0 */ public static void toDateTimeString( long elapsedSeconds, char dateSegmentSeparator, char dateTimeSeparator, char timeSegmentSeparator, StringBuilder target) { appendDateString(target, DateUtil.getDayFromSeconds(elapsedSeconds), dateSegmentSeparator); target.append(dateTimeSeparator); appendTimeString(target, DateUtil.getSecondOfDay(elapsedSeconds), timeSegmentSeparator); } private static void appendDateString(StringBuilder buf, int days, char separator) { int year = DateUtil.getYearFromDay(days); boolean leap = DateUtil.isLeap(year); int dayInYear = days - DateUtil.getDayFromYear(year); int month = DateUtil.getMonthOfYear(dayInYear, leap); int day = DateUtil.getDayOfMonth(dayInYear, DateUtil.isLeap(year)); fill(buf, 4, year); buf.append(separator); fill(buf, 2, month); buf.append(separator); fill(buf, 2, day); } private static void appendTimeString(StringBuilder buf, int seconds, char separator) { int hour = seconds / (60 * 60); int minute = seconds / 60 % 60; int second = seconds % 60; fill(buf, 2, hour); buf.append(separator); fill(buf, 2, minute); buf.append(separator); fill(buf, 2, second); } private static void fill(StringBuilder buf, int columns, int value) { if (value < 0) { buf.append('-'); if (value == Integer.MIN_VALUE) { fill(buf, columns - 2, -(value / 10)); buf.append('8'); } else { fill(buf, columns - 1, -value); } } else { int required = countColumns(value); for (int i = required; i < columns; i++) { buf.append('0'); } buf.append(value); } } private static int countColumns(int value) { assert value >= 0; if (value == 0) { return 1; } double log = Math.log10(value); return (int) Math.floor(log) + 1; } private DateUtil() { throw new AssertionError(); } }