package org.arquillian.cube.impl.util;/* * Copyright 2010-2010 LinkedIn, 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. */ import java.util.Arrays; import java.util.Collections; import java.io.Serializable; import java.util.Date; import java.util.EnumMap; import java.util.EnumSet; import java.util.List; /** * This class represents a notion of timespan. Note that the time units goes from milliseconds to year. * It does not go below milliseconds because we rarely use this kind of precision especially since the * java vm does not support it very well. Note that above hour, the timespan is approximate. This * object is immutable and thread safe. * * @author ypujante@linkedin.com */ public class Timespan implements Serializable { private static final long serialVersionUID = 1L; public enum TimeUnit { MILLISECOND(1L, ""), SECOND(1000L * MILLISECOND.getMillisecondsCount(), "s"), MINUTE(60L * SECOND.getMillisecondsCount(), "m"), HOUR(60L * MINUTE.getMillisecondsCount(), "h"), // note that the values below are approximations, and should not be used to compute exact // values! DAY(24L * HOUR.getMillisecondsCount(), "d"), WEEK(7L * DAY.getMillisecondsCount(), "w"), MONTH(30L * DAY.getMillisecondsCount(), "M"), YEAR(365L * DAY.getMillisecondsCount(), "y"); private final long _millisecondsCount; private final String _displayChar; private TimeUnit(long millisecondsCount, String displayChar) { _millisecondsCount = millisecondsCount; _displayChar = displayChar; } public long getMillisecondsCount() { return _millisecondsCount; } public String getDisplayChar() { return _displayChar; } } public final static Timespan ZERO_YEARS = new Timespan(0, TimeUnit.YEAR); public final static Timespan ZERO_MONTHS = new Timespan(0, TimeUnit.MONTH); public final static Timespan ZERO_WEEKS = new Timespan(0, TimeUnit.WEEK); public final static Timespan ZERO_DAYS = new Timespan(0, TimeUnit.DAY); public final static Timespan ZERO_HOURS = new Timespan(0, TimeUnit.HOUR); public final static Timespan ZERO_MINUTES = new Timespan(0, TimeUnit.MINUTE); public final static Timespan ZERO_SECONDS = new Timespan(0, TimeUnit.SECOND); public final static Timespan ZERO_MILLISECONDS = new Timespan(0, TimeUnit.MILLISECOND); public final static Timespan ONE_SECOND = new Timespan(1, TimeUnit.SECOND); public final static Timespan ONE_MINUTE = new Timespan(1, TimeUnit.MINUTE); private final static EnumMap<TimeUnit, Timespan> ZERO_TIMESPANS = new EnumMap<>(TimeUnit.class); private final static TimeUnit[] TIME_UNIT_ORDER; static { final List<TimeUnit> timeUnits = Arrays.asList(TimeUnit.values()); Collections.reverse(timeUnits); TIME_UNIT_ORDER = (TimeUnit[]) timeUnits.toArray(); } static { ZERO_TIMESPANS.put(TimeUnit.YEAR, ZERO_YEARS); ZERO_TIMESPANS.put(TimeUnit.MONTH, ZERO_MONTHS); ZERO_TIMESPANS.put(TimeUnit.WEEK, ZERO_WEEKS); ZERO_TIMESPANS.put(TimeUnit.DAY, ZERO_DAYS); ZERO_TIMESPANS.put(TimeUnit.HOUR, ZERO_HOURS); ZERO_TIMESPANS.put(TimeUnit.MINUTE, ZERO_MINUTES); ZERO_TIMESPANS.put(TimeUnit.SECOND, ZERO_SECONDS); ZERO_TIMESPANS.put(TimeUnit.MILLISECOND, ZERO_MILLISECONDS); } private final static EnumSet<TimeUnit> CANONICAL_TIME_UNITS = EnumSet.range(TimeUnit.MILLISECOND, TimeUnit.YEAR); private final long _duration; private final TimeUnit _timeUnit; /** * Constructor * * @param durationInMilliseconds * the duration in milliseconds */ public Timespan(long durationInMilliseconds) { this(durationInMilliseconds, TimeUnit.MILLISECOND); } /** * Constructor */ public Timespan(long duration, TimeUnit timeUnit) { _duration = duration; _timeUnit = timeUnit; } /** * @return the time unit of this timespan */ public TimeUnit getTimeUnit() { return _timeUnit; } /** * @return the duration of this timespan */ public long getDuration() { return _duration; } /** * Adds another timespan to this timespan and return a brand new one. Note that the unit is * preserved if <code>other</code> has the same unit as 'this'. * * @param other * the timespan to add * * @return a brand new timespan. */ public Timespan add(Timespan other) { if (getTimeUnit() == other.getTimeUnit()) { return new Timespan(getDuration() + other.getDuration(), getTimeUnit()); } return new Timespan(getDurationInMilliseconds() + other.getDurationInMilliseconds(), TimeUnit.MILLISECOND); } /** * Creates and returns a new timespan whose duration is {@code this} * timespan's duration minus the {@code other} timespan's duration. * <p> * The time unit is preserved if {@code other} has the same unit * as {@code this}. * <p> * Negative timespans are not supported, so if the {@code other} * timespan duration is greater than {@code this} timespan duration, * a timespan of zero is returned (i.e., a negative timespan is never * returned). * * @param other * the timespan to subtract from this one * * @return a new timespan representing {@code this - other} */ public Timespan substractWithZeroFloor(Timespan other) { if (getTimeUnit() == other.getTimeUnit()) { long delta = Math.max(0, getDuration() - other.getDuration()); return new Timespan(delta, getTimeUnit()); } long delta = Math.max(0, getDurationInMilliseconds() - other.getDurationInMilliseconds()); return new Timespan(delta, TimeUnit.MILLISECOND); } /** * @return the duration of this timespan in milliseconds */ public long getDurationInMilliseconds() { // 100% equivalent to getDuration(TimeUnit.MILLISECOND) but faster! return getDuration() * getTimeUnit().getMillisecondsCount(); } /** * @param timeUnit * the unit of time you want this timespan ass * * @return the duration of this timespan expressed in the time unit provided. Note that all * units below timeUnit will be truncated! (ex: 3h20m45s will return 3 if timeUnit=HOUR). */ public long getDuration(TimeUnit timeUnit) { return truncate(timeUnit).getDuration(); } /** * @return the duration of this timespan expressed in seconds. If milliseconds are present, * they will be truncated! */ public long getDurationInSeconds() { return getDuration(TimeUnit.SECOND); } /** * @return the duration of this timespan expressed in minutes. If s/ms are present, * they will be truncated! */ public long getDurationInMinutes() { return getDuration(TimeUnit.MINUTE); } /** * @return the duration of this timespan expressed in hours. If m/s/ms are present, * they will be truncated! */ public long getDurationInHours() { return getDuration(TimeUnit.HOUR); } /** * @return a (potentially new) version of this timestamp where the unit is * {@link TimeUnit#MILLISECOND}. */ public Timespan toMillisecondsTimespan() { if (getTimeUnit() == TimeUnit.MILLISECOND) { return this; } return new Timespan(getDurationInMilliseconds(), TimeUnit.MILLISECOND); } /** * Truncates this timespan to the given time unit. Example: if this is 1h20m5s then * <code>truncate(TimeUnit.HOUR)</code> will return 1h, * <code>truncate(TimeUnit.MINUTE)</code> will return 1h20m and * <code>truncate(TimeUnit.SECOND)</code> will return 1h20m5s * * @param timeUnit * the unit you want the timespan in * * @return this timespan if time unit matches otherwise a new one with the given unit */ public Timespan truncate(TimeUnit timeUnit) { if (getTimeUnit() == timeUnit) { return this; } return truncateDurationToUnit(getDurationInMilliseconds(), timeUnit); } /** * There are many ways to represent the same timespan: 3h, 180m... this call return the unique * way to express it such that ms is < 1000, s is < 60, m is < 60, h is < 24, * d is < 7. * * @return this timespan as a canonical representation: an entry for w/dh/m/s/ms is returned * * @see #getAsTimespans(EnumSet) */ public EnumMap<TimeUnit, Timespan> getCanonicalTimespans() { return getAsTimespans(CANONICAL_TIME_UNITS); } /** * @return a string representing this timespan (ex: 3h2m23s). Note that if a duration is missing, * it is not part of the string (the string would be 3h23s and not 3h0m23s). * * @see #getCanonicalTimespans() */ public String getCanonicalString() { return getAsString(CANONICAL_TIME_UNITS); } /** * Decomposes this timespan as a map for each unit provided. Example: if this timespan represents * 63s and you provide m/s/ms then you will get 3 timespans in the map: one of 1mn, one for 3s * and one for 0ms. * * @param timeUnits * the time units you want to be part of the decomposition. * * @return a map containing an entry for each time unit provided. All others are <code>null</code>. */ public EnumMap<TimeUnit, Timespan> getAsTimespans(EnumSet<TimeUnit> timeUnits) { EnumMap<TimeUnit, Timespan> res = new EnumMap<TimeUnit, Timespan>(TimeUnit.class); long durationInMillis = getDurationInMilliseconds(); for (TimeUnit timeUnit : TIME_UNIT_ORDER) { if (timeUnits.contains(timeUnit)) { Timespan timespan = truncateDurationToUnit(durationInMillis, timeUnit); res.put(timeUnit, timespan); durationInMillis -= timespan.getDurationInMilliseconds(); } } return res; } /** * Filters this timespan with only the unit provided. It computes the canonical representation * and keeps only the units that are present in the filter. Example: if this timespan represents * 3d2h25m10s and filter is d/h/m then result is 3d2h25m. * * @param timeUnits * the time units you want to keep in the filtering. * * @return the timespan after the filtering */ public Timespan filter(EnumSet<TimeUnit> timeUnits) { Timespan res = null; EnumMap<TimeUnit, Timespan> canonicalTimespans = getCanonicalTimespans(); for (TimeUnit timeUnit : TIME_UNIT_ORDER) { if (timeUnits.contains(timeUnit)) { Timespan timespan = canonicalTimespans.get(timeUnit); if (timespan != null && timespan.getDuration() > 0) { if (res == null) { res = timespan; } else { res = res.add(timespan); } } } } // in case there is no match we return with smallest timeunit provided if (res == null) { res = ZERO_TIMESPANS.get(timeUnits.iterator().next()); } return res; } /** * Returns a string representing this timespan expressed with the units provided. * * @param timeUnits * the timeunits you want in the decomposition * * @return a string representation using the units. * * @see #getAsTimespans(EnumSet) */ public String getAsString(EnumSet<TimeUnit> timeUnits) { StringBuilder sb = new StringBuilder(); EnumMap<TimeUnit, Timespan> canonicalTimespans = getAsTimespans(timeUnits); for (TimeUnit timeUnit : TIME_UNIT_ORDER) { if (canonicalTimespans.containsKey(timeUnit)) { long duration = canonicalTimespans.get(timeUnit).getDuration(); if (duration > 0) { sb.append(duration).append(timeUnit.getDisplayChar()); } } } if (sb.length() == 0) { sb.append(0); if (timeUnits.contains(getTimeUnit())) { sb.append(getTimeUnit().getDisplayChar()); } } return sb.toString(); } /** * 2 timespans can be different (1h and 3600s) while representing the same duration expressed * in milliseconds... This method tests for this. * * @param timespan * the other timespan to compare with * * @return <code>true</code> if this timespan and the provided one represent the same * duration of time. */ public boolean equalsDurationInMilliseconds(Timespan timespan) { // shortcut when time unit are the same if (timespan.getTimeUnit() == getTimeUnit()) { return timespan.getDuration() == getDuration(); } else { return getDurationInMilliseconds() == timespan.getDurationInMilliseconds(); } } /** * @param baseMilliseconds * the starting point to compute the future time * * @return the absolute time in milliseconds (since jan 01 1970) represented * {@code baseMilliseconds} + this timespan */ public long futureTimeMillis(long baseMilliseconds) { return baseMilliseconds + getDurationInMilliseconds(); } /** * @param baseMillis * the starting point * * @return baseMillis - this timespan */ public long pastTimeMillis(long baseMillis) { return baseMillis - getDurationInMilliseconds(); } /** * @param baseDate * base date to offset from * * @return return the date which is baseDate + value of this timespan */ public Date futureDate(Date baseDate) { return new Date(baseDate.getTime() + getDurationInMilliseconds()); } /** * @param baseDate * base date to offset from * * @return return the date which is baseDate - value of this timespan */ public Date pastDate(Date baseDate) { return new Date(pastTimeMillis(baseDate.getTime())); } /** * Expresses the provided duration in the unit provided. Note that the timespan returned * represent only the truncated version of the duration: if duration is 1002ms and timeunit * is seconds, then the timespan returned is 1 second... leaving behind 2ms. * * @return the timespan */ private static Timespan truncateDurationToUnit(long durationInMillis, TimeUnit timeUnit) { Timespan res; if (durationInMillis >= timeUnit.getMillisecondsCount()) { res = new Timespan(durationInMillis / timeUnit.getMillisecondsCount(), timeUnit); } else { res = ZERO_TIMESPANS.get(timeUnit); } return res; } /** * @return a string representation of the object. */ @Override public String toString() { return getCanonicalString(); } /** * Creates a timespan from a list of other timespans. * * @return a timespan representing the sum of all the timespans provided */ public static Timespan create(Timespan... timespans) { if (timespans == null) { return null; } if (timespans.length == 0) { return ZERO_MILLISECONDS; } Timespan res = timespans[0]; for (int i = 1; i < timespans.length; i++) { Timespan timespan = timespans[i]; res = res.add(timespan); } return res; } /** * Synonym. * * @see #parseTimespan(String) */ public static Timespan parse(String timespan) { return parseTimespan(timespan); } /** * Synonym. * * @see #parseTimespan(String) */ public static Timespan valueOf(String timespan) { return parseTimespan(timespan); } /** * Convenient call when providing milliseconds * * @return the timespan */ public static Timespan milliseconds(long milliseconds) { return new Timespan(milliseconds); } /** * Convenient call when providing seconds * * @return the timespan */ public static Timespan seconds(long seconds) { return new Timespan(seconds, TimeUnit.SECOND); } /** * Convenient call when providing minutes * * @return the timespan */ public static Timespan minutes(long minutes) { return new Timespan(minutes, TimeUnit.MINUTE); } /** * Parses the provided string as a timespan. It should follow the pattern returned by * {@link #getCanonicalString()}. Example: 10m30s * * @return the timespan to parse * * @throws IllegalArgumentException * if the string is not valid */ public static Timespan parseTimespan(String timespan) { if (timespan == null) { return null; } // easiest way to be compliant with docker-compose format timespan = timespan.replace("ms", ""); int len = timespan.length(); if (len == 0) { return ZERO_MILLISECONDS; } int count = 0; int timeUnitOrderIdx = 0; int timeUnitOrderLen = TIME_UNIT_ORDER.length; Timespan[] timespans = new Timespan[timeUnitOrderLen]; int startDigitsIdx = 0; boolean expectingDigits = true; for (int i = 0; i < len; i++) { char c = timespan.charAt(i); if (isDigit(c)) { expectingDigits = false; continue; } if (expectingDigits) { throw new IllegalArgumentException("found " + c + " was expecting a digit"); } for (; timeUnitOrderIdx < timeUnitOrderLen; timeUnitOrderIdx++) { TimeUnit timeUnit = TIME_UNIT_ORDER[timeUnitOrderIdx]; String displayChar = timeUnit.getDisplayChar(); if (displayChar.length() == 0) { throw new IllegalArgumentException("found nothing was expecting: " + c); } if (c == displayChar.charAt(0)) { try { long duration = Long.parseLong(timespan.substring(startDigitsIdx, i)); timespans[timeUnitOrderIdx++] = new Timespan(duration, timeUnit); startDigitsIdx = i + 1; expectingDigits = true; count++; break; } catch (NumberFormatException e) { throw new IllegalArgumentException(e); } } } } if (startDigitsIdx < len) { try { long duration = Long.parseLong(timespan.substring(startDigitsIdx, len)); timespans[timeUnitOrderLen - 1] = new Timespan(duration, TimeUnit.MILLISECOND); count++; } catch (NumberFormatException e) { throw new IllegalArgumentException(e); } } Timespan[] ts = new Timespan[count]; for (int i = 0, idx = 0; i < timespans.length; i++) { Timespan t = timespans[i]; if (t != null) { ts[idx++] = t; } } return create(ts); } private static boolean isDigit(char c) { return c >= '0' && c <= '9'; } /** * Shortcut for creating a timespan and then retrieving the value in milliseconds. * * @param timespan * the timespan as a string * * @return the value in milliseconds */ public static long toMilliseconds(String timespan) { return parseTimespan(timespan).getDurationInMilliseconds(); } }