001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, by Object Refinery Limited 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ---------- 028 * Month.java 029 * ---------- 030 * (C) Copyright 2001-2007, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Chris Boek; 034 * 035 * Changes 036 * ------- 037 * 11-Oct-2001 : Version 1 (DG); 038 * 14-Nov-2001 : Added method to get year as primitive (DG); 039 * Override for toString() method (DG); 040 * 18-Dec-2001 : Changed order of parameters in constructor (DG); 041 * 19-Dec-2001 : Added a new constructor as suggested by Paul English (DG); 042 * 29-Jan-2002 : Worked on the parseMonth() method (DG); 043 * 14-Feb-2002 : Fixed bugs in the Month constructors (DG); 044 * 26-Feb-2002 : Changed getStart(), getMiddle() and getEnd() methods to 045 * evaluate with reference to a particular time zone (DG); 046 * 19-Mar-2002 : Changed API for TimePeriod classes (DG); 047 * 10-Sep-2002 : Added getSerialIndex() method (DG); 048 * 04-Oct-2002 : Fixed errors reported by Checkstyle (DG); 049 * 10-Jan-2003 : Changed base class and method names (DG); 050 * 13-Mar-2003 : Moved to com.jrefinery.data.time package, and implemented 051 * Serializable (DG); 052 * 21-Oct-2003 : Added hashCode() method (DG); 053 * 01-Nov-2005 : Fixed bug 1345383 (argument check in constructor) (DG); 054 * ------------- JFREECHART 1.0.x --------------------------------------------- 055 * 05-Oct-2006 : Updated API docs (DG); 056 * 06-Oct-2006 : Refactored to cache first and last millisecond values (DG); 057 * 04-Apr-2007 : Fixed bug in Month(Date, TimeZone) constructor (CB); 058 * 059 */ 060 061 package org.jfree.data.time; 062 063 import java.io.Serializable; 064 import java.util.Calendar; 065 import java.util.Date; 066 import java.util.TimeZone; 067 068 import org.jfree.date.MonthConstants; 069 import org.jfree.date.SerialDate; 070 071 /** 072 * Represents a single month. This class is immutable, which is a requirement 073 * for all {@link RegularTimePeriod} subclasses. 074 */ 075 public class Month extends RegularTimePeriod implements Serializable { 076 077 /** For serialization. */ 078 private static final long serialVersionUID = -5090216912548722570L; 079 080 /** The month (1-12). */ 081 private int month; 082 083 /** The year in which the month falls. */ 084 private int year; 085 086 /** The first millisecond. */ 087 private long firstMillisecond; 088 089 /** The last millisecond. */ 090 private long lastMillisecond; 091 092 /** 093 * Constructs a new Month, based on the current system time. 094 */ 095 public Month() { 096 this(new Date()); 097 } 098 099 /** 100 * Constructs a new month instance. 101 * 102 * @param month the month (in the range 1 to 12). 103 * @param year the year. 104 */ 105 public Month(int month, int year) { 106 if ((month < 1) || (month > 12)) { 107 throw new IllegalArgumentException("Month outside valid range."); 108 } 109 this.month = month; 110 this.year = year; 111 peg(Calendar.getInstance()); 112 } 113 114 /** 115 * Constructs a new month instance. 116 * 117 * @param month the month (in the range 1 to 12). 118 * @param year the year. 119 */ 120 public Month(int month, Year year) { 121 if ((month < 1) || (month > 12)) { 122 throw new IllegalArgumentException("Month outside valid range."); 123 } 124 this.month = month; 125 this.year = year.getYear(); 126 peg(Calendar.getInstance()); 127 } 128 129 /** 130 * Constructs a new <code>Month</code> instance, based on a date/time and 131 * the default time zone. 132 * 133 * @param time the date/time. 134 */ 135 public Month(Date time) { 136 this(time, RegularTimePeriod.DEFAULT_TIME_ZONE); 137 } 138 139 /** 140 * Constructs a new <code>Month</code> instance, based on a date/time and 141 * a time zone. The first and last millisecond values are initially 142 * pegged to the given time zone also. 143 * 144 * @param time the date/time. 145 * @param zone the time zone (<code>null</code> not permitted). 146 */ 147 public Month(Date time, TimeZone zone) { 148 Calendar calendar = Calendar.getInstance(zone); 149 calendar.setTime(time); 150 this.month = calendar.get(Calendar.MONTH) + 1; 151 this.year = calendar.get(Calendar.YEAR); 152 peg(calendar); 153 } 154 155 /** 156 * Returns the year in which the month falls. 157 * 158 * @return The year in which the month falls (as a Year object). 159 */ 160 public Year getYear() { 161 return new Year(this.year); 162 } 163 164 /** 165 * Returns the year in which the month falls. 166 * 167 * @return The year in which the month falls (as an int). 168 */ 169 public int getYearValue() { 170 return this.year; 171 } 172 173 /** 174 * Returns the month. Note that 1=JAN, 2=FEB, ... 175 * 176 * @return The month. 177 */ 178 public int getMonth() { 179 return this.month; 180 } 181 182 /** 183 * Returns the first millisecond of the month. This will be determined 184 * relative to the time zone specified in the constructor, or in the 185 * calendar instance passed in the most recent call to the 186 * {@link #peg(Calendar)} method. 187 * 188 * @return The first millisecond of the month. 189 * 190 * @see #getLastMillisecond() 191 */ 192 public long getFirstMillisecond() { 193 return this.firstMillisecond; 194 } 195 196 /** 197 * Returns the last millisecond of the month. This will be 198 * determined relative to the time zone specified in the constructor, or 199 * in the calendar instance passed in the most recent call to the 200 * {@link #peg(Calendar)} method. 201 * 202 * @return The last millisecond of the month. 203 * 204 * @see #getFirstMillisecond() 205 */ 206 public long getLastMillisecond() { 207 return this.lastMillisecond; 208 } 209 210 /** 211 * Recalculates the start date/time and end date/time for this time period 212 * relative to the supplied calendar (which incorporates a time zone). 213 * 214 * @param calendar the calendar (<code>null</code> not permitted). 215 * 216 * @since 1.0.3 217 */ 218 public void peg(Calendar calendar) { 219 this.firstMillisecond = getFirstMillisecond(calendar); 220 this.lastMillisecond = getLastMillisecond(calendar); 221 } 222 223 /** 224 * Returns the month preceding this one. 225 * 226 * @return The month preceding this one. 227 */ 228 public RegularTimePeriod previous() { 229 Month result; 230 if (this.month != MonthConstants.JANUARY) { 231 result = new Month(this.month - 1, this.year); 232 } 233 else { 234 if (this.year > 1900) { 235 result = new Month(MonthConstants.DECEMBER, this.year - 1); 236 } 237 else { 238 result = null; 239 } 240 } 241 return result; 242 } 243 244 /** 245 * Returns the month following this one. 246 * 247 * @return The month following this one. 248 */ 249 public RegularTimePeriod next() { 250 Month result; 251 if (this.month != MonthConstants.DECEMBER) { 252 result = new Month(this.month + 1, this.year); 253 } 254 else { 255 if (this.year < 9999) { 256 result = new Month(MonthConstants.JANUARY, this.year + 1); 257 } 258 else { 259 result = null; 260 } 261 } 262 return result; 263 } 264 265 /** 266 * Returns a serial index number for the month. 267 * 268 * @return The serial index number. 269 */ 270 public long getSerialIndex() { 271 return this.year * 12L + this.month; 272 } 273 274 /** 275 * Returns a string representing the month (e.g. "January 2002"). 276 * <P> 277 * To do: look at internationalisation. 278 * 279 * @return A string representing the month. 280 */ 281 public String toString() { 282 return SerialDate.monthCodeToString(this.month) + " " + this.year; 283 } 284 285 /** 286 * Tests the equality of this Month object to an arbitrary object. 287 * Returns true if the target is a Month instance representing the same 288 * month as this object. In all other cases, returns false. 289 * 290 * @param obj the object (<code>null</code> permitted). 291 * 292 * @return <code>true</code> if month and year of this and object are the 293 * same. 294 */ 295 public boolean equals(Object obj) { 296 297 if (obj != null) { 298 if (obj instanceof Month) { 299 Month target = (Month) obj; 300 return (this.month == target.getMonth() 301 && (this.year == target.getYearValue())); 302 } 303 else { 304 return false; 305 } 306 } 307 else { 308 return false; 309 } 310 311 } 312 313 /** 314 * Returns a hash code for this object instance. The approach described by 315 * Joshua Bloch in "Effective Java" has been used here: 316 * <p> 317 * <code>http://developer.java.sun.com/developer/Books/effectivejava 318 * /Chapter3.pdf</code> 319 * 320 * @return A hash code. 321 */ 322 public int hashCode() { 323 int result = 17; 324 result = 37 * result + this.month; 325 result = 37 * result + this.year; 326 return result; 327 } 328 329 /** 330 * Returns an integer indicating the order of this Month object relative to 331 * the specified 332 * object: negative == before, zero == same, positive == after. 333 * 334 * @param o1 the object to compare. 335 * 336 * @return negative == before, zero == same, positive == after. 337 */ 338 public int compareTo(Object o1) { 339 340 int result; 341 342 // CASE 1 : Comparing to another Month object 343 // -------------------------------------------- 344 if (o1 instanceof Month) { 345 Month m = (Month) o1; 346 result = this.year - m.getYearValue(); 347 if (result == 0) { 348 result = this.month - m.getMonth(); 349 } 350 } 351 352 // CASE 2 : Comparing to another TimePeriod object 353 // ----------------------------------------------- 354 else if (o1 instanceof RegularTimePeriod) { 355 // more difficult case - evaluate later... 356 result = 0; 357 } 358 359 // CASE 3 : Comparing to a non-TimePeriod object 360 // --------------------------------------------- 361 else { 362 // consider time periods to be ordered after general objects 363 result = 1; 364 } 365 366 return result; 367 368 } 369 370 /** 371 * Returns the first millisecond of the month, evaluated using the supplied 372 * calendar (which determines the time zone). 373 * 374 * @param calendar the calendar (<code>null</code> not permitted). 375 * 376 * @return The first millisecond of the month. 377 * 378 * @throws NullPointerException if <code>calendar</code> is 379 * <code>null</code>. 380 */ 381 public long getFirstMillisecond(Calendar calendar) { 382 calendar.set(this.year, this.month - 1, 1, 0, 0, 0); 383 calendar.set(Calendar.MILLISECOND, 0); 384 // in the following line, we'd rather call calendar.getTimeInMillis() 385 // to avoid object creation, but that isn't supported in Java 1.3.1 386 return calendar.getTime().getTime(); 387 } 388 389 /** 390 * Returns the last millisecond of the month, evaluated using the supplied 391 * calendar (which determines the time zone). 392 * 393 * @param calendar the calendar (<code>null</code> not permitted). 394 * 395 * @return The last millisecond of the month. 396 * 397 * @throws NullPointerException if <code>calendar</code> is 398 * <code>null</code>. 399 */ 400 public long getLastMillisecond(Calendar calendar) { 401 int eom = SerialDate.lastDayOfMonth(this.month, this.year); 402 calendar.set(this.year, this.month - 1, eom, 23, 59, 59); 403 calendar.set(Calendar.MILLISECOND, 999); 404 // in the following line, we'd rather call calendar.getTimeInMillis() 405 // to avoid object creation, but that isn't supported in Java 1.3.1 406 return calendar.getTime().getTime(); 407 } 408 409 /** 410 * Parses the string argument as a month. 411 * <P> 412 * This method is required to accept the format "YYYY-MM". It will also 413 * accept "MM-YYYY". Anything else, at the moment, is a bonus. 414 * 415 * @param s the string to parse. 416 * 417 * @return <code>null</code> if the string is not parseable, the month 418 * otherwise. 419 */ 420 public static Month parseMonth(String s) { 421 422 Month result = null; 423 if (s != null) { 424 425 // trim whitespace from either end of the string 426 s = s.trim(); 427 428 int i = Month.findSeparator(s); 429 if (i != -1) { 430 String s1 = s.substring(0, i).trim(); 431 String s2 = s.substring(i + 1, s.length()).trim(); 432 433 Year year = Month.evaluateAsYear(s1); 434 int month; 435 if (year != null) { 436 month = SerialDate.stringToMonthCode(s2); 437 if (month == -1) { 438 throw new TimePeriodFormatException( 439 "Can't evaluate the month." 440 ); 441 } 442 result = new Month(month, year); 443 } 444 else { 445 year = Month.evaluateAsYear(s2); 446 if (year != null) { 447 month = SerialDate.stringToMonthCode(s1); 448 if (month == -1) { 449 throw new TimePeriodFormatException( 450 "Can't evaluate the month." 451 ); 452 } 453 result = new Month(month, year); 454 } 455 else { 456 throw new TimePeriodFormatException( 457 "Can't evaluate the year." 458 ); 459 } 460 } 461 462 } 463 else { 464 throw new TimePeriodFormatException( 465 "Could not find separator." 466 ); 467 } 468 469 } 470 return result; 471 472 } 473 474 /** 475 * Finds the first occurrence of ' ', '-', ',' or '.' 476 * 477 * @param s the string to parse. 478 * 479 * @return <code>-1</code> if none of the characters where found, the 480 * position of the first occurence otherwise. 481 */ 482 private static int findSeparator(String s) { 483 484 int result = s.indexOf('-'); 485 if (result == -1) { 486 result = s.indexOf(','); 487 } 488 if (result == -1) { 489 result = s.indexOf(' '); 490 } 491 if (result == -1) { 492 result = s.indexOf('.'); 493 } 494 return result; 495 } 496 497 /** 498 * Creates a year from a string, or returns null (format exceptions 499 * suppressed). 500 * 501 * @param s the string to parse. 502 * 503 * @return <code>null</code> if the string is not parseable, the year 504 * otherwise. 505 */ 506 private static Year evaluateAsYear(String s) { 507 508 Year result = null; 509 try { 510 result = Year.parseYear(s); 511 } 512 catch (TimePeriodFormatException e) { 513 // suppress 514 } 515 return result; 516 517 } 518 519 }