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