hirondelle.date4j.DateTime Maven / Gradle / Ivy
package hirondelle.date4j;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
/**
Building block class for an immutable date-time, with no time zone.
This class is provided as an alternative to java.util.{@link java.util.Date}.
This class can hold :
- a date-and-time : 1958-03-31 18:59:56.123456789
- a date only : 1958-03-31
- a time only : 18:59:56.123456789
Examples
Justification For This Class
Dates and Times In General
The Approach Used By This Class
Two Sets Of Operations
Parsing DateTime - Accepted Formats
Mini-Language for Formatting
Passing DateTime Objects to the Database
Examples
Some quick examples of using this class :
DateTime dateAndTime = new DateTime("2010-01-19 23:59:59");
//highest 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.
*/
public final class DateTime implements Comparable, Serializable {
/** The seven parts of a DateTime object. The DAY represents the day of the month (1..31), not the weekday. */
public enum Unit {
YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, NANOSECONDS;
}
/**
Policy for treating 'day-of-the-month overflow' conditions encountered during some date calculations.
Months are different from other units of time, since the length of a month is not fixed, but rather varies with
both month and year. This leads to problems. Take the following simple calculation, for example :
May 31 + 1 month = ?
What's the answer? Since there is no such thing as June 31, the result of this operation is inherently ambiguous.
This DayOverflow enumeration lists the various policies for treating such situations, as supported by
DateTime.
This table illustrates how the policies behave :
Date
DayOverflow
Result
May 31 + 1 Month
LastDay
June 30
May 31 + 1 Month
FirstDay
July 1
December 31, 2001 + 2 Months
Spillover
March 3
May 31 + 1 Month
Abort
RuntimeException
*/
public enum DayOverflow {
/** Coerce the day to the last day of the month. */
LastDay,
/** Coerce the day to the first day of the next month. */
FirstDay,
/** Spillover the day into the next month. */
Spillover,
/** Throw a RuntimeException. */
Abort;
}
/**
Constructor taking a date-time as a String. The text is trimmed by this class.
When this constructor is called, the underlying text can be in an absolutely arbitrary
form, since it will not, initially, be parsed in any way. This policy of extreme
leniency allows you to use dates in an arbitrary format, without concern over possible
transformations of the date (time zone in particular), and without concerns over possibly bizarre content, such
as '2005-00-00', as seen in some databases, such as MySQL.
However, the moment you attempt to call almost any method
in this class, an attempt will be made to parse
the given date-time string into its constituent parts. Then, if the date-time string does not match one of the
example formats listed below, a RuntimeException will be thrown.
Before calling this constructor, you may wish to call {@link #isParseable(String)} to explicitly test whether a
given String is parseable by this class.
The full date format expected by this class is 'YYYY-MM-YY hh:mm:ss.fffffffff'.
All fields except for the fraction of a second have a fixed width.
In addition, various portions of this format are also accepted by this class.
All of the following dates can be parsed by this class to make a DateTime :
- 2009-12-31 00:00:00.123456789
- 2009-12-31T00:00:00.123456789
- 2009-12-31 00:00:00.12345678
- 2009-12-31 00:00:00.1234567
- 2009-12-31 00:00:00.123456
- 2009-12-31 23:59:59.12345
- 2009-01-31 16:01:01.1234
- 2009-01-01 16:59:00.123
- 2009-01-01 16:00:01.12
- 2009-02-28 16:25:17.1
- 2009-01-01 00:01:01
- 2009-01-01T00:01:01
- 2009-01-01 16:01
- 2009-01-01 16
- 2009-01-01
- 2009-01
- 2009
- 0009
- 9
- 00:00:00.123456789
- 00:00:00.12345678
- 00:00:00.1234567
- 00:00:00.123456
- 23:59:59.12345
- 01:59:59.1234
- 23:01:59.123
- 00:00:00.12
- 00:59:59.1
- 23:59:00
- 23:00:10
- 00:59
The range of each field is :
- year: 1..9999 (leading zeroes optional)
- month: 01..12
- day: 01..31
- hour: 00..23
- minute: 00..59
- second: 00..59
- nanosecond: 0..999999999
Note that database format functions are an option when dealing with date formats.
Since your application is always in control of the SQL used to talk to the database, you can, if needed, usually
use database format functions to alter the format of dates returned in a ResultSet.
*/
public DateTime(String aDateTime) {
fIsAlreadyParsed = false;
if (aDateTime == null) {
throw new IllegalArgumentException("String passed to DateTime constructor is null. You can use an empty string, but not a null reference.");
}
fDateTime = aDateTime;
}
/**
Return true only if the given String follows one of the formats documented by {@link #DateTime(String)}.
If the text is not from a trusted source, then the caller may use this method to validate whether the text
is in a form that's parseable by this class.
*/
public static boolean isParseable(String aCandidateDateTime){
boolean result = true;
try {
DateTime dt = new DateTime(aCandidateDateTime);
dt.ensureParsed();
}
catch (RuntimeException ex){
result = false;
}
return result;
}
/**
Constructor taking each time unit explicitly.
Although all parameters are optional, many operations on this class require year-month-day to be
present.
@param aYear 1..9999, optional
@param aMonth 1..12 , optional
@param aDay 1..31, cannot exceed the number of days in the given month/year, optional
@param aHour 0..23, optional
@param aMinute 0..59, optional
@param aSecond 0..59, optional
@param aNanoseconds 0..999,999,999, optional (allows for databases that store timestamps up to
nanosecond precision).
*/
public DateTime(Integer aYear, Integer aMonth, Integer aDay, Integer aHour, Integer aMinute, Integer aSecond, Integer aNanoseconds) {
fIsAlreadyParsed = true;
fYear = aYear;
fMonth = aMonth;
fDay = aDay;
fHour = aHour;
fMinute = aMinute;
fSecond = aSecond;
fNanosecond = aNanoseconds;
validateState();
}
/**
Factory method returns a DateTime having year-month-day only, with no time portion.
See {@link #DateTime(Integer, Integer, Integer, Integer, Integer, Integer, Integer)} for constraints on the parameters.
*/
public static DateTime forDateOnly(Integer aYear, Integer aMonth, Integer aDay) {
return new DateTime(aYear, aMonth, aDay, null, null, null, null);
}
/**
Factory method returns a DateTime having hour-minute-second-nanosecond only, with no date portion.
See {@link #DateTime(Integer, Integer, Integer, Integer, Integer, Integer, Integer)} for constraints on the parameters.
*/
public static DateTime forTimeOnly(Integer aHour, Integer aMinute, Integer aSecond, Integer aNanoseconds) {
return new DateTime(null, null, null, aHour, aMinute, aSecond, aNanoseconds);
}
/**
Constructor taking a millisecond value and a {@link TimeZone}.
This constructor may be use to convert a java.util.Date into a DateTime.
To use nanosecond precision, please use {@link #forInstantNanos(long, TimeZone)} instead.
@param aMilliseconds must be in the range corresponding to the range of dates supported by this class (year 1..9999); corresponds
to a millisecond instant on the time-line, measured from the epoch used by {@link java.util.Date}.
*/
public static DateTime forInstant(long aMilliseconds, TimeZone aTimeZone) {
Calendar calendar = new GregorianCalendar(aTimeZone);
calendar.setTimeInMillis(aMilliseconds);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1; // 0-based
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY); // 0..23
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
int milliseconds = calendar.get(Calendar.MILLISECOND);
int nanoseconds = milliseconds * 1000 * 1000;
return new DateTime(year, month, day, hour, minute, second, nanoseconds);
}
/**
For the given time zone, return the corresponding time in milliseconds-since-epoch for this DateTime.
This method is meant to help you convert between a DateTime and the
JDK's date-time classes, which are based on the combination of a time zone and a
millisecond value from the Java epoch.
Since DateTime can go to nanosecond accuracy, the return value can
lose precision. The nanosecond value is truncated to milliseconds, not rounded.
To retain nanosecond accuracy, please use {@link #getNanosecondsInstant(TimeZone)} instead.
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public long getMilliseconds(TimeZone aTimeZone){
Integer year = getYear();
Integer month = getMonth();
Integer day = getDay();
//coerce missing times to 0:
Integer hour = getHour() == null ? 0 : getHour();
Integer minute = getMinute() == null ? 0 : getMinute();
Integer second = getSecond() == null ? 0 : getSecond();
Integer nanos = getNanoseconds() == null ? 0 : getNanoseconds();
Calendar calendar = new GregorianCalendar(aTimeZone);
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month-1); // 0-based
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.set(Calendar.HOUR_OF_DAY, hour); // 0..23
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, second);
calendar.set(Calendar.MILLISECOND, nanos/1000000);
return calendar.getTimeInMillis();
}
/**
Constructor taking a nanosecond value and a {@link TimeZone}.
To use milliseconds instead of nanoseconds, please use {@link #forInstant(long, TimeZone)}.
@param aNanoseconds must be in the range corresponding to the range of dates supported by this class (year 1..9999); corresponds
to a nanosecond instant on the time-line, measured from the epoch used by {@link java.util.Date}.
*/
public static DateTime forInstantNanos(long aNanoseconds, TimeZone aTimeZone) {
//these items can be of either sign
long millis = aNanoseconds / MILLION; //integer division truncates towards 0, doesn't round
long nanosRemaining = aNanoseconds % MILLION; //size 0..999,999
//when negative: go to the previous millis, and take the complement of nanosRemaining
if(aNanoseconds < 0){
millis = millis - 1;
nanosRemaining = MILLION + nanosRemaining; //-1 remaining coerced to 999,999
}
//base calculation in millis
Calendar calendar = new GregorianCalendar(aTimeZone);
calendar.setTimeInMillis(millis);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1; // 0-based
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY); // 0..23
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
int milliseconds = calendar.get(Calendar.MILLISECOND);
DateTime withoutNanos = new DateTime(year, month, day, hour, minute, second, milliseconds * MILLION);
//adjust for nanos - this cast is acceptable, because the value's range is 0..999,999:
DateTime withNanos = withoutNanos.plus(0, 0, 0, 0, 0, 0, (int)nanosRemaining, DayOverflow.Spillover);
return withNanos;
}
/**
For the given time zone, return the corresponding time in nanoseconds-since-epoch for this DateTime.
For conversion between a DateTime and the JDK's date-time classes,
you should likely use {@link #getMilliseconds(TimeZone)} instead.
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public long getNanosecondsInstant(TimeZone aTimeZone){
// these are always positive:
Integer year = getYear();
Integer month = getMonth();
Integer day = getDay();
//coerce missing times to 0:
Integer hour = getHour() == null ? 0 : getHour();
Integer minute = getMinute() == null ? 0 : getMinute();
Integer second = getSecond() == null ? 0 : getSecond();
Integer nanos = getNanoseconds() == null ? 0 : getNanoseconds();
int millis = nanos / MILLION; //integer division truncates, doesn't round
int nanosRemaining = nanos % MILLION; //0..999,999 - always positive
//base calculation in millis
Calendar calendar = new GregorianCalendar(aTimeZone);
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month-1); // 0-based
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.set(Calendar.HOUR_OF_DAY, hour); // 0..23
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, second);
calendar.set(Calendar.MILLISECOND, millis);
long baseResult = calendar.getTimeInMillis() * MILLION; // either sign
//the adjustment for nanos is always positive, toward the future:
return baseResult + nanosRemaining;
}
/**
Return the raw date-time String passed to the {@link #DateTime(String)} constructor.
Returns null if that constructor was not called. See {@link #toString()} as well.
*/
public String getRawDateString() {
return fDateTime;
}
/** Return the year, 1..9999. */
public Integer getYear() {
ensureParsed();
return fYear;
}
/** Return the Month, 1..12. */
public Integer getMonth() {
ensureParsed();
return fMonth;
}
/** Return the Day of the Month, 1..31. */
public Integer getDay() {
ensureParsed();
return fDay;
}
/** Return the Hour, 0..23. */
public Integer getHour() {
ensureParsed();
return fHour;
}
/** Return the Minute, 0..59. */
public Integer getMinute() {
ensureParsed();
return fMinute;
}
/** Return the Second, 0..59. */
public Integer getSecond() {
ensureParsed();
return fSecond;
}
/** Return the Nanosecond, 0..999999999. */
public Integer getNanoseconds() {
ensureParsed();
return fNanosecond;
}
/**
Return the Modified Julian Day Number.
The Modified Julian Day Number is defined by astronomers for simplifying the calculation of the number of days between 2 dates.
Returns a monotonically increasing sequence number.
Day 0 is November 17, 1858 00:00:00 (whose Julian Date was 2400000.5).
Using the Modified Julian Day Number instead of the Julian Date has 2 advantages:
- it's a smaller number
- it starts at midnight, not noon (Julian Date starts at noon)
Does not reflect any time portion, if present.
(In spite of its name, this method, like all other methods in this class, uses the
proleptic Gregorian calendar - not the Julian calendar.)
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public Integer getModifiedJulianDayNumber() {
ensureHasYearMonthDay();
int result = calculateJulianDayNumberAtNoon() - 1 - EPOCH_MODIFIED_JD;
return result;
}
/**
Return an index for the weekday for this DateTime.
Returns 1..7 for Sunday..Saturday.
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public Integer getWeekDay() {
ensureHasYearMonthDay();
int dayNumber = calculateJulianDayNumberAtNoon() + 1;
int index = dayNumber % 7;
return index + 1;
}
/**
Return an integer in the range 1..366, representing a count of the number of days from the start of the year.
January 1 is counted as day 1.
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public Integer getDayOfYear() {
ensureHasYearMonthDay();
int k = isLeapYear() ? 1 : 2;
Integer result = ((275 * fMonth) / 9) - k * ((fMonth + 9) / 12) + fDay - 30; // integer division
return result;
}
/**
Returns true only if the year is a leap year.
Requires year to be present; if not, a runtime exception is thrown.
*/
public Boolean isLeapYear() {
ensureParsed();
Boolean result = null;
if (isPresent(fYear)) {
result = isLeapYear(fYear);
}
else {
throw new MissingItem("Year is absent. Cannot determine if leap year.");
}
return result;
}
/**
Return the number of days in the month which holds this DateTime.
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public int getNumDaysInMonth() {
ensureHasYearMonthDay();
return getNumDaysInMonth(fYear, fMonth);
}
/**
Return The week index of this DateTime with respect to a given starting DateTime.
The single parameter to this method defines first day of week number 1.
See {@link #getWeekIndex()} as well.
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public Integer getWeekIndex(DateTime aStartingFromDate) {
ensureHasYearMonthDay();
aStartingFromDate.ensureHasYearMonthDay();
int diff = getModifiedJulianDayNumber() - aStartingFromDate.getModifiedJulianDayNumber();
return (diff / 7) + 1; // integer division
}
/**
Return The week index of this DateTime, taking day 1 of week 1 as Sunday, January 2, 2000.
See {@link #getWeekIndex(DateTime)} as well, which takes an arbitrary date to define
day 1 of week 1.
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public Integer getWeekIndex() {
DateTime start = DateTime.forDateOnly(2000, 1, 2);
return getWeekIndex(start);
}
/**
Return true only if this DateTime has the same year-month-day as the given parameter.
Time is ignored by this method.
Requires year-month-day to be present, both for this DateTime and for
aThat; if not, a runtime exception is thrown.
*/
public boolean isSameDayAs(DateTime aThat) {
boolean result = false;
ensureHasYearMonthDay();
aThat.ensureHasYearMonthDay();
result = (fYear.equals(aThat.fYear) && fMonth.equals(aThat.fMonth) && fDay.equals(aThat.fDay));
return result;
}
/**
'Less than' comparison.
Return true only if this DateTime comes before the given parameter, according to {@link #compareTo(DateTime)}.
*/
public boolean lt(DateTime aThat) {
return compareTo(aThat) < EQUAL;
}
/**
'Less than or equal to' comparison.
Return true only if this DateTime comes before the given parameter, according to {@link #compareTo(DateTime)},
or this DateTime equals the given parameter.
*/
public boolean lteq(DateTime aThat) {
return compareTo(aThat) < EQUAL || equals(aThat);
}
/**
'Greater than' comparison.
Return true only if this DateTime comes after the given parameter, according to {@link #compareTo(DateTime)}.
*/
public boolean gt(DateTime aThat) {
return compareTo(aThat) > EQUAL;
}
/**
'Greater than or equal to' comparison.
Return true only if this DateTime comes after the given parameter, according to {@link #compareTo(DateTime)},
or this DateTime equals the given parameter.
*/
public boolean gteq(DateTime aThat) {
return compareTo(aThat) > EQUAL || equals(aThat);
}
/** Return the smallest non-null time unit encapsulated by this DateTime. */
public Unit getPrecision() {
ensureParsed();
Unit result = null;
if (isPresent(fNanosecond)) {
result = Unit.NANOSECONDS;
}
else if (isPresent(fSecond)) {
result = Unit.SECOND;
}
else if (isPresent(fMinute)) {
result = Unit.MINUTE;
}
else if (isPresent(fHour)) {
result = Unit.HOUR;
}
else if (isPresent(fDay)) {
result = Unit.DAY;
}
else if (isPresent(fMonth)) {
result = Unit.MONTH;
}
else if (isPresent(fYear)) {
result = Unit.YEAR;
}
return result;
}
/**
Truncate this DateTime to the given precision.
The return value will have all items lower than the given precision simply set to
null. In addition, the return value will not include any date-time String passed to the
{@link #DateTime(String)} constructor.
@param aPrecision takes any value except {@link Unit#NANOSECONDS} (since it makes no sense to truncate to the highest
available precision).
*/
public DateTime truncate(Unit aPrecision) {
ensureParsed();
DateTime result = null;
if (Unit.NANOSECONDS == aPrecision) {
throw new IllegalArgumentException("It makes no sense to truncate to nanosecond precision, since that's the highest precision available.");
}
else if (Unit.SECOND == aPrecision) {
result = new DateTime(fYear, fMonth, fDay, fHour, fMinute, fSecond, null);
}
else if (Unit.MINUTE == aPrecision) {
result = new DateTime(fYear, fMonth, fDay, fHour, fMinute, null, null);
}
else if (Unit.HOUR == aPrecision) {
result = new DateTime(fYear, fMonth, fDay, fHour, null, null, null);
}
else if (Unit.DAY == aPrecision) {
result = new DateTime(fYear, fMonth, fDay, null, null, null, null);
}
else if (Unit.MONTH == aPrecision) {
result = new DateTime(fYear, fMonth, null, null, null, null, null);
}
else if (Unit.YEAR == aPrecision) {
result = new DateTime(fYear, null, null, null, null, null, null);
}
return result;
}
/**
Return true only if all of the given units are present in this DateTime.
If a unit is not included in the argument list, then no test is made for its presence or absence
in this DateTime by this method.
*/
public boolean unitsAllPresent(Unit... aUnits) {
boolean result = true;
ensureParsed();
for (Unit unit : aUnits) {
if (Unit.NANOSECONDS == unit) {
result = result && fNanosecond != null;
}
else if (Unit.SECOND == unit) {
result = result && fSecond != null;
}
else if (Unit.MINUTE == unit) {
result = result && fMinute != null;
}
else if (Unit.HOUR == unit) {
result = result && fHour != null;
}
else if (Unit.DAY == unit) {
result = result && fDay != null;
}
else if (Unit.MONTH == unit) {
result = result && fMonth != null;
}
else if (Unit.YEAR == unit) {
result = result && fYear != null;
}
}
return result;
}
/**
Return true only if this DateTime has a non-null values for year, month, and day.
*/
public boolean hasYearMonthDay() {
return unitsAllPresent(Unit.YEAR, Unit.MONTH, Unit.DAY);
}
/**
Return true only if this DateTime has a non-null values for hour, minute, and second.
*/
public boolean hasHourMinuteSecond() {
return unitsAllPresent(Unit.HOUR, Unit.MINUTE, Unit.SECOND);
}
/**
Return true only if all of the given units are absent from this DateTime.
If a unit is not included in the argument list, then no test is made for its presence or absence
in this DateTime by this method.
*/
public boolean unitsAllAbsent(Unit... aUnits) {
boolean result = true;
ensureParsed();
for (Unit unit : aUnits) {
if (Unit.NANOSECONDS == unit) {
result = result && fNanosecond == null;
}
else if (Unit.SECOND == unit) {
result = result && fSecond == null;
}
else if (Unit.MINUTE == unit) {
result = result && fMinute == null;
}
else if (Unit.HOUR == unit) {
result = result && fHour == null;
}
else if (Unit.DAY == unit) {
result = result && fDay == null;
}
else if (Unit.MONTH == unit) {
result = result && fMonth == null;
}
else if (Unit.YEAR == unit) {
result = result && fYear == null;
}
}
return result;
}
/**
Return this DateTime with the time portion coerced to '00:00:00.000000000'.
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public DateTime getStartOfDay() {
ensureHasYearMonthDay();
return getStartEndDateTime(fDay, 0, 0, 0, 0);
}
/**
Return this DateTime with the time portion coerced to '23:59:59.999999999'.
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public DateTime getEndOfDay() {
ensureHasYearMonthDay();
return getStartEndDateTime(fDay, 23, 59, 59, 999999999);
}
/**
Return this DateTime with the time portion coerced to '00:00:00.000000000',
and the day coerced to 1.
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public DateTime getStartOfMonth() {
ensureHasYearMonthDay();
return getStartEndDateTime(1, 0, 0, 0, 0);
}
/**
Return this DateTime with the time portion coerced to '23:59:59.999999999',
and the day coerced to the end of the month.
Requires year-month-day to be present; if not, a runtime exception is thrown.
*/
public DateTime getEndOfMonth() {
ensureHasYearMonthDay();
return getStartEndDateTime(getNumDaysInMonth(), 23, 59, 59, 999999999);
}
/**
Create a new DateTime by adding an interval to this one.
See {@link #plusDays(Integer)} as well.
Changes are always applied by this class in order of decreasing units of time:
years first, then months, and so on. After changing both the year and month, a check on the month-day combination is made before
any change is made to the day. If the day exceeds the number of days in the given month/year, then
(and only then) the given {@link DayOverflow} policy applied, and the day-of-the-month is adusted accordingly.
Afterwards, the day is then changed in the usual way, followed by the remaining items (hour, minute, second, and nanosecond).
The mental model for this method is very similar to that of a car's odometer. When a limit is reach for one unit of time,
then a rollover occurs for a neighbouring unit of time.
The returned value cannot come after 9999-12-13 23:59:59.
This class works with DateTime's having the following items present :
- year-month-day and hour-minute-second (and optional nanoseconds)
- year-month-day only. In this case, if a calculation with a time part is performed, that time part
will be initialized by this class to 00:00:00.0, and the DateTime returned by this class will include a time part.
- hour-minute-second (and optional nanoseconds) only. In this case, the calculation is done starting with the
the arbitrary date 0001-01-01 (in order to remain within a valid state space of DateTime).
@param aNumYears positive, required, in range 0...9999
@param aNumMonths positive, required, in range 0...9999
@param aNumDays positive, required, in range 0...9999
@param aNumHours positive, required, in range 0...9999
@param aNumMinutes positive, required, in range 0...9999
@param aNumSeconds positive, required, in range 0...9999
@param aNumNanoseconds positive, required, in range 0...999999999
*/
public DateTime plus(Integer aNumYears, Integer aNumMonths, Integer aNumDays, Integer aNumHours, Integer aNumMinutes, Integer aNumSeconds, Integer aNumNanoseconds, DayOverflow aDayOverflow) {
DateTimeInterval interval = new DateTimeInterval(this, aDayOverflow);
return interval.plus(aNumYears, aNumMonths, aNumDays, aNumHours, aNumMinutes, aNumSeconds, aNumNanoseconds);
}
/**
Create a new DateTime by subtracting an interval to this one.
See {@link #minusDays(Integer)} as well.
This method has nearly the same behavior as {@link #plus(Integer, Integer, Integer, Integer, Integer, Integer, Integer, DayOverflow)},
except that the return value cannot come before 0001-01-01 00:00:00.
*/
public DateTime minus(Integer aNumYears, Integer aNumMonths, Integer aNumDays, Integer aNumHours, Integer aNumMinutes, Integer aNumSeconds, Integer aNumNanoseconds, DayOverflow aDayOverflow) {
DateTimeInterval interval = new DateTimeInterval(this, aDayOverflow);
return interval.minus(aNumYears, aNumMonths, aNumDays, aNumHours, aNumMinutes, aNumSeconds, aNumNanoseconds);
}
/**
Return a new DateTime by adding an integral number of days to this one.
Requires year-month-day to be present; if not, a runtime exception is thrown.
@param aNumDays can be either sign; if negative, then the days are subtracted.
*/
public DateTime plusDays(Integer aNumDays) {
ensureHasYearMonthDay();
int thisJDAtNoon = getModifiedJulianDayNumber() + 1 + EPOCH_MODIFIED_JD;
int resultJD = thisJDAtNoon + aNumDays;
DateTime datePortion = fromJulianDayNumberAtNoon(resultJD);
return new DateTime(datePortion.getYear(), datePortion.getMonth(), datePortion.getDay(), fHour, fMinute, fSecond, fNanosecond);
}
/**
Return a new DateTime by subtracting an integral number of days from this one.
Requires year-month-day to be present; if not, a runtime exception is thrown.
@param aNumDays can be either sign; if negative, then the days are added.
*/
public DateTime minusDays(Integer aNumDays) {
return plusDays(-1 * aNumDays);
}
/**
The whole number of days between this DateTime and the given parameter.
Requires year-month-day to be present, both for this DateTime and for the aThat
parameter; if not, a runtime exception is thrown.
*/
public int numDaysFrom(DateTime aThat) {
return aThat.getModifiedJulianDayNumber() - this.getModifiedJulianDayNumber();
}
/**
The number of seconds between this DateTime and the given argument.
If any date information is present, in either this DateTime or aThat,
then full year-month-day must be present in both; if not, then the date portion will be ignored, and only the
time portion will contribute to the calculation.
*/
public long numSecondsFrom(DateTime aThat) {
long result = 0;
aThat.ensureParsed(); //since the boolean test may short-circuit for aThat
if(hasYearMonthDay() && aThat.hasYearMonthDay()){
result = numDaysFrom(aThat) * 86400; //intermediate value, just the day portion
}
result = result - this.numSecondsInTimePortion() + aThat.numSecondsInTimePortion();
return result;
}
/**
Output this DateTime as a formatted String using numbers, with no localizable text.
Example:
dt.format("YYYY-MM-DD hh:mm:ss");
would generate text of the form
2009-09-09 18:23:59
If months, weekdays, or AM/PM indicators are output as localizable text, you must use {@link #format(String, Locale)}.
@param aFormat uses the formatting mini-language defined in the class comment.
*/
public String format(String aFormat) {
DateTimeFormatter format = new DateTimeFormatter(aFormat);
return format.format(this);
}
/**
Output this DateTime as a formatted String using numbers and/or localizable text.
This method is intended for alphanumeric output, such as 'Sunday, November 14, 1858 10:00 AM'.
If months and weekdays are output as numbers, you are encouraged to use {@link #format(String)} instead.
@param aFormat uses the formatting mini-language defined in the class comment.
@param aLocale used to generate text for Month, Weekday and AM/PM indicator; required only by patterns which return localized
text, instead of numeric forms.
*/
public String format(String aFormat, Locale aLocale) {
DateTimeFormatter format = new DateTimeFormatter(aFormat, aLocale);
return format.format(this);
}
/**
Output this DateTime as a formatted String using numbers and explicit text for months, weekdays, and AM/PM indicator.
Use of this method is likely relatively rare; it should be used only if the output of {@link #format(String, Locale)} is
inadequate.
@param aFormat uses the formatting mini-language defined in the class comment.
@param aMonths contains text for all 12 months, starting with January; size must be 12.
@param aWeekdays contains text for all 7 weekdays, starting with Sunday; size must be 7.
@param aAmPmIndicators contains text for A.M and P.M. indicators (in that order); size must be 2.
*/
public String format(String aFormat, List aMonths, List aWeekdays, List aAmPmIndicators) {
DateTimeFormatter format = new DateTimeFormatter(aFormat, aMonths, aWeekdays, aAmPmIndicators);
return format.format(this);
}
/**
Return the current date-time.
Combines the return value of {@link System#currentTimeMillis()} with the given {@link TimeZone}.
Only millisecond precision is possible for this method.
*/
public static DateTime now(TimeZone aTimeZone) {
return forInstant(System.currentTimeMillis(), aTimeZone);
}
/**
Return the current date.
As in {@link #now(TimeZone)}, but truncates the time portion, leaving only year-month-day.
*/
public static DateTime today(TimeZone aTimeZone) {
DateTime result = now(aTimeZone);
return result.truncate(Unit.DAY);
}
/** Return true only if this date is in the future, with respect to {@link #now(TimeZone)}. */
public boolean isInTheFuture(TimeZone aTimeZone) {
return now(aTimeZone).lt(this);
}
/** Return true only if this date is in the past, with respect to {@link #now(TimeZone)}. */
public boolean isInThePast(TimeZone aTimeZone) {
return now(aTimeZone).gt(this);
}
/**
Return a DateTime corresponding to a change from one {@link TimeZone} to another.
A DateTime object has an implicit and immutable time zone.
If you need to change the implicit time zone, you can use this method to do so.
Example :
TimeZone fromUK = TimeZone.getTimeZone("Europe/London");
TimeZone toIndonesia = TimeZone.getTimeZone("Asia/Jakarta");
DateTime newDt = oldDt.changeTimeZone(fromUK, toIndonesia);
Requires year-month-day-hour to be present; if not, a runtime exception is thrown.
@param aFromTimeZone the implicit time zone of this object.
@param aToTimeZone the implicit time zone of the DateTime returned by this method.
@return aDateTime corresponding to the change of time zone implied by the 2 parameters.
*/
public DateTime changeTimeZone(TimeZone aFromTimeZone, TimeZone aToTimeZone){
DateTime result = null;
ensureHasYearMonthDay();
if (unitsAllAbsent(Unit.HOUR)){
throw new IllegalArgumentException("DateTime does not include the hour. Cannot change the time zone if no hour is present.");
}
Calendar fromDate = new GregorianCalendar(aFromTimeZone);
fromDate.set(Calendar.YEAR, getYear());
fromDate.set(Calendar.MONTH, getMonth()-1);
fromDate.set(Calendar.DAY_OF_MONTH, getDay());
fromDate.set(Calendar.HOUR_OF_DAY, getHour());
if(getMinute() != null) {
fromDate.set(Calendar.MINUTE, getMinute());
}
else {
fromDate.set(Calendar.MINUTE, 0);
}
//other items zeroed out here, since they don't matter for time zone calculations
fromDate.set(Calendar.SECOND, 0);
fromDate.set(Calendar.MILLISECOND, 0);
//millisecond precision is OK here, since the seconds/nanoseconds are not part of the calc
Calendar toDate = new GregorianCalendar(aToTimeZone);
toDate.setTimeInMillis(fromDate.getTimeInMillis());
//needed if this date has hour, but no minute (bit of an oddball case) :
Integer minute = getMinute() != null ? toDate.get(Calendar.MINUTE) : null;
result = new DateTime(
toDate.get(Calendar.YEAR), toDate.get(Calendar.MONTH) + 1, toDate.get(Calendar.DAY_OF_MONTH),
toDate.get(Calendar.HOUR_OF_DAY), minute, getSecond(), getNanoseconds()
);
return result;
}
/**
Compare this object to another, for ordering purposes.
Uses the 7 date-time elements (year..nanosecond). The Year is considered the most
significant item, and the Nanosecond the least significant item. Null items are placed first in this comparison.
*/
public int compareTo(DateTime aThat) {
if (this == aThat) return EQUAL;
ensureParsed();
aThat.ensureParsed();
ModelUtil.NullsGo nullsGo = ModelUtil.NullsGo.FIRST;
int comparison = ModelUtil.comparePossiblyNull(this.fYear, aThat.fYear, nullsGo);
if (comparison != EQUAL) return comparison;
comparison = ModelUtil.comparePossiblyNull(this.fMonth, aThat.fMonth, nullsGo);
if (comparison != EQUAL) return comparison;
comparison = ModelUtil.comparePossiblyNull(this.fDay, aThat.fDay, nullsGo);
if (comparison != EQUAL) return comparison;
comparison = ModelUtil.comparePossiblyNull(this.fHour, aThat.fHour, nullsGo);
if (comparison != EQUAL) return comparison;
comparison = ModelUtil.comparePossiblyNull(this.fMinute, aThat.fMinute, nullsGo);
if (comparison != EQUAL) return comparison;
comparison = ModelUtil.comparePossiblyNull(this.fSecond, aThat.fSecond, nullsGo);
if (comparison != EQUAL) return comparison;
comparison = ModelUtil.comparePossiblyNull(this.fNanosecond, aThat.fNanosecond, nullsGo);
if (comparison != EQUAL) return comparison;
return EQUAL;
}
/**
Equals method for this object.
Equality is determined by the 7 date-time elements (year..nanosecond).
*/
@Override public boolean equals(Object aThat) {
/*
* Implementation note: it was considered branching this method, according to whether
* the objects are already parsed. That was rejected, since maintaining 'synchronicity'
* with hashCode would not then be possible, since hashCode is based only on one object,
* not two.
*/
ensureParsed();
Boolean result = ModelUtil.quickEquals(this, aThat);
if (result == null) {
DateTime that = (DateTime)aThat;
that.ensureParsed();
result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
}
return result;
}
/**
Hash code for this object.
Uses the same 7 date-time elements (year..nanosecond) as used by
{@link #equals(Object)}.
*/
@Override public int hashCode() {
if (fHashCode == 0) {
ensureParsed();
fHashCode = ModelUtil.hashCodeFor(getSignificantFields());
}
return fHashCode;
}
/**
Intended for debugging and logging only.
To format this DateTime for presentation to the user, see the various format methods.
If the {@link #DateTime(String)} constructor was called, then return that String.
Otherwise, the return value is constructed from each date-time element, in a fixed format, depending
on which time units are present. Example values :
- 2011-04-30 13:59:59.123456789
- 2011-04-30 13:59:59
- 2011-04-30
- 2011-04-30 13:59
- 13:59:59.123456789
- 13:59:59
- and so on...
In the great majority of cases, this will give reasonable output for debugging and logging statements.
In cases where a bizarre combinations of time units is present, the return value is presented in a verbose form.
For example, if all time units are present except for minutes, the return value has this form:
Y:2001 M:1 D:31 h:13 m:null s:59 f:123456789
*/
@Override public String toString() {
String result = "";
if (Util.textHasContent(fDateTime)) {
result = fDateTime;
}
else {
String format = calcToStringFormat();
if(format != null){
result = format(calcToStringFormat());
}
else {
StringBuilder builder = new StringBuilder();
addToString("Y", fYear, builder);
addToString("M", fMonth, builder);
addToString("D", fDay, builder);
addToString("h", fHour, builder);
addToString("m", fMinute, builder);
addToString("s", fSecond, builder);
addToString("f", fNanosecond, builder);
result = builder.toString().trim();
}
}
return result;
}
// PACKAGE-PRIVATE (for unit testing, mostly)
static final class ItemOutOfRange extends RuntimeException {
ItemOutOfRange(String aMessage) {
super(aMessage);
}
private static final long serialVersionUID = 4760138291907517660L;
}
static final class MissingItem extends RuntimeException {
MissingItem(String aMessage) {
super(aMessage);
}
private static final long serialVersionUID = -7359967338896127755L;
}
/** Intended as internal tool, for testing only. Note scope is not public! */
void ensureParsed() {
if (!fIsAlreadyParsed) {
parseDateTimeText();
}
}
/**
Return the number of days in the given month. The returned value depends on the year as
well, because of leap years. Returns null if either year or month are
absent. WRONG - should be public??
Package-private, needed for interval calcs.
*/
static Integer getNumDaysInMonth(Integer aYear, Integer aMonth) {
Integer result = null;
if (aYear != null && aMonth != null) {
if (aMonth == 1) {
result = 31;
}
else if (aMonth == 2) {
result = isLeapYear(aYear) ? 29 : 28;
}
else if (aMonth == 3) {
result = 31;
}
else if (aMonth == 4) {
result = 30;
}
else if (aMonth == 5) {
result = 31;
}
else if (aMonth == 6) {
result = 30;
}
else if (aMonth == 7) {
result = 31;
}
else if (aMonth == 8) {
result = 31;
}
else if (aMonth == 9) {
result = 30;
}
else if (aMonth == 10) {
result = 31;
}
else if (aMonth == 11) {
result = 30;
}
else if (aMonth == 12) {
result = 31;
}
else {
throw new AssertionError("Month is out of range 1..12:" + aMonth);
}
}
return result;
}
static DateTime fromJulianDayNumberAtNoon(int aJDAtNoon) {
//http://www.hermetic.ch/cal_stud/jdn.htm
int l = aJDAtNoon + 68569;
int n = (4 * l) / 146097;
l = l - (146097 * n + 3) / 4;
int i = (4000 * (l + 1)) / 1461001;
l = l - (1461 * i) / 4 + 31;
int j = (80 * l) / 2447;
int d = l - (2447 * j) / 80;
l = j / 11;
int m = j + 2 - (12 * l);
int y = 100 * (n - 49) + i + l;
return DateTime.forDateOnly(y, m, d);
}
// PRIVATE
/*
There are 2 representations of a date - a text form, and a 'parsed' form, in which all
of the elements of the date are separated out. A DateTime starts out with one of these
forms, and may need to generate the other.
*/
/** The text form of a date. @serial */
private String fDateTime;
/* The following 7 items represent the parsed form of a DateTime. */
/** @serial */
private Integer fYear;
/** @serial */
private Integer fMonth;
/** @serial */
private Integer fDay;
/** @serial */
private Integer fHour;
/** @serial */
private Integer fMinute;
/** @serial */
private Integer fSecond;
/** @serial */
private Integer fNanosecond;
/** Indicates if this DateTime has been parsed into its 7 constituents. @serial */
private boolean fIsAlreadyParsed;
/** @serial */
private int fHashCode;
private static final int EQUAL = 0;
private static int EPOCH_MODIFIED_JD = 2400000;
private static final int MILLION = 1000000;
private static final long serialVersionUID = -1300068157085493891L;
/**
Return a the whole number, with no fraction.
The JD at noon is 1 more than the JD at midnight.
*/
private int calculateJulianDayNumberAtNoon() {
//http://www.hermetic.ch/cal_stud/jdn.htm
int y = fYear;
int m = fMonth;
int d = fDay;
int result = (1461 * (y + 4800 + (m - 14) / 12)) / 4 + (367 * (m - 2 - 12 * ((m - 14) / 12))) / 12 - (3 * ((y + 4900 + (m - 14) / 12) / 100)) / 4 + d - 32075;
return result;
}
private void ensureHasYearMonthDay() {
ensureParsed();
if (!hasYearMonthDay()) {
throw new MissingItem("DateTime does not include year/month/day.");
}
}
/** Return the number of seconds in any existing time portion of the date. */
private int numSecondsInTimePortion() {
int result = 0;
if (fSecond != null) {
result = result + fSecond;
}
if (fMinute != null) {
result = result + 60 * fMinute;
}
if (fHour != null) {
result = result + 3600 * fHour;
}
return result;
}
private void validateState() {
checkRange(fYear, 1, 9999, "Year");
checkRange(fMonth, 1, 12, "Month");
checkRange(fDay, 1, 31, "Day");
checkRange(fHour, 0, 23, "Hour");
checkRange(fMinute, 0, 59, "Minute");
checkRange(fSecond, 0, 59, "Second");
checkRange(fNanosecond, 0, 999999999, "Nanosecond");
checkNumDaysInMonth(fYear, fMonth, fDay);
}
private void checkRange(Integer aValue, int aMin, int aMax, String aName) {
if(aValue != null){
if (aValue < aMin || aValue > aMax){
throw new ItemOutOfRange(aName + " is not in the range " + aMin + ".." + aMax + ". Value is:" + aValue);
}
}
}
private void checkNumDaysInMonth(Integer aYear, Integer aMonth, Integer aDay) {
if (hasYearMonthDay(aYear, aMonth, aDay) && aDay > getNumDaysInMonth(aYear, aMonth)) {
throw new ItemOutOfRange("The day-of-the-month value '" + aDay + "' exceeds the number of days in the month: " + getNumDaysInMonth(aYear, aMonth));
}
}
private void parseDateTimeText() {
DateTimeParser parser = new DateTimeParser();
DateTime dateTime = parser.parse(fDateTime);
/*
* This is unusual - we essentially copy from one object to another. This could be
* avoided by building another interface, But defining a top-level interface for this
* simple task is too high a price.
*/
fYear = dateTime.fYear;
fMonth = dateTime.fMonth;
fDay = dateTime.fDay;
fHour = dateTime.fHour;
fMinute = dateTime.fMinute;
fSecond = dateTime.fSecond;
fNanosecond = dateTime.fNanosecond;
validateState();
}
private boolean hasYearMonthDay(Integer aYear, Integer aMonth, Integer aDay) {
return isPresent(aYear, aMonth, aDay);
}
private static boolean isLeapYear(Integer aYear) {
boolean result = false;
if (aYear % 100 == 0) {
// this is a century year
if (aYear % 400 == 0) {
result = true;
}
}
else if (aYear % 4 == 0) {
result = true;
}
return result;
}
private Object[] getSignificantFields() {
return new Object[]{fYear, fMonth, fDay, fHour, fMinute, fSecond, fNanosecond};
}
private void addToString(String aName, Object aValue, StringBuilder aBuilder) {
aBuilder.append(aName + ":" + String.valueOf(aValue) + " ");
}
/** Return true only if all the given arguments are non-null. */
private boolean isPresent(Object... aItems) {
boolean result = true;
for (Object item : aItems) {
if (item == null) {
result = false;
break;
}
}
return result;
}
private DateTime getStartEndDateTime(Integer aDay, Integer aHour, Integer aMinute, Integer aSecond, Integer aNanosecond) {
ensureHasYearMonthDay();
return new DateTime(fYear, fMonth, aDay, aHour, aMinute, aSecond, aNanosecond);
}
private String calcToStringFormat(){
String result = null; //caller will check for this; null means the set of units is bizarre
if(unitsAllPresent(Unit.YEAR) && unitsAllAbsent(Unit.MONTH, Unit.DAY, Unit.HOUR, Unit.MINUTE, Unit.SECOND, Unit.NANOSECONDS)){
result = "YYYY";
}
else if (unitsAllPresent(Unit.YEAR, Unit.MONTH) && unitsAllAbsent(Unit.DAY, Unit.HOUR, Unit.MINUTE, Unit.SECOND, Unit.NANOSECONDS)){
result = "YYYY-MM";
}
else if (unitsAllPresent(Unit.YEAR, Unit.MONTH, Unit.DAY) && unitsAllAbsent(Unit.HOUR, Unit.MINUTE, Unit.SECOND, Unit.NANOSECONDS)){
result = "YYYY-MM-DD";
}
else if (unitsAllPresent(Unit.YEAR, Unit.MONTH, Unit.DAY, Unit.HOUR) && unitsAllAbsent(Unit.MINUTE, Unit.SECOND, Unit.NANOSECONDS)){
result = "YYYY-MM-DD hh";
}
else if (unitsAllPresent(Unit.YEAR, Unit.MONTH, Unit.DAY, Unit.HOUR, Unit.MINUTE) && unitsAllAbsent(Unit.SECOND, Unit.NANOSECONDS)){
result = "YYYY-MM-DD hh:mm";
}
else if (unitsAllPresent(Unit.YEAR, Unit.MONTH, Unit.DAY, Unit.HOUR, Unit.MINUTE, Unit.SECOND) && unitsAllAbsent(Unit.NANOSECONDS)){
result = "YYYY-MM-DD hh:mm:ss";
}
else if (unitsAllPresent(Unit.YEAR, Unit.MONTH, Unit.DAY, Unit.HOUR, Unit.MINUTE, Unit.SECOND, Unit.NANOSECONDS)){
result = "YYYY-MM-DD hh:mm:ss.fffffffff";
}
else if (unitsAllAbsent(Unit.YEAR, Unit.MONTH, Unit.DAY) && unitsAllPresent(Unit.HOUR, Unit.MINUTE, Unit.SECOND, Unit.NANOSECONDS)){
result = "hh:mm:ss.fffffffff";
}
else if (unitsAllAbsent(Unit.YEAR, Unit.MONTH, Unit.DAY, Unit.NANOSECONDS) && unitsAllPresent(Unit.HOUR, Unit.MINUTE, Unit.SECOND)){
result = "hh:mm:ss";
}
else if (unitsAllAbsent(Unit.YEAR, Unit.MONTH, Unit.DAY, Unit.SECOND, Unit.NANOSECONDS) && unitsAllPresent(Unit.HOUR, Unit.MINUTE)){
result = "hh:mm";
}
return result;
}
/**
Always treat de-serialization as a full-blown constructor, by
validating the final state of the de-serialized object.
*/
private void readObject(ObjectInputStream aInputStream) throws ClassNotFoundException, IOException {
//always perform the default de-serialization first
aInputStream.defaultReadObject();
//no mutable fields in this case
validateState();
}
/**
This is the default implementation of writeObject.
Customise if necessary.
*/
private void writeObject(ObjectOutputStream aOutputStream) throws IOException {
//perform the default serialization for all non-transient, non-static fields
aOutputStream.defaultWriteObject();
}
}