1   /* Copyright 2002-2026 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.time;
18  
19  import java.io.IOException;
20  import java.io.Serial;
21  import java.io.Serializable;
22  import java.util.regex.Matcher;
23  import java.util.regex.Pattern;
24  
25  import org.orekit.errors.OrekitIllegalArgumentException;
26  import org.orekit.errors.OrekitInternalError;
27  import org.orekit.errors.OrekitMessages;
28  import org.orekit.utils.formatting.FastLongFormatter;
29  
30  /** Class representing a date broken up as year, month and day components.
31   * <p>This class uses the astronomical convention for calendars,
32   * which is also the convention used by <code>java.util.Date</code>:
33   * a year zero is present between years -1 and +1, and 10 days are
34   * missing in 1582. The calendar used around these special dates are:</p>
35   * <ul>
36   *   <li>up to 0000-12-31 : proleptic julian calendar</li>
37   *   <li>from 0001-01-01 to 1582-10-04: julian calendar</li>
38   *   <li>from 1582-10-15: gregorian calendar</li>
39   * </ul>
40   * <p>Instances of this class are guaranteed to be immutable.</p>
41   * @see TimeComponents
42   * @see DateTimeComponents
43   * @author Luc Maisonobe
44   */
45  public class DateComponents implements Serializable, Comparable<DateComponents> {
46  
47      /** Reference epoch for julian dates: -4712-01-01.
48       * <p>Both <code>java.util.Date</code> and {@link DateComponents} classes
49       * follow the astronomical conventions and consider a year 0 between
50       * years -1 and +1, hence this reference date lies in year -4712 and not
51       * in year -4713 as can be seen in other documents or programs that obey
52       * a different convention (for example the <code>convcal</code> utility).</p>
53       */
54      public static final DateComponents JULIAN_EPOCH;
55  
56      /** Reference epoch for modified julian dates: 1858-11-17. */
57      public static final DateComponents MODIFIED_JULIAN_EPOCH;
58  
59      /** Reference epoch for 1950 dates: 1950-01-01. */
60      public static final DateComponents FIFTIES_EPOCH;
61  
62      /** Reference epoch for CCSDS Time Code Format (CCSDS 301.0-B-4): 1958-01-01. */
63      public static final DateComponents CCSDS_EPOCH;
64  
65      /** Reference epoch for Galileo System Time: 1999-08-22. */
66      public static final DateComponents GALILEO_EPOCH;
67  
68      /** Reference epoch for GPS weeks: 1980-01-06. */
69      public static final DateComponents GPS_EPOCH;
70  
71      /** Reference epoch for QZSS weeks: 1980-01-06. */
72      public static final DateComponents QZSS_EPOCH;
73  
74      /** Reference epoch for NavIC weeks: 1999-08-22. */
75      public static final DateComponents NAVIC_EPOCH;
76  
77      /** Reference epoch for BeiDou weeks: 2006-01-01. */
78      public static final DateComponents BEIDOU_EPOCH;
79  
80      /** Reference epoch for GLONASS four-year interval number: 1996-01-01. */
81      public static final DateComponents GLONASS_EPOCH;
82  
83      /** J2000.0 Reference epoch: 2000-01-01. */
84      public static final DateComponents J2000_EPOCH;
85  
86      /** Java Reference epoch: 1970-01-01. */
87      public static final DateComponents JAVA_EPOCH;
88  
89      /** Maximum supported date.
90       * <p>
91       * This is date 5881610-07-11 which corresponds to {@code Integer.MAX_VALUE}
92       * days after {@link #J2000_EPOCH}.
93       * </p>
94       * @since 9.0
95       */
96      public static final DateComponents MAX_EPOCH;
97  
98      /** Maximum supported date.
99       * <p>
100      * This is date -5877490-03-03, which corresponds to {@code Integer.MIN_VALUE}
101      * days before {@link #J2000_EPOCH}.
102      * </p>
103      * @since 9.0
104      */
105     public static final DateComponents MIN_EPOCH;
106 
107     /** Offset between julian day epoch and modified julian day epoch. */
108     public static final double JD_TO_MJD = 2400000.5;
109 
110     /** Format for one 4 digits integer field.
111      * @since 13.0.3
112      */
113     private static final FastLongFormatter PADDED_FOUR_DIGITS_INTEGER = new FastLongFormatter(4, true);
114 
115     /** Format for one 2 digits integer field.
116      * @since 13.0.3
117      */
118     private static final FastLongFormatter PADDED_TWO_DIGITS_INTEGER = new FastLongFormatter(2, true);
119 
120     /** Serializable UID. */
121     @Serial
122     private static final long serialVersionUID = -2462694707837970938L;
123 
124     /** Factory for proleptic julian calendar (up to 0000-12-31). */
125     private static final YearFactory PROLEPTIC_JULIAN_FACTORY = new ProlepticJulianFactory();
126 
127     /** Factory for julian calendar (from 0001-01-01 to 1582-10-04). */
128     private static final YearFactory JULIAN_FACTORY           = new JulianFactory();
129 
130     /** Factory for gregorian calendar (from 1582-10-15). */
131     private static final YearFactory GREGORIAN_FACTORY        = new GregorianFactory();
132 
133     /** Factory for leap years. */
134     private static final MonthDayFactory LEAP_YEAR_FACTORY    = new LeapYearFactory();
135 
136     /** Factory for non-leap years. */
137     private static final MonthDayFactory COMMON_YEAR_FACTORY  = new CommonYearFactory();
138 
139     /** Offset between J2000 epoch and modified julian day epoch. */
140     private static final int MJD_TO_J2000 = 51544;
141 
142     /** Partial pattern for date start.
143      * @since 13.1.6
144      */
145     private static final String START = "^";
146 
147     /** Partial pattern for date end.
148      * @since 13.1.6
149      */
150     private static final String END = "$";
151 
152     /** Partial pattern for signed number with 4 digits.
153      * @since 13.1.6
154      */
155     private static final String SIGNED_4 = "(-?\\d\\d\\d\\d)";
156 
157     /** Partial pattern for signed number with any number of digits.
158      * @since 13.1.6
159      */
160     private static final String SIGNED_ANY = "(-?\\d+)";
161 
162     /** Partial pattern for positive number with 3 digits.
163      * @since 13.1.6
164      */
165     private static final String POSITIVE_3 = "(\\d\\d\\d)";
166 
167     /** Partial pattern for positive number with 2 digits.
168      * @since 13.1.6
169      */
170     private static final String POSITIVE_2 = "(\\d\\d)";
171 
172     /** Partial pattern for positive number with 1 digit.
173      * @since 13.1.6
174      */
175     private static final String POSITIVE_1 = "(\\d)";
176 
177     /** Partial pattern for dash.
178      * @since 13.1.6
179      */
180     private static final String DASH = "-";
181 
182     /** Partial pattern for week marker.
183      * @since 13.1.6
184      */
185     private static final String WEEK = "W";
186 
187     /** Pattern for basic and extended calendar date with dashes (allows any number of digits in years).
188      * @since 13.1.6
189      */
190     private static final Pattern CALENDAR_WITH_DASHES =
191         Pattern.compile(START + SIGNED_ANY + DASH + POSITIVE_2 + DASH + POSITIVE_2 + END);
192 
193     /** Pattern for basic and extended calendar date without dashes (mandates 4 digits years).
194      * @since 13.1.6
195      */
196     private static final Pattern CALENDAR_WITHOUT_DASHES =
197         Pattern.compile(START + SIGNED_4 + POSITIVE_2 + POSITIVE_2 + END);
198 
199     /** Pattern for ordinal date with dashes (allows any number of digits in years).
200      * @since 13.1.6
201      */
202     private static final Pattern ORDINAL_WITH_DASHES =
203         Pattern.compile(START + SIGNED_ANY + DASH + POSITIVE_3 + END);
204 
205     /** Pattern for ordinal date without dashes (mandates 4 digits years).
206      * @since 13.1.6
207      */
208     private static final Pattern ORDINAL_WITHOUT_DASHES =
209         Pattern.compile(START + SIGNED_4 + POSITIVE_3 + END);
210 
211     /** Pattern for extended format week date wit dashes (allows any number of digits in years).
212      * @since 13.1.6
213      */
214     private static final Pattern WEEK_WITH_DASHES =
215         Pattern.compile(START + SIGNED_ANY + DASH + WEEK + POSITIVE_2 + DASH + POSITIVE_1 + END);
216 
217     /** Pattern for extended format week date without dashes (mandates 4 digits years). */
218     private static final Pattern WEEK_WITHOUT_DASHES =
219         Pattern.compile(START + SIGNED_4 + WEEK + POSITIVE_2 + POSITIVE_1 + END);
220 
221     static {
222         // this static statement makes sure the reference epoch are initialized
223         // once AFTER the various factories have been set up
224         JULIAN_EPOCH          = new DateComponents(-4712,  1,  1);
225         MODIFIED_JULIAN_EPOCH = new DateComponents(1858, 11, 17);
226         FIFTIES_EPOCH         = new DateComponents(1950, 1, 1);
227         CCSDS_EPOCH           = new DateComponents(1958, 1, 1);
228         GALILEO_EPOCH         = new DateComponents(1999, 8, 22);
229         GPS_EPOCH             = new DateComponents(1980, 1, 6);
230         QZSS_EPOCH            = new DateComponents(1980, 1, 6);
231         NAVIC_EPOCH           = new DateComponents(1999, 8, 22);
232         BEIDOU_EPOCH          = new DateComponents(2006, 1, 1);
233         GLONASS_EPOCH         = new DateComponents(1996, 1, 1);
234         J2000_EPOCH           = new DateComponents(2000, 1, 1);
235         JAVA_EPOCH            = new DateComponents(1970, 1, 1);
236         MAX_EPOCH             = new DateComponents(Integer.MAX_VALUE);
237         MIN_EPOCH             = new DateComponents(Integer.MIN_VALUE);
238     }
239 
240     /** Year number. */
241     private final int year;
242 
243     /** Month number. */
244     private final int month;
245 
246     /** Day number. */
247     private final int day;
248 
249     /** Build a date from its components.
250      * @param year year number (may be 0 or negative for BC years)
251      * @param month month number from 1 to 12
252      * @param day day number from 1 to 31
253      * @exception IllegalArgumentException if inconsistent arguments
254      * are given (parameters out of range, february 29 for non-leap years,
255      * dates during the gregorian leap in 1582 ...)
256      */
257     public DateComponents(final int year, final int month, final int day)
258         throws IllegalArgumentException {
259 
260         // very rough range check
261         // (just to avoid ArrayOutOfboundException in MonthDayFactory later)
262         if (month < 1 || month > 12) {
263             throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_MONTH, month);
264         }
265 
266         // start by trusting the parameters
267         this.year  = year;
268         this.month = month;
269         this.day   = day;
270 
271         // build a check date from the J2000 day
272         final DateComponents check = new DateComponents(getJ2000Day());
273 
274         // check the parameters for mismatch
275         // (i.e. invalid date components, like 29 february on non-leap years)
276         if (year != check.year || month != check.month || day != check.day) {
277             throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_YEAR_MONTH_DAY,
278                                                       year, month, day);
279         }
280 
281     }
282 
283     /** Build a date from its components.
284      * @param year year number (may be 0 or negative for BC years)
285      * @param month month enumerate
286      * @param day day number from 1 to 31
287      * @exception IllegalArgumentException if inconsistent arguments
288      * are given (parameters out of range, february 29 for non-leap years,
289      * dates during the gregorian leap in 1582 ...)
290      */
291     public DateComponents(final int year, final Month month, final int day)
292         throws IllegalArgumentException {
293         this(year, month.getNumber(), day);
294     }
295 
296     /** Build a date from a year and day number.
297      * @param year year number (may be 0 or negative for BC years)
298      * @param dayNumber day number in the year from 1 to 366
299      * @exception IllegalArgumentException if dayNumber is out of range
300      * with respect to year
301      */
302     public DateComponents(final int year, final int dayNumber)
303         throws IllegalArgumentException {
304         this(J2000_EPOCH, new DateComponents(year - 1, 12, 31).getJ2000Day() + dayNumber);
305         if (dayNumber != getDayOfYear()) {
306             throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_DAY_NUMBER_IN_YEAR,
307                                                      dayNumber, year);
308         }
309     }
310 
311     /** Build a date from its offset with respect to a {@link #J2000_EPOCH}.
312      * @param offset offset with respect to a {@link #J2000_EPOCH}
313      * @see #getJ2000Day()
314      */
315     public DateComponents(final int offset) {
316 
317         // we follow the astronomical convention for calendars:
318         // we consider a year zero and 10 days are missing in 1582
319         // from 1582-10-15: gregorian calendar
320         // from 0001-01-01 to 1582-10-04: julian calendar
321         // up to 0000-12-31 : proleptic julian calendar
322         YearFactory yFactory = GREGORIAN_FACTORY;
323         if (offset < -152384) {
324             if (offset > -730122) {
325                 yFactory = JULIAN_FACTORY;
326             } else {
327                 yFactory = PROLEPTIC_JULIAN_FACTORY;
328             }
329         }
330         year = yFactory.getYear(offset);
331         final int dayInYear = offset - yFactory.getLastJ2000DayOfYear(year - 1);
332 
333         // handle month/day according to the year being a common or leap year
334         final MonthDayFactory mdFactory =
335             yFactory.isLeap(year) ? LEAP_YEAR_FACTORY : COMMON_YEAR_FACTORY;
336         month = mdFactory.getMonth(dayInYear);
337         day   = mdFactory.getDay(dayInYear, month);
338 
339     }
340 
341     /** Build a date from its offset with respect to a reference epoch.
342      * <p>This constructor is mainly useful to build a date from a modified
343      * julian day (using {@link #MODIFIED_JULIAN_EPOCH}) or a GPS week number
344      * (using {@link #GPS_EPOCH}).</p>
345      * @param epoch reference epoch
346      * @param offset offset with respect to a reference epoch
347      * @see #DateComponents(int)
348      * @see #getMJD()
349      */
350     public DateComponents(final DateComponents epoch, final int offset) {
351         this(epoch.getJ2000Day() + offset);
352     }
353 
354     /** Build a date from week components.
355      * <p>The calendar week number is a number between 1 and 52 or 53 depending
356      * on the year. Week 1 is defined by ISO as the one that includes the first
357      * Thursday of a year. Week 1 may therefore start the previous year and week
358      * 52 or 53 may end in the next year. As an example calendar date 1995-01-01
359      * corresponds to week date 1994-W52-7 (i.e. Sunday in the last week of 1994
360      * is in fact the first day of year 1995). This date would beAnother example is calendar date
361      * 1996-12-31 which corresponds to week date 1997-W01-2 (i.e. Tuesday in the
362      * first week of 1997 is in fact the last day of year 1996).</p>
363      * @param wYear year associated to week numbering
364      * @param week week number in year, from 1 to 52 or 53
365      * @param dayOfWeek day of week, from 1 (Monday) to 7 (Sunday)
366      * @return a builded date
367      * @exception IllegalArgumentException if inconsistent arguments
368      * are given (parameters out of range, week 53 on a 52 weeks year ...)
369      */
370     public static DateComponents createFromWeekComponents(final int wYear, final int week, final int dayOfWeek)
371         throws IllegalArgumentException {
372 
373         final DateComponents firstWeekMonday = new DateComponents(getFirstWeekMonday(wYear));
374         final DateComponents d = new DateComponents(firstWeekMonday, 7 * week + dayOfWeek - 8);
375 
376         // check the parameters for invalid date components
377         if (week != d.getCalendarWeek() || dayOfWeek != d.getDayOfWeek()) {
378             throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_WEEK_DATE,
379                                                      wYear, week, dayOfWeek);
380         }
381 
382         return d;
383 
384     }
385 
386     /** Parse a string in ISO-8601 format to build a date.
387      * <p>The supported formats are:
388      * <ul>
389      *   <li>basic format calendar date: YYYYMMDD</li>
390      *   <li>extended format calendar date: YYYY-MM-DD</li>
391      *   <li>basic format ordinal date: YYYYDDD</li>
392      *   <li>extended format ordinal date: YYYY-DDD</li>
393      *   <li>basic format week date: YYYYWwwD</li>
394      *   <li>extended format week date: YYYY-Www-D</li>
395      * </ul>
396      *
397      * <p> As shown by the list above, only the complete representations defined in section 4.1
398      * of ISO-8601 standard are supported, neither expended representations nor representations
399      * with reduced accuracy are supported.
400      *
401      * <p>
402      * Parsing a single integer as a julian day is <em>not</em> supported as it may be ambiguous
403      * with either the basic format calendar date or the basic format ordinal date depending
404      * on the number of digits.
405      * </p>
406      * @param string string to parse
407      * @return a parsed date
408      * @exception IllegalArgumentException if string cannot be parsed
409      */
410     public static  DateComponents parseDate(final String string) {
411 
412         // is the date a calendar date ?
413         final Matcher calendarMatcherWith = CALENDAR_WITH_DASHES.matcher(string);
414         if (calendarMatcherWith.matches()) {
415             return new DateComponents(Integer.parseInt(calendarMatcherWith.group(1)),
416                                       Integer.parseInt(calendarMatcherWith.group(2)),
417                                       Integer.parseInt(calendarMatcherWith.group(3)));
418         }
419         final Matcher calendarMatcherWithout = CALENDAR_WITHOUT_DASHES.matcher(string);
420         if (calendarMatcherWithout.matches()) {
421             return new DateComponents(Integer.parseInt(calendarMatcherWithout.group(1)),
422                                       Integer.parseInt(calendarMatcherWithout.group(2)),
423                                       Integer.parseInt(calendarMatcherWithout.group(3)));
424         }
425 
426         // is the date an ordinal date ?
427         final Matcher ordinalMatcherWith = ORDINAL_WITH_DASHES.matcher(string);
428         if (ordinalMatcherWith.matches()) {
429             return new DateComponents(Integer.parseInt(ordinalMatcherWith.group(1)),
430                                       Integer.parseInt(ordinalMatcherWith.group(2)));
431         }
432         final Matcher ordinalMatcherWithout = ORDINAL_WITHOUT_DASHES.matcher(string);
433         if (ordinalMatcherWithout.matches()) {
434             return new DateComponents(Integer.parseInt(ordinalMatcherWithout.group(1)),
435                                       Integer.parseInt(ordinalMatcherWithout.group(2)));
436         }
437 
438         // is the date a week date ?
439         final Matcher weekMatcherWith = WEEK_WITH_DASHES.matcher(string);
440         if (weekMatcherWith.matches()) {
441             return createFromWeekComponents(Integer.parseInt(weekMatcherWith.group(1)),
442                                             Integer.parseInt(weekMatcherWith.group(2)),
443                                             Integer.parseInt(weekMatcherWith.group(3)));
444         }
445         final Matcher weekMatcherWithout = WEEK_WITHOUT_DASHES.matcher(string);
446         if (weekMatcherWithout.matches()) {
447             return createFromWeekComponents(Integer.parseInt(weekMatcherWithout.group(1)),
448                                             Integer.parseInt(weekMatcherWithout.group(2)),
449                                             Integer.parseInt(weekMatcherWithout.group(3)));
450         }
451 
452         throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_DATE, string);
453 
454     }
455 
456     /** Get the year number.
457      * @return year number (may be 0 or negative for BC years)
458      */
459     public int getYear() {
460         return year;
461     }
462 
463     /** Get the month.
464      * @return month number from 1 to 12
465      */
466     public int getMonth() {
467         return month;
468     }
469 
470     /** Get the month as an enumerate.
471      * @return month as an enumerate
472      */
473     public Month getMonthEnum() {
474         return Month.getMonth(month);
475     }
476 
477     /** Get the day.
478      * @return day number from 1 to 31
479      */
480     public int getDay() {
481         return day;
482     }
483 
484     /** Get the day number with respect to J2000 epoch.
485      * @return day number with respect to J2000 epoch
486      */
487     public int getJ2000Day() {
488         YearFactory yFactory = GREGORIAN_FACTORY;
489         if (year < 1583) {
490             if (year < 1) {
491                 yFactory = PROLEPTIC_JULIAN_FACTORY;
492             } else if (year < 1582 || month < 10 || month < 11 && day < 5) {
493                 yFactory = JULIAN_FACTORY;
494             }
495         }
496         final MonthDayFactory mdFactory =
497             yFactory.isLeap(year) ? LEAP_YEAR_FACTORY : COMMON_YEAR_FACTORY;
498         return yFactory.getLastJ2000DayOfYear(year - 1) +
499                mdFactory.getDayInYear(month, day);
500     }
501 
502     /** Get the modified julian day.
503      * @return modified julian day
504      */
505     public int getMJD() {
506         return MJD_TO_J2000 + getJ2000Day();
507     }
508 
509     /** Get the calendar week number.
510      * <p>The calendar week number is a number between 1 and 52 or 53 depending
511      * on the year. Week 1 is defined by ISO as the one that includes the first
512      * Thursday of a year. Week 1 may therefore start the previous year and week
513      * 52 or 53 may end in the next year. As an example calendar date 1995-01-01
514      * corresponds to week date 1994-W52-7 (i.e. Sunday in the last week of 1994
515      * is in fact the first day of year 1995). Another example is calendar date
516      * 1996-12-31 which corresponds to week date 1997-W01-2 (i.e. Tuesday in the
517      * first week of 1997 is in fact the last day of year 1996).</p>
518      * @return calendar week number
519      */
520     public int getCalendarWeek() {
521         final int firstWeekMonday = getFirstWeekMonday(year);
522         int daysSincefirstMonday = getJ2000Day() - firstWeekMonday;
523         if (daysSincefirstMonday < 0) {
524             // we are still in a week from previous year
525             daysSincefirstMonday += firstWeekMonday - getFirstWeekMonday(year - 1);
526         } else if (daysSincefirstMonday > 363) {
527             // up to three days at end of year may belong to first week of next year
528             // (by chance, there is no need for a specific check in year 1582 ...)
529             final int weekYearLength = getFirstWeekMonday(year + 1) - firstWeekMonday;
530             if (daysSincefirstMonday >= weekYearLength) {
531                 daysSincefirstMonday -= weekYearLength;
532             }
533         }
534         return 1 + daysSincefirstMonday / 7;
535     }
536 
537     /** Get the monday of a year first week.
538      * @param year year to consider
539      * @return day of the monday of the first weak of year
540      */
541     private static int getFirstWeekMonday(final int year) {
542         final int yearFirst = new DateComponents(year, 1, 1).getJ2000Day();
543         final int offsetToMonday = 4 - (yearFirst + 2) % 7;
544         return yearFirst + offsetToMonday + ((offsetToMonday > 3) ? -7 : 0);
545     }
546 
547     /** Get the day of week.
548      * <p>Day of week is a number between 1 (Monday) and 7 (Sunday).</p>
549      * @return day of week
550      */
551     public int getDayOfWeek() {
552         final int dow = (getJ2000Day() + 6) % 7; // result is between -6 and +6
553         return (dow < 1) ? (dow + 7) : dow;
554     }
555 
556     /** Get the day number in year.
557      * <p>Day number in year is between 1 (January 1st) and either 365 or
558      * 366 inclusive depending on year.</p>
559      * @return day number in year
560      */
561     public int getDayOfYear() {
562         return getJ2000Day() - new DateComponents(year - 1, 12, 31).getJ2000Day();
563     }
564 
565     /** Get a string representation (ISO-8601) of the date.
566      * @return string representation of the date.
567      */
568     public String toString() {
569         try {
570             final StringBuilder builder = new StringBuilder();
571             PADDED_FOUR_DIGITS_INTEGER.appendTo(builder, year);
572             builder.append('-');
573             PADDED_TWO_DIGITS_INTEGER.appendTo(builder, month);
574             builder.append('-');
575             PADDED_TWO_DIGITS_INTEGER.appendTo(builder, day);
576             return builder.toString();
577         } catch (IOException ioe) {
578             // this should never happen
579             throw new OrekitInternalError(ioe);
580         }
581     }
582 
583     /** {@inheritDoc} */
584     public int compareTo(final DateComponents other) {
585         final int j2000Day = getJ2000Day();
586         final int otherJ2000Day = other.getJ2000Day();
587         if (j2000Day < otherJ2000Day) {
588             return -1;
589         } else if (j2000Day > otherJ2000Day) {
590             return 1;
591         }
592         return 0;
593     }
594 
595     /** {@inheritDoc} */
596     public boolean equals(final Object other) {
597         try {
598             final DateComponents otherDate = (DateComponents) other;
599             return otherDate != null && year == otherDate.year &&
600                    month == otherDate.month && day == otherDate.day;
601         } catch (ClassCastException cce) {
602             return false;
603         }
604     }
605 
606     /** {@inheritDoc} */
607     public int hashCode() {
608         return (year << 16) ^ (month << 8) ^ day;
609     }
610 
611     /** Interface for dealing with years sequences according to some calendar. */
612     private interface YearFactory {
613 
614         /** Get the year number for a given day number with respect to J2000 epoch.
615          * @param j2000Day day number with respect to J2000 epoch
616          * @return year number
617          */
618         int getYear(int j2000Day);
619 
620         /** Get the day number with respect to J2000 epoch for new year's Eve.
621          * @param year year number
622          * @return day number with respect to J2000 epoch for new year's Eve
623          */
624         int getLastJ2000DayOfYear(int year);
625 
626         /** Check if a year is a leap or common year.
627          * @param year year number
628          * @return true if year is a leap year
629          */
630         boolean isLeap(int year);
631 
632     }
633 
634     /** Class providing a years sequence compliant with the proleptic Julian calendar. */
635     private static class ProlepticJulianFactory implements YearFactory {
636 
637         /** {@inheritDoc} */
638         public int getYear(final int j2000Day) {
639             return  (int) -((-4L * j2000Day - 2920488L) / 1461L);
640         }
641 
642         /** {@inheritDoc} */
643         public int getLastJ2000DayOfYear(final int year) {
644             return 365 * year + (year + 1) / 4 - 730123;
645         }
646 
647         /** {@inheritDoc} */
648         public boolean isLeap(final int year) {
649             return (year % 4) == 0;
650         }
651 
652     }
653 
654     /** Class providing a years sequence compliant with the Julian calendar. */
655     private static class JulianFactory implements YearFactory {
656 
657         /** {@inheritDoc} */
658         public int getYear(final int j2000Day) {
659             return  (int) ((4L * j2000Day + 2921948L) / 1461L);
660         }
661 
662         /** {@inheritDoc} */
663         public int getLastJ2000DayOfYear(final int year) {
664             return 365 * year + year / 4 - 730122;
665         }
666 
667         /** {@inheritDoc} */
668         public boolean isLeap(final int year) {
669             return (year % 4) == 0;
670         }
671 
672     }
673 
674     /** Class providing a years sequence compliant with the Gregorian calendar. */
675     private static class GregorianFactory implements YearFactory {
676 
677         /** {@inheritDoc} */
678         public int getYear(final int j2000Day) {
679 
680             // year estimate
681             int year = (int) ((400L * j2000Day + 292194288L) / 146097L);
682 
683             // the previous estimate is one unit too high in some rare cases
684             // (240 days in the 400 years gregorian cycle, about 0.16%)
685             if (j2000Day <= getLastJ2000DayOfYear(year - 1)) {
686                 --year;
687             }
688 
689             // exact year
690             return year;
691 
692         }
693 
694         /** {@inheritDoc} */
695         public int getLastJ2000DayOfYear(final int year) {
696             return 365 * year + year / 4 - year / 100 + year / 400 - 730120;
697         }
698 
699         /** {@inheritDoc} */
700         public boolean isLeap(final int year) {
701             return (year % 4) == 0 && ((year % 400) == 0 || (year % 100) != 0);
702         }
703 
704     }
705 
706     /** Interface for dealing with months sequences according to leap/common years. */
707     private interface MonthDayFactory {
708 
709         /** Get the month number for a given day number within year.
710          * @param dayInYear day number within year
711          * @return month number
712          */
713         int getMonth(int dayInYear);
714 
715         /** Get the day number for given month and day number within year.
716          * @param dayInYear day number within year
717          * @param month month number
718          * @return day number
719          */
720         int getDay(int dayInYear, int month);
721 
722         /** Get the day number within year for given month and day numbers.
723          * @param month month number
724          * @param day day number
725          * @return day number within year
726          */
727         int getDayInYear(int month, int day);
728 
729     }
730 
731     /** Class providing the months sequence for leap years. */
732     private static class LeapYearFactory implements MonthDayFactory {
733 
734         /** Months succession definition. */
735         private static final int[] PREVIOUS_MONTH_END_DAY = {
736             0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335
737         };
738 
739         /** {@inheritDoc} */
740         public int getMonth(final int dayInYear) {
741             return (dayInYear < 32) ? 1 : (10 * dayInYear + 313) / 306;
742         }
743 
744         /** {@inheritDoc} */
745         public int getDay(final int dayInYear, final int month) {
746             return dayInYear - PREVIOUS_MONTH_END_DAY[month];
747         }
748 
749         /** {@inheritDoc} */
750         public int getDayInYear(final int month, final int day) {
751             return day + PREVIOUS_MONTH_END_DAY[month];
752         }
753 
754     }
755 
756     /** Class providing the months sequence for common years. */
757     private static class CommonYearFactory implements MonthDayFactory {
758 
759         /** Months succession definition. */
760         private static final int[] PREVIOUS_MONTH_END_DAY = {
761             0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
762         };
763 
764         /** {@inheritDoc} */
765         public int getMonth(final int dayInYear) {
766             return (dayInYear < 32) ? 1 : (10 * dayInYear + 323) / 306;
767         }
768 
769         /** {@inheritDoc} */
770         public int getDay(final int dayInYear, final int month) {
771             return dayInYear - PREVIOUS_MONTH_END_DAY[month];
772         }
773 
774         /** {@inheritDoc} */
775         public int getDayInYear(final int month, final int day) {
776             return day + PREVIOUS_MONTH_END_DAY[month];
777         }
778 
779     }
780 
781 }