package org.dcache.util; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkArgument; import static java.util.concurrent.TimeUnit.*; /** * Utility classes for dealing with time and durations, mostly with pretty- * printing them for human consumption. */ public class TimeUtils { public static final String TIMESTAMP_FORMAT = "yyyy-MM-dd' 'HH:mm:ss.SSS"; /** * <p>Compares time units such that the larger unit is * ordered before the smaller.</p> */ public static class DecreasingTimeUnitComparator implements Comparator<TimeUnit> { @Override public int compare(TimeUnit unit1, TimeUnit unit2) { if (unit1 == null) { return -1; } if (unit2 == null) { return 1; } long nanos1 = unit1.toNanos(1); long nanos2 = unit2.toNanos(1); if (nanos1 == nanos2) { return 0; } if (nanos1 < nanos2) { return 1; } return -1; } } /** * <p>Parses a duration of a given dimension into other dimensions.</p> * * <p>The parsed out dimensions are held in an internal map.</p> * * <p>There can be gaps between the various time dimensions, but it * is understood that successive calls to parse should be * strictly decreasing. Calls to compute a larger unit than * the current unit will throw an exception.</p> * * <p>The dimensions can be recomputed by calling clear(), but * the original duration given to the parser is fixed.</p> */ public static class DurationParser { private final Map<TimeUnit, Long> durations; private final Long duration; private final TimeUnit durationUnit; private TimeUnit current; private long remainder; public DurationParser(Long duration, TimeUnit durationUnit) { this.duration = Preconditions.checkNotNull(duration, "duration was null"); this.durationUnit = Preconditions.checkNotNull(durationUnit, "durationUnit was null"); durations = new HashMap<>(); remainder = durationUnit.toNanos(duration); } public DurationParser parseAll() { parse(TimeUnit.DAYS); parse(TimeUnit.HOURS); parse(TimeUnit.MINUTES); parse(TimeUnit.SECONDS); parse(TimeUnit.MILLISECONDS); parse(TimeUnit.MICROSECONDS); parse(TimeUnit.NANOSECONDS); return this; } public void parse(TimeUnit unit) throws IllegalStateException { checkStrictlyDecreasing(unit); long durationForUnit = unit.convert(remainder, TimeUnit.NANOSECONDS); long durationInNanos = unit.toNanos(durationForUnit); remainder = remainder - durationInNanos; durations.put(unit, durationForUnit); current = unit; } public void clear() { durations.clear(); current = null; remainder = durationUnit.toNanos(duration); } public long get(TimeUnit unit) { Long duration = durations.get(unit); return duration == null ? 0L: duration; } private void checkStrictlyDecreasing(TimeUnit next) throws IllegalStateException { /* * Because this is a decreasing order comparator, * it returns -1 when the first element is larger than the * second. */ if (comparator.compare(next, current) <= 0) { throw new IllegalStateException(next + " is not strictly " + "smaller than " + current); } } } /** * <p>Returns a given duration broken down into constituent units of all * dimensions and formatted according to the duration format string.</p> * * <p>The format markers are:</p> * * <table> * <tr><td>%D</td><td>days</td></tr> * <tr><td>%H</td><td>hours (no leading zero)</td></tr> * <tr><td>%HH</td><td>hours with one leading zero</td></tr> * <tr><td>%m</td><td>minutes (no leading zero)</td></tr> * <tr><td>%mm</td><td>minutes with one leading zero</td></tr> * <tr><td>%s</td><td>seconds (no leading zero)</td></tr> * <tr><td>%ss</td><td>seconds with one leading zero</td></tr> * <tr><td>%S</td><td>milliseconds</td></tr> * <tr><td>%N</td><td>milliseconds+nanoseconds</td></tr> * </table> * * <p>Note that '%' is used here as a reserved symbol as in String formatting. * %s, however, has a different meaning. This formatting string should * not contain any other placeholders than the ones above and cannot * be combined with normal string formatting.</p> */ public static class DurationFormatter { private final String format; private DurationParser durations; public DurationFormatter(String format) { Preconditions.checkNotNull(format, "Format string must be specified."); this.format = format; } public String format(long duration, TimeUnit unit) { Preconditions.checkNotNull(unit, "Duration time unit must be specified."); TimeUnit[] sortedDimensions = getSortedDimensions(); durations = new DurationParser(duration, unit); for (TimeUnit dimension: sortedDimensions) { durations.parse(dimension); } StringBuilder builder = new StringBuilder(); replace(builder); return builder.toString(); } private TimeUnit[] getSortedDimensions() { Set<TimeUnit> units = new HashSet<>(); char[] sequence = format.toCharArray(); for (int c = 0; c < sequence.length; c++) { switch (sequence[c]) { case '%': ++c; switch (sequence[c]) { case 'D': units.add(TimeUnit.DAYS); break; case 'H': units.add(TimeUnit.HOURS); break; case 'm': units.add(TimeUnit.MINUTES); break; case 's': units.add(TimeUnit.SECONDS); break; case 'S': units.add(TimeUnit.MILLISECONDS); break; case 'N': units.add(TimeUnit.MILLISECONDS); units.add(TimeUnit.MICROSECONDS); units.add(TimeUnit.NANOSECONDS); break; default: throw new IllegalArgumentException( "No such formatting symbol " + c); } break; default: } } TimeUnit[] sorted = units.toArray(new TimeUnit[units.size()]); Arrays.sort(sorted, comparator); return sorted; } private void replace(StringBuilder builder) { char[] sequence = format.toCharArray(); for (int c = 0; c < sequence.length; c++) { switch (sequence[c]) { case '%': c = handlePlaceholder(++c, sequence, builder); break; default: builder.append(sequence[c]); } } } private int handlePlaceholder(int c, char[] sequence, StringBuilder builder) { switch (sequence[c]) { case 'D': builder.append(durations.get(TimeUnit.DAYS)); break; case 'H': if (sequence[c+1] == 'H') { ++c; builder.append(leadingZero(durations.get(TimeUnit.HOURS))); } else { builder.append(durations.get(TimeUnit.HOURS)); } break; case 'm': if (sequence[c+1] == 'm') { ++c; builder.append(leadingZero(durations.get(TimeUnit.MINUTES))); } else { builder.append(durations.get(TimeUnit.MINUTES)); } break; case 's': if (sequence[c+1] == 's') { ++c; builder.append(leadingZero(durations.get(TimeUnit.SECONDS))); } else { builder.append(durations.get(TimeUnit.SECONDS)); } break; case 'S': builder.append(durations.get(TimeUnit.MILLISECONDS)); break; case 'N': builder.append(durations.get(TimeUnit.MILLISECONDS)) .append(durations.get(TimeUnit.MICROSECONDS)) .append(durations.get(TimeUnit.NANOSECONDS)); break; default: throw new IllegalArgumentException ("No such formatting symbol " + c); } return c; } private String leadingZero(Long value) { String valueString = String.valueOf(value); if (valueString.length() < 2) { return '0' + valueString; } return valueString; } } public enum TimeUnitFormat { /** * Display time-units in a short format. */ SHORT, /** * Display time-units as whole words. */ LONG } private static final ImmutableMap<TimeUnit,String> SHORT_TIMEUNIT_NAMES = ImmutableMap.<TimeUnit,String>builder(). put(NANOSECONDS, "ns"). put(MICROSECONDS, "\u00B5s"). // U+00B5 is Unicode for microsymbol put(MILLISECONDS, "ms"). put(SECONDS, "s"). put(MINUTES, "min"). put(HOURS, "hours"). put(DAYS, "days"). build(); private static final ImmutableMap<TimeUnit,String> LONG_TIMEUNIT_NAMES = ImmutableMap.<TimeUnit,String>builder(). put(NANOSECONDS, "nanoseconds"). put(MICROSECONDS, "microseconds"). put(MILLISECONDS, "milliseconds"). put(SECONDS, "seconds"). put(MINUTES, "minutes"). put(HOURS, "hours"). put(DAYS, "days"). build(); private static final DecreasingTimeUnitComparator comparator = new DecreasingTimeUnitComparator(); private TimeUtils() { // Prevent instantiation. } public static CharSequence duration(long duration, TimeUnit units, TimeUnitFormat unitFormat) { return appendDuration(new StringBuilder(), duration, units, unitFormat); } /** * @see DurationFormatter * * @param duration to be expressed * @param unit of the duration * @param format as specified above. * @return formatted string */ public static String getFormattedDuration(long duration, TimeUnit unit, String format) { return new DurationFormatter(format).format(duration, unit); } /** * Returns short sting form for given {@ link TimeUnit}. */ public static String unitStringOf(TimeUnit unit) { return SHORT_TIMEUNIT_NAMES.get(unit); } /** * Provide a short, simple human understandable string describing the * supplied duration. The duration is a non-negative value. The output is * appended to the supplied StringBuilder and has the form * {@code <number> <space> <units>}, where {@code <number>} * is an integer and {@code <units>} is defined by the value of unitFormat. */ public static StringBuilder appendDuration(StringBuilder sb, long duration, TimeUnit units, TimeUnitFormat unitFormat) { checkArgument(duration >= 0); Map<TimeUnit,String> unitsFormat = (unitFormat == TimeUnitFormat.SHORT) ? SHORT_TIMEUNIT_NAMES : LONG_TIMEUNIT_NAMES; if (units == NANOSECONDS && duration < MICROSECONDS.toNanos(2)) { sb.append(units.toNanos(duration)).append(' '). append(unitsFormat.get(NANOSECONDS)); return sb; } if (units.toMicros(duration) < MILLISECONDS.toMillis(2) && units.compareTo(MICROSECONDS) <= 0) { sb.append(units.toMicros(duration)).append(' '). append(unitsFormat.get(MICROSECONDS)); return sb; } long durationInMillis = units.toMillis(duration); if (durationInMillis < SECONDS.toMillis(2) && units.compareTo(MILLISECONDS) <= 0) { sb.append(durationInMillis).append(' '). append(unitsFormat.get(MILLISECONDS)); } else if (durationInMillis < MINUTES.toMillis(2) && units.compareTo(SECONDS) <= 0) { sb.append(units.toSeconds(duration)).append(' '). append(unitsFormat.get(SECONDS)); } else if (durationInMillis < HOURS.toMillis(2) && units.compareTo(MINUTES) <= 0) { sb.append(units.toMinutes(duration)).append(' '). append(unitsFormat.get(MINUTES)); } else if (durationInMillis < DAYS.toMillis(2) && units.compareTo(HOURS) <= 0) { sb.append(units.toHours(duration)).append(' '). append(unitsFormat.get(HOURS)); } else { sb.append(units.toDays(duration)).append(' '). append(unitsFormat.get(DAYS)); } return sb; } /** * Returns a description of some point in time using some reference point. * The appended text is {@code <timestamp> <space> <open-parenth> <integer> * <space> <time-unit-word> <space> <relation> <close-parenth>}. Here are * two examples: * <pre> * 2014-04-20 22:40:32.965 (7 seconds ago) * 2014-04-21 02:40:32.965 (3 hours in the future) * </pre> */ public static CharSequence relativeTimestamp(long when, long current) { return appendRelativeTimestamp(new StringBuilder(), when, current); } /** * Append a description of some point in time using some reference point. * The appended text is {@code <timestamp> <space> <open-parenth> <integer> * <space> <time-unit-word> <space> <relation> <close-parenth>}. Here are * two examples: * <pre> * 2014-04-20 22:40:32.965 (7 seconds ago) * 2014-04-21 02:40:32.965 (3 hours in the future) * </pre> */ public static StringBuilder appendRelativeTimestamp(StringBuilder sb, long when, long current) { checkArgument(when > 0); checkArgument(current > 0); SimpleDateFormat iso8601 = new SimpleDateFormat(TIMESTAMP_FORMAT); sb.append(iso8601.format(new Date(when))); long diff = Math.abs(when - current); sb.append(" ("); appendDuration(sb, diff, MILLISECONDS, TimeUnitFormat.LONG); sb.append(' '); sb.append(when < current ? "ago" : "in the future"); sb.append(')'); return sb; } }