001 /* ======================================================================== 002 * JCommon : a free general purpose class library for the Java(tm) platform 003 * ======================================================================== 004 * 005 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jcommon/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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * -------------------- 028 * SpreadsheetDate.java 029 * -------------------- 030 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * $Id: SpreadsheetDate.java,v 1.8 2005/11/03 09:25:39 mungady Exp $ 036 * 037 * Changes 038 * ------- 039 * 11-Oct-2001 : Version 1 (DG); 040 * 05-Nov-2001 : Added getDescription() and setDescription() methods (DG); 041 * 12-Nov-2001 : Changed name from ExcelDate.java to SpreadsheetDate.java (DG); 042 * Fixed a bug in calculating day, month and year from serial 043 * number (DG); 044 * 24-Jan-2002 : Fixed a bug in calculating the serial number from the day, 045 * month and year. Thanks to Trevor Hills for the report (DG); 046 * 29-May-2002 : Added equals(Object) method (SourceForge ID 558850) (DG); 047 * 03-Oct-2002 : Fixed errors reported by Checkstyle (DG); 048 * 13-Mar-2003 : Implemented Serializable (DG); 049 * 04-Sep-2003 : Completed isInRange() methods (DG); 050 * 05-Sep-2003 : Implemented Comparable (DG); 051 * 21-Oct-2003 : Added hashCode() method (DG); 052 * 053 */ 054 055 package org.jfree.date; 056 057 import java.util.Calendar; 058 import java.util.Date; 059 060 /** 061 * Represents a date using an integer, in a similar fashion to the 062 * implementation in Microsoft Excel. The range of dates supported is 063 * 1-Jan-1900 to 31-Dec-9999. 064 * <P> 065 * Be aware that there is a deliberate bug in Excel that recognises the year 066 * 1900 as a leap year when in fact it is not a leap year. You can find more 067 * information on the Microsoft website in article Q181370: 068 * <P> 069 * http://support.microsoft.com/support/kb/articles/Q181/3/70.asp 070 * <P> 071 * Excel uses the convention that 1-Jan-1900 = 1. This class uses the 072 * convention 1-Jan-1900 = 2. 073 * The result is that the day number in this class will be different to the 074 * Excel figure for January and February 1900...but then Excel adds in an extra 075 * day (29-Feb-1900 which does not actually exist!) and from that point forward 076 * the day numbers will match. 077 * 078 * @author David Gilbert 079 */ 080 public class SpreadsheetDate extends SerialDate { 081 082 /** For serialization. */ 083 private static final long serialVersionUID = -2039586705374454461L; 084 085 /** 086 * The day number (1-Jan-1900 = 2, 2-Jan-1900 = 3, ..., 31-Dec-9999 = 087 * 2958465). 088 */ 089 private int serial; 090 091 /** The day of the month (1 to 28, 29, 30 or 31 depending on the month). */ 092 private int day; 093 094 /** The month of the year (1 to 12). */ 095 private int month; 096 097 /** The year (1900 to 9999). */ 098 private int year; 099 100 /** An optional description for the date. */ 101 private String description; 102 103 /** 104 * Creates a new date instance. 105 * 106 * @param day the day (in the range 1 to 28/29/30/31). 107 * @param month the month (in the range 1 to 12). 108 * @param year the year (in the range 1900 to 9999). 109 */ 110 public SpreadsheetDate(final int day, final int month, final int year) { 111 112 if ((year >= 1900) && (year <= 9999)) { 113 this.year = year; 114 } 115 else { 116 throw new IllegalArgumentException( 117 "The 'year' argument must be in range 1900 to 9999." 118 ); 119 } 120 121 if ((month >= MonthConstants.JANUARY) 122 && (month <= MonthConstants.DECEMBER)) { 123 this.month = month; 124 } 125 else { 126 throw new IllegalArgumentException( 127 "The 'month' argument must be in the range 1 to 12." 128 ); 129 } 130 131 if ((day >= 1) && (day <= SerialDate.lastDayOfMonth(month, year))) { 132 this.day = day; 133 } 134 else { 135 throw new IllegalArgumentException("Invalid 'day' argument."); 136 } 137 138 // the serial number needs to be synchronised with the day-month-year... 139 this.serial = calcSerial(day, month, year); 140 141 this.description = null; 142 143 } 144 145 /** 146 * Standard constructor - creates a new date object representing the 147 * specified day number (which should be in the range 2 to 2958465. 148 * 149 * @param serial the serial number for the day (range: 2 to 2958465). 150 */ 151 public SpreadsheetDate(final int serial) { 152 153 if ((serial >= SERIAL_LOWER_BOUND) && (serial <= SERIAL_UPPER_BOUND)) { 154 this.serial = serial; 155 } 156 else { 157 throw new IllegalArgumentException( 158 "SpreadsheetDate: Serial must be in range 2 to 2958465."); 159 } 160 161 // the day-month-year needs to be synchronised with the serial number... 162 calcDayMonthYear(); 163 164 } 165 166 /** 167 * Returns the description that is attached to the date. It is not 168 * required that a date have a description, but for some applications it 169 * is useful. 170 * 171 * @return The description that is attached to the date. 172 */ 173 public String getDescription() { 174 return this.description; 175 } 176 177 /** 178 * Sets the description for the date. 179 * 180 * @param description the description for this date (<code>null</code> 181 * permitted). 182 */ 183 public void setDescription(final String description) { 184 this.description = description; 185 } 186 187 /** 188 * Returns the serial number for the date, where 1 January 1900 = 2 189 * (this corresponds, almost, to the numbering system used in Microsoft 190 * Excel for Windows and Lotus 1-2-3). 191 * 192 * @return The serial number of this date. 193 */ 194 public int toSerial() { 195 return this.serial; 196 } 197 198 /** 199 * Returns a <code>java.util.Date</code> equivalent to this date. 200 * 201 * @return The date. 202 */ 203 public Date toDate() { 204 final Calendar calendar = Calendar.getInstance(); 205 calendar.set(getYYYY(), getMonth() - 1, getDayOfMonth(), 0, 0, 0); 206 return calendar.getTime(); 207 } 208 209 /** 210 * Returns the year (assume a valid range of 1900 to 9999). 211 * 212 * @return The year. 213 */ 214 public int getYYYY() { 215 return this.year; 216 } 217 218 /** 219 * Returns the month (January = 1, February = 2, March = 3). 220 * 221 * @return The month of the year. 222 */ 223 public int getMonth() { 224 return this.month; 225 } 226 227 /** 228 * Returns the day of the month. 229 * 230 * @return The day of the month. 231 */ 232 public int getDayOfMonth() { 233 return this.day; 234 } 235 236 /** 237 * Returns a code representing the day of the week. 238 * <P> 239 * The codes are defined in the {@link SerialDate} class as: 240 * <code>SUNDAY</code>, <code>MONDAY</code>, <code>TUESDAY</code>, 241 * <code>WEDNESDAY</code>, <code>THURSDAY</code>, <code>FRIDAY</code>, and 242 * <code>SATURDAY</code>. 243 * 244 * @return A code representing the day of the week. 245 */ 246 public int getDayOfWeek() { 247 return (this.serial + 6) % 7 + 1; 248 } 249 250 /** 251 * Tests the equality of this date with an arbitrary object. 252 * <P> 253 * This method will return true ONLY if the object is an instance of the 254 * {@link SerialDate} base class, and it represents the same day as this 255 * {@link SpreadsheetDate}. 256 * 257 * @param object the object to compare (<code>null</code> permitted). 258 * 259 * @return A boolean. 260 */ 261 public boolean equals(final Object object) { 262 263 if (object instanceof SerialDate) { 264 final SerialDate s = (SerialDate) object; 265 return (s.toSerial() == this.toSerial()); 266 } 267 else { 268 return false; 269 } 270 271 } 272 273 /** 274 * Returns a hash code for this object instance. 275 * 276 * @return A hash code. 277 */ 278 public int hashCode() { 279 return toSerial(); 280 } 281 282 /** 283 * Returns the difference (in days) between this date and the specified 284 * 'other' date. 285 * 286 * @param other the date being compared to. 287 * 288 * @return The difference (in days) between this date and the specified 289 * 'other' date. 290 */ 291 public int compare(final SerialDate other) { 292 return this.serial - other.toSerial(); 293 } 294 295 /** 296 * Implements the method required by the Comparable interface. 297 * 298 * @param other the other object (usually another SerialDate). 299 * 300 * @return A negative integer, zero, or a positive integer as this object 301 * is less than, equal to, or greater than the specified object. 302 */ 303 public int compareTo(final Object other) { 304 return compare((SerialDate) other); 305 } 306 307 /** 308 * Returns true if this SerialDate represents the same date as the 309 * specified SerialDate. 310 * 311 * @param other the date being compared to. 312 * 313 * @return <code>true</code> if this SerialDate represents the same date as 314 * the specified SerialDate. 315 */ 316 public boolean isOn(final 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</code> if this SerialDate represents an earlier date 327 * compared to the specified SerialDate. 328 */ 329 public boolean isBefore(final SerialDate other) { 330 return (this.serial < other.toSerial()); 331 } 332 333 /** 334 * Returns true if this SerialDate represents the same date as the 335 * specified SerialDate. 336 * 337 * @param other the date being compared to. 338 * 339 * @return <code>true</code> if this SerialDate represents the same date 340 * as the specified SerialDate. 341 */ 342 public boolean isOnOrBefore(final SerialDate other) { 343 return (this.serial <= other.toSerial()); 344 } 345 346 /** 347 * Returns true if this SerialDate represents the same date as the 348 * specified SerialDate. 349 * 350 * @param other the date being compared to. 351 * 352 * @return <code>true</code> if this SerialDate represents the same date 353 * as the specified SerialDate. 354 */ 355 public boolean isAfter(final SerialDate other) { 356 return (this.serial > other.toSerial()); 357 } 358 359 /** 360 * Returns true if this SerialDate represents the same date as the 361 * specified SerialDate. 362 * 363 * @param other the date being compared to. 364 * 365 * @return <code>true</code> if this SerialDate represents the same date as 366 * the specified SerialDate. 367 */ 368 public boolean isOnOrAfter(final SerialDate other) { 369 return (this.serial >= other.toSerial()); 370 } 371 372 /** 373 * Returns <code>true</code> if this {@link SerialDate} is within the 374 * specified range (INCLUSIVE). The date order of d1 and d2 is not 375 * important. 376 * 377 * @param d1 a boundary date for the range. 378 * @param d2 the other boundary date for the range. 379 * 380 * @return A boolean. 381 */ 382 public boolean isInRange(final SerialDate d1, final SerialDate d2) { 383 return isInRange(d1, d2, SerialDate.INCLUDE_BOTH); 384 } 385 386 /** 387 * Returns true if this SerialDate is within the specified range (caller 388 * specifies whether or not the end-points are included). The order of d1 389 * and d2 is not important. 390 * 391 * @param d1 one boundary date for the range. 392 * @param d2 a second boundary date for the range. 393 * @param include a code that controls whether or not the start and end 394 * dates are included in the range. 395 * 396 * @return <code>true</code> if this SerialDate is within the specified 397 * range. 398 */ 399 public boolean isInRange(final SerialDate d1, final SerialDate d2, 400 final int include) { 401 final int s1 = d1.toSerial(); 402 final int s2 = d2.toSerial(); 403 final int start = Math.min(s1, s2); 404 final int end = Math.max(s1, s2); 405 406 final int s = toSerial(); 407 if (include == SerialDate.INCLUDE_BOTH) { 408 return (s >= start && s <= end); 409 } 410 else if (include == SerialDate.INCLUDE_FIRST) { 411 return (s >= start && s < end); 412 } 413 else if (include == SerialDate.INCLUDE_SECOND) { 414 return (s > start && s <= end); 415 } 416 else { 417 return (s > start && s < end); 418 } 419 } 420 421 /** 422 * Calculate the serial number from the day, month and year. 423 * <P> 424 * 1-Jan-1900 = 2. 425 * 426 * @param d the day. 427 * @param m the month. 428 * @param y the year. 429 * 430 * @return the serial number from the day, month and year. 431 */ 432 private int calcSerial(final int d, final int m, final int y) { 433 final int yy = ((y - 1900) * 365) + SerialDate.leapYearCount(y - 1); 434 int mm = SerialDate.AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH[m]; 435 if (m > MonthConstants.FEBRUARY) { 436 if (SerialDate.isLeapYear(y)) { 437 mm = mm + 1; 438 } 439 } 440 final int dd = d; 441 return yy + mm + dd + 1; 442 } 443 444 /** 445 * Calculate the day, month and year from the serial number. 446 */ 447 private void calcDayMonthYear() { 448 449 // get the year from the serial date 450 final int days = this.serial - SERIAL_LOWER_BOUND; 451 // overestimated because we ignored leap days 452 final int overestimatedYYYY = 1900 + (days / 365); 453 final int leaps = SerialDate.leapYearCount(overestimatedYYYY); 454 final int nonleapdays = days - leaps; 455 // underestimated because we overestimated years 456 int underestimatedYYYY = 1900 + (nonleapdays / 365); 457 458 if (underestimatedYYYY == overestimatedYYYY) { 459 this.year = underestimatedYYYY; 460 } 461 else { 462 int ss1 = calcSerial(1, 1, underestimatedYYYY); 463 while (ss1 <= this.serial) { 464 underestimatedYYYY = underestimatedYYYY + 1; 465 ss1 = calcSerial(1, 1, underestimatedYYYY); 466 } 467 this.year = underestimatedYYYY - 1; 468 } 469 470 final int ss2 = calcSerial(1, 1, this.year); 471 472 int[] daysToEndOfPrecedingMonth 473 = AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH; 474 475 if (isLeapYear(this.year)) { 476 daysToEndOfPrecedingMonth 477 = LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH; 478 } 479 480 // get the month from the serial date 481 int mm = 1; 482 int sss = ss2 + daysToEndOfPrecedingMonth[mm] - 1; 483 while (sss < this.serial) { 484 mm = mm + 1; 485 sss = ss2 + daysToEndOfPrecedingMonth[mm] - 1; 486 } 487 this.month = mm - 1; 488 489 // what's left is d(+1); 490 this.day = this.serial - ss2 491 - daysToEndOfPrecedingMonth[this.month] + 1; 492 493 } 494 495 }