![JAR search and dependency download from the Maven repository](/logo.png)
com.day.cq.commons.date.RelativeTimeFormat Maven / Gradle / Ivy
Show all versions of aem-sdk-api Show documentation
/*
* Copyright 1997-2010 Day Management AG
* Barfuesserplatz 6, 4001 Basel, Switzerland
* All Rights Reserved.
*
* This software is the confidential and proprietary information of
* Day Management AG, ("Confidential Information"). You shall not
* disclose such Confidential Information and shall use it only in
* accordance with the terms of the license agreement you entered into
* with Day.
*/
package com.day.cq.commons.date;
import com.day.cq.i18n.I18n;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.Period;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.ResourceBundle;
/**
* The RelativeTimeFormat
provides formatting of relative time periods. Relative time periods in this
* context indicate a duration from a given start date/time to the current system time or the given end date/time.
*
* These periods can be formatted to provide human readable strings describing the duration of that period. For example
* the duration with start date 2010-06-30 3:25:00 PM and end date 2010-06-30 3:25:16 would be 16 seconds. This can for
* example be formatted as "16 seconds ago". With an end date of e.g. 2010-07-02 5:23:00 AM, the resulting formatted
* string could be "Jul 2 (2 days ago)".
*
* The desired formatting can be defined using a formatting pattern
, similar to many date formatting tools
* in existence. Relative time format patterns also result in outputting standard date/time information, e.g. the date
* or hour included in a relative time format string, e.g. "Jun 30 (16 seconds ago)". The format of the standard
* date/time information is delegated to {@link SimpleDateFormat}. Thus, in addition to the main relative time format
* pattern, sub-patterns for the standard date/time blocks can be specified in the constructor ({@link
* #RelativeTimeFormat(String, String, String, String, java.util.ResourceBundle)}. Patterns for time, medium date and
* long date information can be specified.
*
* The relative time formatting also uses localized words for output, such as e.g. the month names (e.g. "Jul" or
* "July") or the words "ago" and "minutes" to e.g. output "2 minutes ago". For localization the default system locale
* is used, unless a custom resource bundle is provided in the constructor. The custom resource bundle must provide a
* locale, otherwise again the default system locale is used, which mit result in mixed language outputs, such as "30.
* June (vor 2 Minuten)" (english/german mixture).
*
* Within date and time pattern strings, unquoted letters from 'A'
to 'Z'
and from
* 'a'
to 'z'
are interpreted as pattern letters representing the components of a date or time
* string. All other characters are not interpreted; they're simply copied into the output string during formatting or
* matched against the input string during parsing.
*
* The format function gives the ability to specify the insertion of the word ago in the resulting string. For example,
* it can be used to get a string like "15 seconds ago". If "ago" is false "15 seconds will be returned.
*
* The following pattern letters are defined (all other characters from 'A'
to 'Z'
and from
* 'a'
to 'z'
are reserved):
*
*
* Chart shows pattern letters, date/time component, presentation, and examples.
*
* Letter
* Date or Time Component
* Presentation
* Examples
*
*
* d
* Number of days of the period (localized)
* Text
* 2 days
*
*
* D
* Dynamic date. This formats the start date of the period according to the amount of time it is in the
* past with respect to period's end date:
*
* - start is last year: date is formated according to the
longPattern
, default:
* MM/dd/yyyy
(see {@link SimpleDateFormat}).
*
* - start is 1 day (24h) or more in the past: date is formated according to the
*
shortPattern
, default: MMM dd
(see {@link SimpleDateFormat}).
*
* - start is less than 24h in the past: date is formated according to the
timePattern
,
* default: HH:mm
(see {@link SimpleDateFormat}).
*
*
* Text
* 21/12/2010
, Jun 30
, 21:34
*
*
* h
* Number of hours in the period (localized)
* Text
* 4 hours
*
*
* m
* Number of minutes in the period (localized)
* Text
* 40 minutes
*
*
* M
* Number of months in the period (localized)
* Text
* 3 months
*
*
* r
* Amount of time since the start of the period (localized). The amount is structured into the following units:
* seconds, minutes, hours, days, months and years. Always the largest non-zero unit is output. E.g. if
* the amount is 2 days, 4 minutes and 54 seconds, the output would be "2 days".
* Text
* 4 months
*
*
* s
* Number of seconds in the period (localized)
* Text
* 16 seconds
*
*
* y
* Number of years in the period (localized)
* Text
* 1 year
*
*
* Y
* Fully dynamic auto-mode. The auto-mode reflects Gmail-style adaptation of the relative time output
* string, and is subject to the following rules:
*
* - start is last year: only the full date is output, according to
longPattern
,
* default: MM/dd/yyyy
.
* - start is 2 weeks or more in the past: the month and day in month are output, according to
*
shortPattern
, default: MMM dd
.
* - start is 24 hours or more in the past: the month and day in month are output, additionally
* the number of days are put in brackets. The month and day in month are formatted according to the
*
shortPattern
.
* - start is less than 24 hours in the past: the time is output according to
*
timePattern
and additionally in brackets the relative amount of time passed according
* to the r pattern character.
* - Do not need to put the boolean to true when format is called with the pattern 'Y'. "ago" will be included automatically
* in the resulting string.
*
*
* Text
* 21/12/2009
*
Jun 30
*
Sep 4 (12 days ago)
*
15:25 (2 minutes ago
*
*
*
*
*/
public class RelativeTimeFormat {
private static final String PATTERN_CHARS = "dDhmMrsyY";
public static final String SHORT = "r";
public static final String FULL = "y M d h m s";
public static final String FACEBOOK = SHORT;
public static final String GMAIL = "Y";
private final ResourceBundle bundle;
private final char[] compiledPattern;
private final Calendar calendar;
private final Calendar now;
private final SimpleDateFormat timeFmt;
private final SimpleDateFormat shortFmt;
private final SimpleDateFormat longFmt;
private Locale locale;
private Period period;
private I18n i18n;
/**
* Constructs a new RelativeTimeFormat
instance.
*
* @param pattern The pattern for the relative time.
*/
public RelativeTimeFormat(final String pattern) {
this(pattern, null, null, null, null);
}
/**
* Constructs a new RelativeTimeFormat
instance.
*
* @param pattern The pattern for the relative time.
* @param bundle The {@link ResourceBundle} for i18n of the time labels (e.g. "days", "seconds", ...), may be
* null
.
*/
public RelativeTimeFormat(final String pattern, final ResourceBundle bundle) {
this(pattern, null, null, null, bundle);
}
/**
* Constructs a new RelativeTimeFormat
instance.
*
* @param pattern The formatting pattern for the relative time.
* @param timePattern The {@link SimpleDateFormat} pattern for time (default: HH:mm), may be null
.
* @param shortPattern The {@link SimpleDateFormat} pattern for month and day (default: MMM d), may be
* null
.
* @param longPattern The {@link SimpleDateFormat} pattern for full date (default: MM/dd/yyyy), may be
* null
.
* @param bundle The {@link ResourceBundle} for i18n of the time labels (e.g. "days", "seconds", ...), may be
* null
.
*/
public RelativeTimeFormat(final String pattern, final String timePattern, final String shortPattern,
final String longPattern, final ResourceBundle bundle) {
if (StringUtils.isBlank(pattern)) {
throw new IllegalArgumentException("pattern may not be null or empty");
}
this.bundle = bundle;
getLocale();
i18n = new I18n(bundle);
// compile the provided relative time pattern
compiledPattern = compile(pattern);
// initialize the reference calendar instance
calendar = Calendar.getInstance(locale);
now = Calendar.getInstance(locale);
// initialize date formats
timeFmt = new SimpleDateFormat(StringUtils.defaultIfEmpty(timePattern,
i18n.get("HH:mm", "Java date format for a time (http://java.sun.com/j2se/1.5.0/docs/api/java/text/SimpleDateFormat.html)")),
locale);
shortFmt = new SimpleDateFormat(StringUtils.defaultIfEmpty(shortPattern,
i18n.get("MMM d", "Java date format for a date (http://java.sun.com/j2se/1.5.0/docs/api/java/text/SimpleDateFormat.html)")),
locale);
longFmt = new SimpleDateFormat(StringUtils.defaultIfEmpty(longPattern,
i18n.get("MM/dd/yyyy", "Java date format for a date (http://java.sun.com/j2se/1.5.0/docs/api/java/text/SimpleDateFormat.html)")),
locale);
}
/**
* Formats the time period from the given start
date/time according to the pattern selected in the
* constructor. The end date/time of the period is the current system time. To specify your own end date/time of the
* period, use {@link #format(long, long)}.
*
* @param start The start date/time of the period to be formatted in milliseconds.
*
* @return A String
representing the formatted period.
*/
public String format(final long start) {
return format(start, System.currentTimeMillis(), false);
}
/**
* Formats the time period from the given start
date/time according to the pattern selected in the
* constructor. The end date/time of the period is the current system time. To specify your own end date/time of the
* period, use {@link #format(long, long)}.
*
* @param start The start date/time of the period to be formatted in milliseconds.
* @param ago The resulting string should contain "ago" (or localized variant)
*
* @return A String
representing the formatted period.
*/
public String format(final long start, final boolean ago) {
return format(start, System.currentTimeMillis(), ago);
}
/**
* Formats the time period as defined via the given start
date/time and the given end
* date/time, according to the pattern selected in the constructor.
*
* @param start The start date/time of the period to be formatted in milliseconds.
* @param end The end date/time of the period to be formatted in milliseconds.
*
* @return A String
representing the formatted period.
*/
public String format(final long start, final long end) {
return format(start, end, false);
}
/**
* Formats the time period as defined via the given start
date/time and the given end
* date/time, according to the pattern selected in the constructor.
*
* @param start The start date/time of the period to be formatted in milliseconds.
* @param end The end date/time of the period to be formatted in milliseconds.
* @param ago The resulting string should contain "ago" (or localized variant)
*
* @return A String
representing the formatted period.
*/
public String format(final long start, final long end, final boolean ago) {
calendar.setTimeInMillis(start);
now.setTimeInMillis(end);
period = new Period(start, end);
final StringBuilder buffer = new StringBuilder();
int i = 0;
for (final char c : compiledPattern) {
final int index = PATTERN_CHARS.indexOf(c);
if (index >= 0) {
if (i == compiledPattern.length-1) {
fieldFormat(index, buffer, ago);
} else {
fieldFormat(index, buffer, false);
}
} else {
buffer.append(c);
}
i++;
}
return buffer.toString();
}
private void fieldFormat(final int index, final StringBuilder buffer, final boolean ago) {
final int years = period.getYears();
final int months = period.getMonths();
final int weeks = period.getWeeks();
final int days = (weeks > 0) ? period.getDays() + weeks * 7 : period.getDays();
final int hours = period.getHours();
final int minutes = period.getMinutes();
final int seconds = period.getSeconds();
String result = "";
// switches for index of "dDhmMrsyY"
switch (index) {
case 0: // 'd' - day
if (days == 1) {
result = ago ? i18n.get("{0} day ago", null, days) : i18n.get("{0} day", null, days);
} else {
result = ago ? i18n.get("{0} days ago", null, days) : i18n.get("{0} days", null, days);
}
break;
case 1: // 'D' - dynamic date
buffer.append(getDynamicDate(months, weeks, days));
break;
case 2: // 'h' - hours
if (hours == 1) {
result = ago ? i18n.get("{0} hour ago", null, hours) : i18n.get("{0} hour", null, hours);
} else {
result = ago ? i18n.get("{0} hours ago", null, hours) : i18n.get("{0} hours", null, hours);
}
break;
case 3: // 'm' - minutes
if (minutes == 1) {
result = ago ? i18n.get("{0} minute ago", null, minutes) : i18n.get("{0} minute", null, minutes);
} else {
result = ago ? i18n.get("{0} minutes ago", null, minutes) : i18n.get("{0} minutes", null, minutes);
}
break;
case 4: // 'M' - months
if (months == 1) {
result = ago ? i18n.get("{0} month ago", null, months) : i18n.get("{0} month", null, months);
} else {
result = ago ? i18n.get("{0} months ago", null, months) : i18n.get("{0} months", null, months);
}
break;
case 5: // 'r' - relative time (auto mode)
buffer.append(getRelative(years, months, days, hours, minutes, seconds, ago));
break;
case 6: // 's' - seconds
if (seconds < 1) {
result = i18n.get("now", null, seconds);
} else {
if (seconds == 1) {
result = ago ? i18n.get("{0} second ago", null, seconds) : i18n.get("{0} second", null, seconds);
} else {
result = ago ? i18n.get("{0} seconds ago", null, seconds) : i18n.get("{0} seconds", null, seconds);
}
}
break;
case 7: // 'y' - years
if (years == 1) {
result = ago ? i18n.get("{0} year ago", null, years) : i18n.get("{0} year", null, years);
} else {
result = ago ? i18n.get("{0} years ago", null, years) : i18n.get("{0} years", null, years);
}
break;
case 8: // 'Y' - full dynamic auto mode
buffer.append(getDynamicDate(months, weeks, days));
if (calendar.get(Calendar.YEAR) == now.get(Calendar.YEAR) && months == 0 && weeks < 2) {
buffer.append(" (")
.append(i18n.get("{0}", null, getRelative(years, months, days, hours, minutes, seconds, true)))
.append(")");
}
break;
default:
}
buffer.append(result);
}
private String getRelative(final int years, final int months, final int days, final int hours,
final int minutes, final int seconds, final boolean ago) {
String label = "";
if (0 < years) {
if (years == 1) {
label = ago ? i18n.get("{0} year ago", null, years) : i18n.get("{0} year", null, years);
} else {
label = ago ? i18n.get("{0} years ago", null, years) : i18n.get("{0} years", null, years);
}
} else if (0 < months) {
if (months == 1) {
label = ago ? i18n.get("{0} month ago", null, months) : i18n.get("{0} month", null, months);
} else {
label = ago ? i18n.get("{0} months ago", null, months) : i18n.get("{0} months", null, months);
}
} else if (0 < days) {
int count = days;
if (count == 1) {
label = ago ? i18n.get("{0} day ago", null, count) : i18n.get("{0} day", null, count);
} else {
label = ago ? i18n.get("{0} days ago", null, count) : i18n.get("{0} days", null, count);
}
} else if (0 < hours) {
if (hours == 1) {
label = ago ? i18n.get("{0} hour ago", null, hours) : i18n.get("{0} hour", null, hours);
} else {
label = ago ? i18n.get("{0} hours ago", null, hours) : i18n.get("{0} hours", null, hours);
}
} else if (0 < minutes) {
if (minutes == 1) {
label = ago ? i18n.get("{0} minute ago", null, minutes) : i18n.get("{0} minute", null, minutes);
} else {
label = ago ? i18n.get("{0} minutes ago", null, minutes) : i18n.get("{0} minutes", null, minutes);
}
} else if (0 <= seconds) {
if (seconds < 1) {
label = i18n.get("now", null, seconds);
} else {
if (seconds == 1) {
label = ago ? i18n.get("{0} second ago", null, seconds) : i18n.get("{0} second", null, seconds);
} else {
label = ago ? i18n.get("{0} seconds ago", null, seconds) : i18n.get("{0} seconds", null, seconds);
}
}
}
return label;
}
private String getDynamicDate(final int months, final int weeks, final int days) {
// period start date is last year => display full numeric date
if (calendar.get(Calendar.YEAR) < now.get(Calendar.YEAR)) {
return longFmt.format(calendar.getTime());
} else if (months > 0 || weeks >= 2) { // period is more than or equal two weeks
return shortFmt.format(calendar.getTime());
} else if (days > 0) { // period is more than or equal 24h and less than two weeks
return shortFmt.format(calendar.getTime());
} else {
return timeFmt.format(calendar.getTime());
}
}
private char[] compile(final String pattern) {
final StringBuilder buffer = new StringBuilder();
final int length = pattern.length();
for (int i = 0; i < length; i++) {
final char c = pattern.charAt(i);
if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) { // punctuation/spaces/braces/...
buffer.append(c);
} else { // alphabetic
if (PATTERN_CHARS.indexOf(c) >= 0) {
buffer.append(c);
} else {
throw new IllegalArgumentException("illegal pattern character \"'" + c + "\'");
}
}
}
return buffer.toString().toCharArray();
}
private Locale getLocale() {
if (null == locale && null != bundle) {
locale = bundle.getLocale();
} else if (null != locale) {
return locale;
}
if (null == locale) {
locale = Locale.getDefault();
}
return locale;
}
}