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