blob: 34d9a2b472c41a23ac1cee3cd18b79a867558694 [file] [log] [blame]
/*
* 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);
}
}