/*---------------------------------------------------------------------------*\
$Id: 882018b3c25ec0a15ab58773eaa10b8ee7907132 $
---------------------------------------------------------------------------
This software is released under a BSD-style license:
Copyright (c) 2004-2007 Brian M. Clapper. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. The end-user documentation included with the redistribution, if any,
must include the following acknowlegement:
"This product includes software developed by Brian M. Clapper
(bmc@clapper.org, http://www.clapper.org/bmc/). That software is
copyright (c) 2004-2007 Brian M. Clapper."
Alternately, this acknowlegement may appear in the software itself,
if wherever such third-party acknowlegements normally appear.
3. Neither the names "clapper.org", "clapper.org Java Utility Library",
nor any of the names of the project contributors may be used to
endorse or promote products derived from this software without prior
written permission. For written permission, please contact
bmc@clapper.org.
4. Products derived from this software may not be called "clapper.org
Java Utility Library", nor may "clapper.org" appear in their names
without prior written permission of Brian M. Clapper.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
NO EVENT SHALL BRIAN M. CLAPPER BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\*---------------------------------------------------------------------------*/
package org.clapper.util.text;
import org.clapper.util.misc.*;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* This class contains methods to parse and format time durations. A time
* duration is a string like "1 second", "10 minutes", "360 days", "4
* hours, 30 seconds". This class can
*
*
* - parse strings of that form to produce delta values (long integers)
*
- produce such strings by subtracting two dates or subtracting
* a date and a duration,
*
- add a duration to or subtract a duration from a Date,
* producing a new Date
*
*
* The parser recognizes the following intervals and synonyms. (The names are
* in English; if resource bundles for other languages exist, the names will
* obviously be different.)
*
*
* - milliseconds (millisecond, ms)
*
- seconds (seconds, sec, secs)
*
- minutes (minute, min, mins)
*
- hours (hour, hr, hrs)
*
- days (day)
*
- weeks (week)
*
*
* Years and months are omitted to avoid the irregularity of leap years and
* different month lengths, respectively. Weeks are honored on input only.
*
* @version $Revision: 6687 $
*
* @since org.clapper.util version 2.4.1
*
* @author Copyright © 2004-2007 Brian M. Clapper
*/
public final class Duration
{
/*----------------------------------------------------------------------*\
Private Classes
\*----------------------------------------------------------------------*/
private static enum DurationType
{
MILLISECOND,
SECOND,
MINUTE,
HOUR,
DAY,
WEEK
};
private static class DurationForFormat
{
final String singular;
final String plural;
DurationForFormat(String singular, String plural)
{
this.singular = singular;
this.plural = plural;
}
};
/*----------------------------------------------------------------------*\
Private Constants
\*----------------------------------------------------------------------*/
private static final long SECOND_MS = 1000;
private static final long MINUTE_MS = SECOND_MS * 60;
private static final long HOUR_MS = MINUTE_MS * 60;
private static final long DAY_MS = HOUR_MS * 24;
private static final long WEEK_MS = DAY_MS * 7;
private static final String BUNDLE_NAME = Duration.class.getName() +
"Bundle";
/*----------------------------------------------------------------------*\
Private Data Items
\*----------------------------------------------------------------------*/
private long duration_in_ms = 0L;
/*----------------------------------------------------------------------*\
Constructor
\*----------------------------------------------------------------------*/
/**
* Default constructor. Equivalent to {@link #Duration(0)}.
*/
public Duration()
{
this(0L);
}
/**
* Create a new Duration object from a long integer representing
* some elapsed number of milliseconds.
*
* @param milliseconds the elapsed milliseconds
*/
public Duration(long milliseconds)
{
duration_in_ms = milliseconds;
}
/**
* Create a new Duration object by determining the amount
* of time between two dates.
*
* @param date1 the first date
* @param date2 the second date
*/
public Duration(Date date1, Date date2)
{
long ms1 = date1.getTime();
long ms2 = date2.getTime();
this.duration_in_ms = Math.abs(ms1 - ms2);
}
/**
* Create a new Duration object by parsing the specified
* duration string. The words in the string are interpreted according to
* the specified locale. If this method cannot locate an appropriate
* resource bundle for the locale, it uses the default bundle (which may
* result in an exception).
*
* @param s the string to parse
* @param locale locale to use
*
* @throws ParseException bad string
*/
public Duration(String s, Locale locale) throws ParseException
{
parse(s, locale);
}
/**
* Create a new Duration object by parsing the specified
* duration string. The current locale is used to interpret the strings.
*
* @param s the string to parse
*
* @throws ParseException bad string
*/
public Duration(String s) throws ParseException
{
parse(s, Locale.getDefault());
}
/*----------------------------------------------------------------------*\
Public Methods
\*----------------------------------------------------------------------*/
/**
* Get the stored duration value, in milliseconds.
*
* @return the stored duration value
*/
public long getDuration()
{
return duration_in_ms;
}
/**
* Format the duration value as a string, like the kind of string
* handled by the {@link #parse} method. This version formats the string
* using the default locale. For a complete description of what this
* method produces, see {@link #format(Locale)}.
*
* @return the formatted string
*/
public String format()
{
return format(Locale.getDefault());
}
/**
* Format the duration value as a string, like the kind of string
* handled by the {@link #parse} method. For instance, a duration value
* of 1 is formatted as "1 millisecond". A duration value of 86460000
* is formatted as "1 day, 1 hour".
*
* @param locale Locale to use for the strings, or null for the default.
* If there's no bundle for the specified locale, English
* is used.
*
* @return the string
*/
public String format(Locale locale)
{
Map map = getFormatterMap(locale);
long milliseconds = duration_in_ms;
long days = milliseconds / DAY_MS;
milliseconds -= (days * DAY_MS);
long hours = milliseconds / HOUR_MS;
milliseconds -= (hours * HOUR_MS);
long minutes = milliseconds / MINUTE_MS;
milliseconds -= (minutes * MINUTE_MS);
long seconds = milliseconds / SECOND_MS;
milliseconds -= (seconds * SECOND_MS);
StringBuilder buf = new StringBuilder();
String sep = "";
if (days > 0)
{
formatForDurationUnit(days, map.get(DurationType.DAY), buf, sep);
sep = ", ";
}
if (hours > 0)
{
formatForDurationUnit(hours, map.get(DurationType.HOUR), buf, sep);
sep = ", ";
}
if (minutes > 0)
{
formatForDurationUnit(minutes, map.get(DurationType.MINUTE), buf, sep);
sep = ", ";
}
if (seconds > 0)
{
formatForDurationUnit(seconds, map.get(DurationType.SECOND), buf, sep);
sep = ", ";
}
if (milliseconds > 0)
{
formatForDurationUnit(milliseconds, map.get(DurationType.MILLISECOND), buf, sep);
sep = ", ";
}
return buf.toString();
}
/**
* Parse a string containing a textual description of a duration,
* setting this object's value to the result of the parse. This method
* parses the tokens using the default locale. See the version of
* {@link #parse(String,Locale) parse()} that takes a Locale
* parameter for a more complete explanation of the supported tokens.
*
* @param s the string to parse
*
* @throws ParseException parse error
*/
public void parse(String s)
throws ParseException
{
parse(s, Locale.getDefault());
}
/**
* Parse a string containing a textual description of a duration,
* setting this object's value to the result of the parse. The string
* contains one or more token pairs. The token pairs are separated by
* commas or white space. Each resulting token pair has a numeric token
* followed by a duration value. Examples will clarify:
*
*
* - 3 days, 19 hours
*
- 12 minutes
*
- 1 hour, 10 minutes, 33 seconds, 5 milliseconds
*
- 5 minutes 3 seconds
*
*
* @param s the string to parse
* @param locale the locale to use when interpreting the tokens, or null
* for the default.
*
* @throws ParseException parse error
*/
public void parse(String s, Locale locale)
throws ParseException
{
// First, get the list of legal tokens.
Map tokenMap = getParserMap(locale);
// Next, break the incoming string into tokens on white space and
// commas. There must be an even number of tokens.
String[] tokens = TextUtil.split(s, ", ");
if ((tokens.length % 2) != 0)
{
throw new ParseException("Malformed duration string \"" + s + "\"",
0);
}
// Now, parse each pair. The first value in each pair must be a number.
// The second value must be a duration token.
duration_in_ms = 0;
for (int i = 0; i < tokens.length; i += 2)
{
// First, parse the number.
long num = 0;
try
{
num = Long.parseLong(tokens[i]);
if (num < 0)
{
throw new ParseException("Unexpected negative value in \"" +
s + "\"",
0);
}
}
catch (NumberFormatException ex)
{
throw new ParseException("In \"" + s +
"\", Expected numeric token \"" +
tokens[i] + "\" is not numeric.",
0);
}
// Next, the duration string.
DurationType t = tokenMap.get(tokens[i+1].toLowerCase());
if (t == null)
{
throw new ParseException("In \"" + s + "\", found unknown " +
"duration \"" + tokens[i + 1] + "\"",
0);
}
switch (t)
{
case MILLISECOND:
duration_in_ms += num;
break;
case SECOND:
duration_in_ms += (num * SECOND_MS);
break;
case MINUTE:
duration_in_ms += (num * MINUTE_MS);
break;
case HOUR:
duration_in_ms += (num * HOUR_MS);
break;
case DAY:
duration_in_ms += (num * DAY_MS);
break;
case WEEK:
duration_in_ms += (num * WEEK_MS);
break;
default:
assert(false);
}
}
}
/**
* Return a string representation of this duration. Note that this method
* is not the same as the {@link #format} method. format()
* produces a natural language phrase, whereas toString() just
* returns the equivalent of
* String.valueOf(Duration.getDuration()).
*
* @return the stringified duration value
*/
@Override
public String toString()
{
return String.valueOf(getDuration());
}
/*----------------------------------------------------------------------*\
Private Methods
\*----------------------------------------------------------------------*/
private void formatForDurationUnit(long count,
DurationForFormat tokens,
StringBuilder buf,
String separator)
{
if (tokens == null)
throw new IllegalStateException("Unexpected null DurationForFormat");
if (count > 0)
{
String duration = (count == 1) ? tokens.singular : tokens.plural;
buf.append(separator);
buf.append(String.valueOf(count));
buf.append(" ");
buf.append(duration);
}
}
private Map getFormatterMap(Locale locale)
{
Map map =
new HashMap();
XResourceBundle bundle =
XResourceBundle.getXResourceBundle(BUNDLE_NAME, locale);
loadBundle(bundle, null, map);
return map;
}
private Map getParserMap(Locale locale)
throws IllegalStateException
{
Map map = new HashMap();
XResourceBundle bundle = XResourceBundle.getXResourceBundle(BUNDLE_NAME,
locale);
loadBundle(bundle, map, null);
// Also allow the default.
bundle = XResourceBundle.getXResourceBundle(BUNDLE_NAME);
loadBundle(bundle, map, null);
return map;
}
private void loadBundle(XResourceBundle bundle,
Map mapByToken,
Map mapByType)
throws IllegalStateException
{
parseTokensFor(DurationType.MILLISECOND,
bundle.getString("millisecondTokens",
"millisecond/milliseconds/ms"),
mapByToken,
mapByType);
parseTokensFor(DurationType.SECOND,
bundle.getString("secondTokens",
"second/seconds/sec/secs"),
mapByToken,
mapByType);
parseTokensFor(DurationType.MINUTE,
bundle.getString("minuteTokens",
"minute/minutes/min/mins"),
mapByToken,
mapByType);
parseTokensFor(DurationType.HOUR,
bundle.getString("hourTokens",
"hour/hours/hr/hrs"),
mapByToken,
mapByType);
parseTokensFor(DurationType.DAY,
bundle.getString("dayTokens", "day/days"),
mapByToken,
mapByType);
parseTokensFor(DurationType.WEEK,
bundle.getString("weekTokens", "week/weeks"),
mapByToken,
mapByType);
}
private void parseTokensFor(DurationType durationType,
String unparsedValue,
Map mapByToken,
Map mapByType)
throws IllegalStateException
{
String[] tokens = TextUtil.split(unparsedValue, '/');
if (tokens.length < 2)
{
throw new IllegalStateException("Error in resource bundle: Must " +
"have at least two tokens in " +
"duration string. \"" +
unparsedValue + "\" only has " +
tokens.length);
}
if (mapByToken != null)
{
for (String token : tokens)
mapByToken.put(token.toLowerCase(), durationType);
}
if (mapByType != null)
{
mapByType.put(durationType,
new DurationForFormat(tokens[0].toLowerCase(),
tokens[1].toLowerCase()));
}
}
}
Ads help maintain this website.