1/*
2 * Copyright (C) 2011 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.caliper.util;
18
19import static com.google.common.base.Preconditions.checkArgument;
20import static com.google.common.base.Preconditions.checkNotNull;
21
22import com.google.common.base.Ascii;
23import com.google.common.collect.ImmutableListMultimap;
24import com.google.common.collect.ImmutableMap;
25import com.google.common.collect.Maps;
26import com.google.common.primitives.Longs;
27
28import java.math.BigDecimal;
29import java.math.MathContext;
30import java.math.RoundingMode;
31import java.util.Collections;
32import java.util.Map;
33import java.util.concurrent.TimeUnit;
34import java.util.regex.Matcher;
35import java.util.regex.Pattern;
36
37import javax.annotation.Nullable;
38
39/**
40 * Represents a nonnegative duration from 0 to 100 days, with picosecond precision.
41 * Contrast with Joda-Time's duration class, which has only millisecond precision but can
42 * represent durations of millions of years.
43 */
44public abstract class ShortDuration implements Comparable<ShortDuration> {
45  // Factories
46
47  public static ShortDuration of(long duration, TimeUnit unit) {
48    if (duration == 0) {
49      return ZERO;
50    }
51    checkArgument(duration >= 0, "negative duration: %s", duration);
52    checkArgument(duration <= MAXES.get(unit),
53        "ShortDuration cannot exceed 100 days: %s %s", duration, unit);
54    long nanos = TimeUnit.NANOSECONDS.convert(duration, unit);
55    return new PositiveShortDuration(nanos * 1000);
56  }
57
58  public static ShortDuration of(BigDecimal duration, TimeUnit unit) {
59    // convert to picoseconds first, to minimize rounding
60    BigDecimal picos = duration.multiply(ONE_IN_PICOS.get(unit));
61    return ofPicos(toLong(picos, RoundingMode.HALF_UP));
62  }
63
64  public static ShortDuration valueOf(String s) {
65    if ("0".equals(s)) {
66      return ZERO;
67    }
68    Matcher matcher = PATTERN.matcher(s);
69    checkArgument(matcher.matches(), "Invalid ShortDuration: %s", s);
70
71    BigDecimal value = new BigDecimal(matcher.group(1));
72    String abbrev = matcher.group(2);
73    TimeUnit unit = ABBREV_TO_UNIT.get(abbrev);
74    checkArgument(unit != null, "Unrecognized time unit: %s", abbrev);
75
76    return of(value, unit);
77  }
78
79  public static ShortDuration zero() {
80    return ZERO;
81  }
82
83  // fortunately no abbreviation starts with 'e', so this should work
84  private static final Pattern PATTERN = Pattern.compile("^([0-9.eE+-]+) ?(\\S+)$");
85
86  private static ShortDuration ofPicos(long picos) {
87    if (picos == 0) {
88      return ZERO;
89    }
90    checkArgument(picos > 0);
91    return new PositiveShortDuration(picos);
92  }
93
94  // TODO(kevinb): we sure seem to convert back and forth with BigDecimal a lot.
95  // Why not just *make* this a BigDecimal?
96  final long picos;
97
98  ShortDuration(long picos) {
99    this.picos = picos;
100  }
101
102  public long toPicos() {
103    return picos;
104  }
105
106  public long to(TimeUnit unit) {
107    return to(unit, RoundingMode.HALF_UP);
108  }
109
110  public abstract long to(TimeUnit unit, RoundingMode roundingMode);
111
112  /*
113   * In Guava, this will probably implement an interface called Quantity, and the following methods
114   * will come from there, so they won't have to be defined here.
115   */
116
117  /**
118   * Returns an instance of this type that represents the sum of this value and {@code
119   * addend}.
120   */
121  public abstract ShortDuration plus(ShortDuration addend);
122
123  /**
124   * Returns an instance of this type that represents the difference of this value and
125   * {@code subtrahend}.
126   */
127  public abstract ShortDuration minus(ShortDuration subtrahend);
128
129  /**
130   * Returns an instance of this type that represents the product of this value and the
131   * integral value {@code multiplicand}.
132   */
133  public abstract ShortDuration times(long multiplicand);
134
135  /**
136   * Returns an instance of this type that represents the product of this value and {@code
137   * multiplicand}, rounded according to {@code roundingMode} if necessary.
138   *
139   * <p>If this class represents an amount that is "continuous" rather than discrete, the
140   * implementation of this method may simply ignore the rounding mode.
141   */
142  public abstract ShortDuration times(BigDecimal multiplicand, RoundingMode roundingMode);
143
144  /**
145   * Returns an instance of this type that represents this value divided by the integral
146   * value {@code divisor}, rounded according to {@code roundingMode} if necessary.
147   *
148   * <p>If this class represents an amount that is "continuous" rather than discrete, the
149   * implementation of this method may simply ignore the rounding mode.
150   */
151  public abstract ShortDuration dividedBy(long divisor, RoundingMode roundingMode);
152
153  /**
154   * Returns an instance of this type that represents this value divided by {@code
155   * divisor}, rounded according to {@code roundingMode} if necessary.
156   *
157   * <p>If this class represents an amount that is "continuous" rather than discrete, the
158   * implementation of this method may simply ignore the rounding mode.
159   */
160  public abstract ShortDuration dividedBy(BigDecimal divisor, RoundingMode roundingMode);
161
162  // Zero
163
164  private static ShortDuration ZERO = new ShortDuration(0) {
165    @Override public long to(TimeUnit unit, RoundingMode roundingMode) {
166      return 0;
167    }
168    @Override public ShortDuration plus(ShortDuration addend) {
169      return addend;
170    }
171    @Override public ShortDuration minus(ShortDuration subtrahend) {
172      checkArgument(this == subtrahend);
173      return this;
174    }
175    @Override public ShortDuration times(long multiplicand) {
176      return this;
177    }
178    @Override public ShortDuration times(BigDecimal multiplicand, RoundingMode roundingMode) {
179      return this;
180    }
181    @Override public ShortDuration dividedBy(long divisor, RoundingMode roundingMode) {
182      return dividedBy(new BigDecimal(divisor), roundingMode);
183    }
184    @Override public ShortDuration dividedBy(BigDecimal divisor, RoundingMode roundingMode) {
185      checkArgument(divisor.compareTo(BigDecimal.ZERO) != 0);
186      return this;
187    }
188    @Override public int compareTo(ShortDuration that) {
189      if (this == that) {
190        return 0;
191      }
192      checkNotNull(that);
193      return -1;
194    }
195    @Override public boolean equals(@Nullable Object that) {
196      return this == that;
197    }
198    @Override public int hashCode() {
199      return 0;
200    }
201    @Override public String toString() {
202      return "0s";
203    }
204  };
205
206  // Non-zero
207
208  private static class PositiveShortDuration extends ShortDuration {
209    private PositiveShortDuration(long picos) {
210      super(picos);
211      checkArgument(picos > 0);
212    }
213
214    @Override public long to(TimeUnit unit, RoundingMode roundingMode) {
215      BigDecimal divisor = ONE_IN_PICOS.get(unit);
216      return toLong(new BigDecimal(picos).divide(divisor), roundingMode);
217    }
218
219    @Override public ShortDuration plus(ShortDuration addend) {
220      return new PositiveShortDuration(picos + addend.picos);
221    }
222
223    @Override public ShortDuration minus(ShortDuration subtrahend) {
224      return ofPicos(picos - subtrahend.picos);
225    }
226
227    @Override public ShortDuration times(long multiplicand) {
228      if (multiplicand == 0) {
229        return ZERO;
230      }
231      checkArgument(multiplicand >= 0, "negative multiplicand: %s", multiplicand);
232      checkArgument(multiplicand <= Long.MAX_VALUE / picos,
233          "product of %s and %s would overflow", this, multiplicand);
234      return new PositiveShortDuration(picos * multiplicand);
235    }
236
237    @Override public ShortDuration times(BigDecimal multiplicand, RoundingMode roundingMode) {
238      BigDecimal product = BigDecimal.valueOf(picos).multiply(multiplicand);
239      return ofPicos(toLong(product, roundingMode));
240    }
241
242    @Override public ShortDuration dividedBy(long divisor, RoundingMode roundingMode) {
243      return dividedBy(new BigDecimal(divisor), roundingMode);
244    }
245
246    @Override public ShortDuration dividedBy(BigDecimal divisor, RoundingMode roundingMode) {
247      BigDecimal product = BigDecimal.valueOf(picos).divide(divisor, roundingMode);
248      return ofPicos(product.longValueExact());
249    }
250
251    @Override public int compareTo(ShortDuration that) {
252      return Longs.compare(this.picos, that.picos);
253    }
254
255    @Override public boolean equals(Object object) {
256      if (object instanceof PositiveShortDuration) {
257        PositiveShortDuration that = (PositiveShortDuration) object;
258        return this.picos == that.picos;
259      }
260      return false;
261    }
262
263    @Override public int hashCode() {
264      return Longs.hashCode(picos);
265    }
266
267    @Override public String toString() {
268      TimeUnit bestUnit = TimeUnit.NANOSECONDS;
269      for (TimeUnit unit : TimeUnit.values()) {
270        if (picosIn(unit) > picos) {
271          break;
272        }
273        bestUnit = unit;
274      }
275      BigDecimal divisor = ONE_IN_PICOS.get(bestUnit);
276
277      return new BigDecimal(picos).divide(divisor, ROUNDER) + preferredAbbrev(bestUnit);
278    }
279
280    private static final MathContext ROUNDER = new MathContext(4);
281  }
282
283  // Private parts
284
285  private static String preferredAbbrev(TimeUnit bestUnit) {
286    return ABBREVIATIONS.get(bestUnit).get(0);
287  }
288
289  private static final ImmutableListMultimap<TimeUnit, String> ABBREVIATIONS =
290      createAbbreviations();
291
292  private static ImmutableListMultimap<TimeUnit, String> createAbbreviations() {
293    ImmutableListMultimap.Builder<TimeUnit, String> builder = ImmutableListMultimap.builder();
294    builder.putAll(TimeUnit.NANOSECONDS, "ns", "nanos");
295    builder.putAll(TimeUnit.MICROSECONDS, "\u03bcs" /*μs*/, "us", "micros");
296    builder.putAll(TimeUnit.MILLISECONDS, "ms", "millis");
297    builder.putAll(TimeUnit.SECONDS, "s", "sec");
298
299    // Do the rest in a JDK5-safe way
300    TimeUnit[] allUnits = TimeUnit.values();
301    if (allUnits.length >= 7) {
302      builder.putAll(allUnits[4], "m", "min");
303      builder.putAll(allUnits[5], "h", "hr");
304      builder.putAll(allUnits[6], "d");
305    }
306
307    for (TimeUnit unit : TimeUnit.values()) {
308      builder.put(unit, Ascii.toLowerCase(unit.name()));
309    }
310    return builder.build();
311  }
312
313  private static final Map<String, TimeUnit> ABBREV_TO_UNIT = createAbbrevToUnitMap();
314
315  private static Map<String, TimeUnit> createAbbrevToUnitMap() {
316    ImmutableMap.Builder<String, TimeUnit> builder = ImmutableMap.builder();
317    for (Map.Entry<TimeUnit, String> entry : ABBREVIATIONS.entries()) {
318      builder.put(entry.getValue(), entry.getKey());
319    }
320    return builder.build();
321  }
322
323  private static final Map<TimeUnit, BigDecimal> ONE_IN_PICOS = createUnitToPicosMap();
324
325  private static Map<TimeUnit, BigDecimal> createUnitToPicosMap() {
326    Map<TimeUnit, BigDecimal> map = Maps.newEnumMap(TimeUnit.class);
327    for (TimeUnit unit : TimeUnit.values()) {
328      map.put(unit, new BigDecimal(picosIn(unit)));
329    }
330    return Collections.unmodifiableMap(map);
331  }
332
333  private static final Map<TimeUnit, Long> MAXES = createMaxesMap();
334
335  private static Map<TimeUnit, Long> createMaxesMap() {
336    Map<TimeUnit, Long> map = Maps.newEnumMap(TimeUnit.class);
337    for (TimeUnit unit : TimeUnit.values()) {
338      // Max is 100 days
339      map.put(unit, unit.convert(100L * 24 * 60 * 60, TimeUnit.SECONDS));
340    }
341    return Collections.unmodifiableMap(map);
342  }
343
344  private static long toLong(BigDecimal bd, RoundingMode roundingMode) {
345    // setScale does not really mutate the BigDecimal
346    return bd.setScale(0, roundingMode).longValueExact();
347  }
348
349  private static long picosIn(TimeUnit unit) {
350    return unit.toNanos(1000);
351  }
352}
353