PageRenderTime 79ms CodeModel.GetById 16ms app.highlight 57ms RepoModel.GetById 1ms app.codeStats 0ms

/src/main/java/org/ocpsoft/prettytime/PrettyTime.java

http://github.com/ocpsoft/prettytime
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}