/* * Copyright 2011 Eric F. Savage, code@efsavage.com * * 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.ajah.util.date; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import com.ajah.util.AjahUtils; /** * Utilities for dealing with Dates, times, intervals, etc. * * @author <a href="http://efsavage.com">Eric F. Savage</a>, <a * href="mailto:code@efsavage.com">code@efsavage.com</a>. */ public class DateUtils { /** * Milliseconds in a minute (60,000) */ public static final long MINUTE_IN_MILLIS = 60000L; /** * Milliseconds in an hour (3,600,000) */ public static final long HOUR_IN_MILLIS = 3_600_000L; /** * Milliseconds in a day (86,400,000) */ public static final long DAY_IN_MILLIS = 86400 * 1000L; /** * Milliseconds in a week (604,800,000) */ public static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS; /** * Formatter that only returns the name of the day, e.g. "Friday" */ public static final DateFormat DAY_OF_WEEK_FORMAT = new SimpleDateFormat("EEEE"); private static final DateFormat NICE_ABSOLUTE_DATE_FORMAT = DateFormat.getDateInstance(DateFormat.SHORT); private static final DateFormat NICE_ABSOLUTE_TIME_FORMAT = DateFormat.getTimeInstance(DateFormat.SHORT); private static DateFormat DAY_FORMAT = new SimpleDateFormat("yyyyMMdd"); public static final DateFormat W3C_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); /** * Adds a number of days to the given date. * * @param date * The date to add the time to. * @param days * The number of days to add, may be negative. * @return The current time plus the the number of days specified. */ public static Date addDays(final Date date, final int days) { final Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.add(Calendar.DAY_OF_YEAR, days); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); return calendar.getTime(); } /** * Adds a number of hours to a date, rolling over other fields as necessary. * * @see Calendar#add(int, int) * @param date * The date to add the hours to. * @param hours * The number of hours to add to the date. * @return The date, with the specified number of hours added to it. */ public static Date addHours(final Date date, final int hours) { // TODO Why are we using a calendar here? final Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.add(Calendar.HOUR, hours); return calendar.getTime(); } /** * Adds a number of hours to the current time. * * @param hours * The number of hours to add, may be negative. * @return The current time plus the the number of hours specified. */ public static Date addHours(final int hours) { return new Date(System.currentTimeMillis() + (hours * HOUR_IN_MILLIS)); } /** * Adds a number of minutes to the current time. * * @param minutes * The number of minutes to add, may be negative. * @return The current time plus the the number of minutes specified. */ public static Date addMinutes(final int minutes) { return new Date(System.currentTimeMillis() + (minutes * MINUTE_IN_MILLIS)); } /** * Returns a date that is the number of days different than the current * time. * * @param offset * The number of days to offset the desired date by. A positive * number will be in the future, a negative number will be in the * past. * @return The new date offset by the number of days. */ public static Date daysOffset(final int offset) { return new Date(System.currentTimeMillis() + CalendarUnit.DAY.getMillis(offset)); } /** * Alias for {@link DateUtils#niceFormatRelative(Date, CalendarUnit)} with * null for the largestUnit parameter. * * @param intervalInMillis * Interval for format, required. * @return The formatted date. */ public static String formatInterval(final long intervalInMillis) { return formatInterval(intervalInMillis, CalendarUnit.YEAR, false); } /** * Alias for {@link DateUtils#niceFormatRelative(Date, CalendarUnit)} with * null for the largestUnit parameter. * * @param intervalInMillis * Interval for format, required. * @param veryShortFormat * True if the shortest possible format is desired. This will * return values like "1d" or "3y". * @return The formatted date. */ public static String formatInterval(final long intervalInMillis, final boolean veryShortFormat) { return formatInterval(intervalInMillis, CalendarUnit.YEAR, veryShortFormat); } /** * Formats an interval in a "nice" format, such as "3 minutes" or * "two years". Note that this method uses integer division without * fractions so it will appear to round down aggressively. If the * largestUnit parameter is used, no units representing longer periods of * time will be used. For example, if the interval is 30 months, and * largestUnit is {@link CalendarUnit#YEAR}, "2 years" will be returned. If * the largestUnit is {@link CalendarUnit#MONTH}, "30 months" will be * returned. * * @param intervalInMillis * Interval to format, required. * @param largestUnit * The largest unit to use as the format unit. * @param veryShortFormat * True if the shortest possible format is desired. This will * return values like "1d" or "3y". * @return The formatted date. */ public static String formatInterval(final long intervalInMillis, final CalendarUnit largestUnit, final boolean veryShortFormat) { if (intervalInMillis < CalendarUnit.SECOND.getMillis() || largestUnit == CalendarUnit.SECOND) { return intervalInMillis + (veryShortFormat ? "ms" : " milliseconds"); } else if (intervalInMillis < 100 * CalendarUnit.SECOND.getMillis() || largestUnit == CalendarUnit.SECOND) { return intervalInMillis / CalendarUnit.SECOND.getMillis() + (veryShortFormat ? "s" : " seconds"); } else if (intervalInMillis < 100 * CalendarUnit.MINUTE.getMillis() || largestUnit == CalendarUnit.MINUTE) { return intervalInMillis / CalendarUnit.MINUTE.getMillis() + (veryShortFormat ? "m" : " minutes"); } else if (intervalInMillis < 36 * CalendarUnit.HOUR.getMillis() || largestUnit == CalendarUnit.HOUR) { return intervalInMillis / CalendarUnit.HOUR.getMillis() + (veryShortFormat ? "h" : " hours"); } else if (intervalInMillis < 10 * CalendarUnit.DAY.getMillis() || largestUnit == CalendarUnit.DAY) { return intervalInMillis / CalendarUnit.DAY.getMillis() + (veryShortFormat ? "d" : " days"); } else if (intervalInMillis < 36 * CalendarUnit.WEEK.getMillis() || largestUnit == CalendarUnit.WEEK) { return intervalInMillis / CalendarUnit.WEEK.getMillis() + (veryShortFormat ? "w" : " weeks"); } else if (!veryShortFormat && (intervalInMillis < 36 * CalendarUnit.MONTH.getMillis() || largestUnit == CalendarUnit.MONTH)) { return intervalInMillis / CalendarUnit.HOUR.getMillis() + " months"; } else { return intervalInMillis / CalendarUnit.YEAR.getMillis() + (veryShortFormat ? "y" : " years"); } } /** * Checks two dates to see if they are the same calendar day. * * @param date1 * The first date. * @param date2 * The second date. * @return true if the dates are the same calendar day, based on the current * timezone and locale. */ public static boolean isSameDay(final Date date1, final Date date2) { return DAY_FORMAT.format(date1).equals(DAY_FORMAT.format(date2)); } /** * Formats a date, adjusting based on the current time, in a "nice" format, * such as "tomorrow" or "next Tuesday" without capitalization. * * @param date * Date to format, required. * @return The formatted date. */ public static String niceFormatAbsolute(final Date date) { return niceFormatAbsolute(date, false); } /** * Formats a date, adjusting based on the current time, in a "nice" format, * such as "tomorrow" or "next Tuesday". * * @param date * Date to format, required. * @param capitalize * If true, the first word will be capitalized if it is not a * proper noun. Date and month names will always be capitalized. * @return The formatted date. */ public static String niceFormatAbsolute(final Date date, final boolean capitalize) { final StringBuffer string = new StringBuffer(); final Calendar then = Calendar.getInstance(); then.setTime(date); final Calendar now = Calendar.getInstance(); final long interval = System.currentTimeMillis() - date.getTime(); if (now.get(Calendar.DAY_OF_MONTH) == then.get(Calendar.DAY_OF_MONTH)) { string.append("today"); } else if ((now.get(Calendar.DAY_OF_MONTH) + 1) == then.get(Calendar.DAY_OF_MONTH)) { string.append("tomorrow"); } else if ((now.get(Calendar.DAY_OF_MONTH) - 1) == then.get(Calendar.DAY_OF_MONTH)) { string.append("yesterday"); } else if (interval < 0 && interval > -WEEK_IN_MILLIS) { // Within the next week // TODO handle the case of it being 9:00 am on tuesday and the date // is 9:01 the following tuesday string.append("next " + DAY_OF_WEEK_FORMAT.format(date)); } else if (interval > 0 && interval < WEEK_IN_MILLIS) { // Within the past week // TODO handle the case of it being 9:00 am on tuesday and the date // is 8:59 the previous tuesday string.append("last " + DAY_OF_WEEK_FORMAT.format(date)); } else { string.append(NICE_ABSOLUTE_DATE_FORMAT.format(date)); } string.append(" at "); string.append(NICE_ABSOLUTE_TIME_FORMAT.format(date)); return string.toString(); } /** * Alias for {@link DateUtils#niceFormatRelative(Date, CalendarUnit)} with * null for the largestUnit parameter. * * @param date * Date for format, required. * @return The formatted date. */ public static String niceFormatRelative(final Date date) { return niceFormatRelative(date, null); } /** * Formats a date, relative to the current time, in a "nice" format, such as * "3 minutes ago" or "in two years". Note that this method uses integer * division without fractions so it will appear to round down aggressively. * If the largestUnit parameter is used, no units representing longer * periods of time will be used. For example, if the interval is 30 months, * and largestUnit is {@link CalendarUnit#YEAR}, "2 years" will be returned. * If the largestUnit is {@link CalendarUnit#MONTH}, "30 months" will be * returned. * * @param date * Date to format. * @param largestUnit * The largest unit to use as the format unit. * @return The formatted date. */ public static String niceFormatRelative(final Date date, final CalendarUnit largestUnit) { if (date == null) { return "Never"; } final long interval = System.currentTimeMillis() - date.getTime(); if (interval > 0) { if (interval < 2000) { return "just now"; } else if (interval < 100 * CalendarUnit.SECOND.getMillis() || largestUnit == CalendarUnit.SECOND) { return interval / CalendarUnit.SECOND.getMillis() + " seconds ago"; } else if (interval < 120 * CalendarUnit.MINUTE.getMillis() || largestUnit == CalendarUnit.MINUTE) { return interval / CalendarUnit.MINUTE.getMillis() + " minutes ago"; } else if (interval < 48 * CalendarUnit.HOUR.getMillis() || largestUnit == CalendarUnit.HOUR) { return interval / CalendarUnit.HOUR.getMillis() + " hours ago"; } else if (interval < 21 * CalendarUnit.DAY.getMillis() || largestUnit == CalendarUnit.DAY) { return interval / CalendarUnit.DAY.getMillis() + " days ago"; } else if (interval < 13 * CalendarUnit.WEEK.getMillis() || largestUnit == CalendarUnit.WEEK) { return interval / CalendarUnit.WEEK.getMillis() + " weeks ago"; } else if (interval < 36 * CalendarUnit.MONTH.getMillis() || largestUnit == CalendarUnit.MONTH) { return interval / CalendarUnit.MONTH.getMillis() + " months ago"; } else { return interval / CalendarUnit.YEAR.getMillis() + " years ago"; } } if (interval > -1000) { return "now"; } else if (interval > -100 * CalendarUnit.SECOND.getMillis() || largestUnit == CalendarUnit.SECOND) { return "in " + -interval / CalendarUnit.SECOND.getMillis() + " seconds"; } else if (interval > -120 * CalendarUnit.MINUTE.getMillis() || largestUnit == CalendarUnit.MINUTE) { return "in " + -interval / CalendarUnit.MINUTE.getMillis() + " minutes"; } else if (interval > -48 * CalendarUnit.HOUR.getMillis() || largestUnit == CalendarUnit.HOUR) { return "in " + -interval / CalendarUnit.HOUR.getMillis() + " hours"; } else if (interval > -14 * CalendarUnit.DAY.getMillis() || largestUnit == CalendarUnit.DAY) { return "in " + -interval / CalendarUnit.DAY.getMillis() + " days"; } else if (interval > -36 * CalendarUnit.WEEK.getMillis() || largestUnit == CalendarUnit.WEEK) { return "in " + -interval / CalendarUnit.WEEK.getMillis() + " weeks"; } else if (interval > -36 * CalendarUnit.MONTH.getMillis() || largestUnit == CalendarUnit.MONTH) { return "in " + -interval / CalendarUnit.MONTH.getMillis() + " months"; } else { return "in " + -interval / CalendarUnit.YEAR.getMillis() + " years"; } } /** * Returns a {@link Long} of the result of {@link Date#getTime()}. If the * supplied date parameter is null, returns a Long with a value of zero. * * @param date * Date to convert to UTC. * @return The date in UTC milliseconds, or zero if date is null. */ public static Long safeToLong(final Date date) { if (date == null) { return null; } return Long.valueOf(date.getTime()); } /** * Returns the date for the day after the current one, at midnight. * * @return The date for the day after the current one, at midnight. */ public static Date tomorrow() { final Calendar calendar = Calendar.getInstance(); calendar.roll(Calendar.DAY_OF_YEAR, true); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); return calendar.getTime(); } /** * Formats a date with a very short formatted version of the difference * between the date and now. * * This will return values like "1d" or "3y". * * @param date * The date to format. * @return Very short formatted version of the date, values like "1d" or * "3y". */ public static String veryShortFormatRelative(final Date date) { return formatInterval(Math.abs(System.currentTimeMillis() - date.getTime()), true); } /** * Determines if the date is a certain number of days old. * * @param date * The date to check. * @param days * The number of days, must be greater than zero. * @return True if older, otherwise false. */ public static boolean isDaysOld(Date date, int days) { AjahUtils.requireParam(date, "date"); if (days <= 0) { throw new IllegalArgumentException("days parameter must be greater than zero"); } return (System.currentTimeMillis() - date.getTime() < (86400000 * days)); } }