st precision is nanosecond, not millisecond: DateTime dateAndTime = new DateTime("2010-01-19 23:59:59.123456789"); DateTime dateOnly = new DateTime("2010-01-19"); DateTime timeOnly = new DateTime("23:59:59"); DateTime dateOnly = DateTime.forDateOnly(2010,01,19); DateTime timeOnly = DateTime.forTimeOnly(23,59,59,0); DateTime dt = new DateTime("2010-01-15 13:59:15"); boolean leap = dt.isLeapYear(); //false dt.getNumDaysInMonth(); //31 dt.getStartOfMonth(); //2010-01-01, 00:00:00 dt.getEndOfDay(); //2010-01-15, 23:59:59 dt.format("YYYY-MM-DD"); //formats as '2010-01-15' dt.plusDays(30); //30 days after Jan 15 dt.numDaysFrom(someDate); //returns an int dueDate.lt(someDate); //less-than dueDate.lteq(someDate); //less-than-or-equal-to DateTime.now(aTimeZone); DateTime.today(aTimeZone); DateTime fromMilliseconds = DateTime.forInstant(31313121L, aTimeZone); birthday.isInFuture(aTimeZone);
Justification For This Class
The fundamental reasons why this class exists are :
- to avoid the embarrassing number of distasteful inadequacies in the JDK's date classes
- to oppose the very "mental model" of the JDK's date-time classes with something significantly simpler
There are 2 distinct mental models for date-times, and they don't play well together :
- timeline - an instant on the timeline, as a physicist would picture it, representing the number of seconds from some epoch. In this picture, such a date-time can have many, many different representations according to calendar and time zone. That is, the date-time, as seen and understood by the end user, can change according to "who's looking at it". It's important to understand that a timeline instant, before being presented to the user, must always have an associated time zone - even in the case of a date only, with no time.
- everyday - a date-time in the Gregorian calendar, such as '2009-05-25 18:25:00', which never changes according to "who's looking at it". Here, the time zone is always both implicit and immutable.
The problem is that java.util. {@link java.util.Date} uses only the timeline style, while most users, most of the time, think in terms of the other mental model - the 'everday' style. In particular, there are a large number of applications which experience problems with time zones, because the timeline model is used instead of the everday model. Such problems are often seen by end users as serious bugs, because telling people the wrong date or time is often a serious issue. These problems make you look stupid.
Date Classes in the JDK are Mediocre
The JDK's classes related to dates are widely regarded as frustrating to work with, for various reasons:
- mistakes regarding time zones are very common
- month indexes are 0-based, leading to off-by-one errors
- difficulty of calculating simple time intervals
- java.util.Date is mutable, but 'building block' classes should be immutable
- numerous other minor nuisances
Joda Time Has Drawbacks As Well
The
Joda Time library is used by some programmers as an alternative to the JDK classes. Joda Time has the following drawbacks :
- it limits precision to milliseconds. Database timestamp values almost always have a precision of microseconds or even nanoseconds. This is a serious defect: a library should never truncate your data, for any reason.
- it's large, with well over 100 items in its javadoc
- in order to stay current, it needs to be manually updated occasionally with fresh time zone data
- it has mutable versions of classes
- it always coerces March 31 + 1 Month to April 30 (for example), without giving you any choice in the matter
- some databases allow invalid date values such as '0000-00-00', but Joda Time doesn't seem to be able to handle them
Dates and Times in General
Civil Timekeeping Is Complex
Civil timekeeping is a byzantine hodge-podge of arcane and arbitrary rules. Consider the following :
- months have varying numbers of days
- one month (February) has a length which depends on the year
- not all years have the same number of days
- time zone rules spring forth arbitrarily from the fecund imaginations of legislators
- summer hours mean that an hour is 'lost' in the spring, while another hour must repeat itself in the autumn, during the switch back to normal time
- summer hour logic varies widely across various jurisdictions
- the cutover from the Julian calendar to the Gregorian calendar happened at different times in different places, which causes a varying number of days to be 'lost' during the cutover
- occasional insertion of leap seconds are used to ensure synchronization with the rotating Earth (whose speed of rotation is gradually slowing down, in an irregular way)
- there is no year 0 (1 BC is followed by 1 AD), except in the reckoning used by astronomers
How Databases Treat Dates
Most databases model dates and times using the Gregorian Calendar in an aggressively simplified form, in which :
- the Gregorian calendar is extended back in time as if it was in use previous to its inception (the 'proleptic' Gregorian calendar)
- the transition between Julian and Gregorian calendars is entirely ignored
- leap seconds are entirely ignored
- summer hours are entirely ignored
- often, even time zones are ignored, in the sense that the underlying database column doesn't usually explicitly store any time zone information.
The final point requires elaboration. Some may doubt its veracity, since they have seen date-time information "change time zone" when retrieved from a database. But this sort of change is usually applied using logic which is external to the data stored in the particular column.
For example, the following items might be used in the calculation of a time zone difference :
- time zone setting for the client (or JDBC driver)
- time zone setting for the client's connection to the database server
- time zone setting of the database server
- time zone setting of the host where the database server resides
(Note as well what's missing from the above list: your own application's logic, and the user's time zone preference.)
When an end user sees such changes to a date-time, all they will say to you is "Why did you change it? That's not what I entered" - and this is a completely valid question. Why did you change it? Because you're using the timeline model instead of the everyday model. Perhaps you're using a inappropriate abstraction for what the user really wants.
The Approach Used By This Class
This class takes the following design approach :
- it models time in the "everyday" style, not in the "timeline" style (see above)
- its precision matches the highest precision used by databases (nanosecond)
- it uses only the proleptic Gregorian Calendar, over the years 1..9999
- it ignores all non-linearities: summer-hours, leap seconds, and the cutover from Julian to Gregorian calendars
- it ignores time zones. Most date-times are stored in columns whose type does not include time zone information (see note above).
- it has (very basic) support for wonky dates, such as the magic value 0000-00-00 used by MySQL
- it's immutable
- it lets you choose among 4 policies for 'day overflow' conditions during calculations
Even though the above list may appear restrictive, it's very likely true that DateTime can handle the dates and times you're currently storing in your database.
Two Sets Of Operations
This class allows for 2 sets of operations: a few "basic" operations, and many "computational" ones.
Basic operations model the date-time as a simple, dumb String, with absolutely no parsing or substructure. This will always allow your application to reflect exactly what is in a ResultSet, with absolutely no modification for time zone, locale, or for anything else.
This is meant as a back-up, to ensure that your application will always be able to, at the very least, display a date-time exactly as it appears in your ResultSet from the database. This style is particularly useful for handling invalid dates such as 2009-00-00, which can in fact be stored by some databases (MySQL, for example). It can also be used to handle unusual items, such as MySQL's TIME datatype.
The basic operations are represented by {@link #DateTime(String)}, {@link #toString()}, and {@link #getRawDateString()}.
Computational operations allow for calculations and formatting. If a computational operation is performed by this class (for example, if the caller asks for the month), then any underlying date-time String must be parseable by this class into its components - year, month, day, and so on. Computational operations require such parsing, while the basic operations do not. Almost all methods in this class are categorized as computational operations.
Parsing DateTime - Accepted Formats
The {@link #DateTime(String)} constructor accepts a
String representation of a date-time.The format of the String can take a number of forms. When retrieving date-times from a database, the majority of cases will have little problem in conforming to these formats. If necessary, your SQL statements can almost always use database formatting functions to generate a String whose format conforms to one of the many formats accepted by the {@link #DateTime(String)} constructor.
The {@link #isParseable(String)} method lets you explicitly test if a given String is in a form that can be parsed by this class.
Mini-Language for Formatting
This class defines a simple mini-language for formatting a
DateTime, used by the various
format methods.
The following table defines the symbols used by this mini-language, and the corresponding text they would generate given the date:
1958-04-09 Wednesday, 03:05:06.123456789 AM
in an English Locale. (Items related to date are in upper case, and items related to time are in lower case.)
Format | Output | Description | Needs Locale? |
YYYY | 1958 | Year | ... |
YY | 58 | Year without century | ... |
M | 4 | Month 1..12 | ... |
MM | 04 | Month 01..12 | ... |
MMM | Apr | Month Jan..Dec | Yes |
MMMM | April | Month January..December | Yes |
DD | 09 | Day 01..31 | ... |
D | 9 | Day 1..31 | ... |
WWWW | Wednesday | Weekday Sunday..Saturday | Yes |
WWW | Wed | Weekday Sun..Sat | Yes |
hh | 03 | Hour 01..23 | ... |
h | 3 | Hour 1..23 | ... |
hh12 | 03 | Hour 01..12 | ... |
h12 | 3 | Hour 1..12 | ... |
a | AM | AM/PM Indicator | Yes |
mm | 05 | Minutes 01..59 | ... |
m | 5 | Minutes 1..59 | ... |
ss | 06 | Seconds 01..59 | ... |
s | 6 | Seconds 1..59 | ... |
f | 1 | Fractional Seconds, 1 decimal | ... |
ff | 12 | Fractional Seconds, 2 decimals | ... |
fff | 123 | Fractional Seconds, 3 decimals | ... |
ffff | 1234 | Fractional Seconds, 4 decimals | ... |
fffff | 12345 | Fractional Seconds, 5 decimals | ... |
ffffff | 123456 | Fractional Seconds, 6 decimals | ... |
fffffff | 1234567 | Fractional Seconds, 7 decimals | ... |
ffffffff | 12345678 | Fractional Seconds, 8 decimals | ... |
fffffffff | 123456789 | Fractional Seconds, 9 decimals | ... |
| | (no example) | Escape characters | ... |
As indicated above, some of these symbols can only be used with an accompanying Locale. In general, if the output is text, not a number, then a Locale will be needed. For example, 'September' is localizable text, while '09' is a numeric representation, which doesn't require a Locale. Thus, the symbol 'MM' can be used without a Locale, while 'MMMM' and 'MMM' both require a Locale, since they generate text, not a number.
The fractional seconds 'f' doesn't perform any rounding.
The escape character '|' allows you to insert arbitrary text. The escape character always appears in pairs; these pairs define a range of characters over which the text will not be interpreted using the special format symbols defined above.
Here are some practical examples of using the above formatting symbols:
Format | Output |
YYYY-MM-DD hh:mm:ss.fffffffff a | 1958-04-09 03:05:06.123456789 AM |
YYYY-MM-DD hh:mm:ss.fff a | 1958-04-09 03:05:06.123 AM |
YYYY-MM-DD | 1958-04-09 |
hh:mm:ss.fffffffff | 03:05:06.123456789 |
hh:mm:ss | 03:05:06 |
YYYY-M-D h:m:s | 1958-4-9 3:5:6 |
WWWW, MMMM D, YYYY | Wednesday, April 9, 1958 |
WWWW, MMMM D, YYYY |at| D a | Wednesday, April 9, 1958 at 3 AM |
In the last example, the escape characters are needed only because 'a', the formating symbol for am/pm, appears in the text.
Passing DateTime Objects to the Database
When a
DateTime is passed as a parameter to an SQL statement, the
DateTime can always be formatted into a
String of a form accepted by the database, using one of the
format methods.