/*
* Copyright 2017 OmniFaces
*
* 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 org.omnifaces.el.functions;
import static org.omnifaces.util.Faces.getLocale;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.omnifaces.util.Faces;
/**
* <p>
* Collection of EL functions for date and time: <code>of:formatDate()</code>, <code>of:formatDateWithTimezone()</code>,
* <code>of:addXxx()</code> like <code>of:addDays()</code>, <code>of:xxxBetween()</code> like <code>of:daysBetween()</code>,
* <code>of:getMonths()</code>, <code>of:getShortMonths()</code>, <code>of:getDaysOfWeek()</code>, <code>of:getShortDaysOfWeek()</code>,
* <code>of:getMonth()</code>, <code>of:getShortMonth()</code>, <code>of:getDayOfWeek()</code> and <code>of:getShortDayOfWeek()</code>.
*
* @author Bauke Scholtz
*/
public final class Dates {
// Constants ------------------------------------------------------------------------------------------------------
private static final Map<Locale, Map<String, Integer>> MONTHS_CACHE = new ConcurrentHashMap<>(3);
private static final Map<Locale, Map<String, Integer>> SHORT_MONTHS_CACHE = new ConcurrentHashMap<>(3);
private static final Map<Locale, Map<String, Integer>> DAYS_OF_WEEK_CACHE = new ConcurrentHashMap<>(3);
private static final Map<Locale, Map<String, Integer>> SHORT_DAYS_OF_WEEK_CACHE = new ConcurrentHashMap<>(3);
private static final TimeZone TIMEZONE_DEFAULT = TimeZone.getDefault();
private static final TimeZone TIMEZONE_UTC = TimeZone.getTimeZone("UTC");
// Constructors ---------------------------------------------------------------------------------------------------
private Dates() {
// Hide constructor.
}
// Formatting -----------------------------------------------------------------------------------------------------
/**
* Format the given date in the given pattern with system default timezone. This is useful when you want to format
* dates in for example the <code>title</code> attribute of an UI component, or the <code>itemLabel</code> attribute
* of select item, or wherever you can't use the <code><f:convertDateTime></code> tag. The format locale will
* be set to the one as obtained by {@link Faces#getLocale()}.
* @param date The date to be formatted in the given pattern.
* @param pattern The pattern to format the given date in.
* @return The date which is formatted in the given pattern.
* @throws NullPointerException When the pattern is <code>null</code>.
* @see #formatDateWithTimezone(Date, String, Object)
*/
public static String formatDate(Date date, String pattern) {
return formatDate(date, pattern, TIMEZONE_DEFAULT);
}
/**
* Format the given date in the given pattern with the given timezone. This is useful when you want to format dates
* in for example the <code>title</code> attribute of an UI component, or the <code>itemLabel</code> attribute of
* select item, or wherever you can't use the <code><f:convertDateTime></code> tag. The format locale will be
* set to the one as obtained by {@link Faces#getLocale()}.
* @param date The date to be formatted in the given pattern.
* @param pattern The pattern to format the given date in.
* @param timezone The timezone to format the given date with, can be either timezone ID as string or
* {@link TimeZone} object.
* @return The date which is formatted in the given pattern.
* @throws NullPointerException When the pattern is <code>null</code>.
*/
public static String formatDateWithTimezone(Date date, String pattern, Object timezone) {
return formatDate(date, pattern,
(timezone instanceof TimeZone) ? ((TimeZone) timezone) : TimeZone.getTimeZone(String.valueOf(timezone)));
}
/**
* Helper method taking {@link TimeZone} instead of {@link String}.
*/
private static String formatDate(Date date, String pattern, TimeZone timezone) {
if (date == null) {
return null;
}
DateFormat formatter = new SimpleDateFormat(pattern, getLocale());
formatter.setTimeZone(timezone);
return formatter.format(date);
}
// Manipulating ---------------------------------------------------------------------------------------------------
/**
* Returns a new date instance which is a sum of the given date and the given amount of years.
* @param date The date to add the given amount of years to.
* @param years The amount of years to be added to the given date. It can be negative.
* @return A new date instance which is a sum of the given date and the given amount of years.
* @throws NullPointerException When the date is <code>null</code>.
*/
public static Date addYears(Date date, int years) {
return add(date, years, Calendar.YEAR);
}
/**
* Returns a new date instance which is a sum of the given date and the given amount of months.
* @param date The date to add the given amount of months to.
* @param months The amount of months to be added to the given date. It can be negative.
* @return A new date instance which is a sum of the given date and the given amount of months.
* @throws NullPointerException When the date is <code>null</code>.
*/
public static Date addMonths(Date date, int months) {
return add(date, months, Calendar.MONTH);
}
/**
* Returns a new date instance which is a sum of the given date and the given amount of weeks.
* @param date The date to add the given amount of weeks to.
* @param weeks The amount of weeks to be added to the given date. It can be negative.
* @return A new date instance which is a sum of the given date and the given amount of weeks.
* @throws NullPointerException When the date is <code>null</code>.
*/
public static Date addWeeks(Date date, int weeks) {
return add(date, weeks, Calendar.WEEK_OF_YEAR);
}
/**
* Returns a new date instance which is a sum of the given date and the given amount of days.
* @param date The date to add the given amount of days to.
* @param days The amount of days to be added to the given date. It can be negative.
* @return A new date instance which is a sum of the given date and the given amount of days.
* @throws NullPointerException When the date is <code>null</code>.
*/
public static Date addDays(Date date, int days) {
return add(date, days, Calendar.DAY_OF_MONTH);
}
/**
* Returns a new date instance which is a sum of the given date and the given amount of hours.
* @param date The date to add the given amount of hours to.
* @param hours The amount of hours to be added to the given date. It can be negative.
* @return A new date instance which is a sum of the given date and the given amount of hours.
* @throws NullPointerException When the date is <code>null</code>.
*/
public static Date addHours(Date date, int hours) {
return add(date, hours, Calendar.HOUR_OF_DAY);
}
/**
* Returns a new date instance which is a sum of the given date and the given amount of minutes.
* @param date The date to add the given amount of minutes to.
* @param minutes The amount of minutes to be added to the given date. It can be negative.
* @return A new date instance which is a sum of the given date and the given amount of minutes.
* @throws NullPointerException When the date is <code>null</code>.
*/
public static Date addMinutes(Date date, int minutes) {
return add(date, minutes, Calendar.MINUTE);
}
/**
* Returns a new date instance which is a sum of the given date and the given amount of seconds.
* @param date The date to add the given amount of seconds to.
* @param seconds The amount of seconds to be added to the given date. It can be negative.
* @return A new date instance which is a sum of the given date and the given amount of seconds.
* @throws NullPointerException When the date is <code>null</code>.
*/
public static Date addSeconds(Date date, int seconds) {
return add(date, seconds, Calendar.SECOND);
}
/**
* Helper method which converts the given date to an UTC calendar and adds the given amount of units to the given
* calendar field.
*/
private static Date add(Date date, int units, int field) {
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.setTime(date);
calendar.setTimeZone(TIMEZONE_UTC);
calendar.add(field, units);
return calendar.getTime();
}
// Calculating ----------------------------------------------------------------------------------------------------
/**
* Returns the amount of years between two given dates.
* This will be negative when the end date is before the start date.
* @param start The start date.
* @param end The end date.
* @return The amount of years between two given dates.
* @throws NullPointerException When a date is <code>null</code>.
*/
public static int yearsBetween(Date start, Date end) {
return dateDiff(start, end, Calendar.YEAR);
}
/**
* Returns the amount of months between two given dates.
* This will be negative when the end date is before the start date.
* @param start The start date.
* @param end The end date.
* @return The amount of months between two given dates.
* @throws NullPointerException When a date is <code>null</code>.
*/
public static int monthsBetween(Date start, Date end) {
return dateDiff(start, end, Calendar.MONTH);
}
/**
* Returns the amount of weeks between two given dates.
* This will be negative when the end date is before the start date.
* @param start The start date.
* @param end The end date.
* @return The amount of weeks between two given dates.
* @throws NullPointerException When a date is <code>null</code>.
*/
public static int weeksBetween(Date start, Date end) {
return dateDiff(start, end, Calendar.WEEK_OF_YEAR);
}
/**
* Returns the amount of days between two given dates.
* This will be negative when the end date is before the start date.
* @param start The start date.
* @param end The end date.
* @return The amount of days between two given dates.
* @throws NullPointerException When a date is <code>null</code>.
*/
public static int daysBetween(Date start, Date end) {
return dateDiff(start, end, Calendar.DAY_OF_MONTH);
}
/**
* Helper method which converts the given dates to UTC calendar without time and returns the unit difference of the
* given calendar field.
*/
private static int dateDiff(Date startDate, Date endDate, int field) {
Calendar start = toUTCCalendarWithoutTime(startDate);
Calendar end = toUTCCalendarWithoutTime(endDate);
int elapsed = 0;
if (start.before(end)) {
while (start.before(end)) {
start.add(field, 1);
elapsed++;
}
}
else if (start.after(end)) {
while (start.after(end)) {
start.add(field, -1);
elapsed--;
}
}
return elapsed;
}
/**
* Helper method to convert given date to an UTC calendar without time part (to prevent potential DST issues).
*/
private static Calendar toUTCCalendarWithoutTime(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.setTime(date);
calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar;
}
/**
* Returns the amount of hours between two given dates.
* This will be negative when the end date is before the start date.
* @param start The start date.
* @param end The end date.
* @return The amount of hours between two given dates.
* @throws NullPointerException When a date is <code>null</code>.
*/
public static long hoursBetween(Date start, Date end) {
return timeDiff(start, end, TimeUnit.HOURS);
}
/**
* Returns the amount of minutes between two given dates.
* This will be negative when the end date is before the start date.
* @param start The start date.
* @param end The end date.
* @return The amount of minutes between two given dates.
* @throws NullPointerException When a date is <code>null</code>.
*/
public static long minutesBetween(Date start, Date end) {
return timeDiff(start, end, TimeUnit.MINUTES);
}
/**
* Returns the amount of seconds between two given dates.
* This will be negative when the end date is before the start date.
* @param start The start date.
* @param end The end date.
* @return The amount of seconds between two given dates.
* @throws NullPointerException When a date is <code>null</code>.
*/
public static long secondsBetween(Date start, Date end) {
return timeDiff(start, end, TimeUnit.SECONDS);
}
/**
* Helper method which calculates the time difference of the given two dates in given time unit.
*/
private static long timeDiff(Date startDate, Date endDate, TimeUnit timeUnit) {
return timeUnit.convert(endDate.getTime() - startDate.getTime(), TimeUnit.MILLISECONDS);
}
// Mappings -------------------------------------------------------------------------------------------------------
/**
* Returns a mapping of month names by month numbers for the current locale. For example: "January=1", "February=2",
* etc. This is useful if you want to for example populate a <code><f:selectItems></code> which shows all
* months. The locale is obtained by {@link Faces#getLocale()}. The mapping is per locale stored in a local cache
* to improve retrieving performance.
* @return Month names for the current locale.
* @see DateFormatSymbols#getMonths()
*/
public static Map<String, Integer> getMonths() {
Locale locale = getLocale();
Map<String, Integer> months = MONTHS_CACHE.get(locale);
if (months == null) {
months = mapMonths(DateFormatSymbols.getInstance(locale).getMonths());
MONTHS_CACHE.put(locale, months);
}
return months;
}
/**
* Returns a mapping of short month names by month numbers for the current locale. For example: "Jan=1", "Feb=2",
* etc. This is useful if you want to for example populate a <code><f:selectItems></code> which shows all
* short months. The locale is obtained by {@link Faces#getLocale()}. The mapping is per locale stored in a local
* cache to improve retrieving performance.
* @return Short month names for the current locale.
* @see DateFormatSymbols#getShortMonths()
*/
public static Map<String, Integer> getShortMonths() {
Locale locale = getLocale();
Map<String, Integer> shortMonths = SHORT_MONTHS_CACHE.get(locale);
if (shortMonths == null) {
shortMonths = mapMonths(DateFormatSymbols.getInstance(locale).getShortMonths());
SHORT_MONTHS_CACHE.put(locale, shortMonths);
}
return shortMonths;
}
/**
* Helper method to map months.
*/
private static Map<String, Integer> mapMonths(String[] months) {
Map<String, Integer> mapping = new LinkedHashMap<>();
for (String month : months) {
if (!month.isEmpty()) { // 13th month may or may not be empty, depending on default calendar.
mapping.put(month, mapping.size() + 1);
}
}
return Collections.unmodifiableMap(mapping);
}
/**
* Returns a mapping of day of week names in ISO 8601 order (Monday first) for the current locale. For example:
* "Monday=1", "Tuesday=2", etc. This is useful if you want to for example populate a <code><f:selectItems></code>
* which shows all days of week. The locale is obtained by {@link Faces#getLocale()}. The mapping is per locale
* stored in a local cache to improve retrieving performance.
* @return Day of week names for the current locale.
* @see DateFormatSymbols#getWeekdays()
*/
public static Map<String, Integer> getDaysOfWeek() {
Locale locale = getLocale();
Map<String, Integer> daysOfWeek = DAYS_OF_WEEK_CACHE.get(locale);
if (daysOfWeek == null) {
daysOfWeek = mapDaysOfWeek(DateFormatSymbols.getInstance(locale).getWeekdays());
DAYS_OF_WEEK_CACHE.put(locale, daysOfWeek);
}
return daysOfWeek;
}
/**
* Returns a mapping of short day of week names in ISO 8601 order (Monday first) for the current locale. For example:
* "Mon=1", "Tue=2", etc. This is useful if you want to for example populate a <code><f:selectItems></code>
* which shows all short days of week. The locale is obtained by {@link Faces#getLocale()}. The mapping is per locale
* stored in a local cache to improve retrieving performance.
* @return Short day of week names for the current locale.
* @see DateFormatSymbols#getShortWeekdays()
*/
public static Map<String, Integer> getShortDaysOfWeek() {
Locale locale = getLocale();
Map<String, Integer> shortDaysOfWeek = SHORT_DAYS_OF_WEEK_CACHE.get(locale);
if (shortDaysOfWeek == null) {
shortDaysOfWeek = mapDaysOfWeek(DateFormatSymbols.getInstance(locale).getShortWeekdays());
SHORT_DAYS_OF_WEEK_CACHE.put(locale, shortDaysOfWeek);
}
return shortDaysOfWeek;
}
/**
* Helper method to map days of week.
*/
private static Map<String, Integer> mapDaysOfWeek(String[] weekdays) {
Map<String, Integer> mapping = new LinkedHashMap<>();
mapping.put(weekdays[Calendar.MONDAY], mapping.size() + 1);
mapping.put(weekdays[Calendar.TUESDAY], mapping.size() + 1);
mapping.put(weekdays[Calendar.WEDNESDAY], mapping.size() + 1);
mapping.put(weekdays[Calendar.THURSDAY], mapping.size() + 1);
mapping.put(weekdays[Calendar.FRIDAY], mapping.size() + 1);
mapping.put(weekdays[Calendar.SATURDAY], mapping.size() + 1);
mapping.put(weekdays[Calendar.SUNDAY], mapping.size() + 1);
return Collections.unmodifiableMap(mapping);
}
/**
* Returns the month name from the mapping associated with the given month number for the current locale. For
* example: "1=January", "2=February", etc. The locale is obtained by {@link Faces#getLocale()}.
* @param monthNumber The month number to return the month name from the mapping for.
* @return The month name form the mapping associated with the given month number.
* @since 1.4
*/
public static String getMonth(Integer monthNumber) {
return getKey(getMonths(), monthNumber);
}
/**
* Returns the short month name from the mapping associated with the given month number for the current locale. For
* example: "1=Jan", "2=Feb", etc. The locale is obtained by {@link Faces#getLocale()}.
* @param monthNumber The month number to return the short month name from the mapping for.
* @return The short month name form the mapping associated with the given month number.
* @since 1.4
*/
public static String getShortMonth(Integer monthNumber) {
return getKey(getShortMonths(), monthNumber);
}
/**
* Returns the day of week name from the mapping associated with the given day of week number in ISO 8601 order
* (Monday first) for the current locale. For example: "1=Monday", "2=Tuesday", etc. The locale is obtained by
* {@link Faces#getLocale()}.
* @param dayOfWeekNumber The day of week number to return the day of week name from the mapping for.
* @return The day of week name from the mapping associated with the given day of week number.
* @since 1.4
*/
public static String getDayOfWeek(Integer dayOfWeekNumber) {
return getKey(getDaysOfWeek(), dayOfWeekNumber);
}
/**
* Returns the short day of week name from the mapping associated with the given day of week number in ISO 8601
* order (Monday first) for the current locale. For example: "1=Mon", "2=Tue", etc. The locale is obtained by
* {@link Faces#getLocale()}.
* @param dayOfWeekNumber The day of week number to return the short day of week name from the mapping for.
* @return The short day of week name from the mapping associated with the given day of week number.
* @since 1.4
*/
public static String getShortDayOfWeek(Integer dayOfWeekNumber) {
return getKey(getShortDaysOfWeek(), dayOfWeekNumber);
}
/**
* Helper method to return the map key from the given map associated with given map value.
*/
private static <K, V> K getKey(Map<K, V> map, V value) {
if (value == null) {
return null; // None of the maps have a null value anyway.
}
for (Entry<K, V> entry : map.entrySet()) {
if (value.equals(entry.getValue())) {
return entry.getKey();
}
}
return null;
}
}