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