/* * [BigDate.java] * * Summary: Manipulate pure dates, without time or time zone. * * Copyright: (c) 1997-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 1997-05-03 initial release. * 1.1 1997-05-04 add setOrdinal, setYYYY, setMM, setDD. * 1.2 1997-05-05 add public static toOrdinal. * 1.3 1997-05-06 add getTimeStamp, warn about lead 0 meaning octal * 1.4 1997-05-06 add getDayOfWeek, getDDD, rename NULL_ORDINAL, NULL_TIMESTAMP * 1.5 1997-05-11 rename getJulian to getOrdinal to avoid confusion with * old Julian vs new Gregorian calendars. Redo 1582 code with named * constants to facilitate changing the switchover point between the old and * new calendars. * 1.6 1997-05-12 shorten the names of constants. * 1.7 1997-06-21 cache last result to avoid recalculation. * 1.8 1997-07-09 add null constructor * 1.9 1997-12-17 add documentation on how to convert to the British * calendar. Modify code to make it simple to switch to the British scheme. * Allow for fact British 100 and 400 Leap year rules kicked in at different * times. Add today method. Calculate constants based on others rather than * specifying individually to make change easier. expose default methods to * extenders of this class, making them protected. Fix bug. Null constructor * was not creating a properly initialised NULL date. There was confusion * then between a null date and a zero date = 1997-01-01. * 2.0 1998-01-26 Peter V. Gadjokov" pointed out a * bug in isValid. The dd was not tested properly to make sure it was <= 31 * The bug did not cause incorrect execution, just slowed it, since a finer * check was done later. * 2.1 1998-01-30 implement Cloneable, Serializable, equals, compareTo, * hashCode add Javadoc comments. isBritish compile time switch. * alphabetise. private, protected status changed. * 2.2 1998-06-18 add getISOWeekNumber, getISODayOfWeek. * 2.3 1998-11-19 new mailing address and phone. * 2.4 1998-11-27 more examples on how to use BigDate in TestDate. * warnings about today and time zones. today is now two methods localToday * and UTCToday. added age method to compute age in years, months and days. * exposed daysInMonth as public. made most methods final for speed. Since * you have source, you can always unfinal them if you need to. * 2.5 1998-11-28 added test harness to prove age routine is working * correctly. Note about ISO 8601:1988 international standard for dates is * YYYY-MM-DD. Corrected major bugs in age. Now works for mixed BC/AD. Works * if as of date is not today. More comments in age routine on how it works. * Now accepts two BigDates instead of two ordinals. Added more examples of * how to use BigDate. * 2.6 1998-11-30 Added more examples to TestDate. * 2.7 1999-08-23 getTimeStamp renamed to getUTCTimeStamp add * getLocalTimeStamp, getUTCDate, getLocalDate today(TimeZone) * 2.8 1999-09-04 add daysInMonth to take the year instead of a * boolean. That way you can usually avoid the isLeap calculation. * 2.9 1999-09-08 add sample isHolidayimplementationn to TestDate. add * dayOfWeek and isoDayOfWeek static versions. * 3.0 1999-09-08 add nthXXXDay and ordinalOfnthXXXDay. * 3.1 1999-09-15 add constructor and access for Astronomer's * Proleptic Julian day numbers. to be consistent with the US Naval * observatory, BC leap years are now 1 5 9, not 4, 8, 12. * 3.2 1999-09-20 add getWeekNumber to complement getISOWeekNumber. * 3.3 1999-09-22 correct toString to display very old or very future * dates correctly * 3.4 1999-10-18 speed up leap year calcs by replacing yyyy % 4 with * yyyy & 3 * 3.5 1999-11-23 give Timezone.getOffset a 1-based Sunday. Avoid * IllegalArgumentException in some JVMs. Place warnings about Sunday base * incompatibilities. * 3.6 1999-11-24 add getCalendarDayOfWeek and calendarDayOfWeek * 3.7 2001-01-26 properly implement Comparable with Object instead of * BigDate * 3.8 2001-03-17 add code to TestDate to calculate how many sleeps until * Christmas. * 3.9 2002-03-16 make toString non-final so you can override it. * 4.0 2002-04-10 addDays convenience method toString now displays ISO * format yyyy-mm-dd TestDate examples changed to use addDays. * 4.1 2003-01-01 * 4.2 2003-01-01 * 4.3 2003-05-18 Default is now British rather than Pope Gregory's rules * for when the missing days were dealt with. * 4.4 2003-08-05 getDowMMDDYY -- alternative to toString, mainly to show * how to roll your own methods. * 4.5 2003-09-12 Added constructor that takes a Date and TimeZone. added * setDateAtTime. * 4.6 2004-05-15 getSeason * 4.7 2005-07-15 isValid( yyyy-mm-dd), new package, com.mindprod.common18, * getCopyright * 4.8 2005-08-28 added constructor that takes a String * argument. * 4.9 2006-03-04 convert to Intellij. Make code more robust by moving * initialisation to a static block so field reordering will not screw it up. * 5.0 2007-10-15 new methods, nearestXXXDay, dayOfWeek, calendarDayOfWeek. * Improved documentation. * 5.1 2008-02-01 add named constants for the months and days of the week. * 5.2 2008-03-20 add dayAbbr, dayName, monthAbbr, monthName * 5.3 2008-12-15 toString now handles BC and non-4-digit years. toYYYYString, * Constructor handles BC and non-4-digit years. * 5.4 2009-04-29 add parseYYYYmmdd to parse incomplete date strings. * 5.5 2009-05-05 add isAnniversary * 5.6 2009-12-09 parsing of String dates is now more relaxed. Now ignores spaces, * commas. Accepts single digit months and days. * 5.7 2011-02-09 add toOrdinal ( yyyy_mm_dd ), throw NumberFormatException instead of IllegalArgumentException. * 5.8 2014-04-29 add isCelebrating. Give all static final constants pure upper case names. * 5.9 2014-08-09 make constructors null-safe. Null generates a NULL_ORDINARY BigDate. */ package com.mindprod.common18; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.Date; import java.util.TimeZone; /** * Manipulate pure dates, without time or time zone. *

* Convert Gregorian YYYY MM DD back and forth to ordinal days since 1970-01-01, Thursday (sometimes called Julian or * datestamp form). BigDate objects are not designed for storing dates in a database, * just for conversion. Long * term storage should store the ordinal either as an int, or possibly as a short. The BigDate constructor stores the * date both in ordinal and Gregorian forms internally. If you store one, it creates the other. *

* The standard Sun Date won't handle dates prior to 1970 among other problems. BigDate handles dates 999,999 BC Jan 1 * to 999,999 AD Dec 31, 0 = 1970-01-01. *

* Are the following quirks of the calendar considered in this code? *

* 1900 is not a leap year (mod 100): yes. *

* 2000 is a leap year (mod 400): yes. *

* The 10 missing days in 1582 October.: yes (Pope Gregory's correction) 1582-10-05 to 1582-10-14 never * happened. *

* Britain and its territories (including the USA and Canada) adopted the Gregorian correction in 1752: * Yes. By * then, 11 days had to be dropped. 1752-09-03 to 1752-09-13 never happened. However, you can modify * constants in BigDate to use the British calendar. Such a change only affects dates prior to 1753 since BigDate * calendar is based on 1970-01-01. toOrdinal with the Gregorian and British scheme will give the same number for * recent dates. You must recompile BigDate with the IS_BRITISH boolean changed to true * .. It was used by * Britain and its colonies which later became the USA and Canada. However Nova Scotia used Pope Gregory's calendar. * see http://mindprod.com/jgloss/missingdays.html PLEASE CONFIGURE IS_BRITISH AND RECOMPILE BigDate IF YOU WISH TO USE * THE BRITISH CALENDAR. *

* missing year 0 between 1 BC and 1 AD. yes. *

* in Roman times leap years occurred at irregular intervals, Considered inauspicious, they were avoided during war. * no. Instead we presume leap years every 4 years even back to 999,999BC. *

* leap seconds: no *

* Normally all you need is one BigDate object that you use for all interconversions with set(ordinal), set(yyy,mm,dd) * and getOrdinal(), getYYYY(), getMM(), getDD(). *

* java.util.Date has some odd habits, using 101 to represent the year 2001, and 11 to represent December. BigDate is * more conventional. You use 2001 to represent the year 2001 and 12 to represent December. *

* BigDate implements proleptic Gregorian and Julian calendars. That is, dates are computed by extrapolating the current * rules indefinitely far backward and forward in time. As a result, BigDate may be used for all years to generate * meaningful and consistent results. However, dates obtained using BigDate are historically accurate only from March 1, * 4 AD onward, when modern Julian calendar rules were adopted. Before this date, leap year rules were applied * irregularly, and before 45 BC the Julian calendar did not even exist. Prior to the institution of the Gregorian * calendar, New Year's Day was March 25. To avoid confusion, this calendar always uses January 1. *

* TODO Future enhancements: - handle time, and time zones, interconversion with GregorianCalendar dates. *

* * @author Roedy Green, Canadian Mind Products * @version 5.9 2014-08-09 make constructors null-safe. Null generates a NULL_ORDINARY BigDate. * @noinspection WeakerAccess, UnusedDeclaration * @since 1997-05-03 */ public final class BigDate implements Cloneable, Serializable, Comparable { /** * PLEASE CONFIGURE IS_BRITISH BEFORE COMPILING. Mysterious missing days in the calendar. Pope Gregory: 1582-10-04 * Thursday, was followed immediately by 1582-10-15 Friday dropping 10 days. British: 1752-09-02 Wednesday was * followed immediately by 1752-09-14 Thursday dropping 12 days. Constant: true if you want the British calender, * false if Pope Gregory's. You must recompile for it to have effect. For Britain, the USA and Canada it should be * true. */ public static final boolean IS_BRITISH = true; /** * April is 4 */ public static final int APR = 4; /** * April is 4 */ public static final int APRIL = 4; /** * August is 8 */ public static final int AUG = 8; /** * August is 8 */ public static final int AUGUST = 8; /** * Constant: when passed to a constructor, it means caller guarantees YYYY MM DD are valid including leap year * effects and missing day effects. BigDate will not bother to check them. * * @noinspection WeakerAccess * @see #CHECK * @see #NORMALIZE * @see #NORMALISE */ public static final int BYPASSCHECK = 1; /** * constant: when passed to a contructor it means BigDate should check that YYYY MM DD are valid. * * @noinspection WeakerAccess * @see #BYPASSCHECK * @see #NORMALIZE * @see #NORMALISE */ public static final int CHECK = 0; /** * December is 12 */ public static final int DEC = 12; /** * December is 12 */ public static final int DECEMBER = 12; /** * February is 2 */ public static final int FEB = 2; /** * February is */ public static final int FEBRUARY = 2; /** * Friday in BigDate is 5 */ public static final int FRI = 5; /** * Friday in BigDate is 5 */ public static final int FRIDAY = 5; /** * January is 1 */ public static final int JAN = 1; /** * January is 1 */ public static final int JANUARY = 1; /** * July is 7 */ public static final int JUL = 7; /** * July is 7 */ public static final int JULY = 7; /** * June is 6 */ public static final int JUN = 6; /** * June is 6 */ public static final int JUNE = 6; /** * March is 3 */ public static final int MAR = 3; /** * March is 3 */ public static final int MARCH = 3; /** * Constant: biggest ordinal that BigDate will accept, corresponds to 999,999 Dec 31 AD. */ public static final int MAX_ORDINAL; /** * Constant: biggest year that BigDate handles, 999,999 AD. */ public static final int MAX_YEAR = 999999; /** * May is 5 */ public static final int MAY = 5; /** * Constant: earliest ordinal that BigDate handles. * It corresponds to 999,999 Jan 01 BC. */ public static final int MIN_ORDINAL; /** * Constant: earliest year that BigDate handles, 999,999 BC. * * @noinspection WeakerAccess */ public static final int MIN_YEAR = -999999; /** * Monday in BigDate is 1 */ public static final int MON = 1; /** * Monday in BigDate is 1 */ public static final int MONDAY = 1; /** * Constant: when passed to a constructor, it means any invalid dates are converted into the equivalent valid ones. * e.g. * 1954-09-31 -> 1954-10-01. * 1954-10- minus 1 -> 1954-09-30 * 1954-13-01 -> 1955-01-01. * * @see #CHECK * @see #BYPASSCHECK * @see #NORMALIZE */ public static final int NORMALISE = 2; /** * Constant: American spelling alias for NORMALISE. * * @see #CHECK * @see #BYPASSCHECK * @see #NORMALISE */ public static final int NORMALIZE; /** * November is 11 */ public static final int NOV = 11; /** * November is 11 */ public static final int NOVEMBER = 11; /** * Constant: ordinal to represent a null date -2,147,483,648, null Gregorian is 0,0,0. */ public static final int NULL_ORDINAL = Integer.MIN_VALUE; /** * October is 10 */ public static final int OCT = 10; /** * October is 10 */ public static final int OCTOBER = 10; /** * Saturday in BigDate is 6 */ public static final int SAT = 6; /** * Saturday in BigDate is 6 */ public static final int SATURDAY = 6; /** * September is 9 */ public static final int SEP = 9; /** * September is 9 */ public static final int SEPTEMBER = 9; /** * Sunday is 0 */ public static final int SUN = 0; /** * Sunday is 0 */ public static final int SUNDAY = 0; /** * Thursday in BigDate is 4 */ public static final int THU = 4; /** * Thursday in BigDate is 4 */ public static final int THURSDAY = 4; /** * Tuesday in BigDate is 2 */ public static final int TUE = 2; /** * Tuesday in BigDate is 2 */ public static final int TUESDAY = 2; /** * Wednesday in BigDate is 3 */ public static final int WED = 3; /** * Wednesday in BigDate is 3 */ public static final int WEDNESDAY = 3; /** * Constant : value for a null TimeStamp -9,223,372,036,854,775,808 * * @noinspection WeakerAccess */ public static final long NULL_TIMESTAMP = Long.MIN_VALUE; /** * used to identify this version of serialised BigDate objects (leave camel case) */ static final long serialVersionUID = 34L; /** * Constant: adjustment to make ordinal 0 come out on 1970-01-01 for AD date calculations. This number was computed * by making an estimate, seeing what value toOrdinal gave for 1970-01-01 and then adjusting this constant so that * 1970-01-01 would come out Ordinal 0. */ private static final int AD_EPOCH_ADJUSTMENT; /** * Constant: adjustment to make ordinal 0 come out to 1970-01-01 for BC date calculations. Account for missing year * 0. */ private static final int BC_EPOCH_ADJUSTMENT; /** * Constant: month of the first date of the Gregorian Calendar. Pope Gregory: 1582-10-15, British: 1752-09-14 * * @noinspection FieldCanBeLocal */ private static final int CG_FIRST_MM; /** * Constant: day of the first day of the month of the Gregorian Calendar Pope Gregory: 1582-10-15, British: 1752 * Sep 14 */ private static final int GC_FIRST_DD; /** * Constant: year of the first date (1752-10-15) of the Gregorian Calendar = 1582. Just after the missing days. * Different parts of the world made the transition at different times. Pope Gregory: 1582-10-04, * British: 1752-09-02. */ private static final int GC_FIRST_YYYY; /** * Constant: year that the mod 100 rule first had any effect. For The Gregorian Calendar, 1600. For the British * Calendar, 1800. Round up to next 100 years after the missing day anomaly. */ private static final int LEAP_100_RULE_YYYY; /** * Constant: year that the mod 400 rule first had any effect. For The Gregorian Calendar, 1600. For the British * Calendar, 2000. Round up to next 400 years after the missing day anomaly. */ private static final int LEAP_400_RULE_YYYY; /** * Constant array: how many days in the year prior to the first of the given month in a leap year. Indexed by * Jan=0. */ private static final int[] LEAP_DAYS_IN_YEAR_PRIOR_TO_MONTH_TABLE = { 0, 31, 60, /* J F M */ 91, 121, 152, /* A M J */ 182, 213, 244, /* J A S */ 274, 305, 335 /* O N D */ }; /** * Constant array: what month does the indexing day number fall in a leap year? Indexed by ddd Jan 1 = 0. */ private static final int[] LEAP_DDD_TO_MM_TABLE; /** * Constant: how many days were lost during the Gregorian correction, 10 for the Gregorian calendar, 11 for the * British. */ private static final int MISSING_DAYS; /** * Adjustment to make Monday come out as day 0 after doing 7 modulus. Accounts for fact MIN_ORDINAL was not a * Monday */ private static final int MONDAY_IS_ZERO_ADJUSTMENT = 3; /** * Constant: day of the last day of the month of the old Julian calendar. Pope Gregory: 1582-10-04, British: 1752 * Sep 2 */ private static final int OJC_LAST_DD; /** * Constant: month of the last date of the old Julian Calendar Pope Gregory: 1582-10-04, British: 1752-09-02 */ private static final int OJC_LAST_MM; /** * Constant: year of the last date (1582-10-04) of the old Julian calendar, just prior to the missing 10 days, = * 1852. Different parts of the world made the transition at different times. Usually 1582-10-04. For British * calender it would be 1752-09-02 . */ private static final int OJC_LAST_YYYY; /** * Constant: Ordinal for 1 AD Jan 01 */ private static final int ORDINAL_0001_01_01AD; /** * Constant: Ordinal for 1 BC Jan 01 */ private static final int ORDINAL_0001_01_01BC; /** * Constant: Ordinal for 4 AD Jan 01 */ private static final int ORDINAL_0004_01_01AD; /** * Constant: Ordinal for 1582-10-15, the first day of the Gregorian calendar. */ private static final int ORDINAL_GC_FIRST; /** * Constant: Ordinal for 1582 Dec 31, the last day of first year of the Gregorian calendar. */ private static final int ORDINAL_GC_FIRST_DEC_31; /** * Constant: ordinal of 1600 Jan 01, the first year when the mod 100 leap year rule first had any effect. */ private static final int ORDINAL_LEAP_RULE_100_START; /** * Constant: ordinal of 1600 Jan 01, the year when the mod 400 leap year rule first had any effect. */ private static final int ORDINAL_LEAP_RULE_400_START; /** * Adjustment to make Sunday come out as day 0 after doing 7 modulus. Accounts for fact MIN_ORDINAL was not a * Sunday. */ private static final int SUNDAY_IS_ZERO_ADJUSTMENT = 4; /** * Constant array: how many days in the year prior to the first of the given month in a non-leap year. Indexed by * Jan=0. */ private static final int[] USUAL_DAYS_IN_YEAR_PRIOR_TO_MONTH_TABLE = { 0, 31, 59, /* J F M */ 90, 120, 151, /* A M J */ 181, 212, 243, /* J A S */ 273, 304, 334 /* O N D */ }; /** * Constant array: how many days are in there in a month, (not a leap year). Indexed by Jan = 0. */ private static final int[] USUAL_DAYS_PER_MONTH_TABLE = { 31, 28, 31, /* J F M */ 30, 31, 30, /* A M J */ 31, 31, 30, /* J A S */ 31, 30, 31 /* O N D */ }; /** * Constant array: what month does the indexing day number fall in a non-leap year? Indexed by ddd Jan 1 = 0. */ private static final int[] USUAL_DDD_TO_MM_TABLE; /** * days of the week 3 letter abbreviations in English. Index Sunday = 0. */ private static final String[] DAY_ABBRS = { "sun", "mon", "tue", "wed", "thu", "fri", "sat" }; /** * full days of the week in English. Index Sunday = 0. */ private static final String[] DAY_NAMES = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }; /** * 3-letter Months of the year in English. index January = 1 */ private static final String[] MONTH_ABBRS = { "???", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; /** * full Months of the year in English. index January = 1 */ private static final String[] MONTH_NAMES = { "unknown", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; static { /* Initialisation order is tricky so we collect all the dependencies here. This way no matter how fields are ordered, initialisation still works. */ // set IS_BRITISH at the top NORMALIZE = NORMALISE; OJC_LAST_YYYY = IS_BRITISH ? 1752 : 1582; OJC_LAST_MM = IS_BRITISH ? 9 : 10; OJC_LAST_DD = IS_BRITISH ? 2 : 4; GC_FIRST_YYYY = IS_BRITISH ? 1752 : 1582; CG_FIRST_MM = IS_BRITISH ? 9 : 10; GC_FIRST_DD = IS_BRITISH ? 14 : 15; LEAP_100_RULE_YYYY = ( ( GC_FIRST_YYYY + 99 ) / 100 ) * 100; LEAP_400_RULE_YYYY = ( ( GC_FIRST_YYYY + 399 ) / 400 ) * 400; MISSING_DAYS = GC_FIRST_DD - OJC_LAST_DD - 1; AD_EPOCH_ADJUSTMENT = -719529; BC_EPOCH_ADJUSTMENT = AD_EPOCH_ADJUSTMENT + 365; MIN_ORDINAL = BigDate.toOrdinal( MIN_YEAR, 1, 1 ); ORDINAL_0001_01_01BC = BigDate.toOrdinal( -1, 1, 1 ); ORDINAL_0001_01_01AD = BigDate.toOrdinal( 1, 1, 1 ); ORDINAL_0004_01_01AD = BigDate.toOrdinal( 4, 1, 1 ); ORDINAL_GC_FIRST = BigDate.toOrdinal( GC_FIRST_YYYY, CG_FIRST_MM, GC_FIRST_DD ); ORDINAL_GC_FIRST_DEC_31 = BigDate.toOrdinal( GC_FIRST_YYYY, 12, 31 ); ORDINAL_LEAP_RULE_100_START = BigDate.toOrdinal( LEAP_100_RULE_YYYY, 1, 1 ); ORDINAL_LEAP_RULE_400_START = BigDate.toOrdinal( LEAP_400_RULE_YYYY, 1, 1 ); MAX_ORDINAL = BigDate.toOrdinal( MAX_YEAR, 12, 31 ); } static { // static initialisation code for USUAL_DDD_TO_MM_TABLE USUAL_DDD_TO_MM_TABLE = new int[ 365 ]; int dddi = 0; for ( int mmi = 1; mmi <= 12; mmi++ ) { int last = daysInMonth( mmi, false ); for ( int ddi = 0; ddi < last; ddi++ ) { USUAL_DDD_TO_MM_TABLE[ dddi++ ] = mmi; } // end for dd } // end for mmm } // end static init static { // static initialisation code for LEAP_DDD_TO_MM_TABLE LEAP_DDD_TO_MM_TABLE = new int[ 366 ]; int dddi = 0; for ( int mmi = 1; mmi <= 12; mmi++ ) { int last = daysInMonth( mmi, true ); for ( int ddi = 0; ddi < last; ddi++ ) { LEAP_DDD_TO_MM_TABLE[ dddi++ ] = mmi; } } } // end static init /** * Ordinal days since Jan 01, 1970. -365968798 to 364522971. i.e. 999,999 BC to 999,999 AD. */ protected int ordinal = NULL_ORDINAL; /** * Day, 1 to 31. If size of BigDate objects were a consideration, you could make this a byte. * * @noinspection WeakerAccess */ protected transient int dd = 0; /** * Month, 1 to 12. If size of BigDate objects were a consideration, you could make this a byte. * * @noinspection WeakerAccess */ protected transient int mm = 0; /** * Year, -999,999 to +999,999, negative is BC, positive is AD, 0 is null. If I were rewriting this, I would likely * encode year 1 BC as 0, and convert on output. That would simplify calculation over the 1AD -1BC barrier. * * @noinspection WeakerAccess */ protected transient int yyyy = 0; /** * how last set was done one of int codes for CHECK BYPASSCHECK NORMALIZE NORMALISE * Default constructor uses CHECK. Initial value suggest initial value is not trusted. */ private int how = BYPASSCHECK; /** * Constructor for the null date. Gets set to null date, NOT current date like Java Date!!. BigDate.localToday() * will create an object initialised to today's date. * * @see #localToday * @see #UTCToday * @see #today */ public BigDate() { } // end constructor /** * Construct a BigDate object given the Proleptic Julian day number. The Proleptic Julian calendar that astronomers * use starts with 0 at noon on 4713 BCE January 1. In contrast, BigDate's Ordinal base is 1970-01-01. * * @param prolepticJulianDay days since 4713 BC Jan 1 noon. Such numbers usually arise in astronomical calculation. * You don't need to concern yourself with the strangeness of the Julian calendar, just * its simple day numbering. BEWARE! after adjusting for noon, fractional parts are * discarded. BigDate tracks only dates, not dates and times. e.g. 2000-03-20 noon is * 2,451,624 in proleptic day numbers. 1970-01-01 is 2,440,588 1600-1-1 is 2,305,448 * 1500-1-1 is 2,268,933 0001-1-1 is 1,721,424 -0001-12-31 is 1,721,423 -0006-1-1 is * 1,719,232 -4713-1-1 is 0 */ public BigDate( double prolepticJulianDay ) { setOrdinal( ( int ) ( ( long ) ( prolepticJulianDay + 0.5/* * noon adjust, * .5 or bigger * really part * of next day */ ) - 2440588L/* * i.e. diff in base epochs of calendars */ ) ); } // end contructor /** * Ordinal constructor. The ordinal must be NULL_ORDINAL or in the range -365968798 to 364522971 i.e. 999,999 BC to * 999,999 AD * * @param ordinal days since 1970-01-01. */ public BigDate( int ordinal ) { // save ordinal field and compute Gregorian equivalent set( ordinal ); } // end contructor /** * Create a BigDate object from a String of the form: yyyy-mm-dd must have 4-digit years, and use dashes between the * number and no sign Does extensive checks considering leap years, missing days etc. * * @param yyyy_mm_dd string of form "yyyy-mm-dd". * yyyy-mm-dd * y-mm-ddBC * 2009-12-31 * 1234-5-31AD * 1970/3/3 AD * 3.01.12 bc * 2000_12_31 * It will not accept a date missing the year, month or day field. Spaces and commas are ignored. */ public BigDate( String yyyy_mm_dd ) { final int[] ymd = relaxedParse( yyyy_mm_dd ); if ( ymd == null ) { throw new NumberFormatException( "unparseable date: " + yyyy_mm_dd ); } // will throw NumberFormatException for an invalid date. set( ymd[ 0 ], ymd[ 1 ], ymd[ 2 ], CHECK ); } // end contructor /** * Copy constructor * * @param b an existing BigDate object to use as a model for cloning another. */ public BigDate( BigDate b ) { if ( b == null ) { set( NULL_ORDINAL ); } else { this.ordinal = b.ordinal; this.yyyy = b.yyyy; this.mm = b.mm; this.dd = b.dd; } } // end contructor /** * Constructor from Date, loses time information. * * @param utc Date ( UTC date/time stamp ) * @param timeZone Which timeZone do you want to know the date for that UTC time. e.g. TimeZone.getDefault(), new * TimeZone("GMT") */ public BigDate( Date utc, TimeZone timeZone ) { if ( utc == null ) { set( NULL_ORDINAL ); } else { setDateAtTime( utc.getTime(), timeZone ); } } /** * Construct a BigDate object given a Gregorian date yyyy, mm, dd; always rejects invalid dates. A null date is * yyyy,mm,dd=0. BEWARE! In Java a lead 0 on an integer implies OCTAL. * * @param yyyy -999,999 (BC) to +999,999 (AD) * @param mm month 1 to 12 (not 0 to 11 as in Sun's Date), no lead 0. * @param dd day 1 to 31, no lead 0. * * @throws NumberFormatException for invalid yyyy mm dd */ public BigDate( int yyyy, int mm, int dd ) { set( yyyy, mm, dd, CHECK ); } // end contructor /** * Construct a BigDate object given a Gregorian date yyyy, mm, dd; allows control of how invalid dates are handled. * A null date is yyyy,mm,dd=0. BEWARE! In Java a lead 0 on an integer implies OCTAL. * * @param yyyy -999,999 (BC) to +999,999 (AD) * @param mm month 1 to 12 (not 0 to 11 as in Sun's Date), no lead 0. * @param dd day 1 to 31, no lead 0. * @param how one of CHECK BYPASSCHECK NORMALIZE NORMALISE */ public BigDate( int yyyy, int mm, int dd, int how ) { // save yyyy, mm, dd, and compete the ordinal equivalent set( yyyy, mm, dd, how ); } // end contructor /** * How many days were there in the year prior to the first day of the given month? * * @param mm month 1 to 12 (not 0 to 11 as in Sun's Date), no lead 0. * @param leap true if you are interested in a leap year. * * @return how many days in year prior to the start of that month. */ protected static int daysInYearPriorToMonth( int mm, boolean leap ) { return leap ? LEAP_DAYS_IN_YEAR_PRIOR_TO_MONTH_TABLE[ mm - 1 ] : USUAL_DAYS_IN_YEAR_PRIOR_TO_MONTH_TABLE[ mm - 1 ]; } // /method /** * Convert day number ddd in year to month. * * @param ddd day number in year Jan 01 = 1, 1 to 366. * @param leap true if year of interest is boolean. * * @return month that day number would fall in. */ protected static int dddToMM( int ddd, boolean leap ) { return leap ? LEAP_DDD_TO_MM_TABLE[ ddd - 1 ] : USUAL_DDD_TO_MM_TABLE[ ddd - 1 ]; } // /method /** * Ordinal date of Jan 01 of the given year. * * @param yyyy year of interest * * @return ordinal of Jan 01 of that year. */ protected static int jan01OfYear( int yyyy ) { int leapAdjustment; if ( yyyy < 0 ) { // years -1 -5 -9 were leap years. // adjustment -1->1 -2->1 -3->1 -4->1 -5->2 -6->2 leapAdjustment = ( 3 - yyyy ) / 4; return ( yyyy * 365 ) - leapAdjustment + BC_EPOCH_ADJUSTMENT; } // years 4 8 12 were leap years // adjustment 1->0 2->0, 3->0, 4->0, 5->1, 6->1, 7->1, 8->1, 9->2 leapAdjustment = ( yyyy - 1 ) / 4; int missingDayAdjust = ( yyyy > GC_FIRST_YYYY ) ? MISSING_DAYS : 0; // mod 100 and mod 400 rules started in 1600 for Gregorian, // but 1800/2000 in the British scheme if ( yyyy > LEAP_100_RULE_YYYY ) { leapAdjustment -= ( yyyy - LEAP_100_RULE_YYYY + 99 ) / 100; } if ( yyyy > LEAP_400_RULE_YYYY ) { leapAdjustment += ( yyyy - LEAP_400_RULE_YYYY + 399 ) / 400; } return yyyy * 365 + leapAdjustment - missingDayAdjust + AD_EPOCH_ADJUSTMENT; } // /method /** * parse a string into yyyy mm dd, where date may have form: * yyyy-mm-dd * y-mm-ddBC * 2009-12-31 * 1234-5-31AD * 1970/3/3 AD * 3.01.12 bc * 2000_12_31 * It will not accept a date missing the year, month or day field. Spaces and commas are ignored. * It does does not check leap year validity, max days in month. * * @param yyyy_mm_dd date string. * * @return array[] yyyy, mm, dd, with yyyy negative for BC. null for unparseable date. */ private static int[] relaxedParse( final String yyyy_mm_dd ) { try { // A regex would be lot simpler, but we can't use it since we are constrained to JDK 1.1 // extract the year final StringBuilder yearSB = new StringBuilder( 6 ); final int yyyy_mm_dd_length = yyyy_mm_dd.length(); String mm_dd = ""; extractYearLoop: for ( int i = 0; i < yyyy_mm_dd_length; i++ ) { char c = yyyy_mm_dd.charAt( i ); switch ( c ) { case '-': case '.': case '/': case '_': // hit field terminator mm_dd = yyyy_mm_dd.substring( i + 1 ); break extractYearLoop; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': yearSB.append( c ); break; case ' ': case ',': // ignore spaces and commas break; default: // hit something that should not be there return null; } } if ( !( 1 <= yearSB.length() && yearSB.length() <= 6 ) ) { return null; } final int yyyy = Integer.parseInt( yearSB.toString() ); // extract the month final StringBuilder monthSB = new StringBuilder( 2 ); final int mm_dd_length = mm_dd.length(); String dd_ad = ""; extractMonthLoop: for ( int i = 0; i < mm_dd_length; i++ ) { char c = mm_dd.charAt( i ); switch ( c ) { case '-': case '.': case '/': case '_': // hit field terminator dd_ad = mm_dd.substring( i + 1 ); break extractMonthLoop; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': monthSB.append( c ); break; case ' ': case ',': // ignore spaces and commas break; default: // hit something that should not be there return null; } } if ( !( 1 <= monthSB.length() && monthSB.length() <= 2 ) ) { return null; } final int mm = Integer.parseInt( monthSB.toString() ); // extract the day final StringBuilder daySB = new StringBuilder( 2 ); final int dd_ad_length = dd_ad.length(); String ad = ""; extractDayLoop: for ( int i = 0; i < dd_ad_length; i++ ) { char c = dd_ad.charAt( i ); switch ( c ) { case 'a': case 'A': case 'b': case 'B': // hit AD/BC ad = dd_ad.substring( i ); break extractDayLoop; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': daySB.append( c ); break; case ' ': case ',': // ignore spaces and commas break; default: // hit something that should not be there return null; } } if ( !( 1 <= daySB.length() && daySB.length() <= 2 ) ) { return null; } final int dd = Integer.parseInt( daySB.toString() ); if ( !( 1 <= dd && dd <= 31 ) ) { return null; } // extract the ad/bc final StringBuilder adSB = new StringBuilder( 2 ); final int ad_length = ad.length(); // extract AD loop for ( int i = 0; i < ad_length; i++ ) { char c = ad.charAt( i ); switch ( c ) { case 'A': case 'B': case 'C': case 'D': // AD BC adSB.append( c ); break; case 'a': case 'b': case 'c': case 'd': // ad bc convert to AD BC adSB.append( Character.toUpperCase( c ) ); break; case ' ': case ',': // ignore spaces and commas break; default: // hit something that should not be there return null; } } final String adString = adSB.toString(); if ( adString.length() == 0 || adString.equals( "AD" ) ) { return new int[] { yyyy, mm, dd }; } else if ( adString.equals( "BC" ) ) { return new int[] { -yyyy, mm, dd }; } else { return null; } } catch ( NumberFormatException e ) { // should never happen since we have already filtered out any junk return null; } } // /method /** * Convert date in form YYYY MM DD into days since the epoch, leaving results internally. * * @see #getOrdinal() */ protected final void calcOrdinal() { ordinal = toOrdinal( yyyy, mm, dd ); } /** * Clean up an invalid date, leaving the results internally * e.g. 1954-09-31 -> 1954-10-01. * 1954-10-(minus 1) -> 1954-09-30 * 1954-13-01 -> 1955-01-01. This lets you do year, month or day * arithmetic. normalise does not recompute the ordinal. */ protected final void normalise() { // yyyy, mm, dd must be set at this point if ( isValid( yyyy, mm, dd ) ) { return; } else if ( mm > 12 ) { yyyy += ( mm - 1 ) / 12; mm = ( ( mm - 1 ) % 12 ) + 1; if ( isValid( yyyy, mm, dd ) ) { return; } } else if ( mm <= 0 ) { // Java's definition of modulus means we need to handle negatives as // a special case. yyyy -= -mm / 12 + 1; mm = 12 - ( -mm % 12 ); if ( isValid( yyyy, mm, dd ) ) { return; } } if ( isValid( yyyy, mm, 1 ) ) { final int oldDD = dd; // poss 0 or negative dd = 1; calcOrdinal(); ordinal += oldDD - 1; toGregorian(); if ( isValid( yyyy, mm, dd ) ) { return; } } throw new NumberFormatException( "date cannot be normalised: " + yyyy + "/" + mm + "/" + dd ); } // /method /** * read back a serialized BigDate object and reconstruct the missing transient fields. readObject leaves results in * this. It does not create a new object. * * @param s stream to read from. * * @throws IOException if can't read object * @throws ClassNotFoundException if unexpected class in the stream. */ private void readObject( ObjectInputStream s ) throws ClassNotFoundException, IOException { s.defaultReadObject(); try { toGregorian();// restore transient fields } catch ( NumberFormatException e ) { throw new java.io.IOException( "bad serialized BigDate" ); } } // /method /** * converts ordinal to YYYY MM DD, leaving results internally. */ protected final void toGregorian() { // ordinal must be set at this point. // handle the null date as a special case if ( ordinal == NULL_ORDINAL ) { yyyy = 0; mm = 0; dd = 0; return; } if ( ordinal > MAX_ORDINAL ) { throw new NumberFormatException( "invalid ordinal date: " + ordinal ); } else if ( ordinal >= ORDINAL_GC_FIRST ) { yyyy = LEAP_400_RULE_YYYY + flooredMulDiv( ordinal - ORDINAL_LEAP_RULE_400_START, 10000, 3652425 );/* 365 + 0.25 - 0.01 - 0.0025 */ /* * division may be done on a negative number. That's ok. We don't * need to mess with the 100RuleYear. The 400RuleYear handles it * all. */ } else if ( ordinal >= ORDINAL_0001_01_01AD ) { // ORDINAL_0001_01_01AD to Oct_04_1582 // year 4 was first AD leap year. yyyy = 4 + flooredMulDiv( ordinal - ORDINAL_0004_01_01AD, 100, 36525 ); /* 365 + 0.25 */ } else if ( ordinal >= MIN_ORDINAL ) { // LowestDate to Dec_31_0001BC // -1 was first BC leap year. // dividend will be negative yyyy = -1 + flooredMulDiv( ordinal - ORDINAL_0001_01_01BC, 100, 36525 ); /* 365 + 0.25 */ } else { throw new NumberFormatException( "invalid ordinal date: " + ordinal ); } int aim = ordinal + 1; // Oct_15_1582 <= ordinal && ordinal <= Dec_31_1582 if ( ORDINAL_GC_FIRST <= ordinal && ordinal <= ORDINAL_GC_FIRST_DEC_31 ) { aim += MISSING_DAYS; } // ddd should be 1..366 int ddd = aim - jan01OfYear( yyyy ); while ( ddd <= 0 ) { // our approximation was too high yyyy--; ddd = aim - jan01OfYear( yyyy ); } boolean leap = isLeap( yyyy ); while ( ddd > ( leap ? 366 : 365 ) ) { // our approximation was too low yyyy++; ddd = aim - jan01OfYear( yyyy ); leap = isLeap( yyyy ); } mm = dddToMM( ddd, leap ); dd = ddd - daysInYearPriorToMonth( mm, leap ); // at this point yyyy, mm and dd have been computed. } // /method /** * s a BigDate object initialised to today's UTC (Greenwich GMT) date, in other words the date and time in * Greenwich England right now without any summer time correction. It works even if Java's default TimeZone is not * configured correctly, but it requires your system clock accurately set to UTC time. Experiment setting your * system date/time to various values and making sure you are getting the expected results. Note the date in the * created object does not keep updating every time you reference it with methods like getOrdinal or getDD. You * always get the date the object was created. * * @return BigDate object initialised to today, in Greenwich. You may modify it further. * @see #localToday * @see #today */ public static BigDate UTCToday() { // 86,400,000 = 1000 * 60 * 60 * 24 = milliseconds per day return new BigDate( ( int ) ( System.currentTimeMillis() / 86400000L ) ); } // /method /** * calculate the age in years, months and days. To compute elapsed time between two dates, use the first as the * birthDate and the second as the asOf. DeathDate for asOf gives age at time of death. * Today for asOf gives age today. * * @param birthDate usually the birth of a person. * @param asOf usually today, the day you want the age as of. asOf must come after birthDate to get a * meaningful result. Usually asOf > birthDate. * Difference is always positive no matter if asOf is > or < birthDate * * @return array of three ints (not Integers). [0]=age in years, [1]=age in months, [2]=age in days. * @see #localToday * @see #today * @see #UTCToday * @see #isAnniversary */ public static int[] age( BigDate birthDate, BigDate asOf ) { if ( birthDate.ordinal >= asOf.ordinal ) { if ( birthDate.ordinal == asOf.ordinal ) { return new int[] { 0, 0, 0 }; } else { // age to date in future, exchange dates so difference positive final BigDate temp = asOf; asOf = birthDate; birthDate = temp; } } int birthYYYY = birthDate.yyyy; int birthMM = birthDate.mm; int birthDD = birthDate.dd; int asOfYYYY = asOf.yyyy; int asOfMM = asOf.mm; int asOfDD = asOf.dd; int ageInYears = asOfYYYY - birthYYYY; int ageInMonths = asOfMM - birthMM; int ageInDays = asOfDD - birthDD; if ( ageInDays < 0 ) { // This does not need to be a while loop because // birthDD is always less than daysInbirthMM month. // Guaranteed after this single treatment, ageInDays will be >= 0. // i.e. ageInDays = asOfDD - birthDD + daysInBirthMM. ageInDays += BigDate.daysInMonth( birthMM, birthYYYY ); ageInMonths--; } if ( ageInMonths < 0 ) { ageInMonths += 12; ageInYears--; } if ( birthYYYY < 0 && asOfYYYY > 0 ) { ageInYears--; } if ( ageInYears < 0 ) { ageInYears = 0; ageInMonths = 0; ageInDays = 0; } int[] result = new int[ 3 ]; result[ 0 ] = ageInYears; result[ 1 ] = ageInMonths; result[ 2 ] = ageInDays; return result; } // /method /** * Get day of week for given ordinal. It is one-based starting with Sunday. * * @param ordinal days since 1970-01-01 to test. * * @return day of week 1=Sunday 2=Monday 3=Tuesday 4=Wednesday 5=Thursday 6=Friday 7=Saturday Compatible with Sun's * 1=Calendar.SUNDAY Not compatible with BigDate.getDayOfWeek. * @see #isoDayOfWeek * @see #dayOfWeek * @see #getCalendarDayOfWeek */ public static int calendarDayOfWeek( int ordinal ) { return dayOfWeek( ordinal ) + 1; } // /method /** * Compare two dates in yyyy mm dd form. * * @param yyyy1 first year * @param mm1 first month 1..12 * @param dd1 first day 1..31 * @param yyyy2 second year * @param mm2 second month 1..12 * @param dd2 second day 1..31 * * @return +ve if date1 > date2, -ve if date1 < date2 and 0 if date1 == date2 * @see #compareTo(Object) */ public static int compare( int yyyy1, int mm1, int dd1, int yyyy2, int mm2, int dd2 ) { int diff = yyyy1 - yyyy2; if ( diff != 0 ) { return diff; } diff = mm1 - mm2; if ( diff != 0 ) { return diff; } return dd1 - dd2; } // /method /** * Get 3-char abbreviation of a given day of the week * * @param dayOfWeek sunday = 0 * * @return abbreviation for day of week, e.g. "sun", all lower case. */ public static String dayAbbr( int dayOfWeek ) { return DAY_ABBRS[ dayOfWeek ]; } /** * Get full name of given day of the week * * @param dayOfWeek sunday = 0 * * @return name of day of week e.g. "Sunday" */ public static String dayName( int dayOfWeek ) { return DAY_NAMES[ dayOfWeek ]; } // /method /** * Get day of week for given ordinal. Is it zero-based starting with Sunday. * * @param ordinal days since 1970-01-01 to test. * * @return day of week 0=Sunday 1=Monday 2=Tuesday 3=Wednesday 4=Thursday 5=Friday 6=Saturday WARNING: not * compatible with 1=Calendar.SUNDAY * @see #calendarDayOfWeek * @see #isoDayOfWeek * @see #getDayOfWeek */ public static int dayOfWeek( int ordinal ) { // modulus in Java is "broken" for negative numbers // so we adjust to make the dividend positive. // By "broken" I mean the official rules for what // is supposed to happen for negative dividends // won't give the desired result in this case. // See modulus in the Java glossary for more details. return ( ordinal == NULL_ORDINAL ) ? 0 : ( ( ordinal + SUNDAY_IS_ZERO_ADJUSTMENT - MIN_ORDINAL ) % 7 ); } // /method /** * How many days are there in a given month? * * @param mm month 1 to 12 (not 0 to 11 as in Sun's Date), no lead 0. * @param leap true if you are interested in a leap year * * @return how many days are in that month */ public static int daysInMonth( int mm, boolean leap ) { if ( mm != 2 ) { return USUAL_DAYS_PER_MONTH_TABLE[ mm - 1 ]; } else { return leap ? 29 : 28; } } // /method /** * How many days are there in a given month? * * @param mm month 1 to 12 (not 0 to 11 as in Sun's Date), no lead 0, or you will get octal. * @param yyyy year of interest. * * @return how many days are in that month */ public static int daysInMonth( int mm, int yyyy ) { if ( mm != 2 ) { return USUAL_DAYS_PER_MONTH_TABLE[ mm - 1 ]; } else { return isLeap( yyyy ) ? 29 : 28; } } // /method /** * Multiply then divide using floored rather than the usual truncated arithmetic, using a long intermediate. * * @param multiplicand one of two numbers to multiply together * @param multiplier one of two numbers to multiply together * @param divisor number to divide by * * @return (multiplicand times multiplier) / divisor */ public static int flooredMulDiv( int multiplicand, int multiplier, int divisor ) { long result = ( long ) multiplicand * ( long ) multiplier; if ( result >= 0 ) { return ( int ) ( result / divisor ); } else { return ( int ) ( ( result - divisor + 1 ) / divisor ); } } // /method /** * Embeds copyright notice * * @return copyright notice * @noinspection SameReturnValue */ public static String getCopyright() { return "BigDate 5.1 freeware Copyright: (c) 1997-2017 Roedy Green, Canadian Mind Products, " + "http://mindprod.com roedyg@mindprod.com"; } // /method /** * Determine if this date is someone's birthdate, or the anniversary of their death, or the anniversary * of a marriage etc. * For example boolean todayIsYourBirthday = BigDate.isAnniversary( born, BigDate.localToday() ); * boolean todayIsYourWeddingAnniversary = BigDate.isAnniversary( weddingDate, BigDate.localToday() ); * boolean todayAnniversaryDeathOfBach = BigDate.isAnniversary( new BigDate("1750-07-28"), BigDate.localToday()); *

* To compute relative anniversary between two dates, use the first as the * birthDate and the second as the asof. *

* February 29 is treated specially, to handle the way people people born on Feb 29 celebrate their birth days on * Feb 28 * in non-leap years. If the asOf year is not a leap year then Feb 28 is considered a match * for Feb 29. However, if the asOf date is Feb 29 then Feb 28 in the birth year is not considered a match. * * @param birthDate usually the birth of a person, or possibly a date. * @param asOf usually today, the day you want the to know if it is an anniversary. asof must come after * birthDate to get a * meaningful result. * * @return true if asOf date in as even number of years after the birthdate. If the asOf date is <= birthDate it * will * return false. * @see #age */ public static boolean isAnniversary( BigDate birthDate, BigDate asOf ) { if ( asOf.compareTo( birthDate ) < 0 ) { return false; } int asOfMM = asOf.mm; int birthMM = birthDate.mm; if ( birthMM != asOfMM ) { return false; } int birthDD = birthDate.dd; int asOfDD = asOf.dd; if ( birthDD == asOfDD ) { return true; } // at this point, months matched, but days did not. // celebrate feb 29 birthday on feb 28 in non leap year. return birthDD == 29 && asOfDD == 28 && birthMM == 2 /* && asOfMM == 2 */ && !isLeap( asOf.yyyy ); } // /method /** * Is the given year a leap year, considering history, mod 100 and mod 400 rules? By 1582, this excess of leap years * had built up noticeably. At the suggestion of astronomers Luigi Lilio and Christopher Clavius, Pope Gregory XIII * dropped 10 days from the calendar. Thursday 1582 October 4 Julian was followed immediately by Friday 1582 October * 15 Gregorian. He decreed that every 100 years, a leap year should be dropped except that every 400 years the leap * year should be restored. Only Italy, Poland, Portugual and Spain went along with the new calendar immediately. * One by one other countries adopted it in different years. Britain and its territories (including the USA and * Canada) adopted it in 1752. By then, 11 days had to be dropped. 1752-09-02 was followed immediately by * 1752-09-14. * The Gregorian calendar is the most widely used scheme. This is the scheme endorsed by the US Naval * observatory. It corrects the year to 365.2425. It gets ahead 1 day every 3289 years. For BC dates, the years the * years 1, 5, 9 are leap years, not 4, 8, 12 as you might expect, from the general rule. * * @param yyyy year to test. * * @return true if the year is a leap year. * @noinspection SimplifiableIfStatement */ public static boolean isLeap( int yyyy ) { // if you change this code, make sure you make corresponding changes to // jan01OfYear, toGregorian, MONDAY_IS_ZERO_ADJUSTMENT, // SUNDAY_IS_ZERO_ADJUSTMENT, // AD_EPOCH_ADJUSTMENT and BC_EPOCH_ADJUSTMENT // yyyy & 3 is a fast way of saying yyyy % 4 if ( yyyy < LEAP_100_RULE_YYYY ) { if ( yyyy < 0 ) { return ( ( yyyy + 1 ) & 3 ) == 0; } else { return ( yyyy & 3 ) == 0; } } if ( ( yyyy & 3 ) != 0 ) { return false; } if ( yyyy % 100 != 0 ) { return true; } if ( yyyy < LEAP_400_RULE_YYYY ) { return false; } return yyyy % 400 == 0; } // /method /** * Test to see if the given yyyy-mm-dd is a date as a String is legitimate. must have 4-digit years, and use dashes * between the number and no sign Does extensive checks considering leap years, missing days etc. * * @param yyyy_mm_dd string of form "yyyy-mm-dd". * yyyy-mm-dd * y-mm-ddBC * 2009-12-31 * 1234-5-31AD * 1970/3/3 AD * 3.01.12 bc * 2000_12_31 * It will not accept a date missing the year, month or day field. Spaces and commas are ignored. * * @return true if that represents a valid date. */ public static boolean isValid( String yyyy_mm_dd ) { final int[] ymd = relaxedParse( yyyy_mm_dd ); return ymd != null && BigDate.isValid( ymd[ 0 ], ymd[ 1 ], ymd[ 2 ] ); } // /method /** * Test to see if the given yyyy, mm, dd date is legitimate. Does extensive checks considering leap years, missing * days etc. * * @param yyyy -999,999 (BC) to +999,999 (AD) * @param mm month 1 to 12 (not 0 to 11 as in Sun's Date), no lead 0. * @param dd day 1 to 31, no lead 0. * * @return true if yyyy mm dd is a valid date. * @noinspection SimplifiableIfStatement */ public static boolean isValid( int yyyy, int mm, int dd ) { // null date 0000 00 00 is considered valid // but otherwise year 0000 never happened. if ( yyyy == 0 ) { return ( mm == 0 ) && ( dd == 0 ); } if ( ( yyyy < MIN_YEAR ) || ( yyyy > MAX_YEAR ) || ( mm < 1 ) || ( mm > 12 ) || ( dd < 1 ) || ( dd > 31 ) ) { return false; } // account for missing 10 days in 1582. // Thursday 1582 October 4 Julian was followed // immediately by Friday 1582 October 15 // Similarly for the British Calendar if ( yyyy == OJC_LAST_YYYY && mm == OJC_LAST_MM && OJC_LAST_DD < dd && dd < GC_FIRST_DD ) { return false; } return dd <= daysInMonth( mm, yyyy ); } // /method /** * Get day of week 1 to 7 for this ordinal according to the ISO standard IS-8601. It is one-based starting with * Monday. * * @param ordinal days since 1970-01-01 to test. * * @return day of week 1=Monday to 7=Sunday, 0 for null date. WARNING: not compatible with 1=Calendar.SUNDAY. * @see BigDate#dayOfWeek(int) */ public static int isoDayOfWeek( int ordinal ) { // modulus in Java is "broken" for negative numbers // so we adjust to make the dividend positive. // By "broken" I mean the official rules for what // is supposed to happen for negative dividends // won't give the desired result in this case. // See modulus in the Java glossary for more details. return ( ordinal == NULL_ORDINAL ) ? 0 : ( ( ordinal + MONDAY_IS_ZERO_ADJUSTMENT - MIN_ORDINAL ) % 7 ) + 1; } // /method /** * Returns a BigDate object initialised to today's local date. It depends on Java's default TimeZone being * configured, and your system clock accurately set to UTC time. Experiment setting your system date/time to various * values and making sure you are getting the expected results. Note the date in the created object does not keep * updating every time you reference it with methods like getOrdinal or getDD. You always get the date the object * was created. It is quite a production to get the local date. Best to ask once and save the today object. * * @return BigDate object initialised to today, local time. You may modify it further. * @see #today * @see #UTCToday * @see #isCelebrating */ public static BigDate localToday() { return today( TimeZone.getDefault() ); } // /method /** * Get 3-char abbreviation of a given month of the year. * * @param mm month number jan = 1 * * @return abbreviation for month, e.g. "Jan" */ public static String monthAbbr( int mm ) { return MONTH_ABBRS[ mm ]; } // /method /** * Get full name of a given month of the year. * * @param mm month number jan = 1 * * @return name of month e.g. "January" */ public static String monthName( int mm ) { return MONTH_NAMES[ mm ]; } // /method /** * Find the first monday in a given month, the 3rd monday or the last Thursday... * * @param which 1=first 2=second 3=third 4=fourth 5=last (might be 4th or 5th) * @param dayOfWeek 0=Sunday 1=Monday 2=Tuesday 3=Wednesday 4=Thursday 5=Friday 6=Saturday WARNING: not compatible * with 1=Calendar.SUNDAY. * @param yyyy year of interest. * @param mm month 1 to 12 (not 0 to 11 as in Sun's Date), no lead 0. * * @return day of month 1..31 */ public static int nthXXXDay( int which, int dayOfWeek, int yyyy, int mm ) { int dayOfWeekOf1st = BigDate .dayOfWeek( BigDate.toOrdinal( yyyy, mm, 1 ) ); int dayOfMonthOfFirstDesiredDay = ( dayOfWeek - dayOfWeekOf1st + 7 ) % 7 + 1; int dayOfMonthOfNthDesiredDay = dayOfMonthOfFirstDesiredDay + ( which - 1 ) * 7; if ( which >= 5 && dayOfMonthOfNthDesiredDay > daysInMonth( mm, yyyy ) ) { dayOfMonthOfNthDesiredDay -= 7; } return dayOfMonthOfNthDesiredDay; } // /method /** * Find the first monday in a given month, the 3rd monday or the last Thursday... * * @param which 1=first 2=second 3=third 4=fourth 5=last (might be 4th or 5th) * @param dayOfWeek 0=Sunday 1=Monday 2=Tuesday 3=Wednesday 4=Thursday 5=Friday 6=Saturday WARNING: not compatible * with 1=Calendar.SUNDAY. * @param yyyy year of interest. * @param mm month 1 to 12 (not 0 to 11 as in Sun's Date), no lead 0. * * @return day of month 1..31 */ public static int ordinalOfnthXXXDay( int which, int dayOfWeek, int yyyy, int mm ) { int dayOfMonthOfNthDesiredDay = nthXXXDay( which, dayOfWeek, yyyy, mm ); return BigDate.toOrdinal( yyyy, mm, dayOfMonthOfNthDesiredDay ); } // /method /** * parse a string into yyyy which may have form: * 1948 * 230BC * 1234AD * It does not allow a space before the BC or AD. * * @param yyyyString date string. * * @return yyyy, negative for BC. * @throws NumberFormatException id the string has non-numeric chars. */ public static int parseYYYY( String yyyyString ) throws NumberFormatException { int ye = yyyyString.length(); boolean bc = false; if ( yyyyString.endsWith( "BC" ) ) { ye -= 2; bc = true; } else if ( yyyyString.endsWith( "AD" ) ) { ye -= 2; } int yyyy = Integer.parseInt( yyyyString.substring( 0, ye ) ); if ( bc ) { yyyy = -yyyy; } return yyyy; } // /method /** * Parse a, possibly incomplete, date String of form yyyy-yy-dd or yyyy-mm or yyyy into a BigDate * Also handles AD/BC, 1-digit months, 1-digit days and 1, 2 or 3-digit years. Date must be a valid normalised * date. * * @param incomplete Date string, usually of form YYYY-MM-DD, possibly just YYYY-MM or YYYY. * * @return BigDate first of year for YYYY or first of month for YYYY-MM, or precise date for YYYY-MM-DD * @see #BigDate(String) */ public static BigDate parseYYYYmmdd( String incomplete ) { final String complete; // count dashes in string switch ( ST.countInstances( incomplete, '-' ) ) { case 2: // date already yyyy-mm-dd complete = incomplete; break; case 1: // was yyyy-mm if ( incomplete.endsWith( "BC" ) || incomplete.endsWith( "AD" ) ) { complete = incomplete.substring( 0, incomplete.length() - 2 ) + "-01" + incomplete.substring( incomplete.length() - 2 ); } else { complete = incomplete + "-01"; } break; case 0: // was yyyy if ( incomplete.endsWith( "BC" ) || incomplete.endsWith( "AD" ) ) { complete = incomplete.substring( 0, incomplete.length() - 2 ) + "-01-01" + incomplete.substring( incomplete.length() - 2 ); } else { complete = incomplete + "-01-01"; } break; default: throw new NumberFormatException( "BigDate.parseYYYYmmdd expects YYYY-MM-DD or YYYY-MM or YYYY." ); } return new BigDate( complete ); } // /method /** * Convert ISO date string in form YYYY-MM-DD into days since the 1970-01-01. This method lets you convert * directly from * Gregorian to ordinal without creating a BigDate object. yyyy-mm-dd must be a valid date. * * @param yyyy_mm_dd iso date string. * * @return ordinal, days since 1970-01-01 */ public static int toOrdinal( String yyyy_mm_dd ) { final int[] ymd = relaxedParse( yyyy_mm_dd ); if ( ymd == null ) { throw new NumberFormatException( "unparseable date: " + yyyy_mm_dd ); } return toOrdinal( ymd[ 0 ], ymd[ 1 ], ymd[ 2 ] ); } // /method /** * Convert date in form YYYY MM DD into days since the 1970-01-01. This method lets you convert directly from * Gregorian to ordinal without creating a BigDate object. yyyy mm dd must be a valid date. * * @param yyyy -999,999 (BC) to +999,999 (AD) * @param mm month 1 to 12 (not 0 to 11 as in Sun's Date), no lead 0. * @param dd day 1 to 31, no lead 0. * * @return ordinal, days since 1970-01-01. */ public static int toOrdinal( int yyyy, int mm, int dd ) { // treat null date as a special case if ( ( yyyy == 0 ) && ( mm == 0 ) && ( dd == 0 ) ) { return NULL_ORDINAL; } // jan01OfYear handles missing day adjustment for years > 1582 // We only need to handle year = 1582 here. int missingDayAdjust = ( yyyy == OJC_LAST_YYYY && ( ( mm == OJC_LAST_MM && dd > OJC_LAST_DD ) || mm > OJC_LAST_MM ) ) ? MISSING_DAYS : 0; return jan01OfYear( yyyy ) + daysInYearPriorToMonth( mm, isLeap( yyyy ) ) - missingDayAdjust + dd - 1; } // /method /** * Returns a BigDate object initialised to the date right now in the given TimeZone. It depends on your system clock * accurately set to UTC time. Experiment setting your system date/time to various values and making sure you are * getting the expected results. Note the date in the created object does not keep updating every time you reference * it with methods like getOrdinal or getDD. You always get the date the object was created. It is quite a * production to get the this date. Best to ask once and save the today object. * * @param timeZone in which we want to know the today's date. * * @return new BigDate object initialised to today at given TimeZone. You may modify it further. * @see #localToday * @see #UTCToday */ public static BigDate today( TimeZone timeZone ) { BigDate d = new BigDate(); d.setDateAtTime( System.currentTimeMillis(), timeZone ); return d; } // /method /** * increment this date by a number of days. * * @param days positive or negative, -1 gets day before this one. * * @return this the original modified BigDate object. */ public final BigDate addDays( int days ) { if ( days == 0 ) { return this; } this.ordinal += days; toGregorian(); return this; } // /method /** * Get day of week for this BigDate. It is one-based starting with Sunday. * * @return day of week 1=Sunday 2=Monday 3=Tuesday 4=Wednesday 5=Thursday 6=Friday 7=Saturday Compatible with Sun's * 1=Calendar.SUNDAY Not compatible with BigDate.getDayOfWeek. * @see #isoDayOfWeek * @see #dayOfWeek * @see #getCalendarDayOfWeek */ public int calendarDayOfWeek() { return calendarDayOfWeek( this.ordinal ); } /** * standard clone. * * @return Because this is JDK 1.1 we return Object, not BigDate. You must cast. */ public Object clone() { try { return super.clone(); } catch ( CloneNotSupportedException e ) { return null; } } // /method /** * Compare BigDates in ascending date order. * Defines default the sort order for BigDate Objects. * Compare this BigDate with another BigDate. * Compares ordinal. * Informally, returns (this-other) or +ve if this is more positive than other. * * @param other other BigDate to compare with this one * * @return +ve if this>other, 0 if this==other, -ve if this<other * @see #compare(int, int, int, int, int, int) */ public final int compareTo( @NotNull Object other ) { // Integer.compare only available in 1.7 return this.ordinal - ( ( BigDate ) other ).ordinal; } // /method /** * Get day of week for this BigDate. Is it zero-based starting with Sunday. * * @return day of week 0=Sunday 1=Monday 2=Tuesday 3=Wednesday 4=Thursday 5=Friday 6=Saturday WARNING: not * compatible with 1=Calendar.SUNDAY * @see #calendarDayOfWeek * @see #isoDayOfWeek * @see #getDayOfWeek */ public int dayOfWeek() { return dayOfWeek( this.ordinal ); } /** * Compares with another BigDate to see if they refer to the same date. * * @param d other BigDate to compare with this one. * * @return true if BigDate d refers to the same date */ public final boolean equals( Object d ) { return d == this || d instanceof BigDate && ( ordinal == ( ( BigDate ) d ).ordinal ); } // /method /** * Get day of week for this BigDate. Is it one-based starting with Sunday. * * @return day of week 1=Sunday 2=Monday 3=Tuesday 4=Wednesday 5=Thursday 6=Friday 7=Saturday Compatible with Sun's * 1=Calendar.SUNDAY Not compatible with BigDate.getDayOfWee * @see #getISODayOfWeek * @see #getDayOfWeek * @see #calendarDayOfWeek */ public final int getCalendarDayOfWeek() { return calendarDayOfWeek( ordinal ); } /** * get day of month for this BigDate. * * @return day 1 to 31, 0 for null date. */ public final int getDD() { return dd; } // /method /** * Get day number in the year for this BigDate. * * @return day number Jan 01 = 1, 1 to 366 */ public final int getDDD() { return ( ordinal == NULL_ORDINAL ) ? 0 : ( ordinal - jan01OfYear( yyyy ) + 1 ); } // /method /** * Get java.util.Date object corresponding to this BigDate, * * @param timeZone We consider this BigDate to have an implied time of 0:00 in this timeZone. * * @return Date or null Result is undefined if BigDate is outside the range handled by Date. * @see #getLocalTimeStamp * @see #getUTCTimeStamp * @see #getTimeStamp * @see #getUTCDate * @see #getLocalDate */ public final java.util.Date getDate( TimeZone timeZone ) { return ordinal == NULL_ORDINAL ? null : new java.util.Date( getTimeStamp( timeZone ) ); } // /method /** * Get day of week for this BigDate. It is zero-based starting with Sunday. * * @return day of week 0=Sunday 1=Monday 2=Tuesday 3=Wednesday 4=Thursday 5=Friday 6=Saturday WARNING: not * compatible with 1=Calendar.SUNDAY * @see #getCalendarDayOfWeek * @see #getISODayOfWeek * @see #dayOfWeek */ public final int getDayOfWeek() { return dayOfWeek( ordinal ); } // /method /** * Get day of week 1 to 7 for this BigDate according to the ISO standard IS-8601. It is one-based starting with * Monday. * * @return day of week 1=Monday to 7=Sunday, 0 for null date. WARNING: not compatible with 1=Calendar.SUNDAY * @see #getCalendarDayOfWeek * @see #getDayOfWeek * @see #isoDayOfWeek */ public final int getISODayOfWeek() { return isoDayOfWeek( ordinal ); } // /method /** * Get week number 1 to 53 of the year this date falls in, according to the rules of ISO standard IS-8601. A week * that lies partly in one year and partly in another is assigned a number in the year in which most of its days * lie. This means that week 1 of any year is the week that contains 4 January, or equivalently week 1 of any year * is the week that contains the first Thursday in January. Most years have 52 weeks, but years that start on a * Thursday and leap years that start on a Wednesday have 53 weeks. Jan 1 may well be in week 53 of the previous * year! Only defined for dates on or after 1600 Jan 01. You can find out how many ISO weeks there are per year with * new BigDate( year, 12, 31).getISOWeekNumber(); * * @return week number 1..53, 0 for null or invalid date. * @see Calendar FAQ */ public final int getISOWeekNumber() { if ( ordinal < ORDINAL_LEAP_RULE_100_START ) { return 0; } int jan04Ordinal = jan01OfYear( yyyy ) + 3; int jan04DayOfWeek = ( jan04Ordinal + MONDAY_IS_ZERO_ADJUSTMENT - MIN_ORDINAL ) % 7;// 0=Monday // 6=Sunday int week1StartOrdinal = jan04Ordinal - jan04DayOfWeek; if ( ordinal < week1StartOrdinal ) {// we are part of the previous year. Don't worry about year 0. jan04Ordinal = jan01OfYear( yyyy - 1 ) + 3; jan04DayOfWeek = ( jan04Ordinal + MONDAY_IS_ZERO_ADJUSTMENT - MIN_ORDINAL ) % 7;// 0=Monday // 6=Sunday week1StartOrdinal = jan04Ordinal - jan04DayOfWeek; } else if ( mm == 12 ) {// see if we are part of next year. Don't worry about year 0. jan04Ordinal = jan01OfYear( yyyy + 1 ) + 3; jan04DayOfWeek = ( jan04Ordinal + MONDAY_IS_ZERO_ADJUSTMENT - MIN_ORDINAL ) % 7;// 0=Monday // 6=Sunday int week1StartNextOrdinal = jan04Ordinal - jan04DayOfWeek; if ( ordinal >= week1StartNextOrdinal ) { week1StartOrdinal = week1StartNextOrdinal; } } return ( ( ordinal - week1StartOrdinal ) / 7 ) + 1; } // /method /** * Get java.util.Date object corresponding to this BigDate. We consider this BigDate to have an implied time of 0:00 * local time. * * @return Date or null Result is undefined if BigDate is outside the range handled by Date. * @see #getLocalTimeStamp */ public final java.util.Date getLocalDate() { return ordinal == NULL_ORDINAL ? null : new java.util.Date( getLocalTimeStamp() ); } // /method /** * Get milliseconds since 1970-01-01 00:00 GMT for this BigDate. Does not account for leap seconds primarily * because we do not know them in advance. N.B. returns long, not int as in many Unix implementations. This the long * that a Sun Date constructor wants. We consider this BigDate to have an implied time of 0:00 local time. * * @return milliseconds since 1970-01-01 00:00 GMT, or NULL_TIMESTAMP. This is NOT a JDBC Timestamp! You can use * this timestamp with java.util.Date.setTime, java.sql.TimeStamp.setTime or java.sql.Date.setTime or in the * constructors. To interconvert, just cast. * @see #getLocalDate */ public final long getLocalTimeStamp() { return getTimeStamp( TimeZone.getDefault() ); } // /method /** * Get month of year for this BigDate. * * @return month 1 to 12, 0 for null date. */ public final int getMM() { return mm; } // /method /** * get days since 1970-01-01 for this BigDate. 1970-01-01 = day 0. * * @return days since 1970-01-01. This is NOT what you want for the java.util.Date constructor. * @see #getLocalTimeStamp * @see #getUTCTimeStamp * @see #getTimeStamp */ public final int getOrdinal() { return ordinal; } // /method /** * Set the ordinal field, and compute the equivalent internal Gregorian yyyy mm dd fields. alias set. * * @param ordinal days since 1970-01-01. */ public final void setOrdinal( int ordinal ) { if ( this.ordinal == ordinal ) { return; } this.ordinal = ordinal; toGregorian(); } // /method /** * @return Julian day number of noon of the date BigDate represents. See notes on the Julian Proleptic calendar * under the constructor. */ public final double getProlepticJulianDay() { if ( ordinal == NULL_ORDINAL ) { return Double.MIN_VALUE; } return ordinal + 2440588L/* diff in base epochs of calendars */ /* no need for noon adjust */; } // /method /** * Get season of year for this BigDate. * * @return 0=spring (Mar, Apr, May) 1=summer (Jun, Jul, Aug) 2=fall (Sep, Oct, Dec) 3=winter Dec, Jan, Feb */ public final int getSeason() { // 1 > 3, 2 > 3, 3 > 0, 4 > 0, 5 > 0, 6 > 1, 7 > 1, 8 > 1, 9 > 2, 10 > // 2, 11 > 2, 12 > 3 return ( ( mm + ( 12 - 3 ) ) % 12 ) / 3; } // /method /** * Get milliseconds since 1970-01-01 00:00 GMT for this BigDate, as at the start of day 0:00. Does not account for * leap seconds primarily because we do not know them in advance. N.B. returns long, not int as in many Unix * implementations. This the long that a Sun Date constructor wants. * * @param timeZone We consider this BigDate to have an implied time of 0:00 in this timeZone. * * @return milliseconds since 1970-01-01 00:00 GMT, or NULL_TIMESTAMP. This is NOT a JDBC Timestamp! You can use * this timestamp with java.util.Date.setTime, java.sql.TimeStamp.setTime or java.sql.Date.setTime or in the * constructors. To interconvert, just cast. * @see #getDate * @see #getUTCTimeStamp * @see #getLocalTimeStamp */ public final long getTimeStamp( TimeZone timeZone ) { if ( ordinal == NULL_ORDINAL ) { return NULL_TIMESTAMP; } /* * Gets the time zone offset, for current date, modified in case of * daylight savings. This is the offset to add *to* UTC to get local * time. Places east of Greenwich have a positive offset. :era the era * of the given date, AD = 1 BC = 0 :year the year in the given date. * Docs are unclear if 1900=0 or 1900. Does not use year in calcs * anyway. :month the month in the given date. Month is 0-based. e.g., 0 * for January. :day the day-in-month of the given date. :dayOfWeek the * day-of-week of the given date. 1=Calendar.SUNDAY. :milliseconds the * millis in day in standard local time. :return the offset * in millis to add *to* GMT to get local time. */ int offsetInMillis = timeZone.getOffset( ordinal >= ORDINAL_0001_01_01AD ? 1 : 0, yyyy, mm - 1, dd, getCalendarDayOfWeek(), 0 ); // 86,400,000 = 1000 * 60 * 60 * 24 = milliseconds per day return ordinal * 86400000L - offsetInMillis; } // /method /** * Get java.util.Date object corresponding to this BigDate. We consider this BigDate to have an implied time of 0:00 * UTC (Greenwich GMT). * * @return Date or null Result is undefined if BigDate is outside the range handled by Date. * @see #getUTCTimeStamp */ public final java.util.Date getUTCDate() { return ordinal == NULL_ORDINAL ? null : new java.util.Date( getUTCTimeStamp() ); } // /method /** * Get milliseconds since 1970-01-01 00:00 GMT for this BigDate. Does not account for leap seconds primarily * because we do not know them in advance. N.B. returns long, not int as in many Unix implementations. This the long * that a Sun Date constructor wants. We consider this BigDate to have an implied time of 0:00 UTC (Greenwich GMT). * * @return milliseconds since 1970-01-01 00:00 GMT, or NULL_TIMESTAMP. This is NOT a JDBC Timestamp! You can use * this timestamp with java.util.Date.setTime, java.sql.TimeStamp.setTime or java.sql.Date.setTime or in the * constructors. To interconvert, just cast. * @see #getUTCDate */ public final long getUTCTimeStamp() { // 86,400,000 = 1000 * 60 * 60 * 24 = milliseconds per day return ordinal == NULL_ORDINAL ? NULL_TIMESTAMP : ( ordinal * 86400000L ); } // /method /** * Get week number 1 to 53 of the year this date falls in. This does NOT follow the rules of ISO standard IS-8601 * section 5.5. Week 1 is the first week with any days in the current year. Weeks start on Sunday. Jan 1 and Dec 31 * are always considered part of the current year. Only defined for dates on or after 1600 Jan 01. * * @return week number 1..53, 0 for null or invalid date. * @see #getISOWeekNumber */ public final int getWeekNumber() { if ( ordinal < ORDINAL_LEAP_RULE_100_START ) { return 0; } int jan01 = jan01OfYear( yyyy ); int jan01DayOfWeek = dayOfWeek( jan01 );// 0=Sunday 1=Monday // 2=Tuesday 3=Wednesday // 4=Thursday 5=Friday // 6=Saturday int sundayOnOrBeforeJan01Ordinal = jan01 - jan01DayOfWeek; return ( ( ordinal - sundayOnOrBeforeJan01Ordinal ) / 7 ) + 1; } // /method /** * Get year for this BigDate. * * @return year -999,999 to 999,999. 0 for null date. negative is BC, positive AD. */ public final int getYYYY() { return yyyy; } // /method /** * hashCode for use in Hashtable/HashMap lookup * * @return the ordinal which is perfectly unique for the date. */ public final int hashCode() { return ordinal; } // /method /** * If this BigDate represents the date a holiday occurs, usually in this year, * it tells you if today is that holiday. You can allow some slop to include * before days before and after days after. If you want it it to return true * only when it is the exact holiday, set them both to 0 or use * compareTo ( BigDate.localToday() ) * * @param before how may days prior to include as celebrating. * @param after how many days after ot include as praying. * * @return true today we are celebrating that holiday . * @see #localToday */ public boolean isCelebrating( int before, int after ) { final int today = localToday().getOrdinal(); return this.ordinal - before <= today && today <= this.ordinal + after; } // /method /** * is this a null BigDate? */ public final boolean isNull() { return ordinal == NULL_ORDINAL; } /** * Find the BigDate with date closest to this one with the given day of week. * * @param dayOfWeek 0=Sunday 1=Monday 2=Tuesday 3=Wednesday 4=Thursday 5=Friday 6=Saturday WARNING: not compatible * with 1=Calendar.SUNDAY. * * @return BigDate object with nearest date to this one with the given day of week. */ public BigDate nearestXXXDay( int dayOfWeek ) { // positive or negative difference -6..0..+6 int diff = dayOfWeek - this.dayOfWeek(); if ( diff >= 4 ) { diff -= 7; } else if ( diff <= -4 ) { diff += 7; } return new BigDate( this.ordinal + diff ); } // /method /** * get days since 1970-01-01 for this BigDate. 1970-01-01 = day 0. * synomym for getOrdinal. Parallels enum.ordinal * * @return days since 1970-01-01. This is NOT what you want for the java.util.Date constructor. * @see #getLocalTimeStamp * @see #getUTCTimeStamp * @see #getTimeStamp */ public final int ordinal() { return ordinal; } // /method /** * Set the ordinal field, and compute the equivalent internal Gregorian yyyy mm dd fields. alias setOrdinal. * * @param ordinal days since 1970-01-01. */ public final void set( int ordinal ) { if ( this.ordinal == ordinal ) { return; } this.ordinal = ordinal; toGregorian(); } // /method /** * Set the yyyy mm dd Gregorian fields, and compute the internal ordinal equivalent. yyyy mm dd are checked for * validity. * * @param yyyy -999,999 (BC) to +999,999 (AD) * @param mm month 1 to 12 (not 0 to 11 as in Sun's Date), no lead 0. * @param dd day 1 to 31, no lead 0. */ public final void set( int yyyy, int mm, int dd ) { set( yyyy, mm, dd, CHECK ); } // /method /** * Set the Gregorian fields, and compute the ordinal equivalent with the same modifiers CHECK, NORMALIZE, * BYPASSCHECK as the constructor. BEWARE! In Java a lead 0 on an integer implies OCTAL. * * @param yyyy -999,999 (BC) to +999,999 (AD) * @param mm month 1 to 12 (not 0 to 11 as in Sun's Date), no lead 0. * @param dd day 1 to 31, no lead 0. * @param how one of CHECK BYPASSCHECK NORMALIZE NORMALISE *

* the following are constants, not enums. * * @see #CHECK * @see #BYPASSCHECK * @see #NORMALIZE * @see #NORMALISE */ public final void set( int yyyy, int mm, int dd, int how ) { if ( this.yyyy == yyyy && this.mm == mm && this.dd == dd && this.how == how ) { return; } this.yyyy = yyyy; this.mm = mm; this.dd = dd; this.how = how;// save how for future short circuit. switch ( how ) { case CHECK: if ( !isValid( yyyy, mm, dd ) ) { throw new NumberFormatException( "invalid date: " + yyyy + "-" + mm + "-" + dd ); } break; case NORMALISE: normalise(); break; case BYPASSCHECK: break; } // end switch calcOrdinal(); } // /method /** * Sets the date that corresponding to a given utc timestamp at a given TimeZone. * * @param utcTimestamp milliseconds since 1970 in UTC time. E.g. Date.getTime * @param timeZone Timezone you want to know the date in at that time. TimeZone.getDefault() TimeZone.getU */ public void setDateAtTime( long utcTimestamp, TimeZone timeZone ) { // get date approximately right. setOrdinal( ( int ) ( utcTimestamp / 86400000L ) ); /* * Gets the time zone offset, for current date, modified in case of * daylight savings. This is the offset to add *to* UTC to get local * time. Places east of Greenwich have a positive offset. :era the era * of the given date, AD = 1 BC = 0 :year the year in the given date. * Docs are unclear if 1900=0 or 1900. Does not use year in calcs * anyway. :month the month in the given date. Month is 0-based. e.g., 0 * for January. :day the day-in-month of the given date. :dayOfWeek the * day-of-week of the given date. 1=Calendar.SUNDAY :milliseconds the * millis in day in standard local time. :return the offset * in millis to add *to* GMT to get local time. */ int offsetInMillis = timeZone.getOffset( ordinal >= ORDINAL_0001_01_01AD ? 1 : 0, yyyy, mm - 1, dd, getCalendarDayOfWeek(), ( int ) ( utcTimestamp % 86400000L ) /* wrong! Should be local standard time. How to get it?? */ ); // do a correction setOrdinal( ( int ) ( ( utcTimestamp + offsetInMillis ) / 86400000L ) ); } // /method /** * Convert date to a human-readable String [wed mm/dd/yy] * * @return this BigDate as a String in form fri 12/31/03 */ public String toDowMMDDYY() { if ( ordinal == NULL_ORDINAL ) { return ""; } return DAY_ABBRS[ getDayOfWeek() ] + " " + ST.toLZ( mm, 2 ) + "/" + ST.toLZ( dd, 2 ) + "/" + ST.toLZ( yyyy % 100, 2 ); } // /method /** * Convert date to a human-readable String. Handles BC with trailing _BC. * * @return this BigDate as a String in form YYYY-MM-DD ISO 8601:1988 international standard format. NOT final so you * can override to suit yourself. */ public String toString() { if ( ordinal == NULL_ORDINAL ) { return ""; } StringBuilder sb = new StringBuilder( 13 ); sb.append( Integer.toString( Math.abs( yyyy ) ) ); sb.append( "-" ); sb.append( ST.toLZ( mm, 2 ) ); sb.append( "-" ); sb.append( ST.toLZ( dd, 2 ) ); if ( yyyy < 1000 ) { if ( yyyy < 0 ) { sb.append( " BC" ); } else { // 0 .. 999 sb.append( " AD" ); } } return sb.toString(); } // /method /** * Convert just year part of date to a human-readable String. Handles BC with trailing BC. * * @return this BigDate as a String in form YYYYBC or yyyyy */ public String toYYYYString() { if ( ordinal == NULL_ORDINAL ) { return ""; } StringBuilder sb = new StringBuilder( 7 ); sb.append( Integer.toString( Math.abs( yyyy ) ) ); if ( yyyy < 1000 ) { if ( yyyy < 0 ) { sb.append( " BC" ); } else { // 0 .. 999 sb.append( " AD" ); } } return sb.toString(); } // /method }