freemarker.template.utility.DateUtil Maven / Gradle / Ivy
Go to download
Google App Engine compliant variation of FreeMarker.
FreeMarker is a "template engine"; a generic tool to generate text output based on templates.
/*
* Copyright (c) 2003 The Visigoth Software Society. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution, if
* any, must include the following acknowledgement:
* "This product includes software developed by the
* Visigoth Software Society (http://www.visigoths.org/)."
* Alternately, this acknowledgement may appear in the software itself,
* if and wherever such third-party acknowledgements normally appear.
*
* 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
* project contributors may be used to endorse or promote products derived
* from this software without prior written permission. For written
* permission, please contact [email protected].
*
* 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
* nor may "FreeMarker" or "Visigoth" appear in their names
* without prior written permission of the Visigoth Software Society.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Visigoth Software Society. For more
* information on the Visigoth Software Society, please see
* http://www.visigoths.org/
*/
package freemarker.template.utility;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
public class DateUtil {
public static final int ACCURACY_HOURS = 4;
public static final int ACCURACY_MINUTES = 5;
public static final int ACCURACY_SECONDS = 6;
public static final int ACCURACY_MILLISECONDS = 7;
public static final TimeZone UTC = TimeZone.getTimeZone("UTC");
private final static TimeZoneOffsetCalculator TIME_ZONE_OFFSET_CALCULATOR
= getTimeZoneOffsetCalculator();
private static TimeZoneOffsetCalculator getTimeZoneOffsetCalculator() {
try {
Class cl = Class.forName(
"freemarker.template.utility.J2SE14TimeZoneOffsetCalculator");
return (TimeZoneOffsetCalculator) cl.newInstance();
} catch (final Throwable e) {
return new TimeZoneOffsetCalculator() {
public int getOffset(TimeZone tz, Date date) {
throw new RuntimeException(
"Failed to create TimeZoneOffsetCalculator. " +
"Note that this feature requires at least " +
"Java 1.4.\nCause exception: " + e);
}
};
}
}
private DateUtil() {
// can't be instantiated
}
/**
* Returns the time zone object for the name (or ID). This differs from
* {@link TimeZone#getTimeZone(String)} in that the latest returns GMT
* if it doesn't recognize the name, while this throws an
* {@link UnrecognizedTimeZoneException}.
*
* @throws UnrecognizedTimeZoneException
*/
public static TimeZone getTimeZone(String name)
throws UnrecognizedTimeZoneException {
if (isGMTish(name)) {
if (name.equalsIgnoreCase("UTC")) {
return UTC;
}
return TimeZone.getTimeZone(name);
}
TimeZone tz = TimeZone.getTimeZone(name);
if (isGMTish(tz.getID())) {
throw new UnrecognizedTimeZoneException(name);
}
return tz;
}
/**
* Tells if a offset or time zone is GMT. GMT is a fuzzy term, it used to
* referred both to UTC and UT1.
*/
private static boolean isGMTish(String name) {
if (name.length() < 3) {
return false;
}
char c1 = name.charAt(0);
char c2 = name.charAt(1);
char c3 = name.charAt(2);
if (
!(
(c1 == 'G' || c1 == 'g')
&& (c2 == 'M' || c2 == 'm')
&& (c3 == 'T' || c3 == 't')
)
&&
!(
(c1 == 'U' || c1 == 'u')
&& (c2 == 'T' || c2 == 't')
&& (c3 == 'C' || c3 == 'c')
)
&&
!(
(c1 == 'U' || c1 == 'u')
&& (c2 == 'T' || c2 == 't')
&& (c3 == '1' || c3 == '1')
)
) {
return false;
}
if (name.length() == 3) {
return true;
}
String offset = name.substring(3);
if (offset.startsWith("+")) {
return offset.equals("+0") || offset.equals("+00")
|| offset.equals("+00:00");
} else {
return offset.equals("-0") || offset.equals("-00")
|| offset.equals("-00:00");
}
}
/**
* Format a date, time or date+time with one of the ISO 8601 extended
* formats. Examples of possible outputs: {@code "2005-11-27T15:30:00+02"},
* {@code "2005-11-27"}, {@code "15:30:00Z"}.
*
* This method is thread-safe.
*
* @param date the date to convert to ISO 8601 string
* @param datePart whether the date part (year, month, day) will be included
* or not
* @param timePart whether the time part (hours, minutes, seconds,
* milliseconds) will be included or not
* @param offsetPart whether the time zone offset part will be included or
* not. This will be shown as an offset to UTC (examples:
* {@code "+01"}, {@code "-02"}, {@code "+04:30"}) or as {@code "Z"}
* for UTC (and for UT1 and for GMT+00, since the Java platform
* doesn't really care about the difference).
* Note that this can't be {@code true} when {@code timePart} is
* {@code false}, because ISO 8601 (2004) doesn't mention such
* patterns.
* @param accuracy tells which parts of the date/time to drop. The
* {@code datePart} and {@code timePart} parameters are stronger than
* this. Note that when {@link #ACCURACY_MILLISECONDS} is specified,
* the milliseconds part will be displayed as fraction seconds
* (like {@code "15:30.00.25"}) with the minimum number of
* digits needed to show the milliseconds without precision lose.
* Thus, if the milliseconds happen to be exactly 0, no fraction
* seconds will be shown at all.
* @param timeZone the time zone in which the date/time will be shown. (You
* may find {@link DateUtil#UTC} handy here.) Note
* that although date-only formats has no time zone offset part,
* the result still depends on the time zone, as days start and end
* at different points in time in different zones.
* @param calendarFactory the factory that will create the calendar used
* internally for calculations. The point of this parameter is that
* creating a new calendar is relatively expensive, so it's desirable
* to reuse calendars and only set their time and zone. (This was
* tested on Sun JDK 1.6 x86 Win, where it gave 2x-3x speedup.)
*/
public static String dateToISO8601String(
Date date,
boolean datePart, boolean timePart, boolean offsetPart,
int accuracy,
TimeZone timeZone,
DateToISO8601CalendarFactory calendarFactory) {
if (!timePart && offsetPart) {
throw new IllegalArgumentException(
"ISO 8601:2004 doesn't specify any formats where the "
+ "offset is shown but the time isn't.");
}
if (timeZone == null) {
timeZone = UTC;
}
GregorianCalendar cal = calendarFactory.get(timeZone, date);
int maxLength;
if (!timePart) {
maxLength = 10; // YYYY-MM-DD
} else {
if (!datePart) {
maxLength = 18; // HH:MM:SS.mmm+00:00
} else {
maxLength = 10 + 1 + 18;
}
}
char[] res = new char[maxLength];
int dstIdx = 0;
if (datePart) {
int x = cal.get(Calendar.YEAR);
if (x > 0 && cal.get(Calendar.ERA) == GregorianCalendar.BC) {
x = -x + 1;
}
if (x >= 0 && x < 9999) {
res[dstIdx++] = (char) ('0' + x / 1000);
res[dstIdx++] = (char) ('0' + x % 1000 / 100);
res[dstIdx++] = (char) ('0' + x % 100 / 10);
res[dstIdx++] = (char) ('0' + x % 10);
} else {
String yearString = String.valueOf(x);
// Re-allocate buffer:
maxLength = maxLength - 4 + yearString.length();
res = new char[maxLength];
for (int i = 0; i < yearString.length(); i++) {
res[dstIdx++] = yearString.charAt(i);
}
}
res[dstIdx++] = '-';
x = cal.get(Calendar.MONTH) + 1;
dstIdx = append00(res, dstIdx, x);
res[dstIdx++] = '-';
x = cal.get(Calendar.DAY_OF_MONTH);
dstIdx = append00(res, dstIdx, x);
if (timePart) {
res[dstIdx++] = 'T';
}
}
if (timePart) {
int x = cal.get(Calendar.HOUR_OF_DAY);
dstIdx = append00(res, dstIdx, x);
if (accuracy >= ACCURACY_MINUTES) {
res[dstIdx++] = ':';
x = cal.get(Calendar.MINUTE);
dstIdx = append00(res, dstIdx, x);
if (accuracy >= ACCURACY_SECONDS) {
res[dstIdx++] = ':';
x = cal.get(Calendar.SECOND);
dstIdx = append00(res, dstIdx, x);
if (accuracy >= ACCURACY_MILLISECONDS) {
x = cal.get(Calendar.MILLISECOND);
if (x != 0) {
if (x > 999) {
// Shouldn't ever happen...
throw new RuntimeException(
"Calendar.MILLISECOND > 999");
}
res[dstIdx++] = '.';
do {
res[dstIdx++] = (char) ('0' + (x / 100));
x = x % 100 * 10;
} while (x != 0);
}
}
}
}
}
if (offsetPart) {
if (timeZone == UTC) {
res[dstIdx++] = 'Z';
} else {
int dt = TIME_ZONE_OFFSET_CALCULATOR.getOffset(timeZone, date);
boolean positive;
if (dt < 0) {
positive = false;
dt = -dt;
} else {
positive = true;
}
dt /= 1000;
int offS = dt % 60;
dt /= 60;
int offM = dt % 60;
dt /= 60;
int offH = dt;
if (offS == 0 && offM == 0 && offH == 0) {
res[dstIdx++] = 'Z';
} else {
res[dstIdx++] = positive ? '+' : '-';
dstIdx = append00(res, dstIdx, offH);
if (offM != 0 || offS != 0) {
res[dstIdx++] = ':';
dstIdx = append00(res, dstIdx, offM);
if (offS != 0) {
res[dstIdx++] = ':';
dstIdx = append00(res, dstIdx, offS);
}
}
}
}
}
return new String(res, 0, dstIdx);
}
/**
* Appends a number between 0 and 99 padded to 2 digits.
*/
private static int append00(char[] res, int dstIdx, int x) {
res[dstIdx++] = (char) ('0' + x / 10);
res[dstIdx++] = (char) ('0' + x % 10);
return dstIdx;
}
/**
* Used internally by {@link DateUtil}; don't use it's implementations for
* anything else.
*/
public interface DateToISO8601CalendarFactory {
/**
* Returns a {@link GregorianCalendar} with the desired time zone and
* time and US locale. The returned calendar is used as read-only.
* It's guaranteed that within a thread the instance returned last time
* is not in use anymore when this method is called again.
*/
GregorianCalendar get(TimeZone tz, Date date);
}
/**
* Non-thread-safe factory that hard-references a calendar internally.
*/
public static final class TrivialDateToISO8601CalendarFactory
implements DateToISO8601CalendarFactory {
private GregorianCalendar calendar;
public GregorianCalendar get(TimeZone tz, Date date) {
if (calendar == null) {
calendar = new GregorianCalendar(tz, Locale.US);
} else {
calendar.setTimeZone(tz);
}
calendar.setTime(date);
return calendar;
}
}
interface TimeZoneOffsetCalculator {
int getOffset(TimeZone tz, Date date);
}
}