View Javadoc

1   /*
2    * Copyright (C) 2006  Tom Gibara
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 2.1 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12   * Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this library; if not, write to the Free Software
16   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17   */
18  package com.tomgibara.pronto.util;
19  
20  import java.io.Serializable;
21  import java.util.regex.Matcher;
22  import java.util.regex.Pattern;
23  
24  /**
25   * <p>
26   * This class may be used to convert between a textual representation for a unit
27   * of time and its millisecond equivalent or vice versa. Instances are immutable
28   * and are safe for concurrent use.
29   * </p>
30   * 
31   * <p>
32   * The duration format parsable by this class consists of list of positively
33   * valued fields in descending order of unit magnitude. The recognized field
34   * units are:
35   * </p>
36   * 
37   * <ul>
38   * <li>Days - unit: d, day or days</li>
39   * <li>Hours - unit: h, hr, hrs, hour, hours</li>
40   * <li>Minutes - unit: m, min, mins, minute, minutes</li>
41   * <li>Seconds - unit: s, sec, secs, second, seconds</li>
42   * <li>Milliseconds - unit: ms, milli, millis, millisecond, milliseconds</li>
43   * </ul>
44   * 
45   * <p>
46   * Spaces may appear between fields but not between numbers and units. Unit
47   * plurality is inconsequential. A string containing no units (eg. the empty
48   * string) may be used to represent zero milliseconds.
49   * </p>
50   * 
51   * <p>
52   * Examples of valid durations:
53   * </p>
54   * 
55   * <ul>
56   * <li>
57   * 
58   * <pre>
59   *    4days 10hours
60   * </pre>
61   * 
62   * long units</li>
63   * <li>
64   * 
65   * <pre>
66   *    10m2s500ms
67   * </pre>
68   * 
69   * short units</li>
70   * <li>
71   * 
72   * <pre>
73   *    10m2s500ms
74   * </pre>
75   * 
76   * mixed units</li>
77   * <li>
78   * 
79   * <pre>
80   *    2hour 1seconds
81   * </pre>
82   * 
83   * inconsequential plurality</li>
84   * <li>
85   * 
86   * <pre>
87   *    1day 25hrs
88   * </pre>
89   * 
90   * value exceeding prior field</li>
91   * </ul>
92   * 
93   * <p>
94   * To convert a string into milliseconds:
95   * 
96   * <pre>
97   * new Duration(str).getTime()
98   * </pre>; to convert a time in milliseconds into a string:
99   * 
100  * <pre>
101  * new Duration(time).toString()
102  * </pre>.
103  * </p>
104  * 
105  * @author Tom Gibara
106  */
107 
108 public final class Duration implements Comparable, Serializable {
109 
110     // statics
111 
112     /**
113      * Serialization id.
114      */
115     private static final long serialVersionUID = -9128850324443225029L;
116 
117     /**
118      * The pattern which all string representations are required to match.
119      */
120 
121     private static final Pattern PATTERN = Pattern.compile("\\s*(?:(\\d+)(?:d|day|days)\\s*)?"
122             + "(?:(\\d+)(?:h|hr|hrs|hour|hours)\\s*)?" + "(?:(\\d+)(?:m|min|mins|minute|minutes)\\s*)?"
123             + "(?:(\\d+)(?:s|sec|secs|second|seconds)\\s*)?"
124             + "(?:(\\d+)(?:ms|milli|millis|millisecond|milliseconds)\\s*)?", Pattern.CASE_INSENSITIVE);
125 
126     /**
127      * Converts a field from the string constructor into a long.
128      * 
129      * @param matcher
130      *            a matcher which has matched PATTERN
131      * @param index
132      *            the captured group containing a long value
133      * @param name
134      *            the name of the duration field for error reporting
135      * @return the index group of the matcher as a long
136      * 
137      * @throws IllegalArgumentException
138      *             if the captured group is not parsable as a long (this should
139      *             be impossible)
140      */
141 
142     private static long parseLong(final Matcher matcher, final int index, final String name)
143             throws IllegalArgumentException {
144         String str = matcher.group(index);
145         try {
146             return str == null ? 0L : Long.parseLong(str);
147         } catch (NumberFormatException e) {
148             throw new IllegalArgumentException(String.format("%s not a number: %s", name, str), e);
149         }
150     }
151 
152     // fields
153 
154     /**
155      * The string which was passed to the constructor, or a generated value
156      * which would evaluate to the number supplied to the long constructor.
157      */
158 
159     private String string;
160 
161     /**
162      * The long which was passed to the constructor, or the evaluation of the
163      * string which was supplied to the constructor.
164      */
165     private long time;
166 
167     // constructors
168 
169     /**
170      * Creates a duration from its string representation.
171      * 
172      * @param string
173      *            the string representation of a duration
174      * @throws IllegalArgumentException
175      *             if the string is null or does not match the required format.
176      */
177 
178     public Duration(final String string) throws IllegalArgumentException {
179         Arguments.notNull(string, "string");
180         Matcher matcher = PATTERN.matcher(string);
181         if (!matcher.matches()) throw new IllegalArgumentException(String.format("%s does not match %s", "string",
182                 matcher.pattern().pattern()));
183 
184         long days = parseLong(matcher, 1, "days");
185         long hours = parseLong(matcher, 2, "hours");
186         long minutes = parseLong(matcher, 3, "minutes");
187         long seconds = parseLong(matcher, 4, "seconds");
188         long milliseconds = parseLong(matcher, 5, "milliseconds");
189 
190         this.time = milliseconds + 1000L * (seconds + 60L * (minutes + 60L * (hours + (24L * days))));
191         this.string = string;
192     }
193 
194     /**
195      * Creates a duration from a number of milliseconds.
196      * 
197      * @param time
198      *            a time in milliseconds
199      * @throws IllegalArgumentException
200      *             if the supplied time is negative
201      */
202 
203     public Duration(final long time) throws IllegalArgumentException {
204         Arguments.notNegative(time, "time");
205         long t = time;
206         long milliseconds = t % 1000L;
207         t /= 1000L;
208         long seconds = t % 60;
209         t /= 60;
210         long minutes = t % 60;
211         t /= 60;
212         long hours = t % 24;
213         t /= 24;
214         long days = t;
215 
216         if (time == 0L) { // special case - empty string is legal, but
217             // probably not preferred by clients
218             string = "0s";
219         } else {
220             StringBuilder sb = new StringBuilder();
221             if (days > 0L) sb.append(days).append(days == 1L ? "day" : "days");
222             if (hours > 0L) sb.append(hours).append(hours == 1L ? "hour" : "hours");
223             if (minutes > 0L) sb.append(minutes).append(minutes == 1L ? "min" : "mins");
224             if (seconds > 0L) sb.append(seconds).append(seconds == 1L ? "second" : "secs");
225             if (milliseconds > 0L) sb.append(milliseconds).append(milliseconds == 1L ? "milli" : "millis");
226             this.string = sb.toString();
227         }
228         this.time = time;
229     }
230 
231     // accessors
232 
233     /**
234      * The time which was passed to the constructor, or in the case that a
235      * string was supplied, the number of milliseconds to which the string
236      * equates.
237      * 
238      * @return the duration in milliseconds
239      */
240 
241     public long getTime() {
242         return time;
243     }
244 
245     // comparable methods
246 
247     /**
248      * Durations ordered by time.
249      * 
250      * @param obj
251      *            a duration object
252      * @return this is before, at or after obj
253      */
254 
255     public int compareTo(final Object obj) {
256         if (obj == this) return 0;
257         Duration that = (Duration) obj;
258         return this.time < that.time ? -1 : this.time == that.time ? 0 : 1;
259     }
260 
261     // object methods
262 
263     /**
264      * Equality is predicated on the millisecond time field. That is, two
265      * durations are equal if the value returned from their getTime() methods is
266      * equal.
267      * 
268      * @param obj
269      *            the object to test for equality
270      * @return whether both objects represent the same duration
271      */
272 
273     @Override
274     public boolean equals(final Object obj) {
275         if (obj == this) return true;
276         if (!(obj instanceof Duration)) return false;
277         Duration that = (Duration) obj;
278         return this.time == that.time;
279     }
280 
281     /**
282      * @return hashcode for this object based on the time
283      */
284 
285     @Override
286     public int hashCode() {
287         return (int) (time ^ (time >>> 32));
288     }
289 
290     /**
291      * @return the string which was passed to the constructor or, in the case
292      *         that a time was supplied, a string which evalutes to an equal
293      *         duration.
294      */
295 
296     @Override
297     public String toString() {
298         return string;
299     }
300 
301 }