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.Set;
036import java.util.TreeSet;
037import java.util.concurrent.TimeUnit;
038
039import org.forgerock.util.Reject;
040
041/**
042 * Represents a duration in english. Cases is not important, plurals units are accepted.
043 * Notice that negative durations are not supported.
044 *
045 * <code>
046 *     6 days
047 *     59 minutes and 1 millisecond
048 *     1 minute and 10 seconds
049 *     42 millis
050 *     unlimited
051 *     none
052 *     zero
053 * </code>
054 */
055public class Duration implements Comparable<Duration> {
056
057    /**
058     * Special duration that represents an unlimited duration (or indefinite).
059     */
060    public static final Duration UNLIMITED = new Duration();
061
062    /**
063     * Special duration that represents a zero-length duration.
064     */
065    public static final Duration ZERO = new Duration(0L, SECONDS);
066
067    /**
068     * Tokens that represents the unlimited duration.
069     */
070    private static final Set<String> UNLIMITED_TOKENS = new TreeSet<>(
071            String.CASE_INSENSITIVE_ORDER);
072    static {
073        UNLIMITED_TOKENS.addAll(asList("unlimited", "indefinite", "infinity", "undefined", "none"));
074    }
075
076    /**
077     * Tokens that represents the zero duration.
078     */
079    private static final Set<String> ZERO_TOKENS = new TreeSet<>(
080            String.CASE_INSENSITIVE_ORDER);
081    static {
082        ZERO_TOKENS.addAll(asList("zero", "disabled"));
083    }
084
085    private long number;
086    private TimeUnit unit;
087
088    /**
089     * Hidden constructor that creates an unlimited duration. The intention is that the only instance representing
090     * unlimited is {@link #UNLIMITED}.
091     */
092    private Duration() {
093        this.number = Long.MAX_VALUE;
094        this.unit = null;
095    }
096
097    /**
098     * Builds a new {@code Duration}.
099     *
100     * @param number number of time unit (cannot be {@literal null}).
101     * @param unit TimeUnit to express the duration in (cannot be {@literal null}).
102     * @deprecated Prefer the use of {@link #duration(long, TimeUnit)}.
103     */
104    @Deprecated
105    public Duration(final Long number, final TimeUnit unit) {
106        Reject.ifTrue(number < 0, "Negative durations are not supported");
107        this.number = number;
108        this.unit = checkNotNull(unit);
109    }
110
111    /**
112     * Provides a {@code Duration}, given a number and time unit.
113     *
114     * @param number number of time unit.
115     * @param unit TimeUnit to express the duration in (cannot be {@literal null}).
116     * @return {@code Duration} instance
117     */
118    public static Duration duration(final long number, final TimeUnit unit) {
119        if (number == 0) {
120            return ZERO;
121        }
122        return new Duration(number, unit);
123    }
124
125    /**
126     * Provides a {@code Duration} that represents the given duration expressed in english.
127     *
128     * @param value
129     *         natural speech duration
130     * @return {@code Duration} instance
131     * @throws IllegalArgumentException
132     *         if the input string is incorrectly formatted.
133     */
134    public static Duration duration(final String value) {
135        List<Duration> composite = new ArrayList<>();
136
137        // Split around ',' and ' and ' patterns
138        String[] fragments = value.split(",| and ");
139
140        // If there is only 1 fragment and that it matches the recognized "unlimited" tokens
141        if (fragments.length == 1) {
142            String trimmed = fragments[0].trim();
143            if (UNLIMITED_TOKENS.contains(trimmed)) {
144                // Unlimited Duration
145                return UNLIMITED;
146            } else if (ZERO_TOKENS.contains(trimmed)) {
147                // Zero-length Duration
148                return ZERO;
149            }
150        }
151
152        // Build the normal duration
153        for (String fragment : fragments) {
154            fragment = fragment.trim();
155
156            if ("".equals(fragment)) {
157                throw new IllegalArgumentException("Cannot parse empty duration, expecting '<value> <unit>' pattern");
158            }
159
160            // Parse the number part
161            int i = 0;
162            StringBuilder numberSB = new StringBuilder();
163            while (Character.isDigit(fragment.charAt(i))) {
164                numberSB.append(fragment.charAt(i));
165                i++;
166            }
167
168            // Ignore whitespace
169            while (Character.isWhitespace(fragment.charAt(i))) {
170                i++;
171            }
172
173            // Parse the time unit part
174            StringBuilder unitSB = new StringBuilder();
175            while ((i < fragment.length()) && Character.isLetter(fragment.charAt(i))) {
176                unitSB.append(fragment.charAt(i));
177                i++;
178            }
179            Long number = Long.valueOf(numberSB.toString());
180            TimeUnit unit = parseTimeUnit(unitSB.toString());
181
182            composite.add(new Duration(number, unit));
183        }
184
185        // Merge components of the composite together
186        Duration duration = new Duration(0L, DAYS);
187        for (Duration elements : composite) {
188            duration.merge(elements);
189        }
190
191        // If someone used '0 ms' for example
192        if (duration.number == 0L) {
193            return ZERO;
194        }
195
196        return duration;
197    }
198
199    /**
200     * Aggregates this Duration with the given Duration. Littlest {@link TimeUnit} will be used as a common ground.
201     *
202     * @param duration
203     *         other Duration
204     */
205    private void merge(final Duration duration) {
206        if (!isUnlimited() && !duration.isUnlimited()) {
207            // find littlest unit
208            // conversion will happen on the littlest unit otherwise we loose details
209            if (unit.ordinal() > duration.unit.ordinal()) {
210                // Other duration is smaller than me
211                number = duration.unit.convert(number, unit) + duration.number;
212                unit = duration.unit;
213            } else {
214                // Other duration is greater than me
215                number = unit.convert(duration.number, duration.unit) + number;
216            }
217        }
218    }
219
220    private static final Map<String, TimeUnit> TIME_UNITS = new HashMap<>();
221    static {
222        for (String days : asList("days", "day", "d")) {
223            TIME_UNITS.put(days, DAYS);
224        }
225        for (String hours : asList("hours", "hour", "h")) {
226            TIME_UNITS.put(hours, HOURS);
227        }
228        for (String minutes : asList("minutes", "minute", "min", "m")) {
229            TIME_UNITS.put(minutes, MINUTES);
230        }
231        for (String seconds : asList("seconds", "second", "sec", "s")) {
232            TIME_UNITS.put(seconds, SECONDS);
233        }
234        for (String ms : asList("milliseconds", "millisecond", "millisec", "millis", "milli", "ms")) {
235            TIME_UNITS.put(ms, MILLISECONDS);
236        }
237        for (String us : asList("microseconds", "microsecond", "microsec", "micros", "micro", "us")) {
238            TIME_UNITS.put(us, MICROSECONDS);
239        }
240        for (String ns : asList("nanoseconds", "nanosecond", "nanosec", "nanos", "nano", "ns")) {
241            TIME_UNITS.put(ns, NANOSECONDS);
242        }
243    }
244
245    /**
246     * Parse the given input string as a {@link TimeUnit}.
247     */
248    private static TimeUnit parseTimeUnit(final String unit) {
249        final String lowercase = unit.toLowerCase(Locale.ENGLISH);
250        final TimeUnit timeUnit = TIME_UNITS.get(lowercase);
251        if (timeUnit != null) {
252            return timeUnit;
253        }
254        throw new IllegalArgumentException(format("TimeUnit %s is not recognized", unit));
255    }
256
257    /**
258     * Returns the number of {@link TimeUnit} this duration represents.
259     *
260     * @return the number of {@link TimeUnit} this duration represents.
261     */
262    public long getValue() {
263        return number;
264    }
265
266    /**
267     * Returns the {@link TimeUnit} this duration is expressed in.
268     *
269     * @return the {@link TimeUnit} this duration is expressed in.
270     */
271    public TimeUnit getUnit() {
272        if (isUnlimited()) {
273            // UNLIMITED originally had TimeUnit.DAYS, so preserve API semantics
274            return TimeUnit.DAYS;
275        }
276        return unit;
277    }
278
279    /**
280     * Convert the current duration to a given {@link TimeUnit}.
281     * Conversions from finer to coarser granularities truncate, so loose precision.
282     *
283     * @param targetUnit
284     *         target unit of the conversion.
285     * @return converted duration
286     * @see TimeUnit#convert(long, TimeUnit)
287     */
288    public Duration convertTo(TimeUnit targetUnit) {
289        if (isUnlimited() || isZero()) {
290            return this;
291        }
292        return new Duration(to(targetUnit), targetUnit);
293    }
294
295    /**
296     * Convert the current duration to a number of given {@link TimeUnit}.
297     * Conversions from finer to coarser granularities truncate, so loose precision.
298     *
299     * @param targetUnit
300     *         target unit of the conversion.
301     * @return converted duration value
302     * @see TimeUnit#convert(long, TimeUnit)
303     */
304    public long to(TimeUnit targetUnit) {
305        if (isUnlimited()) {
306            return number;
307        }
308        return targetUnit.convert(number, unit);
309    }
310
311    /**
312     * Returns {@literal true} if this Duration represents an unlimited (or indefinite) duration.
313     *
314     * @return {@literal true} if this Duration represents an unlimited duration.
315     */
316    public boolean isUnlimited() {
317        return this == UNLIMITED;
318    }
319
320    /**
321     * Returns {@literal true} if this Duration represents a zero-length duration.
322     *
323     * @return {@literal true} if this Duration represents a zero-length duration.
324     */
325    public boolean isZero() {
326        return number == 0;
327    }
328
329    @Override
330    public String toString() {
331        if (isUnlimited()) {
332            return "UNLIMITED";
333        }
334        if (isZero()) {
335            return "ZERO";
336        }
337        return number + " " + unit;
338    }
339
340    @Override
341    public int compareTo(Duration that) {
342        if (this.isUnlimited()) {
343            if (that.isUnlimited()) {
344                // unlimited == unlimited
345                return 0;
346            } else {
347                // unlimited > any value
348                return 1;
349            }
350        }
351        if (that.isUnlimited()) {
352            // any value > unlimited
353            return -1;
354        }
355        if (this.isZero()) {
356            if (that.isZero()) {
357                // 0 == 0
358                return 0;
359            } else {
360                // 0 > any value
361                return -1;
362            }
363        }
364        if (that.isZero()) {
365            // any value > 0
366            return 1;
367        }
368
369        // No special case so let's convert using the smallest unit and check if the biggest duration overflowed
370        // or not during the conversion.
371        final int unitCompare = this.getUnit().compareTo(that.getUnit());
372        final boolean biggestOverflowed;
373        final long thisConverted, thatConverted;
374        if (unitCompare > 0) {
375            thisConverted = this.convertTo(that.getUnit()).getValue();
376            thatConverted = that.getValue();
377            biggestOverflowed = thisConverted == Long.MAX_VALUE;
378        } else if (unitCompare < 0) {
379            thisConverted = this.getValue();
380            thatConverted = that.convertTo(this.getUnit()).getValue();
381            biggestOverflowed = thatConverted == Long.MAX_VALUE;
382        } else {
383            // unitCompare == 0 : both durations are in the same units
384            // No conversion was done so the biggest can't have been overflowed.
385            biggestOverflowed = false;
386            thisConverted = this.getValue();
387            thatConverted = that.getValue();
388        }
389
390
391        return !biggestOverflowed ? Long.compare(thisConverted, thatConverted) : unitCompare;
392    }
393}