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