blob: 34d9a2b472c41a23ac1cee3cd18b79a867558694 [file] [log] [blame]
Paul Duffine2363012015-11-30 16:20:41 +00001/*
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}