ExtendedDateTool handles requirements for expressing a specified date relative to now as a string in a "friendly format" (e.g. "3 minutes ago", "tomorrow", "3 days from now")
Below is the code for a first run through at making a tool to cover these requirements. This version is still very rough around the edges.
Code consists of:
/* * Copyright 2003-2004 The Apache Software Foundation. * * 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.apache.velocity.tools.generic; import java.util.Calendar; import org.apache.velocity.tools.generic.DateTool; /** * Extension to Velocity DateTool which provides methods for expressing * Calendar objects in a human-friendly, relative format * * @author <a href="mailto:c.townson@nature.com">Christopher Townson</a> * * TODO externalize unit conversion methods to dedicated tool? * TODO better handling of strings/pluralisation/i8n? * TODO integration of properties with velocity.properties * TODO clean up problematic handling of dates when time period to be expressed * is not suited to "friendly-format" */ public class ExtendedDateTool extends DateTool { /** * number of milliseconds in a second */ public static final long MILLIS_TO_SECOND = 1000; /** * number of millseconds in a minute */ public static final long MILLIS_TO_MINUTE = 60000; /** * number of milliseconds in an hour */ public static final long MILLIS_TO_HOUR = 3600000; /** * number of milliseconds in a day */ public static final long MILLIS_TO_DAY = 86400000; /** * String to append to dates which are _after_ the current date */ public static final String APPEND_FUTURE = Messages.getString("ExtendedDateTool.string.date.append.future"); /** * String to append to dates which are _before_ the current date */ public static final String APPEND_PAST = Messages.getString("ExtendedDateTool.string.date.append.past"); /** * Strings for unit: seconds (singular) */ public static final String UNIT_SECOND = Messages.getString("ExtendedDateTool.string.unit.seconds.singular"); /** * String for unit seconds (plural) */ public static final String UNIT_SECONDS = Messages.getString("ExtendedDateTool.string.unit.seconds.plural"); /** * String for unit: minutes (singular) */ public static final String UNIT_MINUTE = Messages.getString("ExtendedDateTool.string.unit.minutes.singular"); /** * String for unit minutes (plural) */ public static final String UNIT_MINUTES = Messages.getString("ExtendedDateTool.string.unit.minutes.plural"); /** * String for unit: minutes (singular) */ public static final String UNIT_HOUR = Messages.getString("ExtendedDateTool.string.unit.hours.singular"); /** * String for unit minutes (plural) */ public static final String UNIT_HOURS = Messages.getString("ExtendedDateTool.string.unit.hours.plural"); /** * String for unit: days (singular) */ public static final String UNIT_DAY = Messages.getString("ExtendedDateTool.string.unit.days.singular"); /** * String for unit days (plural) */ public static final String UNIT_DAYS = Messages.getString("ExtendedDateTool.string.unit.days.plural"); /** * String for unit: 1 day into the future */ public static final String YESTERDAY = Messages.getString("ExtendedDateTool.string.date.yesterday"); /** * String for unit: 1 day into the past */ public static final String TOMORROW = Messages.getString("ExtendedDateTool.string.date.tomorrow"); /** * String for unit: weeks (singular) * * Not currently in use as any period over 7 days is better displayed in a * standard date format */ public static final String UNIT_WEEK = Messages.getString("ExtendedDateTool.string.unit.weeks.singular"); /** * String for unit: weeks (plural) * * Currently only used to determine the fact that date should be * displayed in a standard format */ public static final String UNIT_WEEKS = Messages.getString("ExtendedDateTool.string.unit.weeks.plural"); /** * Default output format for dates which are too distant to be suitable * for rendering in "friendly format" */ public static final String DEFAULT_FORMAT = Messages.getString("ExtendedDateTOol.string.date.format.default"); /** * Returns a provided Date instance in a human-friendly String format * * @param then The Calendar object to convert to human-friendly format * @return The date in a human-friendly format (e.g. 3 days ago) */ public String getFriendlyDate(Calendar then) { String friendlyDate = null; String append = null; // get the difference between the now and then as a long long diff = getDifferenceBetweenNowAnd(then); // is Calendar in the past or the future? if (isPast(diff)) { diff -= (diff * 2); append = APPEND_PAST; } else { append = APPEND_FUTURE; } if (isSeconds(diff)) { if (isSingular(diff)) { friendlyDate = convertMillisToSeconds(diff) + " " + UNIT_SECOND + " " + append; } else { friendlyDate = convertMillisToSeconds(diff) + " " + UNIT_SECONDS + " " + append; } } else if (isMinutes(diff)) { if (isSingular(diff)) { friendlyDate = convertMillisToMinutes(diff) + " " + UNIT_MINUTE + " " + append; } else { friendlyDate = convertMillisToMinutes(diff) + " " + UNIT_MINUTES + " " + append; } } else if (isHours(diff)) { if (isSingular(diff)) { friendlyDate = convertMillisToHours(diff) + " " + UNIT_HOUR + " " + append; } else { friendlyDate = convertMillisToHours(diff) + " " + UNIT_HOURS + " " + append; } } else if (isDays(diff)) { if (isSingular(diff) && append == APPEND_FUTURE) { friendlyDate = TOMORROW; } else if(isSingular(diff) && append == APPEND_PAST) { friendlyDate = YESTERDAY; } else { friendlyDate = convertMillisToDays(diff) + " " + UNIT_DAYS + " " + append; } } else { friendlyDate = super.format(DEFAULT_FORMAT,then.getTime()); } // return friendly string return friendlyDate; } /** * Overloaded getFriendlyDate method for convenience from templates * * NB. The date format descriptor only applies to the date as it will be * returned if it is <strong>not</strong> suitable for friendly format * * @param format * @param obj * @return The date in a human-friendly format (e.g. 3 days ago) */ public String getFriendlyDate(String format, Object obj) { return getFriendlyDate(super.toCalendar(super.toDate(format,obj))); } /** * Returns a Java timestamp (milliseconds since 1970-01-01) * * @return long java timestamp for the current date */ public long getTimeInMillis() { return Calendar.getInstance().getTimeInMillis(); } /** * Returns a Java timestamp for the supplied Calendar * * @param cal * @return long the Java timestamp for the supplied calendar */ public long getTimeInMillis(Calendar cal) { return cal.getTimeInMillis(); } /** * Returns the time difference between two points expressed in milliseconds * * @param pointA * @param pointB * @return The time difference in milliseconds */ public long getDifferenceBetween(Calendar pointA, Calendar pointB) { return pointA.getTimeInMillis() - pointB.getTimeInMillis(); } /** * Tests whether the difference between two Calendar instances is * a matter of seconds or not * * @param then The calendar instance for comparison * @return The difference in milliseconds between now and supplied Calendar * Returns a positive long if then is after now and a negative long * if then is before now */ public long getDifferenceBetweenNowAnd(Calendar then) { return getDifferenceBetween(then, Calendar.getInstance()); } /** * COnvert a duration expressed in milliseconds to seconds * * @param millis * @return milliseconds expressed as seconds */ public long convertMillisToSeconds(long millis) { return millis / MILLIS_TO_SECOND; } /** * Convert a duration expressed in milliseconds to minutes * * @param millis * @return milliseconds expressed as minutes */ public long convertMillisToMinutes(long millis) { return millis / MILLIS_TO_MINUTE; } /** * Convert a duration expressed in milliseconds to hours * * @param millis * @return milliseconds expressed as hours */ public long convertMillisToHours(long millis) { return millis / MILLIS_TO_HOUR; } /** * Convert a duration expressed in milliseconds to days * * @param millis * @return milliseconds expressed as days */ public long convertMillisToDays(long millis) { return millis / MILLIS_TO_DAY; } /** * Tests for positive or negative result from timestamp comparison * * @param diff * @return whether or not our compared time was in the past */ private boolean isPast(long diff) { if (diff < 0) { return true; } else { return false; } } /** * Returns the appropriate time unit string for expressing a * millsecond duration in a "friendly" date format * * @param diff * @return The time unit name */ private String calculateUnit(long diff) { if (diff > 0 && diff < 60000) { return UNIT_SECONDS; } else if (diff >= 60000 && diff < 3600000) { return UNIT_MINUTES; } else if (diff >= 3600000 && diff < 86400000) { return UNIT_HOURS; } else if (diff >= 86400000 && diff < 604800000) { return UNIT_DAYS; } else if (diff >= 604800000) { return UNIT_WEEKS; } else { // default return null; } } /** * Returns true if the time unit string is singular; false if the string * needs to be pluralised * * This method will only work on positive longs. * * @param diff * @return Whether or not to pluralise the time unit string */ private boolean isSingular(long diff) { // run this test now to save repeating in conditionals String units = calculateUnit(diff); if (units == UNIT_SECONDS && diff < 2000) { return true; } else if (units == UNIT_MINUTES && diff < 120000) { return true; } else if (units == UNIT_HOURS && diff < 7200000) { return true; } else if (units == UNIT_DAYS && diff < 172800000) { return true; } else if (units == UNIT_WEEKS && diff < 1209600000) { return true; } else { // default return false; } } /** * Do we want to talk in seconds? * * @param diff * @return Whether we are dealing with seconds or not */ private boolean isSeconds(long diff) { if (calculateUnit(diff) == UNIT_SECONDS) { return true; } else { return false; } } /** * Do we want to talk in minutes? * * @param diff * @return Whether we are dealing with minutes or not */ private boolean isMinutes(long diff) { if (calculateUnit(diff) == UNIT_MINUTES) { return true; } else { return false; } } /** * Do we want to talk in hours? * * @param diff * @return Whether we are dealing with hours or not */ private boolean isHours(long diff) { if (calculateUnit(diff) == UNIT_HOURS) { return true; } else { return false; } } /** * Do we want to talk in days? * * @param diff * @return Whether we are dealing with days or not */ private boolean isDays(long diff) { if (calculateUnit(diff) == UNIT_DAYS) { return true; } else { return false; } } /** * Do we want to talk in weeks? * * @param diff * @return Whether we are dealing with weeks or not */ private boolean isWeeks(long diff) { if (calculateUnit(diff) == UNIT_WEEKS) { return true; } else { return false; } } } |
/** * */ package org.apache.velocity.tools.generic; import java.util.MissingResourceException; import java.util.ResourceBundle; /** * Eclipse auto-generated helper class to retrieve localised Strings * for ExtendedDateTool * * @author Christopher Townson */ public class Messages { /** * The bundle name */ private static final String BUNDLE_NAME = "org.apache.velocity.tools.generic.messages"; //$NON-NLS-1$ /** * The resource bundle */ private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); /** * Default constructor * */ private Messages() { // do nothing } /** * Accessor method for retrieving String properties * * @param key The key for the String property to get * @return The string property */ public static String getString(String key) { // TODO Auto-generated method stub try { return RESOURCE_BUNDLE.getString(key); } catch (MissingResourceException e) { return '!' + key + '!'; } } } |
ExtendedDateTool.string.date.append.future=from now ExtendedDateTool.string.date.append.past=ago ExtendedDateTool.string.unit.seconds.singular=second ExtendedDateTool.string.unit.seconds.plural=seconds ExtendedDateTool.string.unit.minutes.singular=minute ExtendedDateTool.string.unit.minutes.plural=minutes ExtendedDateTool.string.unit.hours.singular=hour ExtendedDateTool.string.unit.hours.plural=hours ExtendedDateTool.string.unit.days.singular=day ExtendedDateTool.string.unit.days.plural=days ExtendedDateTool.string.date.yesterday=yesterday ExtendedDateTool.string.date.tomorrow=tomorrow ExtendedDateTool.string.unit.weeks.singular=week ExtendedDateTool.string.unit.weeks.plural=weeks ExtendedDateTool.string.date.format.default=yyyy-MM-dd HH:mm:ss |
<!-- ExtendedDateTool: additional date manipulation methods --> <tool> <key>xDateTool</key> <scope>request</scope> <class>org.apache.velocity.tools.generic.ExtendedDateTool</class> </tool> |
Nathan has done a lot of refactoring on that initial rough version which has resulted in the following code:
/* * Copyright 2006 The Apache Software Foundation. * * 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.apache.velocity.tools.generic; import java.util.Calendar; import java.util.MissingResourceException; import java.util.ResourceBundle; import org.apache.velocity.tools.generic.DateTool; /** * Extension to Velocity DateTool which provides methods for expressing * date objects in a human-friendly, relative format * * @author <a href="mailto:c.townson@nature.com">Christopher Townson</a> */ public class ExtendedDateTool extends DateTool { /** The number of milliseconds in a second. */ public static final long MILLIS_PER_SECOND = 1000; /** The number of millseconds in a minute. */ public static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND; /** The number of milliseconds in an hour. */ public static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE; /** The number of milliseconds in a day. */ public static final long MILLIS_PER_DAY = 24 * MILLIS_PER_HOUR; /** The number of milliseconds in a week. */ public static final long MILLIS_PER_WEEK = 7 * MILLIS_PER_DAY; /** An approximation of the number of milliseconds in a month. */ public static final long MILLIS_PER_MONTH = 30 * MILLIS_PER_DAY; /** An approximation of the number of milliseconds in a year. */ public static final long MILLIS_PER_YEAR = 365 * MILLIS_PER_DAY; /** Array of all "millis per X" values. */ protected static final long[] RELATIVE_UNITS = new long[] { 1, // millis per milli MILLIS_PER_SECOND, MILLIS_PER_MINUTE, MILLIS_PER_HOUR, MILLIS_PER_DAY, MILLIS_PER_WEEK, MILLIS_PER_MONTH, MILLIS_PER_YEAR }; /** Array of all message keys for relative date units. */ protected static final String[] RELATIVE_UNIT_KEYS = new String[] { "date.milliseconds", "date.seconds", "date.minutes", "date.hours", "date.days", "date.weeks", "date.months", "date.years" }; private static final String DEFAULT_RESOURCE_NAME = "org.apache.velocity.tools.generic.messages"; private ResourceBundle textResources; protected String getText(String key) { if (textResources == null) { textResources = ResourceBundle.getBundle(DEFAULT_RESOURCE_NAME, getLocale()); } try { return textResources.getString(key); } catch (MissingResourceException e) { return '!' + key + '!'; } } /** * Returns the value of the largest unit difference between the specified * date and the result of getCalendar() in a human-friendly String format. * * @param date The date to convert to human-friendly format * @return the diff between the specified date and "now" in a * human-friendly format (e.g. 3 days ago) */ public String toRelativeString(Object date) { String friendly = toRelativeString(date, 1); // check for the corner case of "1 day ago" or "1 day from now" // and convert those to "yesterday" or "tomorrow" if (friendly != null && friendly.startsWith("1 "+getText("date.days.singular"))) { if (friendly.endsWith(getText("date.direction.past"))) { return getText("date.days.singular.past"); } else { return getText("date.days.plural.future"); } } return friendly; } /** * Returns the difference between the specified date * and the result of getCalendar() in a human-friendly String format * with up to the specified number of units. * * @param date The date to convert to human-friendly format * @param maxUnitDepth The maximum number of units deep to show * @return the diff between the specified date and "now" in a * human-friendly format (e.g. 3 days 4 hours 2 seconds ago) */ public String toRelativeString(Object date, int maxUnitDepth) { return toRelativeString(date, getCalendar(), maxUnitDepth); } /** * Returns the value of the largest unit difference between the specified * dates in a human-friendly String format. * * @param dateA The primary date to be compared * @param dateB The secondary date to be compared * @return the diff between the specified date and "now" in a * human-friendly format (e.g. 3 days ago) */ public String toRelativeString(Object dateA, Object dateB) { return toRelativeString(dateA, dateB, 1); } /** * Returns the value of the largest unit difference between the specified * dates in a human-friendly String format with up to the specified number * of units. * * @param dateA The primary date to be compared * @param dateB The secondary date to be compared * @param maxUnitDepth The maximum number of units deep to show * @return the diff between the specified date and "now" in a * human-friendly format (e.g. 3 days 4 hours 2 seconds ago) */ public String toRelativeString(Object dateA, Object dateB, int maxUnitDepth) { // get the difference between the date and now Long difference = diff(dateA, dateB); if (difference == null) { return null; } long diff = difference.longValue(); // is the dateA before or after dateB boolean isPast = (diff < 0); if (isPast) { // work only with future values diff *= -1; } // get the positive friendly value String friendly = toRelativeString(diff, maxUnitDepth); // then add the direction if (isPast) { friendly += " " + getText("date.direction.past"); } else { friendly += " " + getText("date.direction.future"); } return friendly; } /** * Converts the specified duration of milliseconds into larger units up to * the specified number of positive units, beginning with the largest * positive unit. e.g. * <code>toRelativeString(181453, 3)</code> will return * "3 minutes 1 second 453 milliseconds", * <code>toRelativeString(181453, 2)</code> will return * "3 minutes 1 second", and * <code>toRelativeString(180000, 2)</code> will return * "3 minutes". */ protected String toRelativeString(long diff, int maxUnitDepth) { if (diff < 0) { return null; } if (diff == 0) { //TODO? should we differentiate between "now" and "same time"? return getText("date.now"); } long value = 0; long remainder = 0; String unitKey = null; // determine the largest unit and calculate the value and remainder for (int i = 0; i < RELATIVE_UNITS.length + 1; i++) { // if diff < MILLIS_PER_MINUTE or we're at the end if (diff < RELATIVE_UNITS[i] || i > RELATIVE_UNITS.length) { // then we're working with seconds value = diff / RELATIVE_UNITS[i - 1]; remainder = diff - (value * RELATIVE_UNITS[i - 1]); unitKey = RELATIVE_UNIT_KEYS[i - 1]; break; } } // select proper pluralization if (value == 1) { unitKey += ".singular"; } else { unitKey += ".plural"; } // combine the value and the unit String output = value + ' ' + getText(unitKey); // recurse over the remainder if it exists and more units are allowed if (maxUnitDepth > 1 && remainder > 0) { output += " " + toRelativeString(remainder, maxUnitDepth - 1); } return output; } /** * Returns the time difference between two dates expressed in milliseconds * * @param dateA * @param dateB * @return The time difference in milliseconds */ public Long diff(Object dateA, Object dateB) { Calendar calA = toCalendar(dateA); Calendar calB = toCalendar(dateB); if (calA == null || calB == null) { return null; } return new Long(calA.getTimeInMillis() - calB.getTimeInMillis()); } /** * @param date The date for comparison * @return The difference in milliseconds between the supplied date and * the date returned by getCalendar() (i.e. "now"). Returns a * positive long if then is after now and a negative long if then * is before now or <code>null</code> if the parameter can not be * converted to a Calendar. */ public Long diff(Object date) { return diff(date, getCalendar()); } } |
date.direction.future=from now date.direction.past=ago date.now=now date.milliseconds.singular=millisecond date.milliseconds.plural=milliseconds date.seconds.singular=second date.seconds.plural=seconds date.minutes.singular=minute date.minutes.plural=minutes date.hours.singular=hour date.hours.plural=hours date.days.singular=day date.days.singular.past=tomorrow date.days.singular.future=yesterday date.days.plural=days date.weeks.singular=week date.weeks.plural=weeks date.months.singular=month date.months.plural=months date.years.singular=year date.years.plural=years |