/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de)
//
// ProjectForge is dual-licensed.
//
// This community edition is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 3 of the License.
//
// This community edition is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////
package org.projectforge.common;
import java.io.Serializable;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants;
import org.projectforge.calendar.TimePeriod;
import org.projectforge.core.ConfigXml;
import org.projectforge.core.Configuration;
import org.projectforge.user.PFUserContext;
import org.projectforge.web.calendar.DateTimeFormatter;
/**
* Parse and formats dates.
*
* @author Kai Reinhard (k.reinhard@micromata.de)
*
*/
public class DateHelper implements Serializable
{
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(DateHelper.class);
private static final long serialVersionUID = -94010735614402146L;
/**
* Number of milliseconds of one minute. DO NOT USE FOR exact date calculations (summer and winter time etc.)!
*/
public static final long MILLIS_MINUTE = 60 * 1000;
/**
* Number of milliseconds of one hour. DO NOT USE FOR exact date calculations (summer and winter time etc.)!
*/
public static final long MILLIS_HOUR = 60 * MILLIS_MINUTE;
/**
* Number of milliseconds of one day. DO NOT USE FOR exact date calculations (summer and winter time etc.)!
*/
public static final long MILLIS_DAY = 24 * MILLIS_HOUR;
/**
* Europe/Berlin
*/
public final static TimeZone EUROPE_BERLIN = TimeZone.getTimeZone("Europe/Berlin");
public static final BigDecimal MILLIS_PER_HOUR = new BigDecimal(MILLIS_HOUR);
public static final BigDecimal HOURS_PER_WORKING_DAY = new BigDecimal(DateTimeFormatter.DEFAULT_HOURS_OF_DAY);
public static final BigDecimal MILLIS_PER_WORKING_DAY = new BigDecimal(MILLIS_HOUR * DateTimeFormatter.DEFAULT_HOURS_OF_DAY);
public static final BigDecimal SECONDS_PER_WORKING_DAY = new BigDecimal(60 * 60 * DateTimeFormatter.DEFAULT_HOURS_OF_DAY);
/**
* UTC
*/
public final static TimeZone UTC = TimeZone.getTimeZone("UTC");
private static final DateFormat FORMAT_ISO_DATE = new SimpleDateFormat(DateFormats.ISO_DATE);
private static final DateFormat FORMAT_ISO_TIMESTAMP = new SimpleDateFormat(DateFormats.ISO_TIMESTAMP_MILLIS);
private static final DateFormat FILENAME_FORMAT_TIMESTAMP = new SimpleDateFormat(DateFormats.ISO_DATE + "_HH-mm");
private static final DateFormat FILENAME_FORMAT_DATE = new SimpleDateFormat(DateFormats.ISO_DATE);
/**
* Compares millis. If both dates are null then they're equal.
* @param d1
* @param d2
* @see Date#getTime()
*/
public static boolean equals(final Date d1, final Date d2)
{
if (d1 == null) {
return d2 == null;
}
if (d2 == null) {
return false;
}
return d1.getTime() == d2.getTime();
}
/**
* thread safe
* @param timezone
*/
public static DateFormat getIsoDateFormat(final TimeZone timezone)
{
final DateFormat df = (DateFormat) FORMAT_ISO_DATE.clone();
if (timezone != null) {
df.setTimeZone(timezone);
}
return df;
}
/**
* thread safe
* @param timezone If null then time zone is ignored.
*/
public static DateFormat getIsoTimestampFormat(final TimeZone timezone)
{
final DateFormat df = (DateFormat) FORMAT_ISO_TIMESTAMP.clone();
if (timezone != null) {
df.setTimeZone(timezone);
}
return df;
}
/**
* thread safe
* @param timezone
*/
public static DateFormat getFilenameFormatTimestamp(final TimeZone timezone)
{
final DateFormat df = (DateFormat) FILENAME_FORMAT_TIMESTAMP.clone();
if (timezone != null) {
df.setTimeZone(timezone);
}
return df;
}
/**
* thread safe
* @param timezone
*/
public static DateFormat getFilenameFormatDate(final TimeZone timezone)
{
final DateFormat df = (DateFormat) FILENAME_FORMAT_DATE.clone();
if (timezone != null) {
df.setTimeZone(timezone);
}
return df;
}
/**
* yyyy-MM-dd HH:mm:ss.S in UTC. Thread safe usage: FOR_TESTCASE_OUTPUT_FORMATTER.get().format(date)
*/
public static final ThreadLocal<DateFormat> FOR_TESTCASE_OUTPUT_FORMATTER = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue()
{
final DateFormat df = new SimpleDateFormat(DateFormats.ISO_TIMESTAMP_MILLIS);
df.setTimeZone(UTC);
return df;
}
};
/**
* Thread safe usage: FOR_TESTCASE_OUTPUT_FORMATTER.get().format(date)
*/
public static final ThreadLocal<DateFormat> TECHNICAL_ISO_UTC = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue()
{
final DateFormat dateFormat = new SimpleDateFormat(DateFormats.ISO_TIMESTAMP_MILLIS + " z");
dateFormat.setTimeZone(UTC);
return dateFormat;
}
};
/**
* @return Short name of day represented by the giving day. The context user's locale and time zone is considered.
*/
public static final String formatShortNameOfDay(final Date date)
{
final DateFormat df = new SimpleDateFormat("EE", PFUserContext.getLocale());
df.setTimeZone(PFUserContext.getTimeZone());
return df.format(date);
}
/**
* Formats the given date as UTC date in ISO format attached TimeZone (UTC).
* @param date
* @return
*/
public static final String formatAsUTC(final Date date)
{
if (date == null) {
return "";
}
return UTC_ISO_DATE.get().format(date);
}
/**
* Thread safe usage: UTC_ISO_DATE.get().format(date)
*/
public static final ThreadLocal<DateFormat> UTC_ISO_DATE = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue()
{
final DateFormat df = new SimpleDateFormat(DateFormats.ISO_TIMESTAMP_MILLIS + " Z");
df.setTimeZone(UTC);
return df;
}
};
/**
* Takes time zone of context user if exist.
* @param date
*/
public static String formatIsoDate(final Date date)
{
return getIsoDateFormat(PFUserContext.getTimeZone()).format(date);
}
/**
* Takes time zone of context user if exist.
* @param date
*/
public static String formatIsoDate(final Date date, final TimeZone timeZone)
{
return getIsoDateFormat(timeZone).format(date);
}
/**
* logError = true
* @param str
* @return
* @see #parseMillis(String, boolean)
*/
public static Date parseMillis(final String str)
{
return parseMillis(str, true);
}
/**
* @param str
* @param logError If true, any ParseException error will be logged if occured.
* @return The parsed date or null, if not parseable.
*/
public static Date parseMillis(final String str, final boolean logError)
{
Date date = null;
try {
final long millis = Long.parseLong(str);
date = new Date(millis);
} catch (final NumberFormatException ex) {
if (logError == true) {
log.error("Could not parse date string (millis expected): " + str, ex);
}
}
return date;
}
public static String formatIsoTimestamp(final Date date)
{
return formatIsoTimestamp(date, PFUserContext.getTimeZone());
}
public static String formatIsoTimestamp(final Date date, final TimeZone timeZone)
{
return getIsoTimestampFormat(timeZone).format(date);
}
/**
* Format yyyy-mm-dd
* @param isoDateString
* @return Parsed date or null if a parse error occurs.
*/
public static Date parseIsoDate(final String isoDateString, final TimeZone timeZone)
{
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
df.setTimeZone(timeZone);
Date date;
try {
date = df.parse(isoDateString);
} catch (final ParseException ex) {
return null;
}
return date;
}
/**
* Format: {@link DateFormats#ISO_TIMESTAMP_MILLIS}
* @param isoDateString
* @return Parsed date or null if a parse error occurs.
*/
public static Date parseIsoTimestamp(final String isoDateString, final TimeZone timeZone)
{
final DateFormat df = new SimpleDateFormat(DateFormats.ISO_TIMESTAMP_MILLIS);
df.setTimeZone(timeZone);
Date date;
try {
date = df.parse(isoDateString);
} catch (final ParseException ex) {
return null;
}
return date;
}
public static String formatIsoTimePeriod(final Date fromDate, final Date toDate)
{
return formatIsoDate(fromDate) + ":" + formatIsoDate(toDate);
}
/**
* Format yyyy-mm-dd:yyyy-mm-dd
* @param isoTimePeriodString
* @return Parsed time period or null if a parse error occurs.
*/
public static TimePeriod parseIsoTimePeriod(final String isoTimePeriodString, final TimeZone timeZone)
{
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
df.setTimeZone(timeZone);
final String[] sa = isoTimePeriodString.split(":");
if (sa.length != 2) {
return null;
}
final Date fromDate = DateHelper.parseIsoDate(sa[0], DateHelper.UTC);
final Date toDate = DateHelper.parseIsoDate(sa[1], DateHelper.UTC);
if (fromDate == null || toDate == null) {
return null;
}
return new TimePeriod(fromDate, toDate);
}
/**
* Output via FOR_TESTCASE_OUTPUT_FORMATTER for test cases.<br/>
* @param dateHolder
* @return
*/
public static final String getForTestCase(final DateHolder dateHolder)
{
return FOR_TESTCASE_OUTPUT_FORMATTER.get().format(dateHolder.getDate());
}
/**
* Output via FOR_TESTCASE_OUTPUT_FORMATTER for test cases.
* @param dateHolder
* @return
*/
public static final String getForTestCase(final Date date)
{
return FOR_TESTCASE_OUTPUT_FORMATTER.get().format(date);
}
public static final String getTimestampAsFilenameSuffix(final Date date)
{
if (date == null) {
return "--";
}
return getFilenameFormatTimestamp(PFUserContext.getTimeZone()).format(date);
}
public static final String getDateAsFilenameSuffix(final Date date)
{
if (date == null) {
return "--";
}
return getFilenameFormatDate(PFUserContext.getTimeZone()).format(date);
}
/**
* Returns a calendar instance. If a context user is given then the user's time zone and locale will be used if given.
*/
public static Calendar getCalendar()
{
return getCalendar(null, null);
}
/**
* Returns a calendar instance. If a context user is given then the user's time zone and locale will be used if given.
* @param locale if given this locale will overwrite any the context user's locale.
*/
public static Calendar getCalendar(final Locale locale)
{
return getCalendar(null, locale);
}
public static Calendar getCalendar(TimeZone timeZone, Locale locale)
{
if (locale == null) {
locale = PFUserContext.getLocale();
}
if (timeZone == null) {
timeZone = PFUserContext.getTimeZone();
}
return Calendar.getInstance(timeZone, locale);
}
public static Calendar getUTCCalendar()
{
return getCalendar(UTC, null);
}
/**
* If stopTime is before startTime a negative value will be returned.
* @param startTime
* @param stopTime
* @return Duration in minutes or 0, if not computable (if start or stop time is null or stopTime is before startTime).
*/
public static long getDuration(final Date startTime, final Date stopTime)
{
if (startTime == null || stopTime == null || stopTime.before(startTime) == true) {
return 0;
}
final long millis = stopTime.getTime() - startTime.getTime();
return millis / 60000;
}
/**
* @return Formatted string without seconds, such as 5:45.
* @param time in millis
*/
public static String formatDuration(final long milliSeconds)
{
final long duration = milliSeconds / 60000;
final long durationHours = duration / 60;
final long durationMinutes = (duration % 60);
final StringBuffer buf = new StringBuffer(10);
buf.append(durationHours);
if (durationMinutes < 10)
buf.append(":0");
else buf.append(':');
buf.append(durationMinutes);
return buf.toString();
}
/**
* Initializes a new ArrayList with -1 ("--") and all 12 month with labels "01", ..., "12".
*/
public static List<LabelValueBean<String, Integer>> getMonthList()
{
final List<LabelValueBean<String, Integer>> list = new ArrayList<LabelValueBean<String, Integer>>();
list.add(new LabelValueBean<String, Integer>("--", -1));
for (int month = 0; month < 12; month++) {
list.add(new LabelValueBean<String, Integer>(StringHelper.format2DigitNumber(month + 1), month));
}
return list;
}
/**
* @param year
* @param month 0-11
* @return "yyyy-mm"
*/
public static String formatMonth(final int year, final int month)
{
final StringBuffer buf = new StringBuffer();
buf.append(year);
if (month >= 0) {
buf.append('-');
final int m = month + 1;
if (m <= 9) {
buf.append('0');
}
buf.append(m);
}
return buf.toString();
}
/**
* Should be used application wide for getting and/or displaying the week of year!
* @param date
* @return Return the week of year. The week of year depends on the Locale set in the Configuration (config.xml). If given date is null
* then -1 is returned. For "de" the first week of year is the first week with a minimum of 4 days in the new year. For "en" the
* first week of the year is the first week with a minimum of 1 days in the new year.
* @see java.util.Calendar#getMinimalDaysInFirstWeek()
* @see Configuration#getDefaultLocale()
*/
public static int getWeekOfYear(final Date date)
{
if (date == null) {
return -1;
}
final Calendar cal = Calendar.getInstance(PFUserContext.getTimeZone(), ConfigXml.getInstance().getDefaultLocale());
cal.setTime(date);
return cal.get(Calendar.WEEK_OF_YEAR);
}
/**
* Should be used application wide for getting and/or displaying the week of year!
* @param calendar (this methods uses the year, month and day of the given Calendar)
* @return Return the week of year. The week of year depends on the Locale set in the Configuration (config.xml). If given date is null
* then -1 is returned. For "de" the first week of year is the first week with a minimum of 4 days in the new year. For "en" the
* first week of the year is the first week with a minimum of 1 days in the new year.
* @see java.util.Calendar#getMinimalDaysInFirstWeek()
* @see Configuration#getDefaultLocale()
*/
public static int getWeekOfYear(final Calendar calendar)
{
if (calendar == null) {
return -1;
}
final Calendar cal = Calendar.getInstance(ConfigXml.getInstance().getDefaultLocale());
cal.set(Calendar.YEAR, calendar.get(Calendar.YEAR));
cal.set(Calendar.MONTH, calendar.get(Calendar.MONDAY));
cal.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH));
return cal.get(Calendar.WEEK_OF_YEAR);
}
/**
* Should be used application wide for getting and/or displaying the week of year!
* @param date
* @return Return the week of year. The week of year depends on the Locale set in the Configuration (config.xml). If given date is null
* then -1 is returned. For "de" the first week of year is the first week with a minimum of 4 days in the new year. For "en" the
* first week of the year is the first week with a minimum of 1 days in the new year.
* @see java.util.Calendar#getMinimalDaysInFirstWeek()
* @see Configuration#getDefaultLocale()
*/
public static int getWeekOfYear(final DateTime date)
{
if (date == null) {
return -1;
}
return getWeekOfYear(date.toDate());
}
/**
* @param d1
* @param d2
* @return True if the dates are both null or both represents the same day (year, month, day) independant of the hours, minutes etc.
* @see DateHolder#isSameDay(Date)
*/
public static boolean isSameDay(final Date d1, final Date d2)
{
if (d1 == null) {
if (d2 == null) {
return true;
} else {
return false;
}
} else if (d2 == null) {
return false;
}
final DateHolder dh = new DateHolder(d1);
return dh.isSameDay(d2);
}
/**
* @param d1
* @param d2
* @return True if the dates are both null or both represents the same day (year, month, day) independant of the hours, minutes etc.
* @see DateHolder#isSameDay(Date)
*/
public static boolean isSameDay(final DateTime d1, final DateTime d2)
{
if (d1 == null) {
if (d2 == null) {
return true;
} else {
return false;
}
} else if (d2 == null) {
return false;
}
return d1.getYear() == d2.getYear() && d1.getDayOfYear() == d2.getDayOfYear();
}
public static boolean dateOfYearBetween(final int month, final int dayOfMonth, final int fromMonth, final int fromDayOfMonth,
final int toMonth, final int toDayOfMonth)
{
if (fromMonth == toMonth) {
if (month != fromMonth) {
return false;
}
if (dayOfMonth < fromDayOfMonth || dayOfMonth > toDayOfMonth) {
return false;
}
} else if (fromMonth < toMonth) {
// e. g. APR - JUN
if (month < fromMonth || month > toMonth) {
// e. g. FEB or JUL
return false;
} else if (month == fromMonth && dayOfMonth < fromDayOfMonth) {
return false;
} else if (month == toMonth && dayOfMonth > toDayOfMonth) {
return false;
}
} else if (fromMonth > toMonth) {
// e. g. NOV - FEB
if (month > toMonth && month < fromMonth) {
// e. g. MAR
return false;
} else if (month == fromMonth && dayOfMonth < fromDayOfMonth) {
return false;
} else if (month == toMonth && dayOfMonth > toDayOfMonth) {
return false;
}
}
return true;
}
/**
* Sets given DateTime (UTC) as local time, meaning e. g. 08:00 UTC will be 08:00 local time.
* @param dateTime
* @return
* @see DateTime#toString(String)
* @see DateHelper#parseIsoDate(String, TimeZone)
*/
public static long getDateTimeAsMillis(final DateTime dateTime)
{
final String isDateString = dateTime.toString(DateFormats.ISO_TIMESTAMP_MILLIS);
final Date date = DateHelper.parseIsoTimestamp(isDateString, PFUserContext.getTimeZone());
return date.getTime();
}
public final static int convertCalendarDayOfWeekToJoda(final int calendarDayOfWeek)
{
if (calendarDayOfWeek == Calendar.SUNDAY) {
return DateTimeConstants.SUNDAY;
}
return calendarDayOfWeek - 1;
}
}