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