/* ************************************************************************
#
# DivConq
#
# http://divconq.com/
#
# Copyright:
# Copyright 2014 eTimeline, LLC. All rights reserved.
#
# License:
# See the license.txt file in the project's top-level directory for details.
#
# Authors:
# * Andy White
#
************************************************************************ */
package divconq.util;
import java.text.SimpleDateFormat;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.joda.time.LocalTime;
import org.joda.time.Months;
import org.joda.time.Weeks;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
import org.joda.time.format.ISODateTimeFormat;
import divconq.bus.Message;
import divconq.lang.BigDateTime;
import divconq.lang.CoreLocalTime;
/**
* DivConq uses the Joda date time library for nearly all date/time processing.
* DivConq also assumes that date time in string format is typically in ISO format.
* Joda has a setting to indicate which timezone the Hub is running in, all methods
* that follow use that timezone setting.
*
* @author Andy
*
*/
public class TimeUtil {
static public final DateTimeFormatter stampFmt = DateTimeFormat.forPattern("yyyyMMdd'T'HHmmssSSS'Z'");
static public final DateTimeFormatter sqlStampFmt = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS");
static public final SimpleDateFormat sqlStampReformat = new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS");
static public final DateTimeFormatter parseTimeFormat = new DateTimeFormatterBuilder()
//.appendPattern("HH:mm")
//.appendPattern("HH:mm:ss")
.appendPattern("HH:mm:ss.SSS")
.toFormatter();
/*
System.out.println("pt: " + ISODateTimeFormat.localTimeParser().parseDateTime("16:20:45"));
DateTimeZone dtz = DateTimeZone.forID("America/Chicago");
System.out.println(startDST(dtz, 2011));
System.out.println(endDST(dtz, 2011));
*/
/**
* check if a date is before today, ignore the time just look at the date
*
* @param d date to check
* @return true if it comes before today
*/
static public boolean isBeforeToday(DateTime d) {
return d.toLocalDate().isBefore(new LocalDate());
}
/**
* try to supply a time for a date, if it fails it may be because of DST and that time (hour) is skipped on that date.
* So try again to supply a time +1 hour to see if it helps.
*
* @param d date to set time into
* @param t time to set to
* @return datetime with the supplied time (maybe +1 hour) or null
*/
static public DateTime withTime(DateTime d, LocalTime t) {
try {
return d.withTime(t.getHourOfDay(), t.getMinuteOfHour(), t.getSecondOfMinute(), t.getMillisOfSecond());
}
catch (Exception x) {
// TODO hour +1 is a hack, should work in USA/Canada - and probably lots of places - but maybe not everywhere
if (TimeUtil.checkDST(d) == DaylightTransition.START)
return d.withTime(t.getHourOfDay() + 1, t.getMinuteOfHour(), t.getSecondOfMinute(), t.getMillisOfSecond());
}
return null;
}
/**
* try to supply a time for a date, if it fails it may be because of DST and that time (hour) is skipped on that date.
* So try again to supply a time +1 hour to see if it helps.
*
* @param dt date to set time into
* @param clt time to set to
* @return datetime with the supplied time (maybe +1 hour) or null
*/
static public DateTime withTime(DateTime dt, CoreLocalTime clt) {
return dt.withTime(clt.getHour(), clt.getMinute(), clt.getSecond(), clt.getMillSec());
}
/**
* try to get a date at midnight tomorrow, if no midnight due to DST then it may be 1am
*
* @param d date from which to calculate tomorrow
* @return datetime of midnight, or closest to midnight, tomorrow
*/
static public DateTime nextDayAtMidnight(DateTime d) {
return d.withTime(0, 0, 0, 0).plusDays(1);
}
/*
System.out.println("1: " + ISODateTimeFormat.localTimeParser().parseDateTime("T15:00:00").toLocalTime());
System.out.println("2: " + ISODateTimeFormat.localTimeParser().parseDateTime("15:00:00").toLocalTime());
System.out.println("3: " + ISODateTimeFormat.localTimeParser().parseDateTime("15").toLocalTime());
System.out.println("1: " + ISODateTimeFormat.timeParser().parseDateTime("T15:00:00Z").toLocalTime());
System.out.println("2: " + ISODateTimeFormat.timeParser().parseDateTime("15:00:00Z").toLocalTime());
System.out.println("3: " + ISODateTimeFormat.timeParser().parseDateTime("15").toLocalTime());
*/
/**
* parse a string assuming ISO format
*
* @param t string with iso formatted date
* @return datetime if parsed, else null
*/
static public DateTime parseDateTime(String t) {
if (StringUtil.isEmpty(t))
return null;
try {
return ISODateTimeFormat.dateOptionalTimeParser().parseDateTime(t);
}
catch (Exception x) {
// TODO System.out.println(x.toString());
try {
return TimeUtil.stampFmt.parseDateTime(t);
}
catch (Exception x2) {
// TODO System.out.println(x.toString());
}
}
return null;
}
/**
* parse just the time
*
* @param t string with iso formatted time
* @return time if parsed, or null
*/
static public LocalTime parseLocalTime(String t) {
if (StringUtil.isEmpty(t))
return null;
try {
return ISODateTimeFormat.timeParser().parseDateTime(t).toLocalTime();
}
catch (Exception x) {
}
return null;
}
/**
* return number of weeks since Jan 5, 1970
*
* @param v date to calculate week number off of
* @return number of weeks
*/
static public int getWeekNumber(DateTime v) {
DateTime root = new DateTime(1970, 1, 5, 0, 0, 0, 0, v.getZone()); // use the same zone
return Weeks.weeksBetween(root, v).getWeeks();
//long n = v.getMillis() - 345600000; // start of first week
//return (int) (n / 604800000);
}
/**
* return datetime at start of week number relative to Jan 5, 1970
*
* @param weekNum the week number to use
* @return datetime that week started
*/
static public DateTime getStartOfWeek(int weekNum) {
return new DateTime(1970, 1, 5, 0, 0, 0, 0).plusWeeks(weekNum); // use default zone
//long n = (long)weekNum * 604800000;
//return new DateTime(n + 345600000);
}
/**
* return number of months since Jan 1, 1970
*
* @param v date to calculate month number off of
* @return number of months
*/
static public int getMonthNumber(DateTime v) {
DateTime root = new DateTime(1970, 1, 1, 0, 0, 0, 0, v.getZone()); // use the same zone
return Months.monthsBetween(root, v).getMonths();
//int n = (v.getYear() - 1970) * 12;
//return n + v.getMonthOfYear() - 1;
}
/**
* return datetime at start of month number relative to Jan 1, 1970
*
* @param monthNum the month number to use
* @return datetime that month started
*/
static public DateTime getStartOfMonth(int monthNum) {
return new DateTime(1970, 1, 1, 0, 0, 0, 0).plusMonths(monthNum);
//return new DateTime((monthNum / 12) + 1970, (monthNum % 12) + 1, 1, 0, 0, 0, 0);
}
/**
* Format date and time in Long format
*
* @param at datetime to format
* @param zone which timezone to use
* @param locale which locale to use
* @return formatted datetime
*/
static public String fmtDateTimeLong(DateTime at, String zone, String locale) {
if (at == null)
return null;
return TimeUtil.fmtDateTimeLong(at.withZone(TimeUtil.selectZone(zone)), locale);
}
/**
* Format date and time in Long format
*
* @param at datetime to format
* @param zone which timezone to use
* @param locale which locale to use
* @return formatted datetime
*/
static public String fmtDateTimeLong(DateTime at, DateTimeZone zone, String locale) {
if (at == null)
return null;
return TimeUtil.fmtDateTimeLong(at.withZone(zone) , locale);
}
/**
* Format date and time in Long format
*
* @param at datetime to format
* @param locale which locale to use
* @return formatted datetime
*/
static public String fmtDateTimeLong(DateTime at, String locale) {
if (at == null)
return null;
// TODO user locale to look up format
DateTimeFormatter fmt = DateTimeFormat.forPattern("MMMM dd yyyy hh:mm:ss a z");
return fmt.print(at);
}
/**
* Format date in Long format
*
* @param at datetime to format
* @param zone which timezone to use
* @param locale which locale to use
* @return formatted date
*/
static public String fmtDateLong(DateTime at, String zone, String locale) {
if (at == null)
return null;
return TimeUtil.fmtDateLong(at.withZone(TimeUtil.selectZone(zone)), locale);
}
/**
* Format date in Long format
*
* @param at datetime to format
* @param zone which timezone to use
* @param locale which locale to use
* @return formatted date
*/
static public String fmtDateLong(DateTime at, DateTimeZone zone, String locale) {
if (at == null)
return null;
return TimeUtil.fmtDateLong(at.withZone(zone) , locale);
}
/**
* Format date in Long format
*
* @param at datetime to format
* @param locale which locale to use
* @return formatted date
*/
static public String fmtDateLong(DateTime at, String locale) {
if (at == null)
return null;
// TODO user locale to look up format
DateTimeFormatter fmt = DateTimeFormat.forPattern("MMMM dd yyyy");
return fmt.print(at);
}
/**
* gets the correct timezone for use based on setting in a message
* Useful because request messages may hold info on their timezone.
*
* @param msg source of timezone metadata
* @return the selected timezone
*/
static public DateTimeZone selectZone(Message msg) {
return TimeUtil.selectZone(msg.getFieldAsString("TimeZone"));
}
/**
* try to lookup a timezone, but use default if it fails
*
* @param zoneId id of the timezone desired
* @return timezone to use
*/
static public DateTimeZone selectZone(String zoneId) {
DateTimeZone zone = DateTimeZone.getDefault();
try {
if (StringUtil.isNotEmpty(zoneId))
zone = DateTimeZone.forID(zoneId);
}
catch (Exception x) {
}
return zone;
}
/**
* Parse string to CoreLocalTime - a flexible format that allows hours > 23, useful for some settings.
*
* @param t string holding hours:minutes:seconds
* @return time object
*/
static public CoreLocalTime parseCoreLocalTime(String t) {
if (StringUtil.isEmpty(t))
return null;
String[] parts = t.trim().split(":");
int h = 0;
int m = 0;
int s = 0;
if (parts.length >= 1)
h = (int)StringUtil.parseInt(parts[0], 0);
if (parts.length >= 2)
m = (int)StringUtil.parseInt(parts[1], 0);
if (parts.length >= 3)
s = (int)StringUtil.parseInt(parts[2], 0);
try {
return new CoreLocalTime(h, m, s, 0);
}
catch (Exception x) {
}
return null;
}
/**
* detect if given datetime lands on a DST transition
*
* @param n date to check
* @return START if is a start of DST, END if is a end of DST, NA means day is not a transition
*/
public static DaylightTransition checkDST(DateTime n) {
DateTime start = TimeUtil.startDST(n.getYear());
if (start.toLocalDate().equals(n.toLocalDate()))
return DaylightTransition.START;
DateTime end = TimeUtil.startDST(n.getYear());
if (end.toLocalDate().equals(n.toLocalDate()))
return DaylightTransition.END;
return DaylightTransition.NA;
}
public enum DaylightTransition {
NA,
START,
END
}
/**
* get the DST start transition for a given year
*
* @param zone timezone to use for DST rules
* @param year the year to use, e.g. 2012
* @return datetime of the start transition
*/
public static DateTime startDST(DateTimeZone zone, int year) {
return new DateTime(zone.nextTransition(new DateTime(year, 1, 1, 0, 0, 0, 0, zone).getMillis()));
}
/**
* get the DST start transition for a given year
*
* @param year the year to use, e.g. 2012
* @return datetime of the start transition
*/
public static DateTime startDST(int year) {
DateTimeZone zone = DateTimeZone.getDefault();
return new DateTime(zone.nextTransition(new DateTime(year, 1, 1, 0, 0, 0, 0, zone).getMillis()));
}
/**
* get the DST end transition for a given year
*
* @param zone timezone to use for DST rules
* @param year the year to use, e.g. 2012
* @return datetime of the end transition
*/
public static DateTime endDST(DateTimeZone zone, int year) {
return new DateTime(zone.previousTransition(new DateTime(year + 1, 1, 1, 0, 0, 0, 0, zone).getMillis()));
}
/**
* get the DST end transition for a given year
*
* @param year the year to use, e.g. 2012
* @return datetime of the end transition
*/
public static DateTime endDST(int year) {
DateTimeZone zone = DateTimeZone.getDefault();
return new DateTime(zone.previousTransition(new DateTime(year + 1, 1, 1, 0, 0, 0, 0, zone).getMillis()));
}
/**
* BigDateTime can handle dates into the billions of years, both past and future. It is designed to be
* a versatile way to handle historical data. Dates are based off proleptic Gregorian, time is optional
* but if present then is based off UTC zone.
*
* @param date in string format
* @return converted to object or null if not able to parse
*
* @see divconq.lang.BigDateTime
*/
public static BigDateTime parseBigDateTime(String date) {
return BigDateTime.parse(date).getResult();
}
// this assumes that the time stamp - as formatted - will be in UTC time. All DC values should be store in UTC time.
public static DateTime convertSqlDate(java.sql.Timestamp v) {
if (v == null)
return null;
String dt = TimeUtil.sqlStampReformat.format(v) + "Z";
return TimeUtil.stampFmt.parseDateTime(dt);
}
}