All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.clapper.util.text.Duration Maven / Gradle / Ivy

The newest version!
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. * * @since org.clapper.util version 2.4.1 */ 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 new 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())); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy