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