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 * TimeSeries.java 029 * --------------- 030 * (C) Copyright 2001-2008, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Bryan Scott; 034 * Nick Guenther; 035 * 036 * Changes 037 * ------- 038 * 11-Oct-2001 : Version 1 (DG); 039 * 14-Nov-2001 : Added listener mechanism (DG); 040 * 15-Nov-2001 : Updated argument checking and exceptions in add() method (DG); 041 * 29-Nov-2001 : Added properties to describe the domain and range (DG); 042 * 07-Dec-2001 : Renamed TimeSeries --> BasicTimeSeries (DG); 043 * 01-Mar-2002 : Updated import statements (DG); 044 * 28-Mar-2002 : Added a method add(TimePeriod, double) (DG); 045 * 27-Aug-2002 : Changed return type of delete method to void (DG); 046 * 04-Oct-2002 : Added itemCount and historyCount attributes, fixed errors 047 * reported by Checkstyle (DG); 048 * 29-Oct-2002 : Added series change notification to addOrUpdate() method (DG); 049 * 28-Jan-2003 : Changed name back to TimeSeries (DG); 050 * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented 051 * Serializable (DG); 052 * 01-May-2003 : Updated equals() method (see bug report 727575) (DG); 053 * 14-Aug-2003 : Added ageHistoryCountItems method (copied existing code for 054 * contents) made a method and added to addOrUpdate. Made a 055 * public method to enable ageing against a specified time 056 * (eg now) as opposed to lastest time in series (BS); 057 * 15-Oct-2003 : Added fix for setItemCount method - see bug report 804425. 058 * Modified exception message in add() method to be more 059 * informative (DG); 060 * 13-Apr-2004 : Added clear() method (DG); 061 * 21-May-2004 : Added an extra addOrUpdate() method (DG); 062 * 15-Jun-2004 : Fixed NullPointerException in equals() method (DG); 063 * 29-Nov-2004 : Fixed bug 1075255 (DG); 064 * 17-Nov-2005 : Renamed historyCount --> maximumItemAge (DG); 065 * 28-Nov-2005 : Changed maximumItemAge from int to long (DG); 066 * 01-Dec-2005 : New add methods accept notify flag (DG); 067 * ------------- JFREECHART 1.0.x --------------------------------------------- 068 * 24-May-2006 : Improved error handling in createCopy() methods (DG); 069 * 01-Sep-2006 : Fixed bugs in removeAgedItems() methods - see bug report 070 * 1550045 (DG); 071 * 22-Mar-2007 : Simplified getDataItem(RegularTimePeriod) - see patch 1685500 072 * by Nick Guenther (DG); 073 * 31-Oct-2007 : Implemented faster hashCode() (DG); 074 * 21-Nov-2007 : Fixed clone() method (bug 1832432) (DG); 075 * 10-Jan-2008 : Fixed createCopy(RegularTimePeriod, RegularTimePeriod) (bug 076 * 1864222) (DG); 077 * 078 */ 079 080 package org.jfree.data.time; 081 082 import java.io.Serializable; 083 import java.lang.reflect.InvocationTargetException; 084 import java.lang.reflect.Method; 085 import java.util.Collection; 086 import java.util.Collections; 087 import java.util.Date; 088 import java.util.List; 089 import java.util.TimeZone; 090 091 import org.jfree.data.general.Series; 092 import org.jfree.data.general.SeriesChangeEvent; 093 import org.jfree.data.general.SeriesException; 094 import org.jfree.util.ObjectUtilities; 095 096 /** 097 * Represents a sequence of zero or more data items in the form (period, value). 098 */ 099 public class TimeSeries extends Series implements Cloneable, Serializable { 100 101 /** For serialization. */ 102 private static final long serialVersionUID = -5032960206869675528L; 103 104 /** Default value for the domain description. */ 105 protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time"; 106 107 /** Default value for the range description. */ 108 protected static final String DEFAULT_RANGE_DESCRIPTION = "Value"; 109 110 /** A description of the domain. */ 111 private String domain; 112 113 /** A description of the range. */ 114 private String range; 115 116 /** The type of period for the data. */ 117 protected Class timePeriodClass; 118 119 /** The list of data items in the series. */ 120 protected List data; 121 122 /** The maximum number of items for the series. */ 123 private int maximumItemCount; 124 125 /** 126 * The maximum age of items for the series, specified as a number of 127 * time periods. 128 */ 129 private long maximumItemAge; 130 131 /** 132 * Creates a new (empty) time series. By default, a daily time series is 133 * created. Use one of the other constructors if you require a different 134 * time period. 135 * 136 * @param name the series name (<code>null</code> not permitted). 137 */ 138 public TimeSeries(Comparable name) { 139 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION, 140 Day.class); 141 } 142 143 /** 144 * Creates a new (empty) time series with the specified name and class 145 * of {@link RegularTimePeriod}. 146 * 147 * @param name the series name (<code>null</code> not permitted). 148 * @param timePeriodClass the type of time period (<code>null</code> not 149 * permitted). 150 */ 151 public TimeSeries(Comparable name, Class timePeriodClass) { 152 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION, 153 timePeriodClass); 154 } 155 156 /** 157 * Creates a new time series that contains no data. 158 * <P> 159 * Descriptions can be specified for the domain and range. One situation 160 * where this is helpful is when generating a chart for the time series - 161 * axis labels can be taken from the domain and range description. 162 * 163 * @param name the name of the series (<code>null</code> not permitted). 164 * @param domain the domain description (<code>null</code> permitted). 165 * @param range the range description (<code>null</code> permitted). 166 * @param timePeriodClass the type of time period (<code>null</code> not 167 * permitted). 168 */ 169 public TimeSeries(Comparable name, String domain, String range, 170 Class timePeriodClass) { 171 super(name); 172 this.domain = domain; 173 this.range = range; 174 this.timePeriodClass = timePeriodClass; 175 this.data = new java.util.ArrayList(); 176 this.maximumItemCount = Integer.MAX_VALUE; 177 this.maximumItemAge = Long.MAX_VALUE; 178 } 179 180 /** 181 * Returns the domain description. 182 * 183 * @return The domain description (possibly <code>null</code>). 184 * 185 * @see #setDomainDescription(String) 186 */ 187 public String getDomainDescription() { 188 return this.domain; 189 } 190 191 /** 192 * Sets the domain description and sends a <code>PropertyChangeEvent</code> 193 * (with the property name <code>Domain</code>) to all registered 194 * property change listeners. 195 * 196 * @param description the description (<code>null</code> permitted). 197 * 198 * @see #getDomainDescription() 199 */ 200 public void setDomainDescription(String description) { 201 String old = this.domain; 202 this.domain = description; 203 firePropertyChange("Domain", old, description); 204 } 205 206 /** 207 * Returns the range description. 208 * 209 * @return The range description (possibly <code>null</code>). 210 * 211 * @see #setRangeDescription(String) 212 */ 213 public String getRangeDescription() { 214 return this.range; 215 } 216 217 /** 218 * Sets the range description and sends a <code>PropertyChangeEvent</code> 219 * (with the property name <code>Range</code>) to all registered listeners. 220 * 221 * @param description the description (<code>null</code> permitted). 222 * 223 * @see #getRangeDescription() 224 */ 225 public void setRangeDescription(String description) { 226 String old = this.range; 227 this.range = description; 228 firePropertyChange("Range", old, description); 229 } 230 231 /** 232 * Returns the number of items in the series. 233 * 234 * @return The item count. 235 */ 236 public int getItemCount() { 237 return this.data.size(); 238 } 239 240 /** 241 * Returns the list of data items for the series (the list contains 242 * {@link TimeSeriesDataItem} objects and is unmodifiable). 243 * 244 * @return The list of data items. 245 */ 246 public List getItems() { 247 return Collections.unmodifiableList(this.data); 248 } 249 250 /** 251 * Returns the maximum number of items that will be retained in the series. 252 * The default value is <code>Integer.MAX_VALUE</code>. 253 * 254 * @return The maximum item count. 255 * 256 * @see #setMaximumItemCount(int) 257 */ 258 public int getMaximumItemCount() { 259 return this.maximumItemCount; 260 } 261 262 /** 263 * Sets the maximum number of items that will be retained in the series. 264 * If you add a new item to the series such that the number of items will 265 * exceed the maximum item count, then the FIRST element in the series is 266 * automatically removed, ensuring that the maximum item count is not 267 * exceeded. 268 * 269 * @param maximum the maximum (requires >= 0). 270 * 271 * @see #getMaximumItemCount() 272 */ 273 public void setMaximumItemCount(int maximum) { 274 if (maximum < 0) { 275 throw new IllegalArgumentException("Negative 'maximum' argument."); 276 } 277 this.maximumItemCount = maximum; 278 int count = this.data.size(); 279 if (count > maximum) { 280 delete(0, count - maximum - 1); 281 } 282 } 283 284 /** 285 * Returns the maximum item age (in time periods) for the series. 286 * 287 * @return The maximum item age. 288 * 289 * @see #setMaximumItemAge(long) 290 */ 291 public long getMaximumItemAge() { 292 return this.maximumItemAge; 293 } 294 295 /** 296 * Sets the number of time units in the 'history' for the series. This 297 * provides one mechanism for automatically dropping old data from the 298 * time series. For example, if a series contains daily data, you might set 299 * the history count to 30. Then, when you add a new data item, all data 300 * items more than 30 days older than the latest value are automatically 301 * dropped from the series. 302 * 303 * @param periods the number of time periods. 304 * 305 * @see #getMaximumItemAge() 306 */ 307 public void setMaximumItemAge(long periods) { 308 if (periods < 0) { 309 throw new IllegalArgumentException("Negative 'periods' argument."); 310 } 311 this.maximumItemAge = periods; 312 removeAgedItems(true); // remove old items and notify if necessary 313 } 314 315 /** 316 * Returns the time period class for this series. 317 * <p> 318 * Only one time period class can be used within a single series (enforced). 319 * If you add a data item with a {@link Year} for the time period, then all 320 * subsequent data items must also have a {@link Year} for the time period. 321 * 322 * @return The time period class (never <code>null</code>). 323 */ 324 public Class getTimePeriodClass() { 325 return this.timePeriodClass; 326 } 327 328 /** 329 * Returns a data item for the series. 330 * 331 * @param index the item index (zero-based). 332 * 333 * @return The data item. 334 * 335 * @see #getDataItem(RegularTimePeriod) 336 */ 337 public TimeSeriesDataItem getDataItem(int index) { 338 return (TimeSeriesDataItem) this.data.get(index); 339 } 340 341 /** 342 * Returns the data item for a specific period. 343 * 344 * @param period the period of interest (<code>null</code> not allowed). 345 * 346 * @return The data item matching the specified period (or 347 * <code>null</code> if there is no match). 348 * 349 * @see #getDataItem(int) 350 */ 351 public TimeSeriesDataItem getDataItem(RegularTimePeriod period) { 352 int index = getIndex(period); 353 if (index >= 0) { 354 return (TimeSeriesDataItem) this.data.get(index); 355 } 356 else { 357 return null; 358 } 359 } 360 361 /** 362 * Returns the time period at the specified index. 363 * 364 * @param index the index of the data item. 365 * 366 * @return The time period. 367 */ 368 public RegularTimePeriod getTimePeriod(int index) { 369 return getDataItem(index).getPeriod(); 370 } 371 372 /** 373 * Returns a time period that would be the next in sequence on the end of 374 * the time series. 375 * 376 * @return The next time period. 377 */ 378 public RegularTimePeriod getNextTimePeriod() { 379 RegularTimePeriod last = getTimePeriod(getItemCount() - 1); 380 return last.next(); 381 } 382 383 /** 384 * Returns a collection of all the time periods in the time series. 385 * 386 * @return A collection of all the time periods. 387 */ 388 public Collection getTimePeriods() { 389 Collection result = new java.util.ArrayList(); 390 for (int i = 0; i < getItemCount(); i++) { 391 result.add(getTimePeriod(i)); 392 } 393 return result; 394 } 395 396 /** 397 * Returns a collection of time periods in the specified series, but not in 398 * this series, and therefore unique to the specified series. 399 * 400 * @param series the series to check against this one. 401 * 402 * @return The unique time periods. 403 */ 404 public Collection getTimePeriodsUniqueToOtherSeries(TimeSeries series) { 405 406 Collection result = new java.util.ArrayList(); 407 for (int i = 0; i < series.getItemCount(); i++) { 408 RegularTimePeriod period = series.getTimePeriod(i); 409 int index = getIndex(period); 410 if (index < 0) { 411 result.add(period); 412 } 413 } 414 return result; 415 416 } 417 418 /** 419 * Returns the index for the item (if any) that corresponds to a time 420 * period. 421 * 422 * @param period the time period (<code>null</code> not permitted). 423 * 424 * @return The index. 425 */ 426 public int getIndex(RegularTimePeriod period) { 427 if (period == null) { 428 throw new IllegalArgumentException("Null 'period' argument."); 429 } 430 TimeSeriesDataItem dummy = new TimeSeriesDataItem( 431 period, Integer.MIN_VALUE); 432 return Collections.binarySearch(this.data, dummy); 433 } 434 435 /** 436 * Returns the value at the specified index. 437 * 438 * @param index index of a value. 439 * 440 * @return The value (possibly <code>null</code>). 441 */ 442 public Number getValue(int index) { 443 return getDataItem(index).getValue(); 444 } 445 446 /** 447 * Returns the value for a time period. If there is no data item with the 448 * specified period, this method will return <code>null</code>. 449 * 450 * @param period time period (<code>null</code> not permitted). 451 * 452 * @return The value (possibly <code>null</code>). 453 */ 454 public Number getValue(RegularTimePeriod period) { 455 456 int index = getIndex(period); 457 if (index >= 0) { 458 return getValue(index); 459 } 460 else { 461 return null; 462 } 463 464 } 465 466 /** 467 * Adds a data item to the series and sends a 468 * {@link org.jfree.data.general.SeriesChangeEvent} to all registered 469 * listeners. 470 * 471 * @param item the (timeperiod, value) pair (<code>null</code> not 472 * permitted). 473 */ 474 public void add(TimeSeriesDataItem item) { 475 add(item, true); 476 } 477 478 /** 479 * Adds a data item to the series and sends a 480 * {@link org.jfree.data.general.SeriesChangeEvent} to all registered 481 * listeners. 482 * 483 * @param item the (timeperiod, value) pair (<code>null</code> not 484 * permitted). 485 * @param notify notify listeners? 486 */ 487 public void add(TimeSeriesDataItem item, boolean notify) { 488 if (item == null) { 489 throw new IllegalArgumentException("Null 'item' argument."); 490 } 491 if (!item.getPeriod().getClass().equals(this.timePeriodClass)) { 492 StringBuffer b = new StringBuffer(); 493 b.append("You are trying to add data where the time period class "); 494 b.append("is "); 495 b.append(item.getPeriod().getClass().getName()); 496 b.append(", but the TimeSeries is expecting an instance of "); 497 b.append(this.timePeriodClass.getName()); 498 b.append("."); 499 throw new SeriesException(b.toString()); 500 } 501 502 // make the change (if it's not a duplicate time period)... 503 boolean added = false; 504 int count = getItemCount(); 505 if (count == 0) { 506 this.data.add(item); 507 added = true; 508 } 509 else { 510 RegularTimePeriod last = getTimePeriod(getItemCount() - 1); 511 if (item.getPeriod().compareTo(last) > 0) { 512 this.data.add(item); 513 added = true; 514 } 515 else { 516 int index = Collections.binarySearch(this.data, item); 517 if (index < 0) { 518 this.data.add(-index - 1, item); 519 added = true; 520 } 521 else { 522 StringBuffer b = new StringBuffer(); 523 b.append("You are attempting to add an observation for "); 524 b.append("the time period "); 525 b.append(item.getPeriod().toString()); 526 b.append(" but the series already contains an observation"); 527 b.append(" for that time period. Duplicates are not "); 528 b.append("permitted. Try using the addOrUpdate() method."); 529 throw new SeriesException(b.toString()); 530 } 531 } 532 } 533 if (added) { 534 // check if this addition will exceed the maximum item count... 535 if (getItemCount() > this.maximumItemCount) { 536 this.data.remove(0); 537 } 538 539 removeAgedItems(false); // remove old items if necessary, but 540 // don't notify anyone, because that 541 // happens next anyway... 542 if (notify) { 543 fireSeriesChanged(); 544 } 545 } 546 547 } 548 549 /** 550 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 551 * to all registered listeners. 552 * 553 * @param period the time period (<code>null</code> not permitted). 554 * @param value the value. 555 */ 556 public void add(RegularTimePeriod period, double value) { 557 // defer argument checking... 558 add(period, value, true); 559 } 560 561 /** 562 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 563 * to all registered listeners. 564 * 565 * @param period the time period (<code>null</code> not permitted). 566 * @param value the value. 567 * @param notify notify listeners? 568 */ 569 public void add(RegularTimePeriod period, double value, boolean notify) { 570 // defer argument checking... 571 TimeSeriesDataItem item = new TimeSeriesDataItem(period, value); 572 add(item, notify); 573 } 574 575 /** 576 * Adds a new data item to the series and sends 577 * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered 578 * listeners. 579 * 580 * @param period the time period (<code>null</code> not permitted). 581 * @param value the value (<code>null</code> permitted). 582 */ 583 public void add(RegularTimePeriod period, Number value) { 584 // defer argument checking... 585 add(period, value, true); 586 } 587 588 /** 589 * Adds a new data item to the series and sends 590 * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered 591 * listeners. 592 * 593 * @param period the time period (<code>null</code> not permitted). 594 * @param value the value (<code>null</code> permitted). 595 * @param notify notify listeners? 596 */ 597 public void add(RegularTimePeriod period, Number value, boolean notify) { 598 // defer argument checking... 599 TimeSeriesDataItem item = new TimeSeriesDataItem(period, value); 600 add(item, notify); 601 } 602 603 /** 604 * Updates (changes) the value for a time period. Throws a 605 * {@link SeriesException} if the period does not exist. 606 * 607 * @param period the period (<code>null</code> not permitted). 608 * @param value the value (<code>null</code> permitted). 609 */ 610 public void update(RegularTimePeriod period, Number value) { 611 TimeSeriesDataItem temp = new TimeSeriesDataItem(period, value); 612 int index = Collections.binarySearch(this.data, temp); 613 if (index >= 0) { 614 TimeSeriesDataItem pair = (TimeSeriesDataItem) this.data.get(index); 615 pair.setValue(value); 616 fireSeriesChanged(); 617 } 618 else { 619 throw new SeriesException( 620 "TimeSeries.update(TimePeriod, Number): period does not exist." 621 ); 622 } 623 624 } 625 626 /** 627 * Updates (changes) the value of a data item. 628 * 629 * @param index the index of the data item. 630 * @param value the new value (<code>null</code> permitted). 631 */ 632 public void update(int index, Number value) { 633 TimeSeriesDataItem item = getDataItem(index); 634 item.setValue(value); 635 fireSeriesChanged(); 636 } 637 638 /** 639 * Adds or updates data from one series to another. Returns another series 640 * containing the values that were overwritten. 641 * 642 * @param series the series to merge with this. 643 * 644 * @return A series containing the values that were overwritten. 645 */ 646 public TimeSeries addAndOrUpdate(TimeSeries series) { 647 TimeSeries overwritten = new TimeSeries("Overwritten values from: " 648 + getKey(), series.getTimePeriodClass()); 649 for (int i = 0; i < series.getItemCount(); i++) { 650 TimeSeriesDataItem item = series.getDataItem(i); 651 TimeSeriesDataItem oldItem = addOrUpdate(item.getPeriod(), 652 item.getValue()); 653 if (oldItem != null) { 654 overwritten.add(oldItem); 655 } 656 } 657 return overwritten; 658 } 659 660 /** 661 * Adds or updates an item in the times series and sends a 662 * {@link org.jfree.data.general.SeriesChangeEvent} to all registered 663 * listeners. 664 * 665 * @param period the time period to add/update (<code>null</code> not 666 * permitted). 667 * @param value the new value. 668 * 669 * @return A copy of the overwritten data item, or <code>null</code> if no 670 * item was overwritten. 671 */ 672 public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period, 673 double value) { 674 return addOrUpdate(period, new Double(value)); 675 } 676 677 /** 678 * Adds or updates an item in the times series and sends a 679 * {@link org.jfree.data.general.SeriesChangeEvent} to all registered 680 * listeners. 681 * 682 * @param period the time period to add/update (<code>null</code> not 683 * permitted). 684 * @param value the new value (<code>null</code> permitted). 685 * 686 * @return A copy of the overwritten data item, or <code>null</code> if no 687 * item was overwritten. 688 */ 689 public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period, 690 Number value) { 691 692 if (period == null) { 693 throw new IllegalArgumentException("Null 'period' argument."); 694 } 695 TimeSeriesDataItem overwritten = null; 696 697 TimeSeriesDataItem key = new TimeSeriesDataItem(period, value); 698 int index = Collections.binarySearch(this.data, key); 699 if (index >= 0) { 700 TimeSeriesDataItem existing 701 = (TimeSeriesDataItem) this.data.get(index); 702 overwritten = (TimeSeriesDataItem) existing.clone(); 703 existing.setValue(value); 704 removeAgedItems(false); // remove old items if necessary, but 705 // don't notify anyone, because that 706 // happens next anyway... 707 fireSeriesChanged(); 708 } 709 else { 710 this.data.add(-index - 1, new TimeSeriesDataItem(period, value)); 711 712 // check if this addition will exceed the maximum item count... 713 if (getItemCount() > this.maximumItemCount) { 714 this.data.remove(0); 715 } 716 717 removeAgedItems(false); // remove old items if necessary, but 718 // don't notify anyone, because that 719 // happens next anyway... 720 fireSeriesChanged(); 721 } 722 return overwritten; 723 724 } 725 726 /** 727 * Age items in the series. Ensure that the timespan from the youngest to 728 * the oldest record in the series does not exceed maximumItemAge time 729 * periods. Oldest items will be removed if required. 730 * 731 * @param notify controls whether or not a {@link SeriesChangeEvent} is 732 * sent to registered listeners IF any items are removed. 733 */ 734 public void removeAgedItems(boolean notify) { 735 // check if there are any values earlier than specified by the history 736 // count... 737 if (getItemCount() > 1) { 738 long latest = getTimePeriod(getItemCount() - 1).getSerialIndex(); 739 boolean removed = false; 740 while ((latest - getTimePeriod(0).getSerialIndex()) 741 > this.maximumItemAge) { 742 this.data.remove(0); 743 removed = true; 744 } 745 if (removed && notify) { 746 fireSeriesChanged(); 747 } 748 } 749 } 750 751 /** 752 * Age items in the series. Ensure that the timespan from the supplied 753 * time to the oldest record in the series does not exceed history count. 754 * oldest items will be removed if required. 755 * 756 * @param latest the time to be compared against when aging data 757 * (specified in milliseconds). 758 * @param notify controls whether or not a {@link SeriesChangeEvent} is 759 * sent to registered listeners IF any items are removed. 760 */ 761 public void removeAgedItems(long latest, boolean notify) { 762 763 // find the serial index of the period specified by 'latest' 764 long index = Long.MAX_VALUE; 765 try { 766 Method m = RegularTimePeriod.class.getDeclaredMethod( 767 "createInstance", new Class[] {Class.class, Date.class, 768 TimeZone.class}); 769 RegularTimePeriod newest = (RegularTimePeriod) m.invoke( 770 this.timePeriodClass, new Object[] {this.timePeriodClass, 771 new Date(latest), TimeZone.getDefault()}); 772 index = newest.getSerialIndex(); 773 } 774 catch (NoSuchMethodException e) { 775 e.printStackTrace(); 776 } 777 catch (IllegalAccessException e) { 778 e.printStackTrace(); 779 } 780 catch (InvocationTargetException e) { 781 e.printStackTrace(); 782 } 783 784 // check if there are any values earlier than specified by the history 785 // count... 786 boolean removed = false; 787 while (getItemCount() > 0 && (index 788 - getTimePeriod(0).getSerialIndex()) > this.maximumItemAge) { 789 this.data.remove(0); 790 removed = true; 791 } 792 if (removed && notify) { 793 fireSeriesChanged(); 794 } 795 } 796 797 /** 798 * Removes all data items from the series and sends a 799 * {@link SeriesChangeEvent} to all registered listeners. 800 */ 801 public void clear() { 802 if (this.data.size() > 0) { 803 this.data.clear(); 804 fireSeriesChanged(); 805 } 806 } 807 808 /** 809 * Deletes the data item for the given time period and sends a 810 * {@link SeriesChangeEvent} to all registered listeners. If there is no 811 * item with the specified time period, this method does nothing. 812 * 813 * @param period the period of the item to delete (<code>null</code> not 814 * permitted). 815 */ 816 public void delete(RegularTimePeriod period) { 817 int index = getIndex(period); 818 if (index >= 0) { 819 this.data.remove(index); 820 fireSeriesChanged(); 821 } 822 } 823 824 /** 825 * Deletes data from start until end index (end inclusive). 826 * 827 * @param start the index of the first period to delete. 828 * @param end the index of the last period to delete. 829 */ 830 public void delete(int start, int end) { 831 if (end < start) { 832 throw new IllegalArgumentException("Requires start <= end."); 833 } 834 for (int i = 0; i <= (end - start); i++) { 835 this.data.remove(start); 836 } 837 fireSeriesChanged(); 838 } 839 840 /** 841 * Returns a clone of the time series. 842 * <P> 843 * Notes: 844 * <ul> 845 * <li>no need to clone the domain and range descriptions, since String 846 * object is immutable;</li> 847 * <li>we pass over to the more general method clone(start, end).</li> 848 * </ul> 849 * 850 * @return A clone of the time series. 851 * 852 * @throws CloneNotSupportedException not thrown by this class, but 853 * subclasses may differ. 854 */ 855 public Object clone() throws CloneNotSupportedException { 856 TimeSeries clone = (TimeSeries) super.clone(); 857 clone.data = (List) ObjectUtilities.deepClone(this.data); 858 return clone; 859 } 860 861 /** 862 * Creates a new timeseries by copying a subset of the data in this time 863 * series. 864 * 865 * @param start the index of the first time period to copy. 866 * @param end the index of the last time period to copy. 867 * 868 * @return A series containing a copy of this times series from start until 869 * end. 870 * 871 * @throws CloneNotSupportedException if there is a cloning problem. 872 */ 873 public TimeSeries createCopy(int start, int end) 874 throws CloneNotSupportedException { 875 876 if (start < 0) { 877 throw new IllegalArgumentException("Requires start >= 0."); 878 } 879 if (end < start) { 880 throw new IllegalArgumentException("Requires start <= end."); 881 } 882 TimeSeries copy = (TimeSeries) super.clone(); 883 884 copy.data = new java.util.ArrayList(); 885 if (this.data.size() > 0) { 886 for (int index = start; index <= end; index++) { 887 TimeSeriesDataItem item 888 = (TimeSeriesDataItem) this.data.get(index); 889 TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone(); 890 try { 891 copy.add(clone); 892 } 893 catch (SeriesException e) { 894 e.printStackTrace(); 895 } 896 } 897 } 898 return copy; 899 } 900 901 /** 902 * Creates a new timeseries by copying a subset of the data in this time 903 * series. 904 * 905 * @param start the first time period to copy (<code>null</code> not 906 * permitted). 907 * @param end the last time period to copy (<code>null</code> not 908 * permitted). 909 * 910 * @return A time series containing a copy of this time series from start 911 * until end. 912 * 913 * @throws CloneNotSupportedException if there is a cloning problem. 914 */ 915 public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end) 916 throws CloneNotSupportedException { 917 918 if (start == null) { 919 throw new IllegalArgumentException("Null 'start' argument."); 920 } 921 if (end == null) { 922 throw new IllegalArgumentException("Null 'end' argument."); 923 } 924 if (start.compareTo(end) > 0) { 925 throw new IllegalArgumentException( 926 "Requires start on or before end."); 927 } 928 boolean emptyRange = false; 929 int startIndex = getIndex(start); 930 if (startIndex < 0) { 931 startIndex = -(startIndex + 1); 932 if (startIndex == this.data.size()) { 933 emptyRange = true; // start is after last data item 934 } 935 } 936 int endIndex = getIndex(end); 937 if (endIndex < 0) { // end period is not in original series 938 endIndex = -(endIndex + 1); // this is first item AFTER end period 939 endIndex = endIndex - 1; // so this is last item BEFORE end 940 } 941 if ((endIndex < 0) || (endIndex < startIndex)) { 942 emptyRange = true; 943 } 944 if (emptyRange) { 945 TimeSeries copy = (TimeSeries) super.clone(); 946 copy.data = new java.util.ArrayList(); 947 return copy; 948 } 949 else { 950 return createCopy(startIndex, endIndex); 951 } 952 953 } 954 955 /** 956 * Tests the series for equality with an arbitrary object. 957 * 958 * @param object the object to test against (<code>null</code> permitted). 959 * 960 * @return A boolean. 961 */ 962 public boolean equals(Object object) { 963 if (object == this) { 964 return true; 965 } 966 if (!(object instanceof TimeSeries) || !super.equals(object)) { 967 return false; 968 } 969 TimeSeries s = (TimeSeries) object; 970 if (!ObjectUtilities.equal(getDomainDescription(), 971 s.getDomainDescription())) { 972 return false; 973 } 974 975 if (!ObjectUtilities.equal(getRangeDescription(), 976 s.getRangeDescription())) { 977 return false; 978 } 979 980 if (!getClass().equals(s.getClass())) { 981 return false; 982 } 983 984 if (getMaximumItemAge() != s.getMaximumItemAge()) { 985 return false; 986 } 987 988 if (getMaximumItemCount() != s.getMaximumItemCount()) { 989 return false; 990 } 991 992 int count = getItemCount(); 993 if (count != s.getItemCount()) { 994 return false; 995 } 996 for (int i = 0; i < count; i++) { 997 if (!getDataItem(i).equals(s.getDataItem(i))) { 998 return false; 999 } 1000 } 1001 return true; 1002 } 1003 1004 /** 1005 * Returns a hash code value for the object. 1006 * 1007 * @return The hashcode 1008 */ 1009 public int hashCode() { 1010 int result = super.hashCode(); 1011 result = 29 * result + (this.domain != null ? this.domain.hashCode() 1012 : 0); 1013 result = 29 * result + (this.range != null ? this.range.hashCode() : 0); 1014 result = 29 * result + (this.timePeriodClass != null 1015 ? this.timePeriodClass.hashCode() : 0); 1016 // it is too slow to look at every data item, so let's just look at 1017 // the first, middle and last items... 1018 int count = getItemCount(); 1019 if (count > 0) { 1020 TimeSeriesDataItem item = getDataItem(0); 1021 result = 29 * result + item.hashCode(); 1022 } 1023 if (count > 1) { 1024 TimeSeriesDataItem item = getDataItem(count - 1); 1025 result = 29 * result + item.hashCode(); 1026 } 1027 if (count > 2) { 1028 TimeSeriesDataItem item = getDataItem(count / 2); 1029 result = 29 * result + item.hashCode(); 1030 } 1031 result = 29 * result + this.maximumItemCount; 1032 result = 29 * result + (int) this.maximumItemAge; 1033 return result; 1034 } 1035 1036 }