001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2006, 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-2006, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Bryan Scott; 034 * 035 * $Id: TimeSeries.java,v 1.10.2.10 2007/01/17 15:08:40 mungady Exp $ 036 * 037 * Changes 038 * ------- 039 * 11-Oct-2001 : Version 1 (DG); 040 * 14-Nov-2001 : Added listener mechanism (DG); 041 * 15-Nov-2001 : Updated argument checking and exceptions in add() method (DG); 042 * 29-Nov-2001 : Added properties to describe the domain and range (DG); 043 * 07-Dec-2001 : Renamed TimeSeries --> BasicTimeSeries (DG); 044 * 01-Mar-2002 : Updated import statements (DG); 045 * 28-Mar-2002 : Added a method add(TimePeriod, double) (DG); 046 * 27-Aug-2002 : Changed return type of delete method to void (DG); 047 * 04-Oct-2002 : Added itemCount and historyCount attributes, fixed errors 048 * reported by Checkstyle (DG); 049 * 29-Oct-2002 : Added series change notification to addOrUpdate() method (DG); 050 * 28-Jan-2003 : Changed name back to TimeSeries (DG); 051 * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented 052 * Serializable (DG); 053 * 01-May-2003 : Updated equals() method (see bug report 727575) (DG); 054 * 14-Aug-2003 : Added ageHistoryCountItems method (copied existing code for 055 * contents) made a method and added to addOrUpdate. Made a 056 * public method to enable ageing against a specified time 057 * (eg now) as opposed to lastest time in series (BS); 058 * 15-Oct-2003 : Added fix for setItemCount method - see bug report 804425. 059 * Modified exception message in add() method to be more 060 * informative (DG); 061 * 13-Apr-2004 : Added clear() method (DG); 062 * 21-May-2004 : Added an extra addOrUpdate() method (DG); 063 * 15-Jun-2004 : Fixed NullPointerException in equals() method (DG); 064 * 29-Nov-2004 : Fixed bug 1075255 (DG); 065 * 17-Nov-2005 : Renamed historyCount --> maximumItemAge (DG); 066 * 28-Nov-2005 : Changed maximumItemAge from int to long (DG); 067 * 01-Dec-2005 : New add methods accept notify flag (DG); 068 * ------------- JFREECHART 1.0.0 --------------------------------------------- 069 * 24-May-2006 : Improved error handling in createCopy() methods (DG); 070 * 01-Sep-2006 : Fixed bugs in removeAgedItems() methods - see bug report 071 * 1550045 (DG); 072 * 073 */ 074 075 package org.jfree.data.time; 076 077 import java.io.Serializable; 078 import java.lang.reflect.InvocationTargetException; 079 import java.lang.reflect.Method; 080 import java.util.Collection; 081 import java.util.Collections; 082 import java.util.Date; 083 import java.util.List; 084 import java.util.TimeZone; 085 086 import org.jfree.data.general.Series; 087 import org.jfree.data.general.SeriesChangeEvent; 088 import org.jfree.data.general.SeriesException; 089 import org.jfree.util.ObjectUtilities; 090 091 /** 092 * Represents a sequence of zero or more data items in the form (period, value). 093 */ 094 public class TimeSeries extends Series implements Cloneable, Serializable { 095 096 /** For serialization. */ 097 private static final long serialVersionUID = -5032960206869675528L; 098 099 /** Default value for the domain description. */ 100 protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time"; 101 102 /** Default value for the range description. */ 103 protected static final String DEFAULT_RANGE_DESCRIPTION = "Value"; 104 105 /** A description of the domain. */ 106 private String domain; 107 108 /** A description of the range. */ 109 private String range; 110 111 /** The type of period for the data. */ 112 protected Class timePeriodClass; 113 114 /** The list of data items in the series. */ 115 protected List data; 116 117 /** The maximum number of items for the series. */ 118 private int maximumItemCount; 119 120 /** 121 * The maximum age of items for the series, specified as a number of 122 * time periods. 123 */ 124 private long maximumItemAge; 125 126 /** 127 * Creates a new (empty) time series. By default, a daily time series is 128 * created. Use one of the other constructors if you require a different 129 * time period. 130 * 131 * @param name the series name (<code>null</code> not permitted). 132 */ 133 public TimeSeries(String name) { 134 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION, 135 Day.class); 136 } 137 138 /** 139 * Creates a new (empty) time series with the specified name and class 140 * of {@link RegularTimePeriod}. 141 * 142 * @param name the series name (<code>null</code> not permitted). 143 * @param timePeriodClass the type of time period (<code>null</code> not 144 * permitted). 145 */ 146 public TimeSeries(String name, Class timePeriodClass) { 147 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION, 148 timePeriodClass); 149 } 150 151 /** 152 * Creates a new time series that contains no data. 153 * <P> 154 * Descriptions can be specified for the domain and range. One situation 155 * where this is helpful is when generating a chart for the time series - 156 * axis labels can be taken from the domain and range description. 157 * 158 * @param name the name of the series (<code>null</code> not permitted). 159 * @param domain the domain description (<code>null</code> permitted). 160 * @param range the range description (<code>null</code> permitted). 161 * @param timePeriodClass the type of time period (<code>null</code> not 162 * permitted). 163 */ 164 public TimeSeries(String name, String domain, String range, 165 Class timePeriodClass) { 166 super(name); 167 this.domain = domain; 168 this.range = range; 169 this.timePeriodClass = timePeriodClass; 170 this.data = new java.util.ArrayList(); 171 this.maximumItemCount = Integer.MAX_VALUE; 172 this.maximumItemAge = Long.MAX_VALUE; 173 } 174 175 /** 176 * Returns the domain description. 177 * 178 * @return The domain description (possibly <code>null</code>). 179 * 180 * @see #setDomainDescription(String) 181 */ 182 public String getDomainDescription() { 183 return this.domain; 184 } 185 186 /** 187 * Sets the domain description and sends a <code>PropertyChangeEvent</code> 188 * (with the property name <code>Domain</code>) to all registered 189 * property change listeners. 190 * 191 * @param description the description (<code>null</code> permitted). 192 * 193 * @see #getDomainDescription() 194 */ 195 public void setDomainDescription(String description) { 196 String old = this.domain; 197 this.domain = description; 198 firePropertyChange("Domain", old, description); 199 } 200 201 /** 202 * Returns the range description. 203 * 204 * @return The range description (possibly <code>null</code>). 205 * 206 * @see #setRangeDescription(String) 207 */ 208 public String getRangeDescription() { 209 return this.range; 210 } 211 212 /** 213 * Sets the range description and sends a <code>PropertyChangeEvent</code> 214 * (with the property name <code>Range</code>) to all registered listeners. 215 * 216 * @param description the description (<code>null</code> permitted). 217 * 218 * @see #getRangeDescription() 219 */ 220 public void setRangeDescription(String description) { 221 String old = this.range; 222 this.range = description; 223 firePropertyChange("Range", old, description); 224 } 225 226 /** 227 * Returns the number of items in the series. 228 * 229 * @return The item count. 230 */ 231 public int getItemCount() { 232 return this.data.size(); 233 } 234 235 /** 236 * Returns the list of data items for the series (the list contains 237 * {@link TimeSeriesDataItem} objects and is unmodifiable). 238 * 239 * @return The list of data items. 240 */ 241 public List getItems() { 242 return Collections.unmodifiableList(this.data); 243 } 244 245 /** 246 * Returns the maximum number of items that will be retained in the series. 247 * The default value is <code>Integer.MAX_VALUE</code>. 248 * 249 * @return The maximum item count. 250 * 251 * @see #setMaximumItemCount(int) 252 */ 253 public int getMaximumItemCount() { 254 return this.maximumItemCount; 255 } 256 257 /** 258 * Sets the maximum number of items that will be retained in the series. 259 * If you add a new item to the series such that the number of items will 260 * exceed the maximum item count, then the FIRST element in the series is 261 * automatically removed, ensuring that the maximum item count is not 262 * exceeded. 263 * 264 * @param maximum the maximum (requires >= 0). 265 * 266 * @see #getMaximumItemCount() 267 */ 268 public void setMaximumItemCount(int maximum) { 269 if (maximum < 0) { 270 throw new IllegalArgumentException("Negative 'maximum' argument."); 271 } 272 this.maximumItemCount = maximum; 273 int count = this.data.size(); 274 if (count > maximum) { 275 delete(0, count - maximum - 1); 276 } 277 } 278 279 /** 280 * Returns the maximum item age (in time periods) for the series. 281 * 282 * @return The maximum item age. 283 * 284 * @see #setMaximumItemAge(long) 285 */ 286 public long getMaximumItemAge() { 287 return this.maximumItemAge; 288 } 289 290 /** 291 * Sets the number of time units in the 'history' for the series. This 292 * provides one mechanism for automatically dropping old data from the 293 * time series. For example, if a series contains daily data, you might set 294 * the history count to 30. Then, when you add a new data item, all data 295 * items more than 30 days older than the latest value are automatically 296 * dropped from the series. 297 * 298 * @param periods the number of time periods. 299 * 300 * @see #getMaximumItemAge() 301 */ 302 public void setMaximumItemAge(long periods) { 303 if (periods < 0) { 304 throw new IllegalArgumentException("Negative 'periods' argument."); 305 } 306 this.maximumItemAge = periods; 307 removeAgedItems(true); // remove old items and notify if necessary 308 } 309 310 /** 311 * Returns the time period class for this series. 312 * <p> 313 * Only one time period class can be used within a single series (enforced). 314 * If you add a data item with a {@link Year} for the time period, then all 315 * subsequent data items must also have a {@link Year} for the time period. 316 * 317 * @return The time period class (never <code>null</code>). 318 */ 319 public Class getTimePeriodClass() { 320 return this.timePeriodClass; 321 } 322 323 /** 324 * Returns a data item for the series. 325 * 326 * @param index the item index (zero-based). 327 * 328 * @return The data item. 329 * 330 * @see #getDataItem(RegularTimePeriod) 331 */ 332 public TimeSeriesDataItem getDataItem(int index) { 333 return (TimeSeriesDataItem) this.data.get(index); 334 } 335 336 /** 337 * Returns the data item for a specific period. 338 * 339 * @param period the period of interest (<code>null</code> not allowed). 340 * 341 * @return The data item matching the specified period (or 342 * <code>null</code> if there is no match). 343 * 344 * @see #getDataItem(int) 345 */ 346 public TimeSeriesDataItem getDataItem(RegularTimePeriod period) { 347 if (period == null) { 348 throw new IllegalArgumentException("Null 'period' argument"); 349 } 350 TimeSeriesDataItem dummy = new TimeSeriesDataItem(period, 351 Integer.MIN_VALUE); 352 int index = Collections.binarySearch(this.data, dummy); 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 this.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 Object clone = createCopy(0, getItemCount() - 1); 857 return clone; 858 } 859 860 /** 861 * Creates a new timeseries by copying a subset of the data in this time 862 * series. 863 * 864 * @param start the index of the first time period to copy. 865 * @param end the index of the last time period to copy. 866 * 867 * @return A series containing a copy of this times series from start until 868 * end. 869 * 870 * @throws CloneNotSupportedException if there is a cloning problem. 871 */ 872 public TimeSeries createCopy(int start, int end) 873 throws CloneNotSupportedException { 874 875 if (start < 0) { 876 throw new IllegalArgumentException("Requires start >= 0."); 877 } 878 if (end < start) { 879 throw new IllegalArgumentException("Requires start <= end."); 880 } 881 TimeSeries copy = (TimeSeries) super.clone(); 882 883 copy.data = new java.util.ArrayList(); 884 if (this.data.size() > 0) { 885 for (int index = start; index <= end; index++) { 886 TimeSeriesDataItem item 887 = (TimeSeriesDataItem) this.data.get(index); 888 TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone(); 889 try { 890 copy.add(clone); 891 } 892 catch (SeriesException e) { 893 e.printStackTrace(); 894 } 895 } 896 } 897 return copy; 898 } 899 900 /** 901 * Creates a new timeseries by copying a subset of the data in this time 902 * series. 903 * 904 * @param start the first time period to copy. 905 * @param end the last time period to copy. 906 * 907 * @return A time series containing a copy of this time series from start 908 * until end. 909 * 910 * @throws CloneNotSupportedException if there is a cloning problem. 911 */ 912 public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end) 913 throws CloneNotSupportedException { 914 915 if (start == null) { 916 throw new IllegalArgumentException("Null 'start' argument."); 917 } 918 if (end == null) { 919 throw new IllegalArgumentException("Null 'end' argument."); 920 } 921 if (start.compareTo(end) > 0) { 922 throw new IllegalArgumentException( 923 "Requires start on or before end."); 924 } 925 boolean emptyRange = false; 926 int startIndex = getIndex(start); 927 if (startIndex < 0) { 928 startIndex = -(startIndex + 1); 929 if (startIndex == this.data.size()) { 930 emptyRange = true; // start is after last data item 931 } 932 } 933 int endIndex = getIndex(end); 934 if (endIndex < 0) { // end period is not in original series 935 endIndex = -(endIndex + 1); // this is first item AFTER end period 936 endIndex = endIndex - 1; // so this is last item BEFORE end 937 } 938 if (endIndex < 0) { 939 emptyRange = true; 940 } 941 if (emptyRange) { 942 TimeSeries copy = (TimeSeries) super.clone(); 943 copy.data = new java.util.ArrayList(); 944 return copy; 945 } 946 else { 947 return createCopy(startIndex, endIndex); 948 } 949 950 } 951 952 /** 953 * Tests the series for equality with an arbitrary object. 954 * 955 * @param object the object to test against (<code>null</code> permitted). 956 * 957 * @return A boolean. 958 */ 959 public boolean equals(Object object) { 960 if (object == this) { 961 return true; 962 } 963 if (!(object instanceof TimeSeries) || !super.equals(object)) { 964 return false; 965 } 966 TimeSeries s = (TimeSeries) object; 967 if (!ObjectUtilities.equal( 968 getDomainDescription(), s.getDomainDescription() 969 )) { 970 return false; 971 } 972 973 if (!ObjectUtilities.equal( 974 getRangeDescription(), s.getRangeDescription() 975 )) { 976 return false; 977 } 978 979 if (!getClass().equals(s.getClass())) { 980 return false; 981 } 982 983 if (getMaximumItemAge() != s.getMaximumItemAge()) { 984 return false; 985 } 986 987 if (getMaximumItemCount() != s.getMaximumItemCount()) { 988 return false; 989 } 990 991 int count = getItemCount(); 992 if (count != s.getItemCount()) { 993 return false; 994 } 995 for (int i = 0; i < count; i++) { 996 if (!getDataItem(i).equals(s.getDataItem(i))) { 997 return false; 998 } 999 } 1000 return true; 1001 } 1002 1003 /** 1004 * Returns a hash code value for the object. 1005 * 1006 * @return The hashcode 1007 */ 1008 public int hashCode() { 1009 int result; 1010 result = (this.domain != null ? this.domain.hashCode() : 0); 1011 result = 29 * result + (this.range != null ? this.range.hashCode() : 0); 1012 result = 29 * result + (this.timePeriodClass != null 1013 ? this.timePeriodClass.hashCode() : 0); 1014 result = 29 * result + this.data.hashCode(); 1015 result = 29 * result + this.maximumItemCount; 1016 result = 29 * result + (int) this.maximumItemAge; 1017 return result; 1018 } 1019 1020 }