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