| /* |
| * Copyright (C) 2011 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.google.caliper.util; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import com.google.common.base.Ascii; |
| import com.google.common.collect.ImmutableListMultimap; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Maps; |
| import com.google.common.primitives.Longs; |
| |
| import java.math.BigDecimal; |
| import java.math.MathContext; |
| import java.math.RoundingMode; |
| import java.util.Collections; |
| import java.util.Map; |
| import java.util.concurrent.TimeUnit; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Represents a nonnegative duration from 0 to 100 days, with picosecond precision. |
| * Contrast with Joda-Time's duration class, which has only millisecond precision but can |
| * represent durations of millions of years. |
| */ |
| public abstract class ShortDuration implements Comparable<ShortDuration> { |
| // Factories |
| |
| public static ShortDuration of(long duration, TimeUnit unit) { |
| if (duration == 0) { |
| return ZERO; |
| } |
| checkArgument(duration >= 0, "negative duration: %s", duration); |
| checkArgument(duration <= MAXES.get(unit), |
| "ShortDuration cannot exceed 100 days: %s %s", duration, unit); |
| long nanos = TimeUnit.NANOSECONDS.convert(duration, unit); |
| return new PositiveShortDuration(nanos * 1000); |
| } |
| |
| public static ShortDuration of(BigDecimal duration, TimeUnit unit) { |
| // convert to picoseconds first, to minimize rounding |
| BigDecimal picos = duration.multiply(ONE_IN_PICOS.get(unit)); |
| return ofPicos(toLong(picos, RoundingMode.HALF_UP)); |
| } |
| |
| public static ShortDuration valueOf(String s) { |
| if ("0".equals(s)) { |
| return ZERO; |
| } |
| Matcher matcher = PATTERN.matcher(s); |
| checkArgument(matcher.matches(), "Invalid ShortDuration: %s", s); |
| |
| BigDecimal value = new BigDecimal(matcher.group(1)); |
| String abbrev = matcher.group(2); |
| TimeUnit unit = ABBREV_TO_UNIT.get(abbrev); |
| checkArgument(unit != null, "Unrecognized time unit: %s", abbrev); |
| |
| return of(value, unit); |
| } |
| |
| public static ShortDuration zero() { |
| return ZERO; |
| } |
| |
| // fortunately no abbreviation starts with 'e', so this should work |
| private static final Pattern PATTERN = Pattern.compile("^([0-9.eE+-]+) ?(\\S+)$"); |
| |
| private static ShortDuration ofPicos(long picos) { |
| if (picos == 0) { |
| return ZERO; |
| } |
| checkArgument(picos > 0); |
| return new PositiveShortDuration(picos); |
| } |
| |
| // TODO(kevinb): we sure seem to convert back and forth with BigDecimal a lot. |
| // Why not just *make* this a BigDecimal? |
| final long picos; |
| |
| ShortDuration(long picos) { |
| this.picos = picos; |
| } |
| |
| public long toPicos() { |
| return picos; |
| } |
| |
| public long to(TimeUnit unit) { |
| return to(unit, RoundingMode.HALF_UP); |
| } |
| |
| public abstract long to(TimeUnit unit, RoundingMode roundingMode); |
| |
| /* |
| * In Guava, this will probably implement an interface called Quantity, and the following methods |
| * will come from there, so they won't have to be defined here. |
| */ |
| |
| /** |
| * Returns an instance of this type that represents the sum of this value and {@code |
| * addend}. |
| */ |
| public abstract ShortDuration plus(ShortDuration addend); |
| |
| /** |
| * Returns an instance of this type that represents the difference of this value and |
| * {@code subtrahend}. |
| */ |
| public abstract ShortDuration minus(ShortDuration subtrahend); |
| |
| /** |
| * Returns an instance of this type that represents the product of this value and the |
| * integral value {@code multiplicand}. |
| */ |
| public abstract ShortDuration times(long multiplicand); |
| |
| /** |
| * Returns an instance of this type that represents the product of this value and {@code |
| * multiplicand}, rounded according to {@code roundingMode} if necessary. |
| * |
| * <p>If this class represents an amount that is "continuous" rather than discrete, the |
| * implementation of this method may simply ignore the rounding mode. |
| */ |
| public abstract ShortDuration times(BigDecimal multiplicand, RoundingMode roundingMode); |
| |
| /** |
| * Returns an instance of this type that represents this value divided by the integral |
| * value {@code divisor}, rounded according to {@code roundingMode} if necessary. |
| * |
| * <p>If this class represents an amount that is "continuous" rather than discrete, the |
| * implementation of this method may simply ignore the rounding mode. |
| */ |
| public abstract ShortDuration dividedBy(long divisor, RoundingMode roundingMode); |
| |
| /** |
| * Returns an instance of this type that represents this value divided by {@code |
| * divisor}, rounded according to {@code roundingMode} if necessary. |
| * |
| * <p>If this class represents an amount that is "continuous" rather than discrete, the |
| * implementation of this method may simply ignore the rounding mode. |
| */ |
| public abstract ShortDuration dividedBy(BigDecimal divisor, RoundingMode roundingMode); |
| |
| // Zero |
| |
| private static ShortDuration ZERO = new ShortDuration(0) { |
| @Override public long to(TimeUnit unit, RoundingMode roundingMode) { |
| return 0; |
| } |
| @Override public ShortDuration plus(ShortDuration addend) { |
| return addend; |
| } |
| @Override public ShortDuration minus(ShortDuration subtrahend) { |
| checkArgument(this == subtrahend); |
| return this; |
| } |
| @Override public ShortDuration times(long multiplicand) { |
| return this; |
| } |
| @Override public ShortDuration times(BigDecimal multiplicand, RoundingMode roundingMode) { |
| return this; |
| } |
| @Override public ShortDuration dividedBy(long divisor, RoundingMode roundingMode) { |
| return dividedBy(new BigDecimal(divisor), roundingMode); |
| } |
| @Override public ShortDuration dividedBy(BigDecimal divisor, RoundingMode roundingMode) { |
| checkArgument(divisor.compareTo(BigDecimal.ZERO) != 0); |
| return this; |
| } |
| @Override public int compareTo(ShortDuration that) { |
| if (this == that) { |
| return 0; |
| } |
| checkNotNull(that); |
| return -1; |
| } |
| @Override public boolean equals(@Nullable Object that) { |
| return this == that; |
| } |
| @Override public int hashCode() { |
| return 0; |
| } |
| @Override public String toString() { |
| return "0s"; |
| } |
| }; |
| |
| // Non-zero |
| |
| private static class PositiveShortDuration extends ShortDuration { |
| private PositiveShortDuration(long picos) { |
| super(picos); |
| checkArgument(picos > 0); |
| } |
| |
| @Override public long to(TimeUnit unit, RoundingMode roundingMode) { |
| BigDecimal divisor = ONE_IN_PICOS.get(unit); |
| return toLong(new BigDecimal(picos).divide(divisor), roundingMode); |
| } |
| |
| @Override public ShortDuration plus(ShortDuration addend) { |
| return new PositiveShortDuration(picos + addend.picos); |
| } |
| |
| @Override public ShortDuration minus(ShortDuration subtrahend) { |
| return ofPicos(picos - subtrahend.picos); |
| } |
| |
| @Override public ShortDuration times(long multiplicand) { |
| if (multiplicand == 0) { |
| return ZERO; |
| } |
| checkArgument(multiplicand >= 0, "negative multiplicand: %s", multiplicand); |
| checkArgument(multiplicand <= Long.MAX_VALUE / picos, |
| "product of %s and %s would overflow", this, multiplicand); |
| return new PositiveShortDuration(picos * multiplicand); |
| } |
| |
| @Override public ShortDuration times(BigDecimal multiplicand, RoundingMode roundingMode) { |
| BigDecimal product = BigDecimal.valueOf(picos).multiply(multiplicand); |
| return ofPicos(toLong(product, roundingMode)); |
| } |
| |
| @Override public ShortDuration dividedBy(long divisor, RoundingMode roundingMode) { |
| return dividedBy(new BigDecimal(divisor), roundingMode); |
| } |
| |
| @Override public ShortDuration dividedBy(BigDecimal divisor, RoundingMode roundingMode) { |
| BigDecimal product = BigDecimal.valueOf(picos).divide(divisor, roundingMode); |
| return ofPicos(product.longValueExact()); |
| } |
| |
| @Override public int compareTo(ShortDuration that) { |
| return Longs.compare(this.picos, that.picos); |
| } |
| |
| @Override public boolean equals(Object object) { |
| if (object instanceof PositiveShortDuration) { |
| PositiveShortDuration that = (PositiveShortDuration) object; |
| return this.picos == that.picos; |
| } |
| return false; |
| } |
| |
| @Override public int hashCode() { |
| return Longs.hashCode(picos); |
| } |
| |
| @Override public String toString() { |
| TimeUnit bestUnit = TimeUnit.NANOSECONDS; |
| for (TimeUnit unit : TimeUnit.values()) { |
| if (picosIn(unit) > picos) { |
| break; |
| } |
| bestUnit = unit; |
| } |
| BigDecimal divisor = ONE_IN_PICOS.get(bestUnit); |
| |
| return new BigDecimal(picos).divide(divisor, ROUNDER) + preferredAbbrev(bestUnit); |
| } |
| |
| private static final MathContext ROUNDER = new MathContext(4); |
| } |
| |
| // Private parts |
| |
| private static String preferredAbbrev(TimeUnit bestUnit) { |
| return ABBREVIATIONS.get(bestUnit).get(0); |
| } |
| |
| private static final ImmutableListMultimap<TimeUnit, String> ABBREVIATIONS = |
| createAbbreviations(); |
| |
| private static ImmutableListMultimap<TimeUnit, String> createAbbreviations() { |
| ImmutableListMultimap.Builder<TimeUnit, String> builder = ImmutableListMultimap.builder(); |
| builder.putAll(TimeUnit.NANOSECONDS, "ns", "nanos"); |
| builder.putAll(TimeUnit.MICROSECONDS, "\u03bcs" /*μs*/, "us", "micros"); |
| builder.putAll(TimeUnit.MILLISECONDS, "ms", "millis"); |
| builder.putAll(TimeUnit.SECONDS, "s", "sec"); |
| |
| // Do the rest in a JDK5-safe way |
| TimeUnit[] allUnits = TimeUnit.values(); |
| if (allUnits.length >= 7) { |
| builder.putAll(allUnits[4], "m", "min"); |
| builder.putAll(allUnits[5], "h", "hr"); |
| builder.putAll(allUnits[6], "d"); |
| } |
| |
| for (TimeUnit unit : TimeUnit.values()) { |
| builder.put(unit, Ascii.toLowerCase(unit.name())); |
| } |
| return builder.build(); |
| } |
| |
| private static final Map<String, TimeUnit> ABBREV_TO_UNIT = createAbbrevToUnitMap(); |
| |
| private static Map<String, TimeUnit> createAbbrevToUnitMap() { |
| ImmutableMap.Builder<String, TimeUnit> builder = ImmutableMap.builder(); |
| for (Map.Entry<TimeUnit, String> entry : ABBREVIATIONS.entries()) { |
| builder.put(entry.getValue(), entry.getKey()); |
| } |
| return builder.build(); |
| } |
| |
| private static final Map<TimeUnit, BigDecimal> ONE_IN_PICOS = createUnitToPicosMap(); |
| |
| private static Map<TimeUnit, BigDecimal> createUnitToPicosMap() { |
| Map<TimeUnit, BigDecimal> map = Maps.newEnumMap(TimeUnit.class); |
| for (TimeUnit unit : TimeUnit.values()) { |
| map.put(unit, new BigDecimal(picosIn(unit))); |
| } |
| return Collections.unmodifiableMap(map); |
| } |
| |
| private static final Map<TimeUnit, Long> MAXES = createMaxesMap(); |
| |
| private static Map<TimeUnit, Long> createMaxesMap() { |
| Map<TimeUnit, Long> map = Maps.newEnumMap(TimeUnit.class); |
| for (TimeUnit unit : TimeUnit.values()) { |
| // Max is 100 days |
| map.put(unit, unit.convert(100L * 24 * 60 * 60, TimeUnit.SECONDS)); |
| } |
| return Collections.unmodifiableMap(map); |
| } |
| |
| private static long toLong(BigDecimal bd, RoundingMode roundingMode) { |
| // setScale does not really mutate the BigDecimal |
| return bd.setScale(0, roundingMode).longValueExact(); |
| } |
| |
| private static long picosIn(TimeUnit unit) { |
| return unit.toNanos(1000); |
| } |
| } |