001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014-2016 ForgeRock AS. 015 */ 016 017package org.forgerock.util.time; 018 019import static java.lang.String.format; 020import static java.util.Arrays.asList; 021import static java.util.concurrent.TimeUnit.DAYS; 022import static java.util.concurrent.TimeUnit.HOURS; 023import static java.util.concurrent.TimeUnit.MICROSECONDS; 024import static java.util.concurrent.TimeUnit.MILLISECONDS; 025import static java.util.concurrent.TimeUnit.MINUTES; 026import static java.util.concurrent.TimeUnit.NANOSECONDS; 027import static java.util.concurrent.TimeUnit.SECONDS; 028import static org.forgerock.util.Reject.checkNotNull; 029 030import java.util.ArrayList; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Locale; 034import java.util.Map; 035import java.util.Objects; 036import java.util.Set; 037import java.util.TreeSet; 038import java.util.concurrent.TimeUnit; 039 040import org.forgerock.util.Reject; 041 042/** 043 * Represents a duration in english. Cases is not important, plurals units are accepted. 044 * Notice that negative durations are not supported. 045 * 046 * <code> 047 * 6 days 048 * 59 minutes and 1 millisecond 049 * 1 minute and 10 seconds 050 * 42 millis 051 * unlimited 052 * none 053 * zero 054 * </code> 055 */ 056public class Duration implements Comparable<Duration> { 057 058 /** 059 * Special duration that represents an unlimited duration (or indefinite). 060 */ 061 public static final Duration UNLIMITED = new Duration(); 062 063 /** 064 * Special duration that represents a zero-length duration. 065 */ 066 public static final Duration ZERO = new Duration(0L, SECONDS); 067 068 /** 069 * Tokens that represents the unlimited duration. 070 */ 071 private static final Set<String> UNLIMITED_TOKENS = new TreeSet<>( 072 String.CASE_INSENSITIVE_ORDER); 073 static { 074 UNLIMITED_TOKENS.addAll(asList("unlimited", "indefinite", "infinity", "undefined", "none")); 075 } 076 077 /** 078 * Tokens that represents the zero duration. 079 */ 080 private static final Set<String> ZERO_TOKENS = new TreeSet<>( 081 String.CASE_INSENSITIVE_ORDER); 082 static { 083 ZERO_TOKENS.addAll(asList("zero", "disabled")); 084 } 085 086 private long number; 087 private TimeUnit unit; 088 089 /** 090 * Hidden constructor that creates an unlimited duration. The intention is that the only instance representing 091 * unlimited is {@link #UNLIMITED}. 092 */ 093 private Duration() { 094 this.number = Long.MAX_VALUE; 095 this.unit = null; 096 } 097 098 /** 099 * Builds a new {@code Duration}. 100 * 101 * @param number number of time unit (cannot be {@literal null}). 102 * @param unit TimeUnit to express the duration in (cannot be {@literal null}). 103 * @deprecated Prefer the use of {@link #duration(long, TimeUnit)}. 104 */ 105 @Deprecated 106 public Duration(final Long number, final TimeUnit unit) { 107 Reject.ifTrue(number < 0, "Negative durations are not supported"); 108 this.number = number; 109 this.unit = checkNotNull(unit); 110 } 111 112 /** 113 * Provides a {@code Duration}, given a number and time unit. 114 * 115 * @param number number of time unit. 116 * @param unit TimeUnit to express the duration in (cannot be {@literal null}). 117 * @return {@code Duration} instance 118 */ 119 public static Duration duration(final long number, final TimeUnit unit) { 120 if (number == 0) { 121 return ZERO; 122 } 123 return new Duration(number, unit); 124 } 125 126 /** 127 * Provides a {@code Duration} that represents the given duration expressed in english. 128 * 129 * @param value 130 * natural speech duration 131 * @return {@code Duration} instance 132 * @throws IllegalArgumentException 133 * if the input string is incorrectly formatted. 134 */ 135 public static Duration duration(final String value) { 136 List<Duration> composite = new ArrayList<>(); 137 138 // Split around ',' and ' and ' patterns 139 String[] fragments = value.split(",| and "); 140 141 // If there is only 1 fragment and that it matches the recognized "unlimited" tokens 142 if (fragments.length == 1) { 143 String trimmed = fragments[0].trim(); 144 if (UNLIMITED_TOKENS.contains(trimmed)) { 145 // Unlimited Duration 146 return UNLIMITED; 147 } else if (ZERO_TOKENS.contains(trimmed)) { 148 // Zero-length Duration 149 return ZERO; 150 } 151 } 152 153 // Build the normal duration 154 for (String fragment : fragments) { 155 fragment = fragment.trim(); 156 157 if ("".equals(fragment)) { 158 throw new IllegalArgumentException("Cannot parse empty duration, expecting '<value> <unit>' pattern"); 159 } 160 161 // Parse the number part 162 int i = 0; 163 StringBuilder numberSB = new StringBuilder(); 164 while (Character.isDigit(fragment.charAt(i))) { 165 numberSB.append(fragment.charAt(i)); 166 i++; 167 } 168 169 // Ignore whitespace 170 while (Character.isWhitespace(fragment.charAt(i))) { 171 i++; 172 } 173 174 // Parse the time unit part 175 StringBuilder unitSB = new StringBuilder(); 176 while ((i < fragment.length()) && Character.isLetter(fragment.charAt(i))) { 177 unitSB.append(fragment.charAt(i)); 178 i++; 179 } 180 Long number = Long.valueOf(numberSB.toString()); 181 TimeUnit unit = parseTimeUnit(unitSB.toString()); 182 183 composite.add(new Duration(number, unit)); 184 } 185 186 // Merge components of the composite together 187 Duration duration = new Duration(0L, DAYS); 188 for (Duration elements : composite) { 189 duration.merge(elements); 190 } 191 192 // If someone used '0 ms' for example 193 if (duration.number == 0L) { 194 return ZERO; 195 } 196 197 return duration; 198 } 199 200 /** 201 * Aggregates this Duration with the given Duration. Littlest {@link TimeUnit} will be used as a common ground. 202 * 203 * @param duration 204 * other Duration 205 */ 206 private void merge(final Duration duration) { 207 if (!isUnlimited() && !duration.isUnlimited()) { 208 // find littlest unit 209 // conversion will happen on the littlest unit otherwise we loose details 210 if (unit.ordinal() > duration.unit.ordinal()) { 211 // Other duration is smaller than me 212 number = duration.unit.convert(number, unit) + duration.number; 213 unit = duration.unit; 214 } else { 215 // Other duration is greater than me 216 number = unit.convert(duration.number, duration.unit) + number; 217 } 218 } 219 } 220 221 private static final Map<String, TimeUnit> TIME_UNITS = new HashMap<>(); 222 static { 223 for (String days : asList("days", "day", "d")) { 224 TIME_UNITS.put(days, DAYS); 225 } 226 for (String hours : asList("hours", "hour", "h")) { 227 TIME_UNITS.put(hours, HOURS); 228 } 229 for (String minutes : asList("minutes", "minute", "min", "m")) { 230 TIME_UNITS.put(minutes, MINUTES); 231 } 232 for (String seconds : asList("seconds", "second", "sec", "s")) { 233 TIME_UNITS.put(seconds, SECONDS); 234 } 235 for (String ms : asList("milliseconds", "millisecond", "millisec", "millis", "milli", "ms")) { 236 TIME_UNITS.put(ms, MILLISECONDS); 237 } 238 for (String us : asList("microseconds", "microsecond", "microsec", "micros", "micro", "us", "\u03BCs", 239 "\u00B5s")) { // the last two support 'mu' and 'micro sign' abbreviations 240 TIME_UNITS.put(us, MICROSECONDS); 241 } 242 for (String ns : asList("nanoseconds", "nanosecond", "nanosec", "nanos", "nano", "ns")) { 243 TIME_UNITS.put(ns, NANOSECONDS); 244 } 245 } 246 247 /** 248 * Parse the given input string as a {@link TimeUnit}. 249 */ 250 private static TimeUnit parseTimeUnit(final String unit) { 251 final String lowercase = unit.toLowerCase(Locale.ENGLISH); 252 final TimeUnit timeUnit = TIME_UNITS.get(lowercase); 253 if (timeUnit != null) { 254 return timeUnit; 255 } 256 throw new IllegalArgumentException(format("TimeUnit %s is not recognized", unit)); 257 } 258 259 /** 260 * Returns the number of {@link TimeUnit} this duration represents. 261 * 262 * @return the number of {@link TimeUnit} this duration represents. 263 */ 264 public long getValue() { 265 return number; 266 } 267 268 /** 269 * Returns the {@link TimeUnit} this duration is expressed in. 270 * 271 * @return the {@link TimeUnit} this duration is expressed in. 272 */ 273 public TimeUnit getUnit() { 274 if (isUnlimited()) { 275 // UNLIMITED originally had TimeUnit.DAYS, so preserve API semantics 276 return TimeUnit.DAYS; 277 } 278 return unit; 279 } 280 281 /** 282 * Convert the current duration to a given {@link TimeUnit}. 283 * Conversions from finer to coarser granularities truncate, so loose precision. 284 * 285 * @param targetUnit 286 * target unit of the conversion. 287 * @return converted duration 288 * @see TimeUnit#convert(long, TimeUnit) 289 */ 290 public Duration convertTo(TimeUnit targetUnit) { 291 if (isUnlimited() || isZero()) { 292 return this; 293 } 294 return new Duration(to(targetUnit), targetUnit); 295 } 296 297 /** 298 * Convert the current duration to a number of given {@link TimeUnit}. 299 * Conversions from finer to coarser granularities truncate, so loose precision. 300 * 301 * @param targetUnit 302 * target unit of the conversion. 303 * @return converted duration value 304 * @see TimeUnit#convert(long, TimeUnit) 305 */ 306 public long to(TimeUnit targetUnit) { 307 if (isUnlimited()) { 308 return number; 309 } 310 return targetUnit.convert(number, unit); 311 } 312 313 /** 314 * Returns {@literal true} if this Duration represents an unlimited (or indefinite) duration. 315 * 316 * @return {@literal true} if this Duration represents an unlimited duration. 317 */ 318 public boolean isUnlimited() { 319 return this == UNLIMITED; 320 } 321 322 /** 323 * Returns {@literal true} if this Duration represents a zero-length duration. 324 * 325 * @return {@literal true} if this Duration represents a zero-length duration. 326 */ 327 public boolean isZero() { 328 return number == 0; 329 } 330 331 @Override 332 public String toString() { 333 if (isUnlimited()) { 334 return "UNLIMITED"; 335 } 336 if (isZero()) { 337 return "ZERO"; 338 } 339 return number + " " + unit; 340 } 341 342 @Override 343 public int compareTo(Duration that) { 344 if (this.isUnlimited()) { 345 if (that.isUnlimited()) { 346 // unlimited == unlimited 347 return 0; 348 } else { 349 // unlimited > any value 350 return 1; 351 } 352 } 353 if (that.isUnlimited()) { 354 // any value > unlimited 355 return -1; 356 } 357 if (this.isZero()) { 358 if (that.isZero()) { 359 // 0 == 0 360 return 0; 361 } else { 362 // 0 > any value 363 return -1; 364 } 365 } 366 if (that.isZero()) { 367 // any value > 0 368 return 1; 369 } 370 371 // No special case so let's convert using the smallest unit and check if the biggest duration overflowed 372 // or not during the conversion. 373 final int unitCompare = this.getUnit().compareTo(that.getUnit()); 374 final boolean biggestOverflowed; 375 final long thisConverted, thatConverted; 376 if (unitCompare > 0) { 377 thisConverted = this.convertTo(that.getUnit()).getValue(); 378 thatConverted = that.getValue(); 379 biggestOverflowed = thisConverted == Long.MAX_VALUE; 380 } else if (unitCompare < 0) { 381 thisConverted = this.getValue(); 382 thatConverted = that.convertTo(this.getUnit()).getValue(); 383 biggestOverflowed = thatConverted == Long.MAX_VALUE; 384 } else { 385 // unitCompare == 0 : both durations are in the same units 386 // No conversion was done so the biggest can't have been overflowed. 387 biggestOverflowed = false; 388 thisConverted = this.getValue(); 389 thatConverted = that.getValue(); 390 } 391 392 393 return !biggestOverflowed ? Long.compare(thisConverted, thatConverted) : unitCompare; 394 } 395 396 @Override 397 public boolean equals(Object other) { 398 if (this == other) { 399 return true; 400 } 401 402 if (!(other instanceof Duration)) { 403 return false; 404 } 405 406 Duration duration = (Duration) other; 407 return number == duration.number && unit == duration.unit; 408 } 409 410 @Override 411 public int hashCode() { 412 return Objects.hash(number, unit); 413 } 414 415}