1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
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
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
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) {
217
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
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
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
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 }