001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, by David Gilbert and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * --------------------
028 * SpreadsheetDate.java
029 * --------------------
030 * (C) Copyright 2006-2022, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 *
035 */
036package org.jfree.chart.date;
037
038import java.util.Calendar;
039import java.util.Date;
040
041/**
042 * Represents a date using an integer, in a similar fashion to the
043 * implementation in Microsoft Excel.  The range of dates supported is
044 * 1-Jan-1900 to 31-Dec-9999.
045 * <P>
046 * Be aware that there is a deliberate bug in Excel that recognises the year
047 * 1900 as a leap year when in fact it is not a leap year. You can find more
048 * information on the Microsoft website in article Q181370:
049 * <P>
050 * http://support.microsoft.com/support/kb/articles/Q181/3/70.asp
051 * <P>
052 * Excel uses the convention that 1-Jan-1900 = 1.  This class uses the
053 * convention 1-Jan-1900 = 2.
054 * The result is that the day number in this class will be different to the
055 * Excel figure for January and February 1900...but then Excel adds in an extra
056 * day (29-Feb-1900 which does not actually exist!) and from that point forward
057 * the day numbers will match.
058 */
059public class SpreadsheetDate extends SerialDate {
060
061    /** For serialization. */
062    private static final long serialVersionUID = -2039586705374454461L;
063    
064    /** 
065     * The day number (1-Jan-1900 = 2, 2-Jan-1900 = 3, ..., 31-Dec-9999 = 
066     * 2958465). 
067     */
068    private final int serial;
069
070    /** The day of the month (1 to 28, 29, 30 or 31 depending on the month). */
071    private final int day;
072
073    /** The month of the year (1 to 12). */
074    private final int month;
075
076    /** The year (1900 to 9999). */
077    private final int year;
078
079    /**
080     * Creates a new date instance.
081     *
082     * @param day  the day (in the range 1 to 28/29/30/31).
083     * @param month  the month (in the range 1 to 12).
084     * @param year  the year (in the range 1900 to 9999).
085     */
086    public SpreadsheetDate(int day, int month, int year) {
087
088        if ((year >= 1900) && (year <= 9999)) {
089            this.year = year;
090        }
091        else {
092            throw new IllegalArgumentException(
093                "The 'year' argument must be in range 1900 to 9999.");
094        }
095
096        if ((month >= MonthConstants.JANUARY) 
097                && (month <= MonthConstants.DECEMBER)) {
098            this.month = month;
099        }
100        else {
101            throw new IllegalArgumentException(
102                "The 'month' argument must be in the range 1 to 12.");
103        }
104
105        if ((day >= 1) && (day <= SerialDate.lastDayOfMonth(month, year))) {
106            this.day = day;
107        } else {
108            throw new IllegalArgumentException("Invalid 'day' argument.");
109        }
110
111        // the serial number needs to be synchronised with the day-month-year...
112        this.serial = calcSerial(day, month, year);
113    }
114
115    /**
116     * Standard constructor - creates a new date object representing the
117     * specified day number (which should be in the range 2 to 2958465.
118     *
119     * @param serial  the serial number for the day (range: 2 to 2958465).
120     */
121    public SpreadsheetDate(int serial) {
122
123        if ((serial >= SERIAL_LOWER_BOUND) && (serial <= SERIAL_UPPER_BOUND)) {
124            this.serial = serial;
125        }
126        else {
127            throw new IllegalArgumentException(
128                "SpreadsheetDate: Serial must be in range 2 to 2958465.");
129        }
130
131        // the day-month-year needs to be synchronised with the serial number...
132        // get the year from the serial date
133        final int days = this.serial - SERIAL_LOWER_BOUND;
134        // overestimated because we ignored leap days
135        final int overestimatedYYYY = 1900 + (days / 365);
136        final int leaps = SerialDate.leapYearCount(overestimatedYYYY);
137        final int nonleapdays = days - leaps;
138        // underestimated because we overestimated years
139        int underestimatedYYYY = 1900 + (nonleapdays / 365);
140
141        if (underestimatedYYYY == overestimatedYYYY) {
142            this.year = underestimatedYYYY;
143        } else {
144            int ss1 = calcSerial(1, 1, underestimatedYYYY);
145            while (ss1 <= this.serial) {
146                underestimatedYYYY = underestimatedYYYY + 1;
147                ss1 = calcSerial(1, 1, underestimatedYYYY);
148            }
149            this.year = underestimatedYYYY - 1;
150        }
151
152        final int ss2 = calcSerial(1, 1, this.year);
153
154        int[] daysToEndOfPrecedingMonth 
155                = AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH;
156
157        if (isLeapYear(this.year)) {
158            daysToEndOfPrecedingMonth 
159                    = LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH;
160        }
161
162        // get the month from the serial date
163        int mm = 1;
164        int sss = ss2 + daysToEndOfPrecedingMonth[mm] - 1;
165        while (sss < this.serial) {
166            mm = mm + 1;
167            sss = ss2 + daysToEndOfPrecedingMonth[mm] - 1;
168        }
169        this.month = mm - 1;
170
171        // what's left is d(+1);
172        this.day = this.serial - ss2 
173                 - daysToEndOfPrecedingMonth[this.month] + 1;
174
175    }
176
177    /**
178     * Returns the serial number for the date, where 1 January 1900 = 2
179     * (this corresponds, almost, to the numbering system used in Microsoft
180     * Excel for Windows and Lotus 1-2-3).
181     *
182     * @return The serial number of this date.
183     */
184    @Override
185    public int toSerial() {
186        return this.serial;
187    }
188
189    /**
190     * Returns a {@code java.util.Date} equivalent to this date.
191     *
192     * @return The date.
193     */
194    @Override
195    public Date toDate() {
196        Calendar calendar = Calendar.getInstance();
197        calendar.set(getYYYY(), getMonth() - 1, getDayOfMonth(), 0, 0, 0);
198        return calendar.getTime();
199    }
200
201    /**
202     * Returns the year (assume a valid range of 1900 to 9999).
203     *
204     * @return The year.
205     */
206    @Override
207    public int getYYYY() {
208        return this.year;
209    }
210
211    /**
212     * Returns the month (January = 1, February = 2, March = 3).
213     *
214     * @return The month of the year.
215     */
216    @Override
217    public int getMonth() {
218        return this.month;
219    }
220
221    /**
222     * Returns the day of the month.
223     *
224     * @return The day of the month.
225     */
226    @Override
227    public int getDayOfMonth() {
228        return this.day;
229    }
230
231    /**
232     * Returns a code representing the day of the week.
233     * <P>
234     * The codes are defined in the {@link SerialDate} class as: 
235     * {@code SUNDAY}, {@code MONDAY}, {@code TUESDAY}, 
236     * {@code WEDNESDAY}, {@code THURSDAY}, {@code FRIDAY}, and 
237     * {@code SATURDAY}.
238     *
239     * @return A code representing the day of the week.
240     */
241    @Override
242    public int getDayOfWeek() {
243        return (this.serial + 6) % 7 + 1;
244    }
245
246    /**
247     * Tests the equality of this date with an arbitrary object.
248     * <P>
249     * This method will return true ONLY if the object is an instance of the
250     * {@link SerialDate} base class, and it represents the same day as this
251     * {@link SpreadsheetDate}.
252     *
253     * @param object  the object to compare ({@code null} permitted).
254     *
255     * @return A boolean.
256     */
257    @Override
258    public boolean equals(Object object) {
259
260        if (object instanceof SerialDate) {
261            SerialDate s = (SerialDate) object;
262            return (s.toSerial() == this.toSerial());
263        } else {
264            return false;
265        }
266
267    }
268
269    /**
270     * Returns a hash code for this object instance.
271     * 
272     * @return A hash code.
273     */
274    @Override
275    public int hashCode() {
276        return toSerial();
277    }
278
279    /**
280     * Returns the difference (in days) between this date and the specified 
281     * 'other' date.
282     *
283     * @param other  the date being compared to.
284     *
285     * @return The difference (in days) between this date and the specified 
286     *         'other' date.
287     */
288    @Override
289    public int compare(SerialDate other) {
290        return this.serial - other.toSerial();
291    }
292
293    /**
294     * Implements the method required by the Comparable interface.
295     * 
296     * @param other  the other object (usually another SerialDate).
297     * 
298     * @return A negative integer, zero, or a positive integer as this object 
299     *         is less than, equal to, or greater than the specified object.
300     */
301    @Override
302    public int compareTo(Object other) {
303        return compare((SerialDate) other);    
304    }
305    
306    /**
307     * Returns true if this SerialDate represents the same date as the
308     * specified SerialDate.
309     *
310     * @param other  the date being compared to.
311     *
312     * @return {@code true} if this SerialDate represents the same date as
313     *         the specified SerialDate.
314     */
315    @Override
316    public boolean isOn(SerialDate other) {
317        return (this.serial == other.toSerial());
318    }
319
320    /**
321     * Returns true if this SerialDate represents an earlier date compared to
322     * the specified SerialDate.
323     *
324     * @param other  the date being compared to.
325     *
326     * @return {@code true} if this SerialDate represents an earlier date
327     *         compared to the specified SerialDate.
328     */
329    @Override
330    public boolean isBefore(SerialDate other) {
331        return (this.serial < other.toSerial());
332    }
333
334    /**
335     * Returns true if this SerialDate represents the same date as the
336     * specified SerialDate.
337     *
338     * @param other  the date being compared to.
339     *
340     * @return {@code true} if this SerialDate represents the same date
341     *         as the specified SerialDate.
342     */
343    @Override
344    public boolean isOnOrBefore(SerialDate other) {
345        return (this.serial <= other.toSerial());
346    }
347
348    /**
349     * Returns true if this SerialDate represents the same date as the
350     * specified SerialDate.
351     *
352     * @param other  the date being compared to.
353     *
354     * @return {@code true} if this SerialDate represents the same date
355     *         as the specified SerialDate.
356     */
357    @Override
358    public boolean isAfter(SerialDate other) {
359        return (this.serial > other.toSerial());
360    }
361
362    /**
363     * Returns true if this SerialDate represents the same date as the
364     * specified SerialDate.
365     *
366     * @param other  the date being compared to.
367     *
368     * @return {@code true} if this SerialDate represents the same date as
369     *         the specified SerialDate.
370     */
371    @Override
372    public boolean isOnOrAfter(SerialDate other) {
373        return (this.serial >= other.toSerial());
374    }
375
376    /**
377     * Returns {@code true} if this {@link SerialDate} is within the 
378     * specified range (INCLUSIVE).  The date order of d1 and d2 is not 
379     * important.
380     *
381     * @param d1  a boundary date for the range.
382     * @param d2  the other boundary date for the range.
383     *
384     * @return A boolean.
385     */
386    @Override
387    public boolean isInRange(SerialDate d1, SerialDate d2) {
388        return isInRange(d1, d2, SerialDate.INCLUDE_BOTH);
389    }
390
391    /**
392     * Returns true if this SerialDate is within the specified range (caller
393     * specifies whether or not the end-points are included).  The order of d1
394     * and d2 is not important.
395     *
396     * @param d1  one boundary date for the range.
397     * @param d2  a second boundary date for the range.
398     * @param include  a code that controls whether or not the start and end 
399     *                 dates are included in the range.
400     *
401     * @return {@code true} if this SerialDate is within the specified 
402     *         range.
403     */
404    @Override
405    public boolean isInRange(SerialDate d1, SerialDate d2, int include) {
406        int s1 = d1.toSerial();
407        int s2 = d2.toSerial();
408        int start = Math.min(s1, s2);
409        int end = Math.max(s1, s2);
410        
411        int s = toSerial();
412        if (include == SerialDate.INCLUDE_BOTH) {
413            return (s >= start && s <= end);
414        }
415        else if (include == SerialDate.INCLUDE_FIRST) {
416            return (s >= start && s < end);            
417        }
418        else if (include == SerialDate.INCLUDE_SECOND) {
419            return (s > start && s <= end);            
420        }
421        else {
422            return (s > start && s < end);            
423        }    
424    }
425
426    /**
427     * Calculate the serial number from the day, month and year.
428     * <P>
429     * 1-Jan-1900 = 2.
430     *
431     * @param d  the day.
432     * @param m  the month.
433     * @param y  the year.
434     *
435     * @return the serial number from the day, month and year.
436     */
437    private int calcSerial(int d, int m, int y) {
438        int yy = ((y - 1900) * 365) + SerialDate.leapYearCount(y - 1);
439        int mm = SerialDate.AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH[m];
440        if (m > MonthConstants.FEBRUARY) {
441            if (SerialDate.isLeapYear(y)) {
442                mm = mm + 1;
443            }
444        }
445        int dd = d;
446        return yy + mm + dd + 1;
447    }
448
449}
450