org.fujion.common.DateUtil Maven / Gradle / Ivy
/*
* #%L
* fujion
* %%
* Copyright (C) 2018 Fujion Framework
* %%
* 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.
*
* #L%
*/
package org.fujion.common;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.lang.time.FastDateFormat;
/**
* Utility methods for managing dates.
*/
public class DateUtil {
private static ThreadLocal decimalFormat = new ThreadLocal() {
@Override
protected DecimalFormat initialValue() {
return new DecimalFormat("##0.##");
}
};
private static final String HL7_DATE_ONLY_PATTERN = "yyyyMMdd";
private static final String HL7_DATE_TIME_PATTERN = HL7_DATE_ONLY_PATTERN + "HHmmssz";
private static final String UNKNOWN = "Unknown";
/*
* Defines a regular expression pattern representing a permissible
* extended style date that can be converted into a traditional date.
*
* Such a value starts with either 't' or 'n', and then optionally plus or
* minus a numeric value of 'd' (days), 'm' (months), or 'y' (years).
*/
private static final Pattern PATTERN_EXT_DATE = Pattern
.compile("^\\s*[t|n]{1}\\s*([+|-]{1}\\s*[\\d]*\\s*[s|n|h|d|m|y]?)?\\s*$");
/*
* Defines a regular expression pattern representing a value ending in one
* of the acceptable extended style date units (d, m, or y).
*/
private static final Pattern PATTERN_SPECIFIES_UNITS = Pattern.compile("^.*[s|n|h|d|m|y]$");
/*
* Defines a regular expression pattern for extracting a numeric prefix from a string.
*/
private static final Pattern PATTERN_NUMERIC_PREFIX = Pattern.compile("^-?[\\d\\.]+");
private static final double[] MS_FP = new double[] { 31557600000.0, 2592000000.0, 604800000.0, 86400000.0, 3600000.0,
60000.0, 1000.0, 1.0 };
private static final long[] MS_LG = new long[] { 31557600000L, 2592000000L, 604800000L, 86400000L, 3600000L, 60000L,
1000L, 1L };
/**
* Labels for time units. TODO: externalize these for localization support.
*/
public static String[][] TIME_UNIT = new String[][] { { "year", "years", "yr", "yrs" },
{ "month", "months", "mo", "mos" }, { "week", "weeks", "wk", "wks" }, { "day", "days", "day", "days" },
{ "hour", "hours", "hr", "hrs" }, { "minute", "minutes", "min", "mins" }, { "second", "seconds", "sec", "secs" },
{ "millisecond", "milliseconds", "ms", "ms" } };
/**
* Represents time units in order of increasing precision.
*/
public enum TimeUnit {
YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS
}
/**
* Enum representing common date formats.
*/
public enum Format {
//@formatter:off
WITH_TZ("dd-MMM-yyyy HH:mm zzz"),
WITHOUT_TZ("dd-MMM-yyyy HH:mm"),
WITHOUT_TIME("dd-MMM-yyyy"),
HL7(HL7_DATE_TIME_PATTERN),
HL7_WITHOUT_TIME(HL7_DATE_ONLY_PATTERN),
JS_WITH_TZ("yyyy-MM-dd HH:mm zzz"),
JS_WITHOUT_TZ("yyyy-MM-dd HH:mm"),
JS_WITHOUT_TIME("yyyy-MM-dd"),
TO_STRING("EEE MMM dd HH:mm:ss zzz yyyy");
//@formatter:on
private String pattern;
private Format(String pattern) {
this.pattern = pattern;
}
/**
* Returns the format pattern.
*
* @return The format pattern.
*/
public String getPattern() {
return pattern;
}
/**
* Returns a formatter for this date format.
*
* @return A formatter.
*/
public FastDateFormat getFormatter() {
boolean ignoreTime = this == WITHOUT_TIME || this == HL7_WITHOUT_TIME;
return FastDateFormat.getInstance(pattern, ignoreTime ? TimeZone.getDefault() : getLocalTimeZone());
}
/**
* Formats an input date.
*
* @param date The date to format.
* @return The formatted date.
*/
public String format(Date date) {
return date == null ? "" : getFormatter().format(date);
}
/**
* Parses an input value.
*
* @param value The value to parse.
* @return The resulting date value if successful.
* @throws ParseException Date parsing exception.
*/
public Date parse(String value) throws ParseException {
return parseDate(value, pattern);
}
}
/**
*
* Convert a string value to a date/time. Attempts to convert using the four locale-specific
* date formats (FULL, LONG, MEDIUM, SHORT). If these fail, looks to see if T+/-offset or
* N+/-offset is used.
*
*
* TODO: probably we can make the "Java parse" portion a bit smarter by using a better variety
* of formats, maybe to catch Euro-style input as well.
*
*
* TODO: probably we can add something like "t+d" or "t-y" as valid cases; in these scenarios,
* the coefficient was omitted and could be defaulted to 1.
*
*
* @param s String
containing value to be converted.
* @return Date
object corresponding to the input value, or null
if
* the parsing failed to resolve a valid Date.
*/
public static Date parseDate(String s) {
Date result = null;
if (s != null && !s.isEmpty()) {
s = s.toLowerCase(); // make lc
if ((PATTERN_EXT_DATE.matcher(s)).matches()) { // is an extended date?
try {
s = s.replaceAll("\\s+", ""); // strip space since they not
// delim
String _k = s.substring(1); // _k will ultimately be the multiplier value
char k = 'd'; // k = s, n, h, d (default), m, or y
if (1 == s.length()) {
_k = "0";
} else {
if ((PATTERN_SPECIFIES_UNITS.matcher(s)).matches()) {
_k = s.substring(1, s.length() - 1);
k = s.charAt(s.length() - 1);
}
}
if ('+' == _k.charAt(0)) { // clip positive coefficient...
_k = _k.substring(1);
}
int field = Calendar.DAY_OF_YEAR;
int offset = Integer.parseInt(_k);
Calendar c = Calendar.getInstance();
c.setLenient(false);
if (s.charAt(0) == 't') {
c.setTime(DateUtil.today());
}
switch (k) {
case 'y': // years
field = Calendar.YEAR;
break;
case 'm': // months
field = Calendar.MONTH;
break;
case 'h': // hours
field = Calendar.HOUR_OF_DAY;
break;
case 'n': // minutes
field = Calendar.MINUTE;
break;
case 's': // seconds
field = Calendar.SECOND;
break;
}
c.add(field, offset);
result = c.getTime();
// format
} catch (Exception e) {
return null; // found unparseable date (e.g. t-y)
}
} else {
result = tryParse(s);
if (result != null) {
return result;
}
s = s.replaceAll("[\\.|-]", "/"); // dots, dashes to slashes
result = tryParse(s);
if (result != null) {
return result;
}
s = s.replaceAll("\\s", "/"); // last chance to parse: spaces to
result = tryParse(s); // slashes!
}
}
return result;
}
/**
* Attempts to parse an input value using one of several patterns.
*
* @param value String to parse.
* @param patterns Patterns to be tried in succession until parsing succeeds.
* @return The resulting date value.
* @throws ParseException Date parsing exception.
*/
public static Date parseDate(String value, String... patterns) throws ParseException {
return DateUtils.parseDate(value, patterns);
}
/**
* Attempts to parse a string containing a date representation using several different date
* patterns.
*
* @param value String to parse
* @return If the parsing was successful, returns the date value represented by the input value.
* Otherwise, returns null.
*/
private static Date tryParse(String value) {
for (Format format : Format.values()) {
try {
return format.parse(value);
} catch (Exception e) {}
}
for (int i = 3; i >= 0; i--) {
try {
return DateFormat.getDateInstance(i).parse(value);
} catch (Exception e) {}
}
return null;
}
/**
* Clones a date.
*
* @param date Date to clone.
* @return A clone of the original date, or null if the original date was null.
*/
public static Date cloneDate(Date date) {
return date == null ? null : new Date(date.getTime());
}
/**
* Adds specified number of days to date and, optionally strips the time component.
*
* @param date Date value to process.
* @param daysOffset # of days to add.
* @param stripTime If true, strip the time component.
* @return Input value the specified operations applied.
*/
public static Date addDays(Date date, int daysOffset, boolean stripTime) {
if (date == null) {
return null;
}
Calendar calendar = Calendar.getInstance();
calendar.setLenient(false); // Make sure the calendar will not perform
// automatic correction.
calendar.setTime(date); // Set the time of the calendar to the given
// date.
if (stripTime) { // Remove the hours, minutes, seconds and milliseconds.
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
}
calendar.add(Calendar.DAY_OF_MONTH, daysOffset);
return calendar.getTime();
}
/**
* Strips the time component from a date.
*
* @param date Original date.
* @return Date without the time component.
*/
public static Date stripTime(Date date) {
return addDays(date, 0, true);
}
/**
* Returns the input date with the time set to the end of the day.
*
* @param date Original date.
* @return Date with time set to end of day.
*/
public static Date endOfDay(Date date) {
if (date == null) {
return null;
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 23);
calendar.set(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 59);
calendar.set(Calendar.MILLISECOND, 999);
return calendar.getTime();
}
/**
* Returns a date with the current time.
*
* @return Current date and time.
*/
public static Date now() {
return new Date();
}
/**
* Returns a date with the current day (no time).
*
* @return Current date.
*/
public static Date today() {
return stripTime(now());
}
/**
* Compares two dates. Allows nulls.
*
* @param date1 First date to compare.
* @param date2 Second date to compare.
* @return Result of comparison.
*/
public static int compare(Date date1, Date date2) {
long diff = date1 == date2 ? 0 : date1 == null ? -1 : date2 == null ? 1 : date1.getTime() - date2.getTime();
return diff < 0 ? -1 : diff > 0 ? 1 : 0;
}
/**
* Converts a date/time value to a string, using the format dd-mmm-yyyy hh:mm. Because we cannot
* determine the absence of a time from a time of 24:00, we must assume a time of 24:00 means
* that no time is present and strip that from the return value.
*
* @param date Date value to convert.
* @return Formatted string representation of the specified date, or an empty string if date is
* null.
*/
public static String formatDate(Date date) {
return formatDate(date, false, false);
}
/**
* Converts a date/time value to a string, using the format dd-mmm-yyyy hh:mm. Because we cannot
* determine the absence of a time from a time of 24:00, we must assume a time of 24:00 means
* that no time is present and strip that from the return value.
*
* @param date Date value to convert.
* @param showTimezone If true, time zone information is also appended.
* @return Formatted string representation of the specified date, or an empty string if date is
* null.
*/
public static String formatDate(Date date, boolean showTimezone) {
return formatDate(date, showTimezone, false);
}
/**
* Converts a date/time value to a string, using the format dd-mmm-yyyy hh:mm. Because we cannot
* determine the absence of a time from a time of 24:00, we must assume a time of 24:00 means
* that no time is present and strip that from the return value.
*
* @param date Date value to convert
* @param showTimezone If true, time zone information is also appended.
* @param ignoreTime If true, the time component is ignored.
* @return Formatted string representation of the specified date, or an empty string if date is
* null.
*/
public static String formatDate(Date date, boolean showTimezone, boolean ignoreTime) {
ignoreTime = ignoreTime || !hasTime(date);
Format format = ignoreTime ? Format.WITHOUT_TIME : showTimezone ? Format.WITH_TZ : Format.WITHOUT_TZ;
return format.format(date);
}
/**
* Same as formatDate(Date, boolean) except replaces the time separator with the specified
* string.
*
* @param date Date value to convert
* @param timeSeparator String to use in place of default time separator
* @return Formatted string representation of the specified date using the specified time
* separator.
*/
public static String formatDate(Date date, String timeSeparator) {
return formatDate(date).replaceFirst(" ", timeSeparator);
}
/**
* Convert a date to HL7 format.
*
* @param date Date to convert.
* @return The HL7-formatted date.
*/
public static String toHL7(Date date) {
Format format = hasTime(date) ? Format.HL7_WITHOUT_TIME : Format.HL7;
return format.format(date);
}
/**
* Returns true if the date has an associated time.
*
* @param date Date value to check.
* @return True if the date has a time component.
*/
public static boolean hasTime(Date date) {
if (date == null) {
return false;
}
long time1 = date.getTime();
long time2 = stripTime(date).getTime();
return time1 != time2; // Do not use "Date.equals" since date may be of type Timestamp.
}
/**
* Return elapsed time in ms to displayable format with units.
*
* @param elapsed Elapsed time in ms.
* @return Elapsed time in displayable format.
*/
public static String formatElapsed(double elapsed) {
return formatElapsed(elapsed, true, false, false);
}
/**
* Return elapsed time in ms to displayable format with units.
*
* @param elapsed Elapsed time in ms.
* @return Elapsed time in displayable format.
* @param minUnits Minimum units for return value (null = ms).
*/
public static String formatElapsed(double elapsed, TimeUnit minUnits) {
return formatElapsed(elapsed, true, false, false, minUnits);
}
/**
* Return elapsed time in ms to displayable format with units.
*
* @param elapsed Elapsed time in ms.
* @param pluralize If true, pluralize units when appropriate.
* @param abbreviated If true, use abbreviated form of units.
* @param round If true, round result to an integer.
* @return Elapsed time in displayable format.
*/
public static String formatElapsed(double elapsed, boolean pluralize, boolean abbreviated, boolean round) {
return formatElapsed(elapsed, pluralize, abbreviated, round, null);
}
/**
* Return elapsed time in ms to displayable format with units.
*
* @param elapsed Elapsed time in ms.
* @param pluralize If true, pluralize units when appropriate.
* @param abbreviated If true, use abbreviated form of units.
* @param round If true, round result to an integer.
* @param minUnits Minimum units for return value (null = ms).
* @return Elapsed time in displayable format.
*/
public static String formatElapsed(double elapsed, boolean pluralize, boolean abbreviated, boolean round,
TimeUnit minUnits) {
int index = (minUnits == null ? TimeUnit.MILLISECONDS : minUnits).ordinal();
String prefix = "";
if (elapsed < 0) {
elapsed = -elapsed;
prefix = "-";
}
for (int i = 0; i <= index; i++) {
if (elapsed >= MS_FP[i] || i == index) {
elapsed /= MS_FP[i];
index = i;
break;
}
}
if (round) {
elapsed = Math.floor(elapsed);
}
return prefix + decimalFormat.get().format(elapsed) + " "
+ getDurationUnits(index, pluralize && elapsed != 1.0, abbreviated);
}
/**
* Parses an elapsed time string, returning time in milliseconds.
*
* @param value The string value to parse.
* @return The elapsed time value in milliseconds.
*/
public static double parseElapsed(String value) {
return parseElapsed(value, TimeUnit.MILLISECONDS);
}
/**
* Parses an elapsed time string, returning time in specified units.
*
* @param value The string value to parse.
* @param units The units of the returned value (defaults to ms).
* @return The elapsed time value in the requested units.
*/
public static double parseElapsed(String value, TimeUnit units) {
Matcher matcher = PATTERN_NUMERIC_PREFIX.matcher(value);
if (!matcher.find()) {
return 0;
}
int i = matcher.end();
double result;
try {
result = Double.parseDouble(value.substring(0, i));
} catch (NumberFormatException e) {
return 0;
}
value = value.substring(i).trim().toLowerCase();
for (TimeUnit tu : TimeUnit.values()) {
for (String unit : TIME_UNIT[tu.ordinal()]) {
if (unit.equals(value)) {
result *= MS_FP[tu.ordinal()];
if (units != null && units != TimeUnit.MILLISECONDS) {
result /= MS_FP[units.ordinal()];
}
return result;
}
}
}
return 0;
}
/**
* Formats a duration in ms.
*
* @param duration Duration in ms.
* @return Formatted duration.
*/
public static String formatDuration(long duration) {
return formatDuration(duration, null);
}
/**
* Formats a duration in ms to the specified accuracy.
*
* @param duration Duration in ms.
* @param accuracy Accuracy of output.
* @return Formatted duration.
*/
public static String formatDuration(long duration, TimeUnit accuracy) {
return formatDuration(duration, accuracy, true, false);
}
/**
* Formats a duration in ms to the specified accuracy.
*
* @param duration Duration in ms.
* @param accuracy Accuracy of output.
* @param pluralize If true, pluralize units when appropriate.
* @param abbreviated If true, use abbreviated form of units.
* @return Formatted duration.
*/
public static String formatDuration(long duration, TimeUnit accuracy, boolean pluralize, boolean abbreviated) {
StringBuilder sb = new StringBuilder();
if (duration < 0) {
duration = -duration;
sb.append('-');
}
accuracy = accuracy == null ? TimeUnit.MILLISECONDS : accuracy;
int last = accuracy.ordinal();
boolean empty = true;
for (int i = 0; i <= last; i++) {
long val = duration / MS_LG[i];
duration -= val * MS_LG[i];
if (val != 0 || (empty && i == last)) {
if (!empty) {
sb.append(' ');
} else {
empty = false;
}
sb.append(val).append(' ').append(getDurationUnits(i, pluralize && val != 1, abbreviated));
}
}
return sb.toString();
}
private static String getDurationUnits(TimeUnit accuracy, boolean plural, boolean abbreviated) {
return getDurationUnits(accuracy.ordinal(), plural, abbreviated);
}
private static String getDurationUnits(int index, boolean plural, boolean abbreviated) {
int which = (plural ? 1 : 0) + (abbreviated ? 2 : 0);
return TIME_UNIT[index][which];
}
/**
* Returns the user's time zone.
*
* @return The user's time zone.
*/
public static TimeZone getLocalTimeZone() {
return Localizer.getTimeZone();
}
/**
*
* Returns age as a formatted string expressed in days, months, or years, depending on whether
* person is an infant (< 2 mos), toddler (> 2 mos, < 2 yrs), or more than 2 years old.
*
*
* @param dob Date of person's birth
* @return the age display string
*/
public static String formatAge(Date dob) {
return formatAge(dob, true, null);
}
/**
*
* Returns age as a formatted string expressed in days, months, or years, depending on whether
* person is an infant (< 2 mos), toddler (> 2 mos, < 2 yrs), or more than 2 years old.
*
*
* Allows the caller to specify an "as-of" date. The calculated age will be as-of the
* provided date, rather than as-of the current date.
*
*
* Allows the caller to specify whether or not to pluralize the age units in the age display
* string.
*
*
* @param dob Date of person's birth
* @param pluralize If true, pluralize the age units in the age display string.
* @param refDate The date as of which to calculate the Person's age (null means today).
* @return the age display string
*/
public static String formatAge(Date dob, boolean pluralize, Date refDate) {
if (dob == null) {
return UNKNOWN;
}
Calendar asOf = Calendar.getInstance();
asOf.setTimeInMillis(refDate == null ? System.currentTimeMillis() : refDate.getTime());
Calendar bd = Calendar.getInstance();
bd.setTime(dob);
long birthDateInDays = (asOf.getTimeInMillis() - bd.getTimeInMillis()) / 1000 / 60 / 60 / 24;
if (birthDateInDays < 0) {
return UNKNOWN;
}
if (birthDateInDays <= 1) {
return "newborn";
}
// If person is less than 2 months old, then display age in days
if (birthDateInDays <= 60) {
return formatUnits(birthDateInDays, TimeUnit.DAYS, pluralize);
}
int birthYear = bd.get(Calendar.YEAR);
int birthMonth = bd.get(Calendar.MONTH);
int birthDay = bd.get(Calendar.DATE);
int refYear = asOf.get(Calendar.YEAR);
int refMonth = asOf.get(Calendar.MONTH);
int refDay = asOf.get(Calendar.DATE);
if (birthDateInDays <= 730) {
// If person is more than 2 months but less than 2 years then display age in months
if (refMonth >= birthMonth && refDay >= birthDay) {
// If person has had a birthday already this year
return formatUnits((refYear - birthYear) * 12 + refMonth - birthMonth, TimeUnit.MONTHS, pluralize);
}
// then age in months = # years old * 12 + months so far this year
// If person has not yet had a birthday this year, subtract 1 month
return formatUnits((refYear - birthYear) * 12 + refMonth - birthMonth - 1, TimeUnit.MONTHS, pluralize);
}
// If person is more than 2 years old then display age in years
return formatUnits(getAgeInYears(birthYear, birthMonth, birthDay, refYear, refMonth, refDay), TimeUnit.YEARS,
pluralize);
}
private static int getAgeInYears(int birthYear, int birthMonth, int birthDay, int refYear, int refMonth, int refDay) {
// If person has had a birthday already this year
if (refMonth > birthMonth || (refMonth == birthMonth && refDay >= birthDay)) {
return refYear - birthYear;
}
// If person has not yet had a birthday this year, subtract 1
return refYear - birthYear - 1;
}
private static String formatUnits(long value, TimeUnit accuracy, boolean pluralize) {
return value + " " + getDurationUnits(accuracy, pluralize && value != 1, true);
}
/**
* Converts day, month, and year to a date.
*
* @param day Day of month.
* @param month Month (1=January, etc.)
* @param year Year (4 digit).
* @return Date instance.
*/
public static Date toDate(int day, int month, int year) {
return toDate(day, month, year, 0, 0, 0);
}
/**
* Converts day, month, year and time parameters to a date.
*
* @param day Day of month.
* @param month Month (1=January, etc.)
* @param year Year (4 digit).
* @param hr Hour of day.
* @param min Minutes past the hour.
* @param sec Seconds past the minute.
* @return Date instance.
*/
public static Date toDate(int day, int month, int year, int hr, int min, int sec) {
Calendar cal = Calendar.getInstance(Localizer.getTimeZone());
cal.set(year, month - 1, day, hr, min, sec);
cal.set(Calendar.MILLISECOND, 0);
return cal.getTime();
}
/**
* Enforce static class.
*/
private DateUtil() {
}
}