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
082package org.jfree.data.time;
083
084import java.io.Serializable;
085import java.lang.reflect.InvocationTargetException;
086import java.lang.reflect.Method;
087import java.util.Collection;
088import java.util.Collections;
089import java.util.Date;
090import java.util.List;
091import java.util.TimeZone;
092
093import org.jfree.data.general.Series;
094import org.jfree.data.general.SeriesChangeEvent;
095import org.jfree.data.general.SeriesException;
096import 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 */
105public 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}