/**
* Copyright (C) 2010 MediaShelf <http://www.yourmediashelf.com/>
*
* This file is part of fedora-client.
*
* fedora-client is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* fedora-client is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with fedora-client. If not, see <http://www.gnu.org/licenses/>.
*/
package com.yourmediashelf.fedora.util;
import java.text.DecimalFormat;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
/**
* Date and time utility methods.
*
* @author Edwin Shin
*/
public class DateUtility {
/**
* <p>
* Regular expression string for the yearFrag:
* </p>
* <p>
* a numeral consisting of at least four decimal digits, optionally preceded
* by a minus sign; leading '0' digits are prohibited except to bring the
* digit count up to four.
* </p>
*/
private final static String yearFrag = "(-?([1-9][0-9]{3,}|0[0-9]{3}))";
/**
* <p>
* Regular expression string for the monthFrag:
* </p>
* <p>
* a numeral consisting of exactly two decimal digits.
* </p>
*/
private final static String monthFrag = "(0[1-9]|1[0-2])";
/**
* <p>
* Regular expression string for the dayFrag:
* </p>
* <p>
* a numeral consisting of exactly two decimal digits.
* </p>
* <p>
* Note that this regex does not enforce the day-of-month constraint, which
* states:
* </p>
* <p>
* The day value must be no more than 30 if month is one of 4, 6, 9, or 11;
* no more than 28 if month is 2 and year is not divisible 4, or is
* divisible by 100 but not by 400; and no more than 29 if month is 2 and
* year is divisible by 400, or by 4 but not by 100.
* </p>
*/
private final static String dayFrag = "(0[1-9]|[12][0-9]|3[01])";
/**
* <p>
* Regular expression string for the hourFrag:
* </p>
* <p>
* a numeral consisting of exactly two decimal digits.
* </p>
*/
private final static String hourFrag = "([01][0-9]|2[0-3])";
/**
* <p>
* Regular expression string for the minuteFrag:
* </p>
* <p>
* a numeral consisting of exactly two decimal digits.
* </p>
*/
private final static String minuteFrag = "([0-5][0-9])";
/**
* <p>
* Regular expression string for the secondFrag:
* </p>
* <p>
* a numeral consisting of exactly two decimal digits, or two decimal
* digits, a decimal point, and one or more trailing digits.
* </p>
*/
private final static String secondFrag = "([0-5][0-9])(\\.([0-9]+))?";
/**
* <p>
* Regular expression string for the endOfDayFrag:
* </p>
* <p>
* combines the {@link hourFrag}, {@link minuteFrag}, {@link minuteFrag},
* and their separators to represent midnight of the day, which is the first
* moment of the next day.
* </p>
*/
private final static String endOfDayFrag = String.format(
"(%s:%s:%s|(24:00:00(\\.0+)?))", hourFrag, minuteFrag, secondFrag);
/**
* <p>
* Regular expression string for the timezoneFrag:
* </p>
* <p>
* if present, specifies an offset between UTC and local time. Time zone
* offsets are a count of minutes (expressed in timezoneFrag as a count of
* hours and minutes) that are added or subtracted from UTC time to get the
* "local" time. 'Z' is an alternative representation of the time zone
* offset '00:00', which is, of course, zero minutes from UTC.
* </p>
*/
private final static String timezoneFrag =
"(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?";
/**
* Concatenation of the {@link yearFrag}, {@link monthFrag}, {@link dayFrag}
* , {@link endOfDayFrag}, {@link timezoneFrag}, and their separators.
*/
private final static String combined = String.format("%s-%s-%sT%s%s",
yearFrag, monthFrag, dayFrag, endOfDayFrag, timezoneFrag);
private final static Pattern XSD_DATETIME = Pattern.compile(combined);
private final static ConcurrentMap<String, DateTimeFormatter> formatters =
new ConcurrentHashMap<String, DateTimeFormatter>();
/**
* Parses lexical representations of xsd:dateTimes, e.g.
* "2010-01-31T14:21:03.001Z". Fractional seconds and timezone offset are
* optional.
*
* <p>Note: fractional seconds are only supported to three digits of
* precision.</p>
*
* @param input
* an XML Schema 1.1 dateTime
* @return a DateTime representing the input
* @see "http://www.w3.org/TR/xmlschema11-2/#dateTime"
*/
public static DateTime parseXSDDateTime(String input) {
Matcher m = XSD_DATETIME.matcher(input);
if (!m.find()) {
throw new IllegalArgumentException(input +
" is not a valid XML Schema 1.1 dateTime.");
}
int year = Integer.parseInt(m.group(1));
int month = Integer.parseInt(m.group(3));
int day = Integer.parseInt(m.group(4));
int hour = 0, minute = 0, second = 0, millis = 0;
boolean hasEndOfDayFrag = m.group(11) != null;
if (!hasEndOfDayFrag) {
hour = Integer.parseInt(m.group(6));
minute = Integer.parseInt(m.group(7));
second = Integer.parseInt(m.group(8));
// Parse fractional seconds
// m.group(9), if not null/empty should be Strings such as ".5" or
// ".050" which convert to 500 and 50, respectively.
if (m.group(9) != null && !m.group(9).isEmpty()) {
// parse as Double as a quick hack to drop trailing 0s.
// e.g. ".0500" becomes 0.05
double d = Double.parseDouble(m.group(9));
// Something like the following would allow for int-sized
// precision, but joda-time 1.6 only supports millis (i.e. <= 999).
// see: org.joda.time.field.FieldUtils.verifyValueBounds
// int digits = String.valueOf(d).length() - 2;
// fractionalSeconds = (int) (d * Math.pow(10, digits));
millis = (int) (d * 1000);
}
}
DateTimeZone zone = null;
if (m.group(13) != null) {
String tmp = m.group(13);
if (tmp.equals("Z")) {
tmp = "+00:00";
}
zone = DateTimeZone.forID(tmp);
}
DateTime dt =
new DateTime(year, month, day, hour, minute, second, millis,
zone);
if (hasEndOfDayFrag) {
return dt.plusDays(1);
}
return dt;
}
/**
* Convenience method that accepts a {@link java.util.Date}.
*
* @param date
* @return An xsd:dateTime (in UTC) representation of date.
* @see #getXSDFormatter(DateTime)
*/
public static String getXSDDateTime(Date date) {
return getXSDDateTime(new DateTime(date));
}
/**
* Formats a {@link DateTime} as an xsd:dateTime in canonical form.
*
* <p>Note: fractional seconds are only supported to a maximum of three
* digits.</p>
*
* @param dateTime
* @return An xsd:dateTime (in UTC) representation of date.
* @see "http://www.w3.org/TR/xmlschema11-2/#dateTime"
*/
public static String getXSDDateTime(DateTime dateTime) {
return dateTime.withZone(DateTimeZone.UTC).toString(
getXSDFormatter(dateTime));
}
public static DateTimeFormatter getXSDFormatter(DateTime date) {
int len = 0;
int millis = date.getMillisOfSecond();
if (millis > 0) {
// 0.050 becomes .05 (up to three digits, dropping trailing 0s)
DecimalFormat df = new DecimalFormat(".###");
double d = millis / 1000.0;
len = String.valueOf(df.format(d)).length() - 1;
}
return getXSDFormatter(len);
}
/**
* Returns an xsd:dateTime formatter with the specified millisecond precision.
*
* @param millisLength number of digits of millisecond precision. Currently,
* only 0-3 are valid arguments.
* @return a formatter for yyyy-MM-dd'T'HH:mm:ss[.SSS]Z
*/
public static DateTimeFormatter getXSDFormatter(int millisLength) {
String key = String.valueOf(millisLength);
if (formatters.get(key) == null) {
DateTimeFormatterBuilder bldr =
new DateTimeFormatterBuilder().appendYear(4, 9)
.appendLiteral('-').appendMonthOfYear(2)
.appendLiteral('-').appendDayOfMonth(2)
.appendLiteral('T').appendHourOfDay(2)
.appendLiteral(':').appendMinuteOfHour(2)
.appendLiteral(':').appendSecondOfMinute(2);
if (millisLength > 0) {
bldr =
bldr.appendLiteral('.').appendFractionOfSecond(
millisLength, millisLength);
}
bldr = bldr.appendTimeZoneOffset("Z", true, 2, 4);
formatters.putIfAbsent(key, bldr.toFormatter());
}
return formatters.get(key);
}
}