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