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 * Week.java 029 * --------- 030 * (C) Copyright 2001-2022, by David Gilbert and Contributors. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Aimin Han; 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.internal.Args; 045 046/** 047 * A calendar week. All years are considered to have 53 weeks, numbered from 1 048 * to 53, although in many cases the 53rd week is empty. Most of the time, the 049 * 1st week of the year *begins* in the previous calendar year, but it always 050 * finishes in the current year (this behaviour matches the workings of the 051 * {@code GregorianCalendar} class). 052 * <P> 053 * This class is immutable, which is a requirement for all 054 * {@link RegularTimePeriod} subclasses. 055 */ 056public class Week extends RegularTimePeriod implements Serializable { 057 058 /** For serialization. */ 059 private static final long serialVersionUID = 1856387786939865061L; 060 061 /** Constant for the first week in the year. */ 062 public static final int FIRST_WEEK_IN_YEAR = 1; 063 064 /** Constant for the last week in the year. */ 065 public static final int LAST_WEEK_IN_YEAR = 53; 066 067 /** The year in which the week falls. */ 068 private short year; 069 070 /** The week (1-53). */ 071 private byte week; 072 073 /** The first millisecond. */ 074 private long firstMillisecond; 075 076 /** The last millisecond. */ 077 private long lastMillisecond; 078 079 /** 080 * Creates a new time period for the week in which the current system 081 * date/time falls. 082 * The time zone and locale are determined by the calendar 083 * returned by {@link RegularTimePeriod#getCalendarInstance()}. 084 */ 085 public Week() { 086 this(new Date()); 087 } 088 089 /** 090 * Creates a time period representing the week in the specified year. 091 * The time zone and locale are determined by the calendar 092 * returned by {@link RegularTimePeriod#getCalendarInstance()}. 093 * 094 * @param week the week (1 to 53). 095 * @param year the year (1900 to 9999). 096 */ 097 public Week(int week, int year) { 098 if ((week < FIRST_WEEK_IN_YEAR) || (week > LAST_WEEK_IN_YEAR)) { 099 throw new IllegalArgumentException( 100 "The 'week' argument must be in the range 1 - 53."); 101 } 102 this.week = (byte) week; 103 this.year = (short) year; 104 peg(getCalendarInstance()); 105 } 106 107 /** 108 * Creates a time period representing the week in the specified year. 109 * The time zone and locale are determined by the calendar 110 * returned by {@link RegularTimePeriod#getCalendarInstance()}. 111 * 112 * @param week the week (1 to 53). 113 * @param year the year (1900 to 9999). 114 */ 115 public Week(int week, Year year) { 116 if ((week < FIRST_WEEK_IN_YEAR) || (week > LAST_WEEK_IN_YEAR)) { 117 throw new IllegalArgumentException( 118 "The 'week' argument must be in the range 1 - 53."); 119 } 120 this.week = (byte) week; 121 this.year = (short) year.getYear(); 122 peg(getCalendarInstance()); 123 } 124 125 /** 126 * Creates a time period for the week in which the specified date/time 127 * falls. 128 * The time zone and locale are determined by the calendar 129 * returned by {@link RegularTimePeriod#getCalendarInstance()}. 130 * The locale can affect the day-of-the-week that marks the beginning 131 * of the week, as well as the minimal number of days in the first week 132 * of the year. 133 * 134 * @param time the time ({@code null} not permitted). 135 * 136 * @see #Week(Date, TimeZone, Locale) 137 */ 138 public Week(Date time) { 139 // defer argument checking... 140 this(time, getCalendarInstance()); 141 } 142 143 /** 144 * Creates a time period for the week in which the specified date/time 145 * falls, calculated relative to the specified time zone. 146 * 147 * @param time the date/time ({@code null} not permitted). 148 * @param zone the time zone ({@code null} not permitted). 149 * @param locale the locale ({@code null} not permitted). 150 * 151 * @since 1.0.7 152 */ 153 public Week(Date time, TimeZone zone, Locale locale) { 154 Args.nullNotPermitted(time, "time"); 155 Args.nullNotPermitted(zone, "zone"); 156 Args.nullNotPermitted(locale, "locale"); 157 Calendar calendar = Calendar.getInstance(zone, locale); 158 calendar.setTime(time); 159 160 // sometimes the last few days of the year are considered to fall in 161 // the *first* week of the following year. Refer to the Javadocs for 162 // GregorianCalendar. 163 int tempWeek = calendar.get(Calendar.WEEK_OF_YEAR); 164 if (tempWeek == 1 165 && calendar.get(Calendar.MONTH) == Calendar.DECEMBER) { 166 this.week = 1; 167 this.year = (short) (calendar.get(Calendar.YEAR) + 1); 168 } 169 else { 170 this.week = (byte) Math.min(tempWeek, LAST_WEEK_IN_YEAR); 171 int yyyy = calendar.get(Calendar.YEAR); 172 // alternatively, sometimes the first few days of the year are 173 // considered to fall in the *last* week of the previous year... 174 if (calendar.get(Calendar.MONTH) == Calendar.JANUARY 175 && this.week >= 52) { 176 yyyy--; 177 } 178 this.year = (short) yyyy; 179 } 180 peg(calendar); 181 } 182 183 /** 184 * Constructs a new instance, based on a particular date/time. 185 * The time zone and locale are determined by the {@code calendar} 186 * parameter. 187 * 188 * @param time the date/time ({@code null} not permitted). 189 * @param calendar the calendar to use for calculations ({@code null} not permitted). 190 */ 191 public Week(Date time, Calendar calendar) { 192 calendar.setTime(time); 193 194 // sometimes the last few days of the year are considered to fall in 195 // the *first* week of the following year. Refer to the Javadocs for 196 // GregorianCalendar. 197 int tempWeek = calendar.get(Calendar.WEEK_OF_YEAR); 198 if (tempWeek == 1 199 && calendar.get(Calendar.MONTH) == Calendar.DECEMBER) { 200 this.week = 1; 201 this.year = (short) (calendar.get(Calendar.YEAR) + 1); 202 } 203 else { 204 this.week = (byte) Math.min(tempWeek, LAST_WEEK_IN_YEAR); 205 int yyyy = calendar.get(Calendar.YEAR); 206 // alternatively, sometimes the first few days of the year are 207 // considered to fall in the *last* week of the previous year... 208 if (calendar.get(Calendar.MONTH) == Calendar.JANUARY 209 && this.week >= 52) { 210 yyyy--; 211 } 212 this.year = (short) yyyy; 213 } 214 peg(calendar); 215 } 216 217 /** 218 * Returns the year in which the week falls. 219 * 220 * @return The year (never {@code null}). 221 */ 222 public Year getYear() { 223 return new Year(this.year); 224 } 225 226 /** 227 * Returns the year in which the week falls, as an integer value. 228 * 229 * @return The year. 230 */ 231 public int getYearValue() { 232 return this.year; 233 } 234 235 /** 236 * Returns the week. 237 * 238 * @return The week. 239 */ 240 public int getWeek() { 241 return this.week; 242 } 243 244 /** 245 * Returns the first millisecond of the week. This will be determined 246 * relative to the time zone specified in the constructor, or in the 247 * calendar instance passed in the most recent call to the 248 * {@link #peg(Calendar)} method. 249 * 250 * @return The first millisecond of the week. 251 * 252 * @see #getLastMillisecond() 253 */ 254 @Override 255 public long getFirstMillisecond() { 256 return this.firstMillisecond; 257 } 258 259 /** 260 * Returns the last millisecond of the week. This will be 261 * determined relative to the time zone specified in the constructor, or 262 * in the calendar instance passed in the most recent call to the 263 * {@link #peg(Calendar)} method. 264 * 265 * @return The last millisecond of the week. 266 * 267 * @see #getFirstMillisecond() 268 */ 269 @Override 270 public long getLastMillisecond() { 271 return this.lastMillisecond; 272 } 273 274 /** 275 * Recalculates the start date/time and end date/time for this time period 276 * relative to the supplied calendar (which incorporates a time zone 277 * and information about what day is the first day of the week). 278 * 279 * @param calendar the calendar ({@code null} not permitted). 280 * 281 * @since 1.0.3 282 */ 283 @Override 284 public void peg(Calendar calendar) { 285 this.firstMillisecond = getFirstMillisecond(calendar); 286 this.lastMillisecond = getLastMillisecond(calendar); 287 } 288 289 /** 290 * Returns the week preceding this one. This method will return 291 * {@code null} for some lower limit on the range of weeks (currently 292 * week 1, 1900). For week 1 of any year, the previous week is always week 293 * 53, but week 53 may not contain any days (you should check for this). 294 * No matter what time zone and locale this instance was created with, 295 * the returned instance will use the default calendar for time 296 * calculations, obtained with {@link RegularTimePeriod#getCalendarInstance()}. 297 * 298 * @return The preceding week (possibly {@code null}). 299 */ 300 @Override 301 public RegularTimePeriod previous() { 302 303 Week result; 304 if (this.week != FIRST_WEEK_IN_YEAR) { 305 result = new Week(this.week - 1, this.year); 306 } 307 else { 308 // we need to work out if the previous year has 52 or 53 weeks... 309 if (this.year > 1900) { 310 int yy = this.year - 1; 311 Calendar prevYearCalendar = getCalendarInstance(); 312 prevYearCalendar.set(yy, Calendar.DECEMBER, 31); 313 result = new Week(prevYearCalendar.getActualMaximum( 314 Calendar.WEEK_OF_YEAR), yy); 315 } 316 else { 317 result = null; 318 } 319 } 320 return result; 321 322 } 323 324 /** 325 * Returns the week following this one. This method will return 326 * {@code null} for some upper limit on the range of weeks (currently 327 * week 53, 9999). For week 52 of any year, the following week is always 328 * week 53, but week 53 may not contain any days (you should check for 329 * this). 330 * No matter what time zone and locale this instance was created with, 331 * the returned instance will use the default calendar for time 332 * calculations, obtained with {@link RegularTimePeriod#getCalendarInstance()}. 333 * 334 * @return The following week (possibly {@code null}). 335 */ 336 @Override 337 public RegularTimePeriod next() { 338 339 Week result; 340 if (this.week < 52) { 341 result = new Week(this.week + 1, this.year); 342 } 343 else { 344 Calendar calendar = getCalendarInstance(); 345 calendar.set(this.year, Calendar.DECEMBER, 31); 346 int actualMaxWeek 347 = calendar.getActualMaximum(Calendar.WEEK_OF_YEAR); 348 if (this.week < actualMaxWeek) { 349 result = new Week(this.week + 1, this.year); 350 } 351 else { 352 if (this.year < 9999) { 353 result = new Week(FIRST_WEEK_IN_YEAR, this.year + 1); 354 } 355 else { 356 result = null; 357 } 358 } 359 } 360 return result; 361 362 } 363 364 /** 365 * Returns a serial index number for the week. 366 * 367 * @return The serial index number. 368 */ 369 @Override 370 public long getSerialIndex() { 371 return this.year * 53L + this.week; 372 } 373 374 /** 375 * Returns the first millisecond of the week, evaluated using the supplied 376 * calendar (which determines the time zone). 377 * 378 * @param calendar the calendar ({@code null} not permitted). 379 * 380 * @return The first millisecond of the week. 381 * 382 * @throws NullPointerException if {@code calendar} is 383 * {@code null}. 384 */ 385 @Override 386 public long getFirstMillisecond(Calendar calendar) { 387 Calendar c = (Calendar) calendar.clone(); 388 c.clear(); 389 c.set(Calendar.YEAR, this.year); 390 c.set(Calendar.WEEK_OF_YEAR, this.week); 391 c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek()); 392 c.set(Calendar.HOUR, 0); 393 c.set(Calendar.MINUTE, 0); 394 c.set(Calendar.SECOND, 0); 395 c.set(Calendar.MILLISECOND, 0); 396 return c.getTimeInMillis(); 397 } 398 399 /** 400 * Returns the last millisecond of the week, evaluated using the supplied 401 * calendar (which determines the time zone). 402 * 403 * @param calendar the calendar ({@code null} not permitted). 404 * 405 * @return The last millisecond of the week. 406 * 407 * @throws NullPointerException if {@code calendar} is 408 * {@code null}. 409 */ 410 @Override 411 public long getLastMillisecond(Calendar calendar) { 412 Calendar c = (Calendar) calendar.clone(); 413 c.clear(); 414 c.set(Calendar.YEAR, this.year); 415 c.set(Calendar.WEEK_OF_YEAR, this.week + 1); 416 c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek()); 417 c.set(Calendar.HOUR, 0); 418 c.set(Calendar.MINUTE, 0); 419 c.set(Calendar.SECOND, 0); 420 c.set(Calendar.MILLISECOND, 0); 421 return c.getTimeInMillis() - 1; 422 } 423 424 /** 425 * Returns a string representing the week (e.g. "Week 9, 2002"). 426 * 427 * TODO: look at internationalisation. 428 * 429 * @return A string representing the week. 430 */ 431 @Override 432 public String toString() { 433 return "Week " + this.week + ", " + this.year; 434 } 435 436 /** 437 * Tests the equality of this Week object to an arbitrary object. Returns 438 * true if the target is a Week instance representing the same week as this 439 * object. In all other cases, returns false. 440 * 441 * @param obj the object ({@code null} permitted). 442 * 443 * @return {@code true} if week and year of this and object are the 444 * same. 445 */ 446 @Override 447 public boolean equals(Object obj) { 448 449 if (obj == this) { 450 return true; 451 } 452 if (!(obj instanceof Week)) { 453 return false; 454 } 455 Week that = (Week) obj; 456 if (this.week != that.week) { 457 return false; 458 } 459 if (this.year != that.year) { 460 return false; 461 } 462 return true; 463 464 } 465 466 /** 467 * Returns a hash code for this object instance. The approach described by 468 * Joshua Bloch in "Effective Java" has been used here: 469 * <p> 470 * {@code http://developer.java.sun.com/developer/Books/effectivejava 471 * /Chapter3.pdf} 472 * 473 * @return A hash code. 474 */ 475 @Override 476 public int hashCode() { 477 int result = 17; 478 result = 37 * result + this.week; 479 result = 37 * result + this.year; 480 return result; 481 } 482 483 /** 484 * Returns an integer indicating the order of this Week object relative to 485 * the specified object: 486 * 487 * negative == before, zero == same, positive == after. 488 * 489 * @param o1 the object to compare. 490 * 491 * @return negative == before, zero == same, positive == after. 492 */ 493 @Override 494 public int compareTo(Object o1) { 495 496 int result; 497 498 // CASE 1 : Comparing to another Week object 499 // -------------------------------------------- 500 if (o1 instanceof Week) { 501 Week w = (Week) o1; 502 result = this.year - w.getYear().getYear(); 503 if (result == 0) { 504 result = this.week - w.getWeek(); 505 } 506 } 507 508 // CASE 2 : Comparing to another TimePeriod object 509 // ----------------------------------------------- 510 else if (o1 instanceof RegularTimePeriod) { 511 // more difficult case - evaluate later... 512 result = 0; 513 } 514 515 // CASE 3 : Comparing to a non-TimePeriod object 516 // --------------------------------------------- 517 else { 518 // consider time periods to be ordered after general objects 519 result = 1; 520 } 521 522 return result; 523 524 } 525 526 /** 527 * Parses the string argument as a week. 528 * <P> 529 * This method is required to accept the format "YYYY-Wnn". It will also 530 * accept "Wnn-YYYY". Anything else, at the moment, is a bonus. 531 * 532 * @param s string to parse. 533 * 534 * @return {@code null} if the string is not parseable, the week 535 * otherwise. 536 */ 537 public static Week parseWeek(String s) { 538 539 Week result = null; 540 if (s != null) { 541 542 // trim whitespace from either end of the string 543 s = s.trim(); 544 545 int i = Week.findSeparator(s); 546 if (i != -1) { 547 String s1 = s.substring(0, i).trim(); 548 String s2 = s.substring(i + 1, s.length()).trim(); 549 550 Year y = Week.evaluateAsYear(s1); 551 int w; 552 if (y != null) { 553 w = Week.stringToWeek(s2); 554 if (w == -1) { 555 throw new TimePeriodFormatException( 556 "Can't evaluate the week."); 557 } 558 result = new Week(w, y); 559 } 560 else { 561 y = Week.evaluateAsYear(s2); 562 if (y != null) { 563 w = Week.stringToWeek(s1); 564 if (w == -1) { 565 throw new TimePeriodFormatException( 566 "Can't evaluate the week."); 567 } 568 result = new Week(w, y); 569 } 570 else { 571 throw new TimePeriodFormatException( 572 "Can't evaluate the year."); 573 } 574 } 575 576 } 577 else { 578 throw new TimePeriodFormatException( 579 "Could not find separator."); 580 } 581 582 } 583 return result; 584 585 } 586 587 /** 588 * Finds the first occurrence of ' ', '-', ',' or '.' 589 * 590 * @param s the string to parse. 591 * 592 * @return {@code -1} if none of the characters was found, the 593 * index of the first occurrence otherwise. 594 */ 595 private static int findSeparator(String s) { 596 597 int result = s.indexOf('-'); 598 if (result == -1) { 599 result = s.indexOf(','); 600 } 601 if (result == -1) { 602 result = s.indexOf(' '); 603 } 604 if (result == -1) { 605 result = s.indexOf('.'); 606 } 607 return result; 608 } 609 610 /** 611 * Creates a year from a string, or returns null (format exceptions 612 * suppressed). 613 * 614 * @param s string to parse. 615 * 616 * @return {@code null} if the string is not parseable, the year 617 * otherwise. 618 */ 619 private static Year evaluateAsYear(String s) { 620 621 Year result = null; 622 try { 623 result = Year.parseYear(s); 624 } 625 catch (TimePeriodFormatException e) { 626 // suppress 627 } 628 return result; 629 630 } 631 632 /** 633 * Converts a string to a week. 634 * 635 * @param s the string to parse. 636 * @return {@code -1} if the string does not contain a week number, 637 * the number of the week otherwise. 638 */ 639 private static int stringToWeek(String s) { 640 641 int result = -1; 642 s = s.replace('W', ' '); 643 s = s.trim(); 644 try { 645 result = Integer.parseInt(s); 646 if ((result < 1) || (result > LAST_WEEK_IN_YEAR)) { 647 result = -1; 648 } 649 } 650 catch (NumberFormatException e) { 651 // suppress 652 } 653 return result; 654 655 } 656 657}