001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2014-2016 ForgeRock AS.
015 */
016
017package org.forgerock.util.time;
018
019import static java.lang.String.format;
020import static java.util.Arrays.asList;
021import static java.util.concurrent.TimeUnit.DAYS;
022import static java.util.concurrent.TimeUnit.HOURS;
023import static java.util.concurrent.TimeUnit.MICROSECONDS;
024import static java.util.concurrent.TimeUnit.MILLISECONDS;
025import static java.util.concurrent.TimeUnit.MINUTES;
026import static java.util.concurrent.TimeUnit.NANOSECONDS;
027import static java.util.concurrent.TimeUnit.SECONDS;
028import static org.forgerock.util.Reject.checkNotNull;
029
030import java.util.ArrayList;
031import java.util.HashMap;
032import java.util.List;
033import java.util.Locale;
034import java.util.Map;
035import java.util.Objects;
036import java.util.Set;
037import java.util.TreeSet;
038import java.util.concurrent.TimeUnit;
039
040import org.forgerock.util.Reject;
041
042/**
043 * Represents a duration in english. Cases is not important, plurals units are accepted.
044 * Notice that negative durations are not supported.
045 *
046 * <code>
047 *     6 days
048 *     59 minutes and 1 millisecond
049 *     1 minute and 10 seconds
050 *     42 millis
051 *     unlimited
052 *     none
053 *     zero
054 * </code>
055 */
056public class Duration implements Comparable<Duration> {
057
058    /**
059     * Special duration that represents an unlimited duration (or indefinite).
060     */
061    public static final Duration UNLIMITED = new Duration();
062
063    /**
064     * Special duration that represents a zero-length duration.
065     */
066    public static final Duration ZERO = new Duration(0L, SECONDS);
067
068    /**
069     * Tokens that represents the unlimited duration.
070     */
071    private static final Set<String> UNLIMITED_TOKENS = new TreeSet<>(
072            String.CASE_INSENSITIVE_ORDER);
073    static {
074        UNLIMITED_TOKENS.addAll(asList("unlimited", "indefinite", "infinity", "undefined", "none"));
075    }
076
077    /**
078     * Tokens that represents the zero duration.
079     */
080    private static final Set<String> ZERO_TOKENS = new TreeSet<>(
081            String.CASE_INSENSITIVE_ORDER);
082    static {
083        ZERO_TOKENS.addAll(asList("zero", "disabled"));
084    }
085
086    private long number;
087    private TimeUnit unit;
088
089    /**
090     * Hidden constructor that creates an unlimited duration. The intention is that the only instance representing
091     * unlimited is {@link #UNLIMITED}.
092     */
093    private Duration() {
094        this.number = Long.MAX_VALUE;
095        this.unit = null;
096    }
097
098    /**
099     * 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}