blob: 45816deff0249874e08efd9b549b3832fc0c4047 [file] [log] [blame]
package org.apache.velocity.tools.generic;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
import java.util.Collections;
import java.util.Calendar;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import org.apache.velocity.tools.Scope;
import org.apache.velocity.tools.config.ValidScope;
/**
* Tool for comparing {@link java.util.Date} and {@link Calendar} values
* in Velocity templates. This is a subclass of {@link DateTool}
* and thus provides all the functionality of that tool and
* augments it with the ability to find the relationship between
* any date and the current date, or between any two dates.
* This comparison can result in either a textual representation
* of the relationship (e.g. "3 weeks, 2 days ago", "tomorrow", or
* "3 hrs away") or the value of a specific time unit may be requested.
* When using the textual representations, you can configure the
* tool to use alternate resource bundles and to skip over units
* you do not want to be included.
* <p><pre>
* Example of formatting the "current" date:
* $date.whenIs('2005-07-04') -> 1 year ago
* $date.whenIs('2007-02-15').full -> 1 year 32 weeks 2 days 17 hours 38 minutes 44 seconds 178 milliseconds ago
* $date.whenIs('2007-02-15').days -> -730
* $date.whenIs($date.calendar) -> now
* $date.whenIs('2005-07-04', '2005-07-04') -> same time
* $date.difference('2005-07-04','2005-07-04') -> 0 milliseconds
* $date.difference('2005-07-04','2007-02-15').abbr -> 1 yr
*
* Example tools.xml config (if you want to use this with VelocityView):
* &lt;tools&gt;
* &lt;toolbox scope="application"&gt;
* &lt;tool class="org.apache.velocity.tools.generic.ComparisonDateTool"
* format="yyyy-MM-dd" depth="1" skip="month,week,millisecond"
* bundle="org.apache.velocity.tools.generic.times"/&gt;
* &lt;/toolbox&gt;
* &lt;/tools&gt;
* </pre></p>
*
* @author Nathan Bubna
* @author Chris Townsen
* @since VelocityTools 1.4
* @version $Revision$ $Date: 2006-04-04 12:35:17 -0700 (Tue, 04 Apr 2006) $
*/
@ValidScope(Scope.APPLICATION)
public class ComparisonDateTool extends DateTool
{
/** The number of milliseconds in a second. */
public static final long MILLIS_PER_SECOND = 1000l;
/** The number of millseconds in a minute. */
public static final long MILLIS_PER_MINUTE = 60l * MILLIS_PER_SECOND;
/** The number of milliseconds in an hour. */
public static final long MILLIS_PER_HOUR = 60l * MILLIS_PER_MINUTE;
/** The number of milliseconds in a day. */
public static final long MILLIS_PER_DAY = 24l * MILLIS_PER_HOUR;
/** The number of milliseconds in a week. */
public static final long MILLIS_PER_WEEK = 7l * MILLIS_PER_DAY;
/** An approximation of the number of milliseconds in a month. */
public static final long MILLIS_PER_MONTH = 30l * MILLIS_PER_DAY;
/** An approximation of the number of milliseconds in a year. */
public static final long MILLIS_PER_YEAR = 365l * MILLIS_PER_DAY;
/** The key used for specifying a default locale via toolbox params. */
public static final String BUNDLE_NAME_KEY = "bundle";
/** The key used for specifying a different default depth via toolbox params. */
public static final String DEPTH_KEY = "depth";
/** The key used for specifying time units to be skipped over. */
public static final String SKIPPED_UNITS_KEY = "skip";
/** The default path of the relative format resource bundles. */
public static final String DEFAULT_BUNDLE_NAME =
"org.apache.velocity.tools.generic.times";
// time unit message keys
protected static final String MILLISECOND_KEY = "millisecond";
protected static final String SECOND_KEY = "second";
protected static final String MINUTE_KEY = "minute";
protected static final String HOUR_KEY = "hour";
protected static final String DAY_KEY = "day";
protected static final String WEEK_KEY = "week";
protected static final String MONTH_KEY = "month";
protected static final String YEAR_KEY = "year";
/** Array of all time unit message keys to their millisecond conversion factor. */
protected static final Map TIME_UNITS;
static
{
Map units = new LinkedHashMap(8);
units.put(MILLISECOND_KEY, Long.valueOf(1));
units.put(SECOND_KEY, Long.valueOf(MILLIS_PER_SECOND));
units.put(MINUTE_KEY, Long.valueOf(MILLIS_PER_MINUTE));
units.put(HOUR_KEY, Long.valueOf(MILLIS_PER_HOUR));
units.put(DAY_KEY, Long.valueOf(MILLIS_PER_DAY));
units.put(WEEK_KEY, Long.valueOf(MILLIS_PER_WEEK));
units.put(MONTH_KEY, Long.valueOf(MILLIS_PER_MONTH));
units.put(YEAR_KEY, Long.valueOf(MILLIS_PER_YEAR));
TIME_UNITS = Collections.unmodifiableMap(units);
}
// special message keys/prefixes/suffixes
protected static final String CURRENT_PREFIX = "current.";
protected static final String AFTER_KEY = "after";
protected static final String BEFORE_KEY = "before";
protected static final String EQUAL_KEY = "equal";
protected static final String ZERO_KEY = "zero";
protected static final String ABBR_SUFFIX = ".abbr";
protected static final String ONE_DAY_SUFFIX = ".day";
protected static final String PLURAL_SUFFIX = "s";
// display types
protected static final int CURRENT_TYPE = 0;
protected static final int RELATIVE_TYPE = 1;
protected static final int DIFF_TYPE = 2;
private String bundleName = DEFAULT_BUNDLE_NAME;
private ResourceBundle defaultBundle;
private Map timeUnits = TIME_UNITS;
private int depth = 1;
/**
* Calls the superclass implementation, then looks for a bundle name
* and any time units to be skipped.
*/
protected void configure(ValueParser values)
{
// do DateTool config
super.configure(values);
// look for an alternate bundle
String bundle = values.getString(BUNDLE_NAME_KEY);
if (bundle != null)
{
this.bundleName = bundle;
}
this.depth = values.getInt(DEPTH_KEY, 1);
// look for time units to be ignored
String[] skip = values.getStrings(SKIPPED_UNITS_KEY);
if (skip != null)
{
timeUnits = new LinkedHashMap(TIME_UNITS);
for (int i=0; i < skip.length; i++)
{
timeUnits.remove(skip[i]);
}
}
}
/**
* Retrieves the specified text resource.
*/
protected String getText(String key, Locale locale)
{
Locale defaultLocale = getLocale();
ResourceBundle bundle = null;
// if there is no locale or the specified locale equals the tool's default
if (locale == null || locale.equals(defaultLocale))
{
if (defaultBundle == null)
{
// load the bundle for the default locale
try
{
// and cache it
defaultBundle = ResourceBundle.getBundle(this.bundleName,
defaultLocale);
}
catch (MissingResourceException e) {}
}
// use the default locale's bundle
bundle = defaultBundle;
}
else
{
// load the bundle for the specified locale
try
{
bundle = ResourceBundle.getBundle(this.bundleName, locale);
}
catch (MissingResourceException e) {}
}
// if we found a bundle...
if (bundle != null)
{
try
{
// try to return the specified key
return bundle.getString(key);
}
catch (MissingResourceException e) {}
}
// otherwise, return the key as an error
return "???" + key + "???";
}
// ------------------------- static millisecond conversions ----------------
/**
* Returns the number of whole Years in the specified number of milliseconds.
*/
public static long toYears(long ms)
{
return ms / MILLIS_PER_YEAR;
}
/**
* Returns the number of whole Months in the specified number of milliseconds.
*/
public static long toMonths(long ms)
{
return ms / MILLIS_PER_MONTH;
}
/**
* Returns the number of whole Weeks in the specified number of milliseconds.
*/
public static long toWeeks(long ms)
{
return ms / MILLIS_PER_WEEK;
}
/**
* Returns the number of whole Days in the specified number of milliseconds.
*/
public static long toDays(long ms)
{
return ms / MILLIS_PER_DAY;
}
/**
* Returns the number of whole Hours in the specified number of milliseconds.
*/
public static long toHours(long ms)
{
return ms / MILLIS_PER_HOUR;
}
/**
* Returns the number of whole Minutes in the specified number of milliseconds.
*/
public static long toMinutes(long ms)
{
return ms / MILLIS_PER_MINUTE;
}
/**
* Returns the number of whole Seconds in the specified number of milliseconds.
*/
public static long toSeconds(long ms)
{
return ms / MILLIS_PER_SECOND;
}
// ------------------------- date comparison ---------------------------
/**
* Returns a {@link Comparison} between the result of
* {@link #getCalendar()} and the specified date. The default
* rendering of that Comparison will be the largest unit difference
* between the dates followed by a description of their relative position.
*
* @param then The date in question
*/
public Comparison whenIs(Object then)
{
return compare(getCalendar(), then, CURRENT_TYPE);
}
/**
* Returns a {@link Comparison} between the second specified date
* and the first specified date. The default
* rendering of that Comparison will be the largest unit difference
* between the dates followed by a description of their relative position.
*
* @param now The date to use as representative of "now"
* @param then The date in question
*/
public Comparison whenIs(Object now, Object then)
{
return compare(now, then, RELATIVE_TYPE);
}
/**
* Returns a {@link Comparison} between the result of
* the second specified date and the first specified date. The default
* rendering of that Comparison will be the largest unit difference
* between the dates.
*
* @param now The date to use as representative of "now"
* @param then The secondary date
*/
public Comparison difference(Object now, Object then)
{
return compare(now, then, DIFF_TYPE);
}
protected Comparison compare(Object now, Object then, int type)
{
Calendar calThen = toCalendar(then);
Calendar calNow = toCalendar(now);
if (calThen == null || calNow == null)
{
return null;
}
long ms = calThen.getTimeInMillis() - calNow.getTimeInMillis();
return new Comparison(ms, type, this.depth, false, null);
}
/**
* @param ms The time in milliseconds
* @param type Whether the time should be represented as relative to "now",
* relative to some other time, or as a mere difference.
* @param depth The maximum number of units deep to show
* @param abbr Whether the units should be abbreviated or not
* @param loc The locale to be used when looking up resources
*/
protected String toString(long ms, int type, int depth,
boolean abbr, Locale loc)
{
// first check if there is a difference
if (ms == 0)
{
String sameKey = (abbr) ? ABBR_SUFFIX : "";
if (type == CURRENT_TYPE)
{
sameKey = CURRENT_PREFIX + EQUAL_KEY + sameKey;
}
else if (type == RELATIVE_TYPE)
{
sameKey = EQUAL_KEY + sameKey;
}
else
{
sameKey = ZERO_KEY + sameKey;
}
return getText(sameKey, loc);
}
boolean isBefore = false;
if (ms < 0)
{
isBefore = true;
// convert() only works with positive values
ms *= -1;
}
// get the base value
String friendly = toString(ms, depth, abbr, loc);
// if we only want the difference...
if (type == DIFF_TYPE)
{
// add the sign (if negative)
if (isBefore)
{
friendly = "-" + friendly;
}
// then return without direction suffix
return friendly;
}
// otherwise, get the appropriate direction key
String directionKey = (isBefore) ? BEFORE_KEY : AFTER_KEY;
if (type == CURRENT_TYPE)
{
directionKey = CURRENT_PREFIX + directionKey;
if (friendly != null && friendly.startsWith("1"))
{
// check for the corner case of "1 day ago" or "1 day away"
// and convert those to "yesterday" or "tomorrow"
String dayKey = (abbr) ? DAY_KEY + ABBR_SUFFIX : DAY_KEY;
if (friendly.equals("1 " + getText(dayKey, loc)))
{
// add .day
directionKey += ONE_DAY_SUFFIX;
// and return only the value of this key
// (which means we throw away the friendly value
// and don't bother abbreviating things)
return getText(directionKey, loc);
}
}
}
// in the default bundle, this doesn't change anything.
// but in may in user-provided bundles
if (abbr)
{
directionKey += ABBR_SUFFIX;
}
// then combine them
return friendly + " " + getText(directionKey, loc);
}
/**
* Converts the specified positive duration of milliseconds into larger
* units up to the specified number of positive units, beginning with the
* largest positive unit. e.g.
* <code>toString(181453, 3, false, null)</code> will return
* "3 minutes 1 second 453 milliseconds",
* <code>toString(181453, 2, false, null)</code> will return
* "3 minutes 1 second", and
* <code>toString(180000, 2, true, null)</code> will return
* "3 min".
*/
protected String toString(long diff, int maxUnitDepth,
boolean abbreviate, Locale locale)
{
// these cases should be handled elsewhere
if (diff <= 0)
{
return null;
}
// can't go any deeper than we have units
if (maxUnitDepth > timeUnits.size())
{
maxUnitDepth = timeUnits.size();
}
long value = 0;
long remainder = 0;
// determine the largest unit and calculate the value and remainder
Iterator i = timeUnits.keySet().iterator();
String unitKey = (String)i.next();
Long unit = (Long)timeUnits.get(unitKey);
while (i.hasNext())
{
// get the next unit
String nextUnitKey = (String)i.next();
Long nextUnit = (Long)timeUnits.get(nextUnitKey);
// e.g. if diff < <nextUnit>
if (diff < nextUnit.longValue())
{
// then we're working with <unit>
value = diff / unit.longValue();
remainder = diff - (value * unit.longValue());
break;
}
// shift to the next unit
unitKey = nextUnitKey;
unit = nextUnit;
}
// if it was years, then we haven't done the math yet
if (unitKey.equals(YEAR_KEY))
{
value = diff / unit.longValue();
remainder = diff - (value * unit.longValue());
}
// select proper pluralization
if (value != 1)
{
unitKey += PLURAL_SUFFIX;
}
if (abbreviate)
{
unitKey += ABBR_SUFFIX;
}
// combine the value and the unit
String output = value + " " + getText(unitKey, locale);
// recurse over the remainder if it exists and more units are allowed
if (maxUnitDepth > 1 && remainder > 0)
{
output += " " + toString(remainder, maxUnitDepth - 1,
abbreviate, locale);
}
return output;
}
public class Comparison
{
private final long milliseconds;
private final int type;
private final int maxUnitDepth;
private final boolean abbreviate;
private final Locale locale;
public Comparison(long ms, int type, int depth, boolean abbr, Locale loc)
{
this.milliseconds = ms;
this.type = type;
this.maxUnitDepth = depth;
this.abbreviate = abbr;
this.locale = loc;
}
/**
* Sets whether or not this comparison is to be rendered in
* abbreviated form or not. By default, it is not abbreviated.
*/
public Comparison abbr(boolean abbr)
{
return new Comparison(this.milliseconds, this.type,
this.maxUnitDepth, abbr, this.locale);
}
/**
* Set the maximum number of units to render for this comparison.
* By default, this is set to 1 unit.
*/
public Comparison depth(int depth)
{
return new Comparison(this.milliseconds, this.type,
depth, this.abbreviate, this.locale);
}
/**
* Sets the locale used to look up the textual portions of the
* rendering. This defaults to the Locale configured for this tool,
* if any. If no locale was configured, this defaults to the system
* default.
*/
public Comparison locale(Locale loc)
{
return new Comparison(this.milliseconds, this.type,
this.maxUnitDepth, this.abbreviate, loc);
}
/**
* Return the approximate number of years between the dates being compared.
*/
public long getYears()
{
return ComparisonDateTool.toYears(this.milliseconds);
}
/**
* Return the approximate number of months between the dates being compared.
*/
public long getMonths()
{
return ComparisonDateTool.toMonths(this.milliseconds);
}
/**
* Return the number of weeks between the dates being compared.
*/
public long getWeeks()
{
return ComparisonDateTool.toWeeks(this.milliseconds);
}
/**
* Return the number of days between the dates being compared.
*/
public long getDays()
{
return ComparisonDateTool.toDays(this.milliseconds);
}
/**
* Return the number of hours between the dates being compared.
*/
public long getHours()
{
return ComparisonDateTool.toHours(this.milliseconds);
}
/**
* Return the number of minutes between the dates being compared.
*/
public long getMinutes()
{
return ComparisonDateTool.toMinutes(this.milliseconds);
}
/**
* Return the number of seconds between the dates being compared.
*/
public long getSeconds()
{
return ComparisonDateTool.toSeconds(this.milliseconds);
}
/**
* Return the number of milliseconds between the dates being compared.
*/
public long getMilliseconds()
{
return this.milliseconds;
}
/**
* Sets the {@link #depth(int depth)} to which this comparison is rendered
* to the maximum number of time units available to the tool. By default,
* there are 8 units available, but the tool may be configured to "skip"
* any of the standard units, thus shortening the maximum depth.
*/
public Comparison getFull()
{
return depth(ComparisonDateTool.this.timeUnits.size());
}
/**
* Sets this comparison to be rendered as a
* {@link ComparisonDateTool#difference}. This effectively means that
* the comparison will render as a period of time, without any suffix
* to describe the relative position of the dates being compared (e.g. "later"
* or "ago").
*/
public Comparison getDifference()
{
return new Comparison(this.milliseconds, DIFF_TYPE,
this.maxUnitDepth, this.abbreviate, this.locale);
}
/**
* Sets this comparison to be rendered as if it where generated using
* the {@link ComparisonDateTool#whenIs(Object now, Object then)} method.
* This effectively means that the comparison will render with a suffix
* to describe the relative position of the dates being compared (e.g. "later"
* or "ago").
*/
public Comparison getRelative()
{
return new Comparison(this.milliseconds, RELATIVE_TYPE,
this.maxUnitDepth, this.abbreviate, this.locale);
}
/**
* This is equivalent to calling {@link #abbr(boolean abbr)} with
* {@code true} as the argument, thus setting this comparison to be
* rendered in abbreviated form.
*/
public Comparison getAbbr()
{
return abbr(true);
}
/**
* Renders this comparison to a String.
*/
public String toString()
{
return ComparisonDateTool.this.toString(this.milliseconds,
this.type,
this.maxUnitDepth,
this.abbreviate,
this.locale);
}
}
}