/* * #%L * carewebframework * %% * Copyright (C) 2008 - 2016 Regenstrief Institute, Inc. * %% * 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. * * This Source Code Form is also subject to the terms of the Health-Related * Additional Disclaimer of Warranty and Limitation of Liability available at * * http://www.carewebframework.org/licensing/disclaimer. * * #L% */ package org.carewebframework.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> decimalFormat = new ThreadLocal<DecimalFormat>() { @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 }; 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" } }; public enum TimeUnit { YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS }; /** * Enum representing common date formats. */ public enum Format { 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); 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); } } /** * <p> * 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. * </p> * <p> * 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. * </p> * <p> * 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. * </p> * * @param s <code>String</code> containing value to be converted. * @return <code>Date</code> object corresponding to the input value, or <code>null</code> 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(); } /** * <p> * 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. * </p> * * @param dob Date of person's birth * @return the age display string */ public static String formatAge(Date dob) { return formatAge(dob, true, null); } /** * <p> * 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. * </p> * <p> * 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. * </p> * <p> * Allows the caller to specify whether or not to pluralize the age units in the age display * string. * </p> * * @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() { } }