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