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