Home | History | Annotate | Download | only in util
      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 
     17 package com.google.caliper.util;
     18 
     19 import static com.google.common.base.Preconditions.checkArgument;
     20 import static com.google.common.base.Preconditions.checkNotNull;
     21 
     22 import com.google.common.base.Ascii;
     23 import com.google.common.collect.ImmutableListMultimap;
     24 import com.google.common.collect.ImmutableMap;
     25 import com.google.common.collect.Maps;
     26 import com.google.common.primitives.Longs;
     27 
     28 import java.math.BigDecimal;
     29 import java.math.MathContext;
     30 import java.math.RoundingMode;
     31 import java.util.Collections;
     32 import java.util.Map;
     33 import java.util.concurrent.TimeUnit;
     34 import java.util.regex.Matcher;
     35 import java.util.regex.Pattern;
     36 
     37 import 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  */
     44 public 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