| DateConverter.java |
/*
* $Id: DateConverter.java,v 1.46 2012/02/27 22:26:04 agoubard Exp $
*
* See the COPYRIGHT file for redistribution and use restrictions.
*/
package org.xins.common.text;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.text.SimpleDateFormat;
import org.xins.common.MandatoryArgumentChecker;
/**
* Utility class for converting a UNIX Epoch date to a human-readable time
* stamp.
*
* <p>For example, the date 26 July 2003, time 17:03, 59 seconds and 653
* milliseconds will convert to the string
* <code>"2003.07.26 17:03:59.653"</code>.
*
* <p>See <a href="http://en.wikipedia.org/wiki/Unix_Epoch">the Wikipedia
* article about the UNIX Epoch</a> for more information.
*
* @version $Revision: 1.46 $ $Date: 2012/02/27 22:26:04 $
* @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
* @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
*
* @since XINS 1.0.0
*/
public class DateConverter {
/**
* All decimal digit characters, <code>'0'</code> to <code>'9'</code>.
*/
private static final char[] DEC_DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
};
/**
* Two-length string representations of the digits 0 to 99.
*/
private static final char[][] VALUES;
/**
* Date formatter that is used as a slow but accurate method for formatting
* the date. Never <code>null</code>.
*/
private final SimpleDateFormat _formatter;
/**
* Length of the produced timestamp string. If the century is printed, it's
* 18, otherwise it's 16.
*/
private final int _length;
/**
* Cached date, as a number of milliseconds since the Epoch.
*/
private long _cachedDate;
/**
* Cached date, as a number of minutes since the Epoch.
*/
private long _cachedMinutes;
/**
* Part of the cached date, the number of seconds in the current minute.
*/
private long _cachedJustSeconds;
/**
* Character buffer containing the formatted version of the cached date.
* See {@link #_cachedDate}. The length is always equal to
* {@link #_length}.
*/
private char[] _cachedDateBuffer;
/**
* Class initialized. Initializes the {@link #VALUES} array.
*/
static {
// Fill the VALUES array
VALUES = new char[100][];
for (int i = 0; i < 100; i++) {
int first = ((int) '0') + (i / 10);
int second = ((int) '0') + (i % 10);
VALUES[i] = new char[] { (char) first, (char) second };
}
}
/**
* Creates a new <code>DateConverter</code>.
*
* @param withCentury
* <code>true</code> if the century should be in the result,
* <code>false</code> otherwise.
*
* @since XINS 1.3.0
*/
public DateConverter(boolean withCentury) {
// Determine the length of the formatted date strings
_length = withCentury ? 18 : 16;
_cachedDateBuffer = new char[_length];
// Construct a formatter for slow formatting
String format = "yyMMdd-HHmmssSSS";
if (withCentury) {
format = "yy" + format;
}
_formatter = new SimpleDateFormat(format);
// Pre-cache the current date
recompute(System.currentTimeMillis());
}
/**
* Convert the specified <code>long</code> to a human-readable time stamp.
* The current time zone is used.
*
* @param time
* the time stamp to be converted to a human-readable character string,
* as a number of milliseconds since the
* <a href="http://en.wikipedia.org/wiki/Unix_Epoch">UNIX Epoch</a>.
* must be greater than {@link Long#MIN_VALUE} and smaller than
* {@link Long#MAX_VALUE}.
*
* @return
* the converted character string, cannot be <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>n == {@link Long#MIN_VALUE}
* || n == {@link Long#MAX_VALUE}</code>.
*
* @since XINS 1.5.0
*/
public static String toDateString(long time)
throws IllegalArgumentException {
return toDateString(TimeZone.getDefault(), time);
}
/**
* Convert the specified <code>long</code> to a human-readable time stamp.
*
* @param timeZone
* the time zone to use, cannot be <code>null</code>.
*
* @param n
* the time stamp to be converted to a human-readable character string,
* as a number of milliseconds since the Epoch (midnight January 1,
* 1970), must be greater than {@link Long#MIN_VALUE} and smaller than
* {@link Long#MAX_VALUE}.
*
* @return
* the converted character string, cannot be <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>n == {@link Long#MIN_VALUE} || n == {@link Long#MAX_VALUE} || timeZone == null</code>.
*/
public static String toDateString(TimeZone timeZone, long n)
throws IllegalArgumentException {
// Check preconditions
MandatoryArgumentChecker.check("timeZone", timeZone);
if (n == Long.MIN_VALUE) {
throw new IllegalArgumentException("n == Long.MIN_VALUE");
} else if (n == Long.MAX_VALUE) {
throw new IllegalArgumentException("n == Long.MAX_VALUE");
}
StringBuilder buffer = new StringBuilder(23);
GregorianCalendar calendar = new GregorianCalendar(timeZone);
calendar.setTimeInMillis(n);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int min = calendar.get(Calendar.MINUTE);
int sec = calendar.get(Calendar.SECOND);
int ms = calendar.get(Calendar.MILLISECOND);
// Append year followed by a dot, length is now 5
buffer.append(year);
buffer.append('.');
// Append month followed by a dot, length is now 8
// Note that the month int is 0-based
buffer.append(VALUES[month + 1]);
buffer.append('.');
// Append day followed by a space, length is now 11
buffer.append(VALUES[day]);
buffer.append(' ');
// Append hour followed by a colon, length is now 14
buffer.append(VALUES[hour]);
buffer.append(':');
// Append minute followed by a colon, length is now 17
buffer.append(VALUES[min]);
buffer.append(':');
// Append second followed by a dot, length is now 20
buffer.append(VALUES[sec]);
buffer.append('.');
// Append milli-second, length is now 23
if (ms < 10) {
buffer.append("00");
} else if (ms < 100) {
buffer.append('0');
}
buffer.append(String.valueOf(ms));
return buffer.toString();
}
/**
* Recomputes the cached formatted date.
*
* @param date
* the timestamp to reinitialize the cache with, as a number of
* milliseconds since the Epoch.
*/
private void recompute(long date) {
// Store the cached date
_cachedDate = date;
_cachedMinutes = date / 60000L;
long seconds = date / 1000L;
_cachedJustSeconds = (int) (seconds % 60L);
// Format the date
synchronized (_formatter) {
String s = _formatter.format(new Date(_cachedDate));
s.getChars(0, _length, _cachedDateBuffer, 0);
}
}
/**
* Formats the specified timestamp as a <code>String</code>. Depending on
* the setting of the <em>withCentury</em> property (passed to the
* constructor), the format is as follows. If <em>withCentury</em> is set,
* then the format is:
*
* <blockquote><em>CCYYMMDD-hhmmssSSS</em> (length is 18)</blockquote>
*
* Otherwise, if <em>withCentury</em> is not set, then the format is
* without the century:
*
* <blockquote><em>YYMMDD-hhmmssSSS</em> (length is 16)</blockquote>
*
* <p>The timestamp is a number of milliseconds since the Epoch (midnight
* at the start of January 1, 1970, GMT). See
* {@link System#currentTimeMillis()}.
*
* <p>Note: This method is <em>not</em> thread-safe.
*
* @param date
* the timestamp, in milliseconds since the Epoch, must be >=
* <code>0L</code>.
*
* @return
* a character string with the specified date in the correct format.
*
* @throws IllegalArgumentException
* if <code>date < 0L</code>.
*
* @since XINS 1.3.0
*/
public String format(long date)
throws IllegalArgumentException {
// Check precondition
if (date < 0L) {
throw new IllegalArgumentException("date (" + date + ") < 0L");
}
// Reserve a character buffer
char[] buffer = new char[_length];
// Perform formatting
format(date, buffer, 0);
// Convert the buffer to a String object
return new String(buffer);
}
/**
* Formats the specified timestamp as a string in a character buffer.
* Depending on the setting of the <em>withCentury</em> property (passed
* to the constructor), the format is as follows. If <em>withCentury</em>
* is set, then the format is:
*
* <blockquote><em>CCYYMMDD-hhmmssSSS</em> (length is 18)</blockquote>
*
* Otherwise, if <em>withCentury</em> is not set, then the format is
* without the century:
*
* <blockquote><em>YYMMDD-hhmmssSSS</em> (length is 16)</blockquote>
*
* <p>The timestamp is a number of milliseconds since the Epoch (midnight
* at the start of January 1, 1970, GMT). See
* {@link System#currentTimeMillis()}.
*
* <p>Note: This method is <em>not</em> thread-safe.
*
* @param date
* the timestamp, in milliseconds since the Epoch, must be >=
* <code>0L</code>.
*
* @param buffer
* the character buffer to put the formatted timestamp in, cannot be
* <code>null</code>.
*
* @param offset
* the offset into the character buffer.
*
* @throws IllegalArgumentException
* if <code>date < 0L</code>.
*
* @throws NullPointerException
* if <code>buffer == null</code>.
*
* @throws IndexOutOfBoundsException
* if <code>offset</code> is invalid (less than <code>0</code>) or
* incorrect (not leaving enough room for the formatter date).
*
* @since XINS 1.3.0
*/
public void format(long date, char[] buffer, int offset)
throws IllegalArgumentException,
NullPointerException,
IndexOutOfBoundsException {
// Check precondition
if (date < 0L) {
throw new IllegalArgumentException("date (" + date + ") < 0L");
}
// Cache the length of the generated string
int length = _length;
// Compute the delta with the cached date
long delta = date - _cachedDate;
// If we are in the same millisecond, then short-circuit
if (delta == 0) {
System.arraycopy(_cachedDateBuffer, 0, buffer, offset, length);
return;
}
// Determine the number of seconds and milliseconds
long minutes = date / 60000L;
long seconds = date / 1000L;
int justSeconds = (int) (seconds % 60L);
int justMillis = (int) (date % 1000L);
// We are in the same minute
if (minutes == _cachedMinutes) {
// First copy the whole cached formatted string
System.arraycopy(_cachedDateBuffer, 0, buffer, offset, length);
// If we are not in the same second, correct the seconds
int pos = (length + offset) - 5;
if (justSeconds != _cachedJustSeconds) {
buffer[pos++] = DEC_DIGITS[justSeconds / 10];
buffer[pos++] = DEC_DIGITS[justSeconds % 10];
} else {
pos++;
pos++;
}
// Correct the milliseconds
buffer[pos++] = DEC_DIGITS[ justMillis / 100 ];
buffer[pos++] = DEC_DIGITS[(justMillis % 100) / 10];
buffer[pos ] = DEC_DIGITS[ justMillis % 10 ];
// We are not in the same minute, so recompute
} else {
recompute(date);
System.arraycopy(_cachedDateBuffer, 0, buffer, offset, length);
}
}
}