This Confluence has been LDAP enabled, if you are an ASF Committer, please use your LDAP Credentials to login. Any problems file an INFRA jira ticket please.

Page tree
Skip to end of metadata
Go to start of metadata

Initial Rough Version

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:

  • A tool for the toolbox
/*
 * 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;
		}
	}
}

  • A default Eclipse messages handler
/**
 *
 */
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 + '!';
		}
	}
}

  • A messages.properties file
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
  • A toolbox.xml entry
<!-- ExtendedDateTool: additional date manipulation methods -->
<tool>
	<key>xDateTool</key>
	<scope>request</scope>
	<class>org.apache.velocity.tools.generic.ExtendedDateTool</class>
</tool>

Refactored Version

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());
    }

}
  • And the following updated messages.properties
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
  • No labels