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 * Quarter.java 029 * ------------ 030 * (C) Copyright 2001-2022, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): -; 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 * Defines a quarter (in a given year). The range supported is Q1 1900 to 049 * Q4 9999. This class is immutable, which is a requirement for all 050 * {@link RegularTimePeriod} subclasses. 051 */ 052public class Quarter extends RegularTimePeriod implements Serializable { 053 054 /** For serialization. */ 055 private static final long serialVersionUID = 3810061714380888671L; 056 057 /** Constant for quarter 1. */ 058 public static final int FIRST_QUARTER = 1; 059 060 /** Constant for quarter 4. */ 061 public static final int LAST_QUARTER = 4; 062 063 /** The first month in each quarter. */ 064 public static final int[] FIRST_MONTH_IN_QUARTER = { 065 0, MonthConstants.JANUARY, MonthConstants.APRIL, MonthConstants.JULY, 066 MonthConstants.OCTOBER 067 }; 068 069 /** The last month in each quarter. */ 070 public static final int[] LAST_MONTH_IN_QUARTER = { 071 0, MonthConstants.MARCH, MonthConstants.JUNE, MonthConstants.SEPTEMBER, 072 MonthConstants.DECEMBER 073 }; 074 075 /** The year in which the quarter falls. */ 076 private short year; 077 078 /** The quarter (1-4). */ 079 private byte quarter; 080 081 /** The first millisecond. */ 082 private long firstMillisecond; 083 084 /** The last millisecond. */ 085 private long lastMillisecond; 086 087 /** 088 * Constructs a new Quarter, based on the current system date/time. 089 * The time zone and locale are determined by the calendar 090 * returned by {@link RegularTimePeriod#getCalendarInstance()}. 091 */ 092 public Quarter() { 093 this(new Date()); 094 } 095 096 /** 097 * Constructs a new quarter. 098 * The time zone and locale are determined by the calendar 099 * returned by {@link RegularTimePeriod#getCalendarInstance()}. 100 * 101 * @param year the year (1900 to 9999). 102 * @param quarter the quarter (1 to 4). 103 */ 104 public Quarter(int quarter, int year) { 105 if ((quarter < FIRST_QUARTER) || (quarter > LAST_QUARTER)) { 106 throw new IllegalArgumentException("Quarter outside valid range."); 107 } 108 this.year = (short) year; 109 this.quarter = (byte) quarter; 110 peg(getCalendarInstance()); 111 } 112 113 /** 114 * Constructs a new quarter. 115 * The time zone and locale are determined by the calendar 116 * returned by {@link RegularTimePeriod#getCalendarInstance()}. 117 * 118 * @param quarter the quarter (1 to 4). 119 * @param year the year (1900 to 9999). 120 */ 121 public Quarter(int quarter, Year year) { 122 if ((quarter < FIRST_QUARTER) || (quarter > LAST_QUARTER)) { 123 throw new IllegalArgumentException("Quarter outside valid range."); 124 } 125 this.year = (short) year.getYear(); 126 this.quarter = (byte) quarter; 127 peg(getCalendarInstance()); 128 } 129 130 /** 131 * Constructs a new instance, based on a date/time. 132 * The time zone and locale are determined by the calendar 133 * returned by {@link RegularTimePeriod#getCalendarInstance()}. 134 * 135 * @param time the date/time ({@code null} not permitted). 136 * 137 * @see #Quarter(Date, TimeZone, Locale) 138 */ 139 public Quarter(Date time) { 140 this(time, getCalendarInstance()); 141 } 142 143 /** 144 * Creates a new {@code Quarter} instance, using the specified 145 * zone and locale. 146 * 147 * @param time the current time. 148 * @param zone the time zone. 149 * @param locale the locale. 150 * 151 * @since 1.0.12 152 */ 153 public Quarter(Date time, TimeZone zone, Locale locale) { 154 Calendar calendar = Calendar.getInstance(zone, locale); 155 calendar.setTime(time); 156 int month = calendar.get(Calendar.MONTH) + 1; 157 this.quarter = (byte) SerialDate.monthCodeToQuarter(month); 158 this.year = (short) calendar.get(Calendar.YEAR); 159 peg(calendar); 160 } 161 162 /** 163 * Constructs a new instance, based on a particular date/time. 164 * The time zone and locale are determined by the {@code calendar} 165 * parameter. 166 * 167 * @param time the date/time ({@code null} not permitted). 168 * @param calendar the calendar to use for calculations ({@code null} not permitted). 169 */ 170 public Quarter(Date time, Calendar calendar) { 171 calendar.setTime(time); 172 int month = calendar.get(Calendar.MONTH) + 1; 173 this.quarter = (byte) SerialDate.monthCodeToQuarter(month); 174 this.year = (short) calendar.get(Calendar.YEAR); 175 peg(calendar); 176 } 177 178 /** 179 * Returns the quarter. 180 * 181 * @return The quarter. 182 */ 183 public int getQuarter() { 184 return this.quarter; 185 } 186 187 /** 188 * Returns the year. 189 * 190 * @return The year. 191 */ 192 public Year getYear() { 193 return new Year(this.year); 194 } 195 196 /** 197 * Returns the year. 198 * 199 * @return The year. 200 * 201 * @since 1.0.3 202 */ 203 public int getYearValue() { 204 return this.year; 205 } 206 207 /** 208 * Returns the first millisecond of the quarter. 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 quarter. 214 * 215 * @see #getLastMillisecond() 216 */ 217 @Override 218 public long getFirstMillisecond() { 219 return this.firstMillisecond; 220 } 221 222 /** 223 * Returns the last millisecond of the quarter. This will be 224 * determined relative to the time zone specified in the constructor, or 225 * in the calendar instance passed in the most recent call to the 226 * {@link #peg(Calendar)} method. 227 * 228 * @return The last millisecond of the quarter. 229 * 230 * @see #getFirstMillisecond() 231 */ 232 @Override 233 public long getLastMillisecond() { 234 return this.lastMillisecond; 235 } 236 237 /** 238 * Recalculates the start date/time and end date/time for this time period 239 * relative to the supplied calendar (which incorporates a time zone). 240 * 241 * @param calendar the calendar ({@code null} not permitted). 242 * 243 * @since 1.0.3 244 */ 245 @Override 246 public void peg(Calendar calendar) { 247 this.firstMillisecond = getFirstMillisecond(calendar); 248 this.lastMillisecond = getLastMillisecond(calendar); 249 } 250 251 /** 252 * Returns the quarter preceding this one. 253 * No matter what time zone and locale this instance was created with, 254 * the returned instance will use the default calendar for time 255 * calculations, obtained with {@link RegularTimePeriod#getCalendarInstance()}. 256 * 257 * @return The quarter preceding this one (or {@code null} if this is 258 * Q1 1900). 259 */ 260 @Override 261 public RegularTimePeriod previous() { 262 Quarter result; 263 if (this.quarter > FIRST_QUARTER) { 264 result = new Quarter(this.quarter - 1, this.year); 265 } 266 else { 267 if (this.year > 1900) { 268 result = new Quarter(LAST_QUARTER, this.year - 1); 269 } 270 else { 271 result = null; 272 } 273 } 274 return result; 275 } 276 277 /** 278 * Returns the quarter following this one. 279 * No matter what time zone and locale this instance was created with, 280 * the returned instance will use the default calendar for time 281 * calculations, obtained with {@link RegularTimePeriod#getCalendarInstance()}. 282 * 283 * @return The quarter following this one (or null if this is Q4 9999). 284 */ 285 @Override 286 public RegularTimePeriod next() { 287 Quarter result; 288 if (this.quarter < LAST_QUARTER) { 289 result = new Quarter(this.quarter + 1, this.year); 290 } 291 else { 292 if (this.year < 9999) { 293 result = new Quarter(FIRST_QUARTER, this.year + 1); 294 } 295 else { 296 result = null; 297 } 298 } 299 return result; 300 } 301 302 /** 303 * Returns a serial index number for the quarter. 304 * 305 * @return The serial index number. 306 */ 307 @Override 308 public long getSerialIndex() { 309 return this.year * 4L + this.quarter; 310 } 311 312 /** 313 * Tests the equality of this Quarter object to an arbitrary object. 314 * Returns {@code true} if the target is a Quarter instance 315 * representing the same quarter as this object. In all other cases, 316 * returns {@code false}. 317 * 318 * @param obj the object ({@code null} permitted). 319 * 320 * @return {@code true} if quarter and year of this and the object are 321 * the same. 322 */ 323 @Override 324 public boolean equals(Object obj) { 325 326 if (obj != null) { 327 if (obj instanceof Quarter) { 328 Quarter target = (Quarter) obj; 329 return (this.quarter == target.getQuarter() 330 && (this.year == target.getYearValue())); 331 } 332 else { 333 return false; 334 } 335 } 336 else { 337 return false; 338 } 339 340 } 341 342 /** 343 * Returns a hash code for this object instance. The approach described by 344 * Joshua Bloch in "Effective Java" has been used here: 345 * <p> 346 * {@code http://developer.java.sun.com/developer/Books/effectivejava 347 * /Chapter3.pdf} 348 * 349 * @return A hash code. 350 */ 351 @Override 352 public int hashCode() { 353 int result = 17; 354 result = 37 * result + this.quarter; 355 result = 37 * result + this.year; 356 return result; 357 } 358 359 /** 360 * Returns an integer indicating the order of this Quarter object relative 361 * to the specified object: 362 * 363 * negative == before, zero == same, positive == after. 364 * 365 * @param o1 the object to compare 366 * 367 * @return negative == before, zero == same, positive == after. 368 */ 369 @Override 370 public int compareTo(Object o1) { 371 372 int result; 373 374 // CASE 1 : Comparing to another Quarter object 375 // -------------------------------------------- 376 if (o1 instanceof Quarter) { 377 Quarter q = (Quarter) o1; 378 result = this.year - q.getYearValue(); 379 if (result == 0) { 380 result = this.quarter - q.getQuarter(); 381 } 382 } 383 384 // CASE 2 : Comparing to another TimePeriod object 385 // ----------------------------------------------- 386 else if (o1 instanceof RegularTimePeriod) { 387 // more difficult case - evaluate later... 388 result = 0; 389 } 390 391 // CASE 3 : Comparing to a non-TimePeriod object 392 // --------------------------------------------- 393 else { 394 // consider time periods to be ordered after general objects 395 result = 1; 396 } 397 398 return result; 399 400 } 401 402 /** 403 * Returns a string representing the quarter (e.g. "Q1/2002"). 404 * 405 * @return A string representing the quarter. 406 */ 407 @Override 408 public String toString() { 409 return "Q" + this.quarter + "/" + this.year; 410 } 411 412 /** 413 * Returns the first millisecond in the Quarter, evaluated using the 414 * supplied calendar (which determines the time zone). 415 * 416 * @param calendar the calendar ({@code null} not permitted). 417 * 418 * @return The first millisecond in the Quarter. 419 * 420 * @throws NullPointerException if {@code calendar} is 421 * {@code null}. 422 */ 423 @Override 424 public long getFirstMillisecond(Calendar calendar) { 425 int month = Quarter.FIRST_MONTH_IN_QUARTER[this.quarter]; 426 calendar.set(this.year, month - 1, 1, 0, 0, 0); 427 calendar.set(Calendar.MILLISECOND, 0); 428 return calendar.getTimeInMillis(); 429 } 430 431 /** 432 * Returns the last millisecond of the Quarter, evaluated using the 433 * supplied calendar (which determines the time zone). 434 * 435 * @param calendar the calendar ({@code null} not permitted). 436 * 437 * @return The last millisecond of the Quarter. 438 * 439 * @throws NullPointerException if {@code calendar} is 440 * {@code null}. 441 */ 442 @Override 443 public long getLastMillisecond(Calendar calendar) { 444 int month = Quarter.LAST_MONTH_IN_QUARTER[this.quarter]; 445 int eom = SerialDate.lastDayOfMonth(month, this.year); 446 calendar.set(this.year, month - 1, eom, 23, 59, 59); 447 calendar.set(Calendar.MILLISECOND, 999); 448 return calendar.getTimeInMillis(); 449 } 450 451 /** 452 * Parses the string argument as a quarter. 453 * <P> 454 * This method should accept the following formats: "YYYY-QN" and "QN-YYYY", 455 * where the "-" can be a space, a forward-slash (/), comma or a dash (-). 456 * @param s A string representing the quarter. 457 * 458 * @return The quarter. 459 */ 460 public static Quarter parseQuarter(String s) { 461 462 // find the Q and the integer following it (remove both from the 463 // string)... 464 int i = s.indexOf("Q"); 465 if (i == -1) { 466 throw new TimePeriodFormatException("Missing Q."); 467 } 468 469 if (i == s.length() - 1) { 470 throw new TimePeriodFormatException("Q found at end of string."); 471 } 472 473 String qstr = s.substring(i + 1, i + 2); 474 int quarter = Integer.parseInt(qstr); 475 String remaining = s.substring(0, i) + s.substring(i + 2, s.length()); 476 477 // replace any / , or - with a space 478 remaining = remaining.replace('/', ' '); 479 remaining = remaining.replace(',', ' '); 480 remaining = remaining.replace('-', ' '); 481 482 // parse the string... 483 Year year = Year.parseYear(remaining.trim()); 484 Quarter result = new Quarter(quarter, year); 485 return result; 486 487 } 488 489}