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-2015 ForgeRock AS.
015 */
016
017package org.forgerock.util.time;
018
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Locale;
023import java.util.Map;
024import java.util.Set;
025import java.util.TreeSet;
026import java.util.concurrent.TimeUnit;
027
028import static java.lang.String.format;
029import static java.util.Arrays.asList;
030import static java.util.concurrent.TimeUnit.*;
031import static org.forgerock.util.Reject.checkNotNull;
032
033/**
034 * Represents a duration in english. Cases is not important, plurals units are accepted.
035 * Notice that negative durations are not supported.
036 *
037 * <code>
038 *     6 days
039 *     59 minutes and 1 millisecond
040 *     1 minute and 10 seconds
041 *     42 millis
042 *     unlimited
043 *     none
044 *     zero
045 * </code>
046 */
047public class Duration {
048
049    /**
050     * Special duration that represents an unlimited duration (or indefinite).
051     */
052    private static final Duration UNLIMITED = new Duration(Long.MAX_VALUE, DAYS);
053
054    /**
055     * Special duration that represents a zero-length duration.
056     */
057    private static final Duration ZERO = new Duration(0L, SECONDS);
058
059    /**
060     * Tokens that represents the unlimited duration.
061     */
062    private static final Set<String> UNLIMITED_TOKENS = new TreeSet<>(
063            String.CASE_INSENSITIVE_ORDER);
064    static {
065        UNLIMITED_TOKENS.addAll(asList("unlimited", "indefinite", "infinity", "undefined"));
066    }
067
068    /**
069     * Tokens that represents the zero duration.
070     */
071    private static final Set<String> ZERO_TOKENS = new TreeSet<>(
072            String.CASE_INSENSITIVE_ORDER);
073    static {
074        ZERO_TOKENS.addAll(asList("zero", "disabled"));
075    }
076
077    private Long number;
078    private TimeUnit unit;
079
080    /**
081     * Builds a new Duration.
082     * @param number number of time unit (cannot be {@literal null}).
083     * @param unit TimeUnit to express the duration in (cannot be {@literal null}).
084     */
085    public Duration(final Long number, final TimeUnit unit) {
086        this.number = checkNotNull(number);
087        this.unit = checkNotNull(unit);
088    }
089
090    /**
091     * Builds a new {@link Duration} that will represents the given duration expressed in english.
092     *
093     * @param value
094     *         natural speech duration
095     * @return a new {@link Duration}
096     * @throws IllegalArgumentException
097     *         if the input string is incorrectly formatted.
098     */
099    public static Duration duration(final String value) {
100        List<Duration> composite = new ArrayList<>();
101
102        // Split around ',' and ' and ' patterns
103        String[] fragments = value.split(",| and ");
104
105        // If there is only 1 fragment and that it matches the recognized "unlimited" tokens
106        if (fragments.length == 1) {
107            String trimmed = fragments[0].trim();
108            if (UNLIMITED_TOKENS.contains(trimmed)) {
109                // Unlimited Duration
110                return UNLIMITED;
111            } else if (ZERO_TOKENS.contains(trimmed)) {
112                // Zero-length Duration
113                return ZERO;
114            }
115        }
116
117        // Build the normal duration
118        for (String fragment : fragments) {
119            fragment = fragment.trim();
120
121            if ("".equals(fragment)) {
122                throw new IllegalArgumentException("Cannot parse empty duration, expecting '<value> <unit>' pattern");
123            }
124
125            // Parse the number part
126            int i = 0;
127            StringBuilder numberSB = new StringBuilder();
128            while (Character.isDigit(fragment.charAt(i))) {
129                numberSB.append(fragment.charAt(i));
130                i++;
131            }
132
133            // Ignore whitespace
134            while (Character.isWhitespace(fragment.charAt(i))) {
135                i++;
136            }
137
138            // Parse the time unit part
139            StringBuilder unitSB = new StringBuilder();
140            while ((i < fragment.length()) && Character.isLetter(fragment.charAt(i))) {
141                unitSB.append(fragment.charAt(i));
142                i++;
143            }
144            Long number = Long.valueOf(numberSB.toString());
145            TimeUnit unit = parseTimeUnit(unitSB.toString());
146
147            composite.add(new Duration(number, unit));
148        }
149
150        // Merge components of the composite together
151        Duration duration = new Duration(0L, DAYS);
152        for (Duration elements : composite) {
153            duration.merge(elements);
154        }
155
156        // If someone used '0 ms' for example
157        if (duration.number == 0L) {
158            return ZERO;
159        }
160
161        return duration;
162    }
163
164    /**
165     * Aggregates this Duration with the given Duration. Littlest {@link TimeUnit} will be used as a common ground.
166     *
167     * @param duration
168     *         other Duration
169     */
170    private void merge(final Duration duration) {
171        // find littlest unit
172        // conversion will happen on the littlest unit otherwise we loose details
173        if (unit.ordinal() > duration.unit.ordinal()) {
174            // Other duration is smaller than me
175            number = duration.unit.convert(number, unit) + duration.number;
176            unit = duration.unit;
177        } else {
178            // Other duration is greater than me
179            number = unit.convert(duration.number, duration.unit) + number;
180        }
181    }
182
183    private static Map<String, TimeUnit> TIME_UNITS = new HashMap<>();
184    static {
185        for (String days : asList("days", "day", "d")) {
186            TIME_UNITS.put(days, DAYS);
187        }
188        for (String hours : asList("hours", "hour", "h")) {
189            TIME_UNITS.put(hours, HOURS);
190        }
191        for (String minutes : asList("minutes", "minute", "min", "m")) {
192            TIME_UNITS.put(minutes, MINUTES);
193        }
194        for (String seconds : asList("seconds", "second", "sec", "s")) {
195            TIME_UNITS.put(seconds, SECONDS);
196        }
197        for (String ms : asList("milliseconds", "millisecond", "millisec", "millis", "milli", "ms")) {
198            TIME_UNITS.put(ms, MILLISECONDS);
199        }
200        for (String us : asList("microseconds", "microsecond", "microsec", "micros", "micro", "us")) {
201            TIME_UNITS.put(us, MICROSECONDS);
202        }
203        for (String ns : asList("nanoseconds", "nanosecond", "nanosec", "nanos", "nano", "ns")) {
204            TIME_UNITS.put(ns, NANOSECONDS);
205        }
206    }
207
208    /**
209     * Parse the given input string as a {@link TimeUnit}.
210     */
211    private static TimeUnit parseTimeUnit(final String unit) {
212        final String lowercase = unit.toLowerCase(Locale.ENGLISH);
213        final TimeUnit timeUnit = TIME_UNITS.get(lowercase);
214        if (timeUnit != null) {
215            return timeUnit;
216        }
217        throw new IllegalArgumentException(format("TimeUnit %s is not recognized", unit));
218    }
219
220    /**
221     * Returns the number of {@link TimeUnit} this duration represents.
222     *
223     * @return the number of {@link TimeUnit} this duration represents.
224     */
225    public long getValue() {
226        return number;
227    }
228
229    /**
230     * Returns the {@link TimeUnit} this duration is expressed in.
231     *
232     * @return the {@link TimeUnit} this duration is expressed in.
233     */
234    public TimeUnit getUnit() {
235        return unit;
236    }
237
238    /**
239     * Convert the current duration to a given {@link TimeUnit}.
240     * Conversions from finer to coarser granularities truncate, so loose precision.
241     *
242     * @param targetUnit
243     *         target unit of the conversion.
244     * @return converted duration
245     * @see TimeUnit#convert(long, TimeUnit)
246     */
247    public Duration convertTo(TimeUnit targetUnit) {
248        return new Duration(targetUnit.convert(number, unit), targetUnit);
249    }
250
251    /**
252     * Convert the current duration to a number of given {@link TimeUnit}.
253     * Conversions from finer to coarser granularities truncate, so loose precision.
254     *
255     * @param targetUnit
256     *         target unit of the conversion.
257     * @return converted duration value
258     * @see TimeUnit#convert(long, TimeUnit)
259     */
260    public long to(TimeUnit targetUnit) {
261        return convertTo(targetUnit).getValue();
262    }
263
264    /**
265     * Returns {@literal true} if this Duration represents an unlimited duration.
266     *
267     * @return {@literal true} if this Duration represents an unlimited duration.
268     */
269    public boolean isUnlimited() {
270        return this == UNLIMITED;
271    }
272
273    /**
274     * Returns {@literal true} if this Duration represents a zero-length duration.
275     *
276     * @return {@literal true} if this Duration represents a zero-length duration.
277     */
278    public boolean isZero() {
279        return this == ZERO;
280    }
281
282    @Override
283    public String toString() {
284        return number + " " + unit;
285    }
286
287}