View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2014-2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.util.time;
18  
19  import static java.lang.String.format;
20  import static java.util.Arrays.asList;
21  import static java.util.concurrent.TimeUnit.DAYS;
22  import static java.util.concurrent.TimeUnit.HOURS;
23  import static java.util.concurrent.TimeUnit.MICROSECONDS;
24  import static java.util.concurrent.TimeUnit.MILLISECONDS;
25  import static java.util.concurrent.TimeUnit.MINUTES;
26  import static java.util.concurrent.TimeUnit.NANOSECONDS;
27  import static java.util.concurrent.TimeUnit.SECONDS;
28  import static org.forgerock.util.Reject.checkNotNull;
29  
30  import java.util.ArrayList;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Map;
35  import java.util.Objects;
36  import java.util.Set;
37  import java.util.TreeSet;
38  import java.util.concurrent.TimeUnit;
39  
40  import org.forgerock.util.Reject;
41  
42  /**
43   * Represents a duration in english. Cases is not important, plurals units are accepted.
44   * Notice that negative durations are not supported.
45   *
46   * <code>
47   *     6 days
48   *     59 minutes and 1 millisecond
49   *     1 minute and 10 seconds
50   *     42 millis
51   *     unlimited
52   *     none
53   *     zero
54   * </code>
55   */
56  public class Duration implements Comparable<Duration> {
57  
58      /**
59       * Special duration that represents an unlimited duration (or indefinite).
60       */
61      public static final Duration UNLIMITED = new Duration();
62  
63      /**
64       * Special duration that represents a zero-length duration.
65       */
66      public static final Duration ZERO = new Duration(0L, SECONDS);
67  
68      /**
69       * Tokens that represents the unlimited duration.
70       */
71      private static final Set<String> UNLIMITED_TOKENS = new TreeSet<>(
72              String.CASE_INSENSITIVE_ORDER);
73      static {
74          UNLIMITED_TOKENS.addAll(asList("unlimited", "indefinite", "infinity", "undefined", "none"));
75      }
76  
77      /**
78       * Tokens that represents the zero duration.
79       */
80      private static final Set<String> ZERO_TOKENS = new TreeSet<>(
81              String.CASE_INSENSITIVE_ORDER);
82      static {
83          ZERO_TOKENS.addAll(asList("zero", "disabled"));
84      }
85  
86      private long number;
87      private TimeUnit unit;
88  
89      /**
90       * Hidden constructor that creates an unlimited duration. The intention is that the only instance representing
91       * unlimited is {@link #UNLIMITED}.
92       */
93      private Duration() {
94          this.number = Long.MAX_VALUE;
95          this.unit = null;
96      }
97  
98      /**
99       * Builds a new {@code Duration}.
100      *
101      * @param number number of time unit (cannot be {@literal null}).
102      * @param unit TimeUnit to express the duration in (cannot be {@literal null}).
103      * @deprecated Prefer the use of {@link #duration(long, TimeUnit)}.
104      */
105     @Deprecated
106     public Duration(final Long number, final TimeUnit unit) {
107         Reject.ifTrue(number < 0, "Negative durations are not supported");
108         this.number = number;
109         this.unit = checkNotNull(unit);
110     }
111 
112     /**
113      * Provides a {@code Duration}, given a number and time unit.
114      *
115      * @param number number of time unit.
116      * @param unit TimeUnit to express the duration in (cannot be {@literal null}).
117      * @return {@code Duration} instance
118      */
119     public static Duration duration(final long number, final TimeUnit unit) {
120         if (number == 0) {
121             return ZERO;
122         }
123         return new Duration(number, unit);
124     }
125 
126     /**
127      * Provides a {@code Duration} that represents the given duration expressed in english.
128      *
129      * @param value
130      *         natural speech duration
131      * @return {@code Duration} instance
132      * @throws IllegalArgumentException
133      *         if the input string is incorrectly formatted.
134      */
135     public static Duration duration(final String value) {
136         List<Duration> composite = new ArrayList<>();
137 
138         // Split around ',' and ' and ' patterns
139         String[] fragments = value.split(",| and ");
140 
141         // If there is only 1 fragment and that it matches the recognized "unlimited" tokens
142         if (fragments.length == 1) {
143             String trimmed = fragments[0].trim();
144             if (UNLIMITED_TOKENS.contains(trimmed)) {
145                 // Unlimited Duration
146                 return UNLIMITED;
147             } else if (ZERO_TOKENS.contains(trimmed)) {
148                 // Zero-length Duration
149                 return ZERO;
150             }
151         }
152 
153         // Build the normal duration
154         for (String fragment : fragments) {
155             fragment = fragment.trim();
156 
157             if ("".equals(fragment)) {
158                 throw new IllegalArgumentException("Cannot parse empty duration, expecting '<value> <unit>' pattern");
159             }
160 
161             // Parse the number part
162             int i = 0;
163             StringBuilder numberSB = new StringBuilder();
164             while (Character.isDigit(fragment.charAt(i))) {
165                 numberSB.append(fragment.charAt(i));
166                 i++;
167             }
168 
169             // Ignore whitespace
170             while (Character.isWhitespace(fragment.charAt(i))) {
171                 i++;
172             }
173 
174             // Parse the time unit part
175             StringBuilder unitSB = new StringBuilder();
176             while ((i < fragment.length()) && Character.isLetter(fragment.charAt(i))) {
177                 unitSB.append(fragment.charAt(i));
178                 i++;
179             }
180             Long number = Long.valueOf(numberSB.toString());
181             TimeUnit unit = parseTimeUnit(unitSB.toString());
182 
183             composite.add(new Duration(number, unit));
184         }
185 
186         // Merge components of the composite together
187         Duration duration = new Duration(0L, DAYS);
188         for (Duration elements : composite) {
189             duration.merge(elements);
190         }
191 
192         // If someone used '0 ms' for example
193         if (duration.number == 0L) {
194             return ZERO;
195         }
196 
197         return duration;
198     }
199 
200     /**
201      * Aggregates this Duration with the given Duration. Littlest {@link TimeUnit} will be used as a common ground.
202      *
203      * @param duration
204      *         other Duration
205      */
206     private void merge(final Duration duration) {
207         if (!isUnlimited() && !duration.isUnlimited()) {
208             // find littlest unit
209             // conversion will happen on the littlest unit otherwise we loose details
210             if (unit.ordinal() > duration.unit.ordinal()) {
211                 // Other duration is smaller than me
212                 number = duration.unit.convert(number, unit) + duration.number;
213                 unit = duration.unit;
214             } else {
215                 // Other duration is greater than me
216                 number = unit.convert(duration.number, duration.unit) + number;
217             }
218         }
219     }
220 
221     private static final Map<String, TimeUnit> TIME_UNITS = new HashMap<>();
222     static {
223         for (String days : asList("days", "day", "d")) {
224             TIME_UNITS.put(days, DAYS);
225         }
226         for (String hours : asList("hours", "hour", "h")) {
227             TIME_UNITS.put(hours, HOURS);
228         }
229         for (String minutes : asList("minutes", "minute", "min", "m")) {
230             TIME_UNITS.put(minutes, MINUTES);
231         }
232         for (String seconds : asList("seconds", "second", "sec", "s")) {
233             TIME_UNITS.put(seconds, SECONDS);
234         }
235         for (String ms : asList("milliseconds", "millisecond", "millisec", "millis", "milli", "ms")) {
236             TIME_UNITS.put(ms, MILLISECONDS);
237         }
238         for (String us : asList("microseconds", "microsecond", "microsec", "micros", "micro", "us", "\u03BCs",
239                 "\u00B5s")) { // the last two support 'mu' and 'micro sign' abbreviations
240             TIME_UNITS.put(us, MICROSECONDS);
241         }
242         for (String ns : asList("nanoseconds", "nanosecond", "nanosec", "nanos", "nano", "ns")) {
243             TIME_UNITS.put(ns, NANOSECONDS);
244         }
245     }
246 
247     /**
248      * Parse the given input string as a {@link TimeUnit}.
249      */
250     private static TimeUnit parseTimeUnit(final String unit) {
251         final String lowercase = unit.toLowerCase(Locale.ENGLISH);
252         final TimeUnit timeUnit = TIME_UNITS.get(lowercase);
253         if (timeUnit != null) {
254             return timeUnit;
255         }
256         throw new IllegalArgumentException(format("TimeUnit %s is not recognized", unit));
257     }
258 
259     /**
260      * Returns the number of {@link TimeUnit} this duration represents.
261      *
262      * @return the number of {@link TimeUnit} this duration represents.
263      */
264     public long getValue() {
265         return number;
266     }
267 
268     /**
269      * Returns the {@link TimeUnit} this duration is expressed in.
270      *
271      * @return the {@link TimeUnit} this duration is expressed in.
272      */
273     public TimeUnit getUnit() {
274         if (isUnlimited()) {
275             // UNLIMITED originally had TimeUnit.DAYS, so preserve API semantics
276             return TimeUnit.DAYS;
277         }
278         return unit;
279     }
280 
281     /**
282      * Convert the current duration to a given {@link TimeUnit}.
283      * Conversions from finer to coarser granularities truncate, so loose precision.
284      *
285      * @param targetUnit
286      *         target unit of the conversion.
287      * @return converted duration
288      * @see TimeUnit#convert(long, TimeUnit)
289      */
290     public Duration convertTo(TimeUnit targetUnit) {
291         if (isUnlimited() || isZero()) {
292             return this;
293         }
294         return new Duration(to(targetUnit), targetUnit);
295     }
296 
297     /**
298      * Convert the current duration to a number of given {@link TimeUnit}.
299      * Conversions from finer to coarser granularities truncate, so loose precision.
300      *
301      * @param targetUnit
302      *         target unit of the conversion.
303      * @return converted duration value
304      * @see TimeUnit#convert(long, TimeUnit)
305      */
306     public long to(TimeUnit targetUnit) {
307         if (isUnlimited()) {
308             return number;
309         }
310         return targetUnit.convert(number, unit);
311     }
312 
313     /**
314      * Returns {@literal true} if this Duration represents an unlimited (or indefinite) duration.
315      *
316      * @return {@literal true} if this Duration represents an unlimited duration.
317      */
318     public boolean isUnlimited() {
319         return this == UNLIMITED;
320     }
321 
322     /**
323      * Returns {@literal true} if this Duration represents a zero-length duration.
324      *
325      * @return {@literal true} if this Duration represents a zero-length duration.
326      */
327     public boolean isZero() {
328         return number == 0;
329     }
330 
331     @Override
332     public String toString() {
333         if (isUnlimited()) {
334             return "UNLIMITED";
335         }
336         if (isZero()) {
337             return "ZERO";
338         }
339         return number + " " + unit;
340     }
341 
342     @Override
343     public int compareTo(Duration that) {
344         if (this.isUnlimited()) {
345             if (that.isUnlimited()) {
346                 // unlimited == unlimited
347                 return 0;
348             } else {
349                 // unlimited > any value
350                 return 1;
351             }
352         }
353         if (that.isUnlimited()) {
354             // any value > unlimited
355             return -1;
356         }
357         if (this.isZero()) {
358             if (that.isZero()) {
359                 // 0 == 0
360                 return 0;
361             } else {
362                 // 0 > any value
363                 return -1;
364             }
365         }
366         if (that.isZero()) {
367             // any value > 0
368             return 1;
369         }
370 
371         // No special case so let's convert using the smallest unit and check if the biggest duration overflowed
372         // or not during the conversion.
373         final int unitCompare = this.getUnit().compareTo(that.getUnit());
374         final boolean biggestOverflowed;
375         final long thisConverted, thatConverted;
376         if (unitCompare > 0) {
377             thisConverted = this.convertTo(that.getUnit()).getValue();
378             thatConverted = that.getValue();
379             biggestOverflowed = thisConverted == Long.MAX_VALUE;
380         } else if (unitCompare < 0) {
381             thisConverted = this.getValue();
382             thatConverted = that.convertTo(this.getUnit()).getValue();
383             biggestOverflowed = thatConverted == Long.MAX_VALUE;
384         } else {
385             // unitCompare == 0 : both durations are in the same units
386             // No conversion was done so the biggest can't have been overflowed.
387             biggestOverflowed = false;
388             thisConverted = this.getValue();
389             thatConverted = that.getValue();
390         }
391 
392 
393         return !biggestOverflowed ? Long.compare(thisConverted, thatConverted) : unitCompare;
394     }
395 
396     @Override
397     public boolean equals(Object other) {
398         if (this == other) {
399             return true;
400         }
401 
402         if (!(other instanceof Duration)) {
403             return false;
404         }
405 
406         Duration duration = (Duration) other;
407         return number == duration.number && unit == duration.unit;
408     }
409 
410     @Override
411     public int hashCode() {
412         return Objects.hash(number, unit);
413     }
414 
415 }