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