/src/main/java/org/ocpsoft/prettytime/PrettyTime.java
Java | 422 lines | 240 code | 39 blank | 143 comment | 29 complexity | 49855e65606f1514963e3bb0650066d0 MD5 | raw file
Possible License(s): Apache-2.0
1/* 2 * Copyright 2012 <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a> 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package org.ocpsoft.prettytime; 17 18import java.util.ArrayList; 19import java.util.Collections; 20import java.util.Date; 21import java.util.LinkedHashMap; 22import java.util.List; 23import java.util.Locale; 24import java.util.Map; 25 26import org.ocpsoft.prettytime.impl.DurationImpl; 27import org.ocpsoft.prettytime.impl.ResourcesTimeFormat; 28import org.ocpsoft.prettytime.impl.ResourcesTimeUnit; 29import org.ocpsoft.prettytime.units.Century; 30import org.ocpsoft.prettytime.units.Day; 31import org.ocpsoft.prettytime.units.Decade; 32import org.ocpsoft.prettytime.units.Hour; 33import org.ocpsoft.prettytime.units.JustNow; 34import org.ocpsoft.prettytime.units.Millennium; 35import org.ocpsoft.prettytime.units.Millisecond; 36import org.ocpsoft.prettytime.units.Minute; 37import org.ocpsoft.prettytime.units.Month; 38import org.ocpsoft.prettytime.units.Second; 39import org.ocpsoft.prettytime.units.TimeUnitComparator; 40import org.ocpsoft.prettytime.units.Week; 41import org.ocpsoft.prettytime.units.Year; 42 43/** 44 * A utility for creating social-networking style timestamps. (e.g. "just now", "moments ago", "3 days ago", 45 * "within 2 months") 46 * <p> 47 * <b>Usage:</b> 48 * <p> 49 * <code> 50 * PrettyTime t = new PrettyTime();<br/> 51 * String timestamp = t.format(new Date());<br/> 52 * //result: moments from now 53 * <p> 54 * </code> 55 * 56 * @author <a href="mailto:lincolnbaxter@gmail.com>Lincoln Baxter, III</a> 57 */ 58public class PrettyTime implements LocaleAware 59{ 60 61 private volatile Date reference; 62 private volatile Locale locale = Locale.getDefault(); 63 private volatile Map<TimeUnit, TimeFormat> formatMap = new LinkedHashMap<TimeUnit, TimeFormat>(); 64 65 /** 66 * Default constructor 67 */ 68 public PrettyTime() 69 { 70 initTimeUnits(); 71 } 72 73 /** 74 * Accept a {@link Date} timestamp to represent the point of reference for comparison. This may be changed by the 75 * user, after construction. 76 * <p> 77 * See {@code PrettyTime.setReference(Date timestamp)}. 78 * 79 * @param reference 80 */ 81 public PrettyTime(final Date reference) 82 { 83 this(); 84 setReference(reference); 85 } 86 87 /** 88 * Construct a new instance using the given {@link Locale} instead of the system default. 89 */ 90 public PrettyTime(final Locale locale) 91 { 92 this.locale = locale; 93 initTimeUnits(); 94 } 95 96 /** 97 * Accept a {@link Date} timestamp to represent the point of reference for comparison. This may be changed by the 98 * user, after construction. Use the given {@link Locale} instead of the system default. 99 * <p> 100 * See {@code PrettyTime.setReference(Date timestamp)}. 101 */ 102 public PrettyTime(final Date reference, final Locale locale) 103 { 104 this(locale); 105 setReference(reference); 106 } 107 108 /** 109 * Calculate the approximate duration between the referenceDate and date 110 */ 111 public Duration approximateDuration(final Date then) 112 { 113 Date ref = reference; 114 if (null == ref) 115 { 116 ref = new Date(); 117 } 118 119 long difference = then.getTime() - ref.getTime(); 120 return calculateDuration(difference); 121 } 122 123 private void initTimeUnits() 124 { 125 addUnit(new JustNow()); 126 addUnit(new Millisecond()); 127 addUnit(new Second()); 128 addUnit(new Minute()); 129 addUnit(new Hour()); 130 addUnit(new Day()); 131 addUnit(new Week()); 132 addUnit(new Month()); 133 addUnit(new Year()); 134 addUnit(new Decade()); 135 addUnit(new Century()); 136 addUnit(new Millennium()); 137 } 138 139 private void addUnit(ResourcesTimeUnit unit) 140 { 141 formatMap.put(unit, new ResourcesTimeFormat(unit, locale)); 142 } 143 144 private Duration calculateDuration(final long difference) 145 { 146 long absoluteDifference = Math.abs(difference); 147 148 // Required for thread-safety 149 List<TimeUnit> units = new ArrayList<TimeUnit>(getUnits().size()); 150 units.addAll(getUnits()); 151 152 DurationImpl result = new DurationImpl(); 153 154 for (int i = 0; i < units.size(); i++) 155 { 156 TimeUnit unit = units.get(i); 157 long millisPerUnit = Math.abs(unit.getMillisPerUnit()); 158 long quantity = Math.abs(unit.getMaxQuantity()); 159 160 boolean isLastUnit = (i == units.size() - 1); 161 162 if ((0 == quantity) && !isLastUnit) 163 { 164 quantity = units.get(i + 1).getMillisPerUnit() / unit.getMillisPerUnit(); 165 } 166 167 // does our unit encompass the time duration? 168 if ((millisPerUnit * quantity > absoluteDifference) || isLastUnit) 169 { 170 result.setUnit(unit); 171 if (millisPerUnit > absoluteDifference) 172 { 173 // we are rounding up: get 1 or -1 for past or future 174 result.setQuantity(getSign(difference, absoluteDifference)); 175 } 176 else 177 { 178 result.setQuantity(difference / millisPerUnit); 179 } 180 result.setDelta(difference - result.getQuantity() * millisPerUnit); 181 break; 182 } 183 184 } 185 return result; 186 } 187 188 private long getSign(final long difference, final long absoluteDifference) 189 { 190 if (0 > difference) 191 { 192 return -1; 193 } 194 else 195 { 196 return 1; 197 } 198 } 199 200 /** 201 * Calculate to the precision of the smallest provided {@link TimeUnit}, the exact duration represented by the 202 * difference between the reference timestamp, and {@code then} 203 * <p> 204 * <b>Note</b>: Precision may be lost if no supplied {@link TimeUnit} is granular enough to represent one millisecond 205 * 206 * @param then The date to be compared against the reference timestamp, or <i>now</i> if no reference timestamp was 207 * provided 208 * @return A sorted {@link List} of {@link Duration} objects, from largest to smallest. Each element in the list 209 * represents the approximate duration (number of times) that {@link TimeUnit} to fit into the previous 210 * element's delta. The first element is the largest {@link TimeUnit} to fit within the total difference 211 * between compared dates. 212 */ 213 public List<Duration> calculatePreciseDuration(final Date then) 214 { 215 if (null == reference) 216 { 217 reference = new Date(); 218 } 219 220 List<Duration> result = new ArrayList<Duration>(); 221 long difference = then.getTime() - reference.getTime(); 222 Duration duration = calculateDuration(difference); 223 result.add(duration); 224 while (0 != duration.getDelta()) 225 { 226 duration = calculateDuration(duration.getDelta()); 227 result.add(duration); 228 } 229 return result; 230 } 231 232 /** 233 * Format the given {@link Date} object. This method applies the {@code PrettyTime.approximateDuration(date)} method 234 * to perform its calculation. If {@code then} is null, it will default to {@code new Date()}; also decorate for 235 * past/future tense. 236 * 237 * @param duration the {@link Date} to be formatted 238 * @return A formatted string representing {@code then} 239 */ 240 public String format(Date then) 241 { 242 if (then == null) 243 { 244 then = new Date(); 245 } 246 Duration d = approximateDuration(then); 247 return format(d); 248 } 249 250 /** 251 * Format the given {@link Date} object. This method applies the {@code PrettyTime.approximateDuration(date)} method 252 * to perform its calculation. If {@code then} is null, it will default to {@code new Date()}; also decorate for 253 * past/future tense. Rounding rules are ignored. 254 * 255 * @param duration the {@link Date} to be formatted 256 * @return A formatted string representing {@code then} 257 */ 258 public String formatUnrounded(Date then) 259 { 260 if (then == null) 261 { 262 then = new Date(); 263 } 264 Duration d = approximateDuration(then); 265 return formatUnrounded(d); 266 } 267 268 /** 269 * Format the given {@link Duration} object, using the {@link TimeFormat} specified by the {@link TimeUnit} contained 270 * within; also decorate for past/future tense. 271 * 272 * @param duration the {@link Duration} to be formatted 273 * @return A formatted string representing {@code duration} 274 */ 275 public String format(final Duration duration) 276 { 277 TimeFormat format = getFormat(duration.getUnit()); 278 String time = format.format(duration); 279 return format.decorate(duration, time); 280 } 281 282 /** 283 * Format the given {@link Duration} object, using the {@link TimeFormat} specified by the {@link TimeUnit} contained 284 * within; also decorate for past/future tense. Rounding rules are ignored. 285 * 286 * @param duration the {@link Duration} to be formatted 287 * @return A formatted string representing {@code duration} 288 */ 289 public String formatUnrounded(Duration duration) 290 { 291 TimeFormat format = getFormat(duration.getUnit()); 292 String time = format.formatUnrounded(duration); 293 return format.decorateUnrounded(duration, time); 294 } 295 296 /** 297 * Format the given {@link Duration} objects, using the {@link TimeFormat} specified by the {@link TimeUnit} 298 * contained within. Rounds only the last {@link Duration} object. 299 * 300 * @param durations the {@link Duration}s to be formatted 301 * @return A list of formatted strings representing {@code durations} 302 */ 303 public String format(final List<Duration> durations) 304 { 305 String result = null; 306 if (durations != null) { 307 StringBuilder builder = new StringBuilder(); 308 Duration duration = null; 309 TimeFormat format = null; 310 for (int i = 0; i < durations.size(); i++) { 311 duration = durations.get(i); 312 format = getFormat(duration.getUnit()); 313 314 boolean isLast = (i == durations.size() - 1); 315 if (!isLast) { 316 builder.append(format.formatUnrounded(duration)); 317 builder.append(" "); 318 } 319 else { 320 builder.append(format.format(duration)); 321 } 322 } 323 result = format.decorateUnrounded(duration, builder.toString()); 324 } 325 return result; 326 } 327 328 /** 329 * Get the registered {@link TimeFormat} for the given {@link TimeUnit} or null if none exists. 330 */ 331 public TimeFormat getFormat(TimeUnit unit) 332 { 333 if (formatMap.get(unit) != null) 334 { 335 return formatMap.get(unit); 336 } 337 return null; 338 } 339 340 /** 341 * Get the current reference timestamp. 342 * <p> 343 * See {@code PrettyTime.setReference(Date timestamp)} 344 * 345 * @return 346 */ 347 public Date getReference() 348 { 349 return reference; 350 } 351 352 /** 353 * Set the reference timestamp. 354 * <p> 355 * If the Date formatted is before the reference timestamp, the format command will produce a String that is in the 356 * past tense. If the Date formatted is after the reference timestamp, the format command will produce a string that 357 * is in the future tense. 358 */ 359 public void setReference(final Date timestamp) 360 { 361 reference = timestamp; 362 } 363 364 /** 365 * Get a {@link List} of the current configured {@link TimeUnit} instances in calculations. 366 * 367 * @return 368 */ 369 public List<TimeUnit> getUnits() 370 { 371 List<TimeUnit> units = new ArrayList<TimeUnit>(formatMap.keySet()); 372 Collections.sort(units, new TimeUnitComparator()); 373 return Collections.unmodifiableList(units); 374 } 375 376 /** 377 * Register the given {@link TimeUnit} and corresponding {@link TimeFormat} instance to be used in calculations. If 378 * an entry already exists for the given {@link TimeUnit}, its format will be overwritten with the given 379 * {@link TimeFormat}. 380 */ 381 public void registerUnit(final TimeUnit unit, TimeFormat format) 382 { 383 formatMap.put(unit, format); 384 } 385 386 /** 387 * Get the currently configured {@link Locale} for this {@link PrettyTime} object. 388 */ 389 public Locale getLocale() 390 { 391 return locale; 392 } 393 394 /** 395 * Set the the {@link Locale} for this {@link PrettyTime} object. This may be an expensive operation, since this 396 * operation calls {@link TimeUnit#setLocale(Locale)} for each {@link TimeUnit} in {@link #getUnits()}. 397 */ 398 @Override 399 public void setLocale(final Locale locale) 400 { 401 this.locale = locale; 402 for (TimeFormat format : formatMap.values()) { 403 if (format instanceof LocaleAware) 404 ((LocaleAware) format).setLocale(locale); 405 } 406 } 407 408 @Override 409 public String toString() 410 { 411 return "PrettyTime [reference=" + reference + ", locale=" + locale + "]"; 412 } 413 414 /** 415 * Remove all registered {@link TimeUnit} instances. 416 */ 417 public void clearUnits() 418 { 419 formatMap.clear(); 420 } 421 422}