001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, 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 * Quarter.java 029 * ------------ 030 * (C) Copyright 2001-2008, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * Changes 036 * ------- 037 * 11-Oct-2001 : Version 1 (DG); 038 * 18-Dec-2001 : Changed order of parameters in constructor (DG); 039 * 19-Dec-2001 : Added a new constructor as suggested by Paul English (DG); 040 * 29-Jan-2002 : Added a new method parseQuarter(String) (DG); 041 * 14-Feb-2002 : Fixed bug in Quarter(Date) constructor (DG); 042 * 26-Feb-2002 : Changed getStart(), getMiddle() and getEnd() methods to 043 * evaluate with reference to a particular time zone (DG); 044 * 19-Mar-2002 : Changed API for TimePeriod classes (DG); 045 * 24-Jun-2002 : Removed main method (just test code) (DG); 046 * 10-Sep-2002 : Added getSerialIndex() method (DG); 047 * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG); 048 * 10-Jan-2003 : Changed base class and method names (DG); 049 * 13-Mar-2003 : Moved to com.jrefinery.data.time package, and implemented 050 * Serializable (DG); 051 * 21-Oct-2003 : Added hashCode() method (DG); 052 * 10-Dec-2005 : Fixed argument checking bug (1377239) in constructor (DG); 053 * ------------- JFREECHART 1.0.x --------------------------------------------- 054 * 05-Oct-2006 : Updated API docs (DG); 055 * 06-Oct-2006 : Refactored to cache first and last millisecond values (DG); 056 * 16-Sep-2008 : Deprecated DEFAULT_TIME_ZONE (DG); 057 * 058 */ 059 060 package org.jfree.data.time; 061 062 import java.io.Serializable; 063 import java.util.Calendar; 064 import java.util.Date; 065 import java.util.TimeZone; 066 067 import org.jfree.date.MonthConstants; 068 import org.jfree.date.SerialDate; 069 070 /** 071 * Defines a quarter (in a given year). The range supported is Q1 1900 to 072 * Q4 9999. This class is immutable, which is a requirement for all 073 * {@link RegularTimePeriod} subclasses. 074 */ 075 public class Quarter extends RegularTimePeriod implements Serializable { 076 077 /** For serialization. */ 078 private static final long serialVersionUID = 3810061714380888671L; 079 080 /** Constant for quarter 1. */ 081 public static final int FIRST_QUARTER = 1; 082 083 /** Constant for quarter 4. */ 084 public static final int LAST_QUARTER = 4; 085 086 /** The first month in each quarter. */ 087 public static final int[] FIRST_MONTH_IN_QUARTER = { 088 0, MonthConstants.JANUARY, MonthConstants.APRIL, MonthConstants.JULY, 089 MonthConstants.OCTOBER 090 }; 091 092 /** The last month in each quarter. */ 093 public static final int[] LAST_MONTH_IN_QUARTER = { 094 0, MonthConstants.MARCH, MonthConstants.JUNE, MonthConstants.SEPTEMBER, 095 MonthConstants.DECEMBER 096 }; 097 098 /** The year in which the quarter falls. */ 099 private short year; 100 101 /** The quarter (1-4). */ 102 private byte quarter; 103 104 /** The first millisecond. */ 105 private long firstMillisecond; 106 107 /** The last millisecond. */ 108 private long lastMillisecond; 109 110 /** 111 * Constructs a new Quarter, based on the current system date/time. 112 */ 113 public Quarter() { 114 this(new Date()); 115 } 116 117 /** 118 * Constructs a new quarter. 119 * 120 * @param year the year (1900 to 9999). 121 * @param quarter the quarter (1 to 4). 122 */ 123 public Quarter(int quarter, int year) { 124 if ((quarter < FIRST_QUARTER) || (quarter > LAST_QUARTER)) { 125 throw new IllegalArgumentException("Quarter outside valid range."); 126 } 127 this.year = (short) year; 128 this.quarter = (byte) quarter; 129 peg(Calendar.getInstance()); 130 } 131 132 /** 133 * Constructs a new quarter. 134 * 135 * @param quarter the quarter (1 to 4). 136 * @param year the year (1900 to 9999). 137 */ 138 public Quarter(int quarter, Year year) { 139 if ((quarter < FIRST_QUARTER) || (quarter > LAST_QUARTER)) { 140 throw new IllegalArgumentException("Quarter outside valid range."); 141 } 142 this.year = (short) year.getYear(); 143 this.quarter = (byte) quarter; 144 peg(Calendar.getInstance()); 145 } 146 147 /** 148 * Constructs a new instance, based on a date/time and the default time 149 * zone. 150 * 151 * @param time the date/time (<code>null</code> not permitted). 152 * 153 * @see #Quarter(Date, TimeZone) 154 */ 155 public Quarter(Date time) { 156 this(time, TimeZone.getDefault()); 157 } 158 159 /** 160 * Constructs a Quarter, based on a date/time and time zone. 161 * 162 * @param time the date/time. 163 * @param zone the zone (<code>null</code> not permitted). 164 */ 165 public Quarter(Date time, TimeZone zone) { 166 // FIXME: need a locale as well as a timezone 167 Calendar calendar = Calendar.getInstance(zone); 168 calendar.setTime(time); 169 int month = calendar.get(Calendar.MONTH) + 1; 170 this.quarter = (byte) SerialDate.monthCodeToQuarter(month); 171 this.year = (short) calendar.get(Calendar.YEAR); 172 peg(calendar); 173 } 174 175 /** 176 * Returns the quarter. 177 * 178 * @return The quarter. 179 */ 180 public int getQuarter() { 181 return this.quarter; 182 } 183 184 /** 185 * Returns the year. 186 * 187 * @return The year. 188 */ 189 public Year getYear() { 190 return new Year(this.year); 191 } 192 193 /** 194 * Returns the year. 195 * 196 * @return The year. 197 * 198 * @since 1.0.3 199 */ 200 public int getYearValue() { 201 return this.year; 202 } 203 204 /** 205 * Returns the first millisecond of the quarter. This will be determined 206 * relative to the time zone specified in the constructor, or in the 207 * calendar instance passed in the most recent call to the 208 * {@link #peg(Calendar)} method. 209 * 210 * @return The first millisecond of the quarter. 211 * 212 * @see #getLastMillisecond() 213 */ 214 public long getFirstMillisecond() { 215 return this.firstMillisecond; 216 } 217 218 /** 219 * Returns the last millisecond of the quarter. This will be 220 * determined relative to the time zone specified in the constructor, or 221 * in the calendar instance passed in the most recent call to the 222 * {@link #peg(Calendar)} method. 223 * 224 * @return The last millisecond of the quarter. 225 * 226 * @see #getFirstMillisecond() 227 */ 228 public long getLastMillisecond() { 229 return this.lastMillisecond; 230 } 231 232 /** 233 * Recalculates the start date/time and end date/time for this time period 234 * relative to the supplied calendar (which incorporates a time zone). 235 * 236 * @param calendar the calendar (<code>null</code> not permitted). 237 * 238 * @since 1.0.3 239 */ 240 public void peg(Calendar calendar) { 241 this.firstMillisecond = getFirstMillisecond(calendar); 242 this.lastMillisecond = getLastMillisecond(calendar); 243 } 244 245 /** 246 * Returns the quarter preceding this one. 247 * 248 * @return The quarter preceding this one (or <code>null</code> if this is 249 * Q1 1900). 250 */ 251 public RegularTimePeriod previous() { 252 Quarter result; 253 if (this.quarter > FIRST_QUARTER) { 254 result = new Quarter(this.quarter - 1, this.year); 255 } 256 else { 257 if (this.year > 1900) { 258 result = new Quarter(LAST_QUARTER, this.year - 1); 259 } 260 else { 261 result = null; 262 } 263 } 264 return result; 265 } 266 267 /** 268 * Returns the quarter following this one. 269 * 270 * @return The quarter following this one (or null if this is Q4 9999). 271 */ 272 public RegularTimePeriod next() { 273 Quarter result; 274 if (this.quarter < LAST_QUARTER) { 275 result = new Quarter(this.quarter + 1, this.year); 276 } 277 else { 278 if (this.year < 9999) { 279 result = new Quarter(FIRST_QUARTER, this.year + 1); 280 } 281 else { 282 result = null; 283 } 284 } 285 return result; 286 } 287 288 /** 289 * Returns a serial index number for the quarter. 290 * 291 * @return The serial index number. 292 */ 293 public long getSerialIndex() { 294 return this.year * 4L + this.quarter; 295 } 296 297 /** 298 * Tests the equality of this Quarter object to an arbitrary object. 299 * Returns <code>true</code> if the target is a Quarter instance 300 * representing the same quarter as this object. In all other cases, 301 * returns <code>false</code>. 302 * 303 * @param obj the object (<code>null</code> permitted). 304 * 305 * @return <code>true</code> if quarter and year of this and the object are 306 * the same. 307 */ 308 public boolean equals(Object obj) { 309 310 if (obj != null) { 311 if (obj instanceof Quarter) { 312 Quarter target = (Quarter) obj; 313 return (this.quarter == target.getQuarter() 314 && (this.year == target.getYearValue())); 315 } 316 else { 317 return false; 318 } 319 } 320 else { 321 return false; 322 } 323 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</code> 332 * 333 * @return A hash code. 334 */ 335 public int hashCode() { 336 int result = 17; 337 result = 37 * result + this.quarter; 338 result = 37 * result + this.year; 339 return result; 340 } 341 342 /** 343 * Returns an integer indicating the order of this Quarter object relative 344 * to the specified object: 345 * 346 * 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 public int compareTo(Object o1) { 353 354 int result; 355 356 // CASE 1 : Comparing to another Quarter object 357 // -------------------------------------------- 358 if (o1 instanceof Quarter) { 359 Quarter q = (Quarter) o1; 360 result = this.year - q.getYearValue(); 361 if (result == 0) { 362 result = this.quarter - q.getQuarter(); 363 } 364 } 365 366 // CASE 2 : Comparing to another TimePeriod object 367 // ----------------------------------------------- 368 else if (o1 instanceof RegularTimePeriod) { 369 // more difficult case - evaluate later... 370 result = 0; 371 } 372 373 // CASE 3 : Comparing to a non-TimePeriod object 374 // --------------------------------------------- 375 else { 376 // consider time periods to be ordered after general objects 377 result = 1; 378 } 379 380 return result; 381 382 } 383 384 /** 385 * Returns a string representing the quarter (e.g. "Q1/2002"). 386 * 387 * @return A string representing the quarter. 388 */ 389 public String toString() { 390 return "Q" + this.quarter + "/" + this.year; 391 } 392 393 /** 394 * Returns the first millisecond in the Quarter, evaluated using the 395 * supplied calendar (which determines the time zone). 396 * 397 * @param calendar the calendar (<code>null</code> not permitted). 398 * 399 * @return The first millisecond in the Quarter. 400 * 401 * @throws NullPointerException if <code>calendar</code> is 402 * <code>null</code>. 403 */ 404 public long getFirstMillisecond(Calendar calendar) { 405 int month = Quarter.FIRST_MONTH_IN_QUARTER[this.quarter]; 406 calendar.set(this.year, month - 1, 1, 0, 0, 0); 407 calendar.set(Calendar.MILLISECOND, 0); 408 // in the following line, we'd rather call calendar.getTimeInMillis() 409 // to avoid object creation, but that isn't supported in Java 1.3.1 410 return calendar.getTime().getTime(); 411 } 412 413 /** 414 * Returns the last millisecond of the Quarter, evaluated using the 415 * supplied calendar (which determines the time zone). 416 * 417 * @param calendar the calendar (<code>null</code> not permitted). 418 * 419 * @return The last millisecond of the Quarter. 420 * 421 * @throws NullPointerException if <code>calendar</code> is 422 * <code>null</code>. 423 */ 424 public long getLastMillisecond(Calendar calendar) { 425 int month = Quarter.LAST_MONTH_IN_QUARTER[this.quarter]; 426 int eom = SerialDate.lastDayOfMonth(month, this.year); 427 calendar.set(this.year, month - 1, eom, 23, 59, 59); 428 calendar.set(Calendar.MILLISECOND, 999); 429 // in the following line, we'd rather call calendar.getTimeInMillis() 430 // to avoid object creation, but that isn't supported in Java 1.3.1 431 return calendar.getTime().getTime(); 432 } 433 434 /** 435 * Parses the string argument as a quarter. 436 * <P> 437 * This method should accept the following formats: "YYYY-QN" and "QN-YYYY", 438 * where the "-" can be a space, a forward-slash (/), comma or a dash (-). 439 * @param s A string representing the quarter. 440 * 441 * @return The quarter. 442 */ 443 public static Quarter parseQuarter(String s) { 444 445 // find the Q and the integer following it (remove both from the 446 // string)... 447 int i = s.indexOf("Q"); 448 if (i == -1) { 449 throw new TimePeriodFormatException("Missing Q."); 450 } 451 452 if (i == s.length() - 1) { 453 throw new TimePeriodFormatException("Q found at end of string."); 454 } 455 456 String qstr = s.substring(i + 1, i + 2); 457 int quarter = Integer.parseInt(qstr); 458 String remaining = s.substring(0, i) + s.substring(i + 2, s.length()); 459 460 // replace any / , or - with a space 461 remaining = remaining.replace('/', ' '); 462 remaining = remaining.replace(',', ' '); 463 remaining = remaining.replace('-', ' '); 464 465 // parse the string... 466 Year year = Year.parseYear(remaining.trim()); 467 Quarter result = new Quarter(quarter, year); 468 return result; 469 470 } 471 472 }