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-2015 ForgeRock AS. 015 */ 016 017package org.forgerock.util.time; 018 019import java.util.ArrayList; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Locale; 023import java.util.Map; 024import java.util.Set; 025import java.util.TreeSet; 026import java.util.concurrent.TimeUnit; 027 028import static java.lang.String.format; 029import static java.util.Arrays.asList; 030import static java.util.concurrent.TimeUnit.*; 031import static org.forgerock.util.Reject.checkNotNull; 032 033/** 034 * Represents a duration in english. Cases is not important, plurals units are accepted. 035 * Notice that negative durations are not supported. 036 * 037 * <code> 038 * 6 days 039 * 59 minutes and 1 millisecond 040 * 1 minute and 10 seconds 041 * 42 millis 042 * unlimited 043 * none 044 * zero 045 * </code> 046 */ 047public class Duration { 048 049 /** 050 * Special duration that represents an unlimited duration (or indefinite). 051 */ 052 private static final Duration UNLIMITED = new Duration(Long.MAX_VALUE, DAYS); 053 054 /** 055 * Special duration that represents a zero-length duration. 056 */ 057 private static final Duration ZERO = new Duration(0L, SECONDS); 058 059 /** 060 * Tokens that represents the unlimited duration. 061 */ 062 private static final Set<String> UNLIMITED_TOKENS = new TreeSet<>( 063 String.CASE_INSENSITIVE_ORDER); 064 static { 065 UNLIMITED_TOKENS.addAll(asList("unlimited", "indefinite", "infinity", "undefined")); 066 } 067 068 /** 069 * Tokens that represents the zero duration. 070 */ 071 private static final Set<String> ZERO_TOKENS = new TreeSet<>( 072 String.CASE_INSENSITIVE_ORDER); 073 static { 074 ZERO_TOKENS.addAll(asList("zero", "disabled")); 075 } 076 077 private Long number; 078 private TimeUnit unit; 079 080 /** 081 * Builds a new Duration. 082 * @param number number of time unit (cannot be {@literal null}). 083 * @param unit TimeUnit to express the duration in (cannot be {@literal null}). 084 */ 085 public Duration(final Long number, final TimeUnit unit) { 086 this.number = checkNotNull(number); 087 this.unit = checkNotNull(unit); 088 } 089 090 /** 091 * Builds a new {@link Duration} that will represents the given duration expressed in english. 092 * 093 * @param value 094 * natural speech duration 095 * @return a new {@link Duration} 096 * @throws IllegalArgumentException 097 * if the input string is incorrectly formatted. 098 */ 099 public static Duration duration(final String value) { 100 List<Duration> composite = new ArrayList<>(); 101 102 // Split around ',' and ' and ' patterns 103 String[] fragments = value.split(",| and "); 104 105 // If there is only 1 fragment and that it matches the recognized "unlimited" tokens 106 if (fragments.length == 1) { 107 String trimmed = fragments[0].trim(); 108 if (UNLIMITED_TOKENS.contains(trimmed)) { 109 // Unlimited Duration 110 return UNLIMITED; 111 } else if (ZERO_TOKENS.contains(trimmed)) { 112 // Zero-length Duration 113 return ZERO; 114 } 115 } 116 117 // Build the normal duration 118 for (String fragment : fragments) { 119 fragment = fragment.trim(); 120 121 if ("".equals(fragment)) { 122 throw new IllegalArgumentException("Cannot parse empty duration, expecting '<value> <unit>' pattern"); 123 } 124 125 // Parse the number part 126 int i = 0; 127 StringBuilder numberSB = new StringBuilder(); 128 while (Character.isDigit(fragment.charAt(i))) { 129 numberSB.append(fragment.charAt(i)); 130 i++; 131 } 132 133 // Ignore whitespace 134 while (Character.isWhitespace(fragment.charAt(i))) { 135 i++; 136 } 137 138 // Parse the time unit part 139 StringBuilder unitSB = new StringBuilder(); 140 while ((i < fragment.length()) && Character.isLetter(fragment.charAt(i))) { 141 unitSB.append(fragment.charAt(i)); 142 i++; 143 } 144 Long number = Long.valueOf(numberSB.toString()); 145 TimeUnit unit = parseTimeUnit(unitSB.toString()); 146 147 composite.add(new Duration(number, unit)); 148 } 149 150 // Merge components of the composite together 151 Duration duration = new Duration(0L, DAYS); 152 for (Duration elements : composite) { 153 duration.merge(elements); 154 } 155 156 // If someone used '0 ms' for example 157 if (duration.number == 0L) { 158 return ZERO; 159 } 160 161 return duration; 162 } 163 164 /** 165 * Aggregates this Duration with the given Duration. Littlest {@link TimeUnit} will be used as a common ground. 166 * 167 * @param duration 168 * other Duration 169 */ 170 private void merge(final Duration duration) { 171 // find littlest unit 172 // conversion will happen on the littlest unit otherwise we loose details 173 if (unit.ordinal() > duration.unit.ordinal()) { 174 // Other duration is smaller than me 175 number = duration.unit.convert(number, unit) + duration.number; 176 unit = duration.unit; 177 } else { 178 // Other duration is greater than me 179 number = unit.convert(duration.number, duration.unit) + number; 180 } 181 } 182 183 private static Map<String, TimeUnit> TIME_UNITS = new HashMap<>(); 184 static { 185 for (String days : asList("days", "day", "d")) { 186 TIME_UNITS.put(days, DAYS); 187 } 188 for (String hours : asList("hours", "hour", "h")) { 189 TIME_UNITS.put(hours, HOURS); 190 } 191 for (String minutes : asList("minutes", "minute", "min", "m")) { 192 TIME_UNITS.put(minutes, MINUTES); 193 } 194 for (String seconds : asList("seconds", "second", "sec", "s")) { 195 TIME_UNITS.put(seconds, SECONDS); 196 } 197 for (String ms : asList("milliseconds", "millisecond", "millisec", "millis", "milli", "ms")) { 198 TIME_UNITS.put(ms, MILLISECONDS); 199 } 200 for (String us : asList("microseconds", "microsecond", "microsec", "micros", "micro", "us")) { 201 TIME_UNITS.put(us, MICROSECONDS); 202 } 203 for (String ns : asList("nanoseconds", "nanosecond", "nanosec", "nanos", "nano", "ns")) { 204 TIME_UNITS.put(ns, NANOSECONDS); 205 } 206 } 207 208 /** 209 * Parse the given input string as a {@link TimeUnit}. 210 */ 211 private static TimeUnit parseTimeUnit(final String unit) { 212 final String lowercase = unit.toLowerCase(Locale.ENGLISH); 213 final TimeUnit timeUnit = TIME_UNITS.get(lowercase); 214 if (timeUnit != null) { 215 return timeUnit; 216 } 217 throw new IllegalArgumentException(format("TimeUnit %s is not recognized", unit)); 218 } 219 220 /** 221 * Returns the number of {@link TimeUnit} this duration represents. 222 * 223 * @return the number of {@link TimeUnit} this duration represents. 224 */ 225 public long getValue() { 226 return number; 227 } 228 229 /** 230 * Returns the {@link TimeUnit} this duration is expressed in. 231 * 232 * @return the {@link TimeUnit} this duration is expressed in. 233 */ 234 public TimeUnit getUnit() { 235 return unit; 236 } 237 238 /** 239 * Convert the current duration to a given {@link TimeUnit}. 240 * Conversions from finer to coarser granularities truncate, so loose precision. 241 * 242 * @param targetUnit 243 * target unit of the conversion. 244 * @return converted duration 245 * @see TimeUnit#convert(long, TimeUnit) 246 */ 247 public Duration convertTo(TimeUnit targetUnit) { 248 return new Duration(targetUnit.convert(number, unit), targetUnit); 249 } 250 251 /** 252 * Convert the current duration to a number of given {@link TimeUnit}. 253 * Conversions from finer to coarser granularities truncate, so loose precision. 254 * 255 * @param targetUnit 256 * target unit of the conversion. 257 * @return converted duration value 258 * @see TimeUnit#convert(long, TimeUnit) 259 */ 260 public long to(TimeUnit targetUnit) { 261 return convertTo(targetUnit).getValue(); 262 } 263 264 /** 265 * Returns {@literal true} if this Duration represents an unlimited duration. 266 * 267 * @return {@literal true} if this Duration represents an unlimited duration. 268 */ 269 public boolean isUnlimited() { 270 return this == UNLIMITED; 271 } 272 273 /** 274 * Returns {@literal true} if this Duration represents a zero-length duration. 275 * 276 * @return {@literal true} if this Duration represents a zero-length duration. 277 */ 278 public boolean isZero() { 279 return this == ZERO; 280 } 281 282 @Override 283 public String toString() { 284 return number + " " + unit; 285 } 286 287}