001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2011, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ---------------
028 * TimeSeries.java
029 * ---------------
030 * (C) Copyright 2001-2011, 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 * 26-May-2009 : Added cache for minY and maxY values (DG);
080 * 09-Jun-2009 : Ensure that TimeSeriesDataItem objects used in underlying
081 *               storage are cloned to keep series isolated from external
082 *               changes (DG);
083 * 10-Jun-2009 : Added addOrUpdate(TimeSeriesDataItem) method (DG);
084 * 31-Aug-2009 : Clear minY and maxY cache values in createCopy (DG);
085 * 
086 */
087
088package org.jfree.data.time;
089
090import java.io.Serializable;
091import java.lang.reflect.InvocationTargetException;
092import java.lang.reflect.Method;
093import java.util.Collection;
094import java.util.Collections;
095import java.util.Date;
096import java.util.Iterator;
097import java.util.List;
098import java.util.TimeZone;
099
100import org.jfree.data.general.Series;
101import org.jfree.data.general.SeriesChangeEvent;
102import org.jfree.data.general.SeriesException;
103import org.jfree.util.ObjectUtilities;
104
105/**
106 * Represents a sequence of zero or more data items in the form (period, value)
107 * where 'period' is some instance of a subclass of {@link RegularTimePeriod}.
108 * The time series will ensure that (a) all data items have the same type of
109 * period (for example, {@link Day}) and (b) that each period appears at
110 * most one time in the series.
111 */
112public class TimeSeries extends Series implements Cloneable, Serializable {
113
114    /** For serialization. */
115    private static final long serialVersionUID = -5032960206869675528L;
116
117    /** Default value for the domain description. */
118    protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time";
119
120    /** Default value for the range description. */
121    protected static final String DEFAULT_RANGE_DESCRIPTION = "Value";
122
123    /** A description of the domain. */
124    private String domain;
125
126    /** A description of the range. */
127    private String range;
128
129    /** The type of period for the data. */
130    protected Class timePeriodClass;
131
132    /** The list of data items in the series. */
133    protected List data;
134
135    /** The maximum number of items for the series. */
136    private int maximumItemCount;
137
138    /**
139     * The maximum age of items for the series, specified as a number of
140     * time periods.
141     */
142    private long maximumItemAge;
143
144    /**
145     * The minimum y-value in the series.
146     * 
147     * @since 1.0.14
148     */
149    private double minY;
150
151    /**
152     * The maximum y-value in the series.
153     *
154     * @since 1.0.14
155     */
156    private double maxY;
157
158    /**
159     * Creates a new (empty) time series.  By default, a daily time series is
160     * created.  Use one of the other constructors if you require a different
161     * time period.
162     *
163     * @param name  the series name (<code>null</code> not permitted).
164     */
165    public TimeSeries(Comparable name) {
166        this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION);
167    }
168
169    /**
170     * Creates a new time series that contains no data.
171     * <P>
172     * Descriptions can be specified for the domain and range.  One situation
173     * where this is helpful is when generating a chart for the time series -
174     * axis labels can be taken from the domain and range description.
175     *
176     * @param name  the name of the series (<code>null</code> not permitted).
177     * @param domain  the domain description (<code>null</code> permitted).
178     * @param range  the range description (<code>null</code> permitted).
179     *
180     * @since 1.0.13
181     */
182    public TimeSeries(Comparable name, String domain, String range) {
183        super(name);
184        this.domain = domain;
185        this.range = range;
186        this.timePeriodClass = null;
187        this.data = new java.util.ArrayList();
188        this.maximumItemCount = Integer.MAX_VALUE;
189        this.maximumItemAge = Long.MAX_VALUE;
190        this.minY = Double.NaN;
191        this.maxY = Double.NaN;
192    }
193
194    /**
195     * Returns the domain description.
196     *
197     * @return The domain description (possibly <code>null</code>).
198     *
199     * @see #setDomainDescription(String)
200     */
201    public String getDomainDescription() {
202        return this.domain;
203    }
204
205    /**
206     * Sets the domain description and sends a <code>PropertyChangeEvent</code>
207     * (with the property name <code>Domain</code>) to all registered
208     * property change listeners.
209     *
210     * @param description  the description (<code>null</code> permitted).
211     *
212     * @see #getDomainDescription()
213     */
214    public void setDomainDescription(String description) {
215        String old = this.domain;
216        this.domain = description;
217        firePropertyChange("Domain", old, description);
218    }
219
220    /**
221     * Returns the range description.
222     *
223     * @return The range description (possibly <code>null</code>).
224     *
225     * @see #setRangeDescription(String)
226     */
227    public String getRangeDescription() {
228        return this.range;
229    }
230
231    /**
232     * Sets the range description and sends a <code>PropertyChangeEvent</code>
233     * (with the property name <code>Range</code>) to all registered listeners.
234     *
235     * @param description  the description (<code>null</code> permitted).
236     *
237     * @see #getRangeDescription()
238     */
239    public void setRangeDescription(String description) {
240        String old = this.range;
241        this.range = description;
242        firePropertyChange("Range", old, description);
243    }
244
245    /**
246     * Returns the number of items in the series.
247     *
248     * @return The item count.
249     */
250    public int getItemCount() {
251        return this.data.size();
252    }
253
254    /**
255     * Returns the list of data items for the series (the list contains
256     * {@link TimeSeriesDataItem} objects and is unmodifiable).
257     *
258     * @return The list of data items.
259     */
260    public List getItems() {
261        // FIXME: perhaps we should clone the data list
262        return Collections.unmodifiableList(this.data);
263    }
264
265    /**
266     * Returns the maximum number of items that will be retained in the series.
267     * The default value is <code>Integer.MAX_VALUE</code>.
268     *
269     * @return The maximum item count.
270     *
271     * @see #setMaximumItemCount(int)
272     */
273    public int getMaximumItemCount() {
274        return this.maximumItemCount;
275    }
276
277    /**
278     * Sets the maximum number of items that will be retained in the series.
279     * If you add a new item to the series such that the number of items will
280     * exceed the maximum item count, then the FIRST element in the series is
281     * automatically removed, ensuring that the maximum item count is not
282     * exceeded.
283     *
284     * @param maximum  the maximum (requires >= 0).
285     *
286     * @see #getMaximumItemCount()
287     */
288    public void setMaximumItemCount(int maximum) {
289        if (maximum < 0) {
290            throw new IllegalArgumentException("Negative 'maximum' argument.");
291        }
292        this.maximumItemCount = maximum;
293        int count = this.data.size();
294        if (count > maximum) {
295            delete(0, count - maximum - 1);
296        }
297    }
298
299    /**
300     * Returns the maximum item age (in time periods) for the series.
301     *
302     * @return The maximum item age.
303     *
304     * @see #setMaximumItemAge(long)
305     */
306    public long getMaximumItemAge() {
307        return this.maximumItemAge;
308    }
309
310    /**
311     * Sets the number of time units in the 'history' for the series.  This
312     * provides one mechanism for automatically dropping old data from the
313     * time series. For example, if a series contains daily data, you might set
314     * the history count to 30.  Then, when you add a new data item, all data
315     * items more than 30 days older than the latest value are automatically
316     * dropped from the series.
317     *
318     * @param periods  the number of time periods.
319     *
320     * @see #getMaximumItemAge()
321     */
322    public void setMaximumItemAge(long periods) {
323        if (periods < 0) {
324            throw new IllegalArgumentException("Negative 'periods' argument.");
325        }
326        this.maximumItemAge = periods;
327        removeAgedItems(true);  // remove old items and notify if necessary
328    }
329
330    /**
331     * Returns the smallest y-value in the series, ignoring any null and
332     * Double.NaN values.  This method returns Double.NaN if there is no
333     * smallest y-value (for example, when the series is empty).
334     *
335     * @return The smallest y-value.
336     *
337     * @see #getMaxY()
338     *
339     * @since 1.0.14
340     */
341    public double getMinY() {
342        return this.minY;
343    }
344
345    /**
346     * Returns the largest y-value in the series, ignoring any Double.NaN
347     * values.  This method returns Double.NaN if there is no largest y-value
348     * (for example, when the series is empty).
349     *
350     * @return The largest y-value.
351     *
352     * @see #getMinY()
353     *
354     * @since 1.0.14
355     */
356    public double getMaxY() {
357        return this.maxY;
358    }
359
360    /**
361     * Returns the time period class for this series.
362     * <p>
363     * Only one time period class can be used within a single series (enforced).
364     * If you add a data item with a {@link Year} for the time period, then all
365     * subsequent data items must also have a {@link Year} for the time period.
366     *
367     * @return The time period class (may be <code>null</code> but only for
368     *     an empty series).
369     */
370    public Class getTimePeriodClass() {
371        return this.timePeriodClass;
372    }
373
374    /**
375     * Returns a data item from the dataset.  Note that the returned object
376     * is a clone of the item in the series, so modifying it will have no 
377     * effect on the data series.
378     * 
379     * @param index  the item index.
380     * 
381     * @return The data item.
382     */
383    public TimeSeriesDataItem getDataItem(int index) {
384        TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index);
385        return (TimeSeriesDataItem) item.clone();
386    }
387
388    /**
389     * Returns the data item for a specific period.  Note that the returned
390     * object is a clone of the item in the series, so modifying it will have
391     * no effect on the data series.
392     *
393     * @param period  the period of interest (<code>null</code> not allowed).
394     *
395     * @return The data item matching the specified period (or
396     *         <code>null</code> if there is no match).
397     *
398     * @see #getDataItem(int)
399     */
400    public TimeSeriesDataItem getDataItem(RegularTimePeriod period) {
401        int index = getIndex(period);
402        if (index >= 0) {
403            return getDataItem(index);
404        }
405        return null;
406    }
407
408    /**
409     * Returns a data item for the series.  This method returns the object
410     * that is used for the underlying storage - you should not modify the
411     * contents of the returned value unless you know what you are doing.
412     *
413     * @param index  the item index (zero-based).
414     *
415     * @return The data item.
416     *
417     * @see #getDataItem(int)
418     *
419     * @since 1.0.14
420     */
421    TimeSeriesDataItem getRawDataItem(int index) {
422        return (TimeSeriesDataItem) this.data.get(index);
423    }
424
425    /**
426     * Returns a data item for the series.  This method returns the object
427     * that is used for the underlying storage - you should not modify the
428     * contents of the returned value unless you know what you are doing.
429     *
430     * @param period  the item index (zero-based).
431     *
432     * @return The data item.
433     *
434     * @see #getDataItem(RegularTimePeriod)
435     *
436     * @since 1.0.14
437     */
438    TimeSeriesDataItem getRawDataItem(RegularTimePeriod period) {
439        int index = getIndex(period);
440        if (index >= 0) {
441            return (TimeSeriesDataItem) this.data.get(index);
442        }
443        return null;
444    }
445
446    /**
447     * Returns the time period at the specified index.
448     *
449     * @param index  the index of the data item.
450     *
451     * @return The time period.
452     */
453    public RegularTimePeriod getTimePeriod(int index) {
454        return getRawDataItem(index).getPeriod();
455    }
456
457    /**
458     * Returns a time period that would be the next in sequence on the end of
459     * the time series.
460     *
461     * @return The next time period.
462     */
463    public RegularTimePeriod getNextTimePeriod() {
464        RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
465        return last.next();
466    }
467
468    /**
469     * Returns a collection of all the time periods in the time series.
470     *
471     * @return A collection of all the time periods.
472     */
473    public Collection getTimePeriods() {
474        Collection result = new java.util.ArrayList();
475        for (int i = 0; i < getItemCount(); i++) {
476            result.add(getTimePeriod(i));
477        }
478        return result;
479    }
480
481    /**
482     * Returns a collection of time periods in the specified series, but not in
483     * this series, and therefore unique to the specified series.
484     *
485     * @param series  the series to check against this one.
486     *
487     * @return The unique time periods.
488     */
489    public Collection getTimePeriodsUniqueToOtherSeries(TimeSeries series) {
490        Collection result = new java.util.ArrayList();
491        for (int i = 0; i < series.getItemCount(); i++) {
492            RegularTimePeriod period = series.getTimePeriod(i);
493            int index = getIndex(period);
494            if (index < 0) {
495                result.add(period);
496            }
497        }
498        return result;
499    }
500
501    /**
502     * Returns the index for the item (if any) that corresponds to a time
503     * period.
504     *
505     * @param period  the time period (<code>null</code> not permitted).
506     *
507     * @return The index.
508     */
509    public int getIndex(RegularTimePeriod period) {
510        if (period == null) {
511            throw new IllegalArgumentException("Null 'period' argument.");
512        }
513        TimeSeriesDataItem dummy = new TimeSeriesDataItem(
514              period, Integer.MIN_VALUE);
515        return Collections.binarySearch(this.data, dummy);
516    }
517
518    /**
519     * Returns the value at the specified index.
520     *
521     * @param index  index of a value.
522     *
523     * @return The value (possibly <code>null</code>).
524     */
525    public Number getValue(int index) {
526        return getRawDataItem(index).getValue();
527    }
528
529    /**
530     * Returns the value for a time period.  If there is no data item with the
531     * specified period, this method will return <code>null</code>.
532     *
533     * @param period  time period (<code>null</code> not permitted).
534     *
535     * @return The value (possibly <code>null</code>).
536     */
537    public Number getValue(RegularTimePeriod period) {
538        int index = getIndex(period);
539        if (index >= 0) {
540            return getValue(index);
541        }
542        return null;
543    }
544
545    /**
546     * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
547     * all registered listeners.
548     *
549     * @param item  the (timeperiod, value) pair (<code>null</code> not
550     *              permitted).
551     */
552    public void add(TimeSeriesDataItem item) {
553        add(item, true);
554    }
555
556    /**
557     * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
558     * all registered listeners.
559     *
560     * @param item  the (timeperiod, value) pair (<code>null</code> not
561     *              permitted).
562     * @param notify  notify listeners?
563     */
564    public void add(TimeSeriesDataItem item, boolean notify) {
565        if (item == null) {
566            throw new IllegalArgumentException("Null 'item' argument.");
567        }
568        item = (TimeSeriesDataItem) item.clone();
569        Class c = item.getPeriod().getClass();
570        if (this.timePeriodClass == null) {
571            this.timePeriodClass = c;
572        }
573        else if (!this.timePeriodClass.equals(c)) {
574            StringBuffer b = new StringBuffer();
575            b.append("You are trying to add data where the time period class ");
576            b.append("is ");
577            b.append(item.getPeriod().getClass().getName());
578            b.append(", but the TimeSeries is expecting an instance of ");
579            b.append(this.timePeriodClass.getName());
580            b.append(".");
581            throw new SeriesException(b.toString());
582        }
583
584        // make the change (if it's not a duplicate time period)...
585        boolean added = false;
586        int count = getItemCount();
587        if (count == 0) {
588            this.data.add(item);
589            added = true;
590        }
591        else {
592            RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
593            if (item.getPeriod().compareTo(last) > 0) {
594                this.data.add(item);
595                added = true;
596            }
597            else {
598                int index = Collections.binarySearch(this.data, item);
599                if (index < 0) {
600                    this.data.add(-index - 1, item);
601                    added = true;
602                }
603                else {
604                    StringBuffer b = new StringBuffer();
605                    b.append("You are attempting to add an observation for ");
606                    b.append("the time period ");
607                    b.append(item.getPeriod().toString());
608                    b.append(" but the series already contains an observation");
609                    b.append(" for that time period. Duplicates are not ");
610                    b.append("permitted.  Try using the addOrUpdate() method.");
611                    throw new SeriesException(b.toString());
612                }
613            }
614        }
615        if (added) {
616            updateBoundsForAddedItem(item);
617            // check if this addition will exceed the maximum item count...
618            if (getItemCount() > this.maximumItemCount) {
619                TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0);
620                updateBoundsForRemovedItem(d);
621            }
622
623            removeAgedItems(false);  // remove old items if necessary, but
624                                     // don't notify anyone, because that
625                                     // happens next anyway...
626            if (notify) {
627                fireSeriesChanged();
628            }
629        }
630
631    }
632
633    /**
634     * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
635     * to all registered listeners.
636     *
637     * @param period  the time period (<code>null</code> not permitted).
638     * @param value  the value.
639     */
640    public void add(RegularTimePeriod period, double value) {
641        // defer argument checking...
642        add(period, value, true);
643    }
644
645    /**
646     * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
647     * to all registered listeners.
648     *
649     * @param period  the time period (<code>null</code> not permitted).
650     * @param value  the value.
651     * @param notify  notify listeners?
652     */
653    public void add(RegularTimePeriod period, double value, boolean notify) {
654        // defer argument checking...
655        TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
656        add(item, notify);
657    }
658
659    /**
660     * Adds a new data item to the series and sends
661     * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered
662     * listeners.
663     *
664     * @param period  the time period (<code>null</code> not permitted).
665     * @param value  the value (<code>null</code> permitted).
666     */
667    public void add(RegularTimePeriod period, Number value) {
668        // defer argument checking...
669        add(period, value, true);
670    }
671
672    /**
673     * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
674     * to all registered listeners.
675     *
676     * @param period  the time period (<code>null</code> not permitted).
677     * @param value  the value (<code>null</code> permitted).
678     * @param notify  notify listeners?
679     */
680    public void add(RegularTimePeriod period, Number value, boolean notify) {
681        // defer argument checking...
682        TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
683        add(item, notify);
684    }
685
686    /**
687     * Updates (changes) the value for a time period.  Throws a
688     * {@link SeriesException} if the period does not exist.
689     *
690     * @param period  the period (<code>null</code> not permitted).
691     * @param value  the value.
692     * 
693     * @since 1.0.14
694     */
695    public void update(RegularTimePeriod period, double value) {
696      update(period, new Double(value));
697    }
698
699    /**
700     * Updates (changes) the value for a time period.  Throws a
701     * {@link SeriesException} if the period does not exist.
702     *
703     * @param period  the period (<code>null</code> not permitted).
704     * @param value  the value (<code>null</code> permitted).
705     */
706    public void update(RegularTimePeriod period, Number value) {
707        TimeSeriesDataItem temp = new TimeSeriesDataItem(period, value);
708        int index = Collections.binarySearch(this.data, temp);
709        if (index < 0) {
710            throw new SeriesException("There is no existing value for the "
711                    + "specified 'period'.");
712        }
713        update(index, value);
714    }
715
716    /**
717     * Updates (changes) the value of a data item.
718     *
719     * @param index  the index of the data item.
720     * @param value  the new value (<code>null</code> permitted).
721     */
722    public void update(int index, Number value) {
723        TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index);
724        boolean iterate = false;
725        Number oldYN = item.getValue();
726        if (oldYN != null) {
727            double oldY = oldYN.doubleValue();
728            if (!Double.isNaN(oldY)) {
729                iterate = oldY <= this.minY || oldY >= this.maxY;
730            }
731        }
732        item.setValue(value);
733        if (iterate) {
734            findBoundsByIteration();
735        }
736        else if (value != null) {
737            double yy = value.doubleValue();
738            this.minY = minIgnoreNaN(this.minY, yy);
739            this.maxY = maxIgnoreNaN(this.maxY, yy);
740        }
741        fireSeriesChanged();
742    }
743
744    /**
745     * Adds or updates data from one series to another.  Returns another series
746     * containing the values that were overwritten.
747     *
748     * @param series  the series to merge with this.
749     *
750     * @return A series containing the values that were overwritten.
751     */
752    public TimeSeries addAndOrUpdate(TimeSeries series) {
753        TimeSeries overwritten = new TimeSeries("Overwritten values from: "
754                + getKey());
755        for (int i = 0; i < series.getItemCount(); i++) {
756            TimeSeriesDataItem item = series.getRawDataItem(i);
757            TimeSeriesDataItem oldItem = addOrUpdate(item.getPeriod(),
758                    item.getValue());
759            if (oldItem != null) {
760                overwritten.add(oldItem);
761            }
762        }
763        return overwritten;
764    }
765
766    /**
767     * Adds or updates an item in the times series and sends a
768     * {@link SeriesChangeEvent} to all registered listeners.
769     *
770     * @param period  the time period to add/update (<code>null</code> not
771     *                permitted).
772     * @param value  the new value.
773     *
774     * @return A copy of the overwritten data item, or <code>null</code> if no
775     *         item was overwritten.
776     */
777    public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
778                                          double value) {
779        return addOrUpdate(period, new Double(value));
780    }
781
782    /**
783     * Adds or updates an item in the times series and sends a
784     * {@link SeriesChangeEvent} to all registered listeners.
785     *
786     * @param period  the time period to add/update (<code>null</code> not
787     *                permitted).
788     * @param value  the new value (<code>null</code> permitted).
789     *
790     * @return A copy of the overwritten data item, or <code>null</code> if no
791     *         item was overwritten.
792     */
793    public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
794                                          Number value) {
795        return addOrUpdate(new TimeSeriesDataItem(period, value));
796    }
797
798    /**
799     * Adds or updates an item in the times series and sends a
800     * {@link SeriesChangeEvent} to all registered listeners.
801     *
802     * @param item  the data item (<code>null</code> not permitted).
803     *
804     * @return A copy of the overwritten data item, or <code>null</code> if no
805     *         item was overwritten.
806     *
807     * @since 1.0.14
808     */
809    public TimeSeriesDataItem addOrUpdate(TimeSeriesDataItem item) {
810
811        if (item == null) {
812            throw new IllegalArgumentException("Null 'period' argument.");
813        }
814        Class periodClass = item.getPeriod().getClass();
815        if (this.timePeriodClass == null) {
816            this.timePeriodClass = periodClass;
817        }
818        else if (!this.timePeriodClass.equals(periodClass)) {
819            String msg = "You are trying to add data where the time "
820                    + "period class is " + periodClass.getName()
821                    + ", but the TimeSeries is expecting an instance of "
822                    + this.timePeriodClass.getName() + ".";
823            throw new SeriesException(msg);
824        }
825        TimeSeriesDataItem overwritten = null;
826        int index = Collections.binarySearch(this.data, item);
827        if (index >= 0) {
828            TimeSeriesDataItem existing
829                    = (TimeSeriesDataItem) this.data.get(index);
830            overwritten = (TimeSeriesDataItem) existing.clone();
831            // figure out if we need to iterate through all the y-values
832            // to find the revised minY / maxY
833            boolean iterate = false;
834            Number oldYN = existing.getValue();
835            double oldY = oldYN != null ? oldYN.doubleValue() : Double.NaN;
836            if (!Double.isNaN(oldY)) {
837                iterate = oldY <= this.minY || oldY >= this.maxY;
838            }
839            existing.setValue(item.getValue());
840            if (iterate) {
841                findBoundsByIteration();
842            }
843            else if (item.getValue() != null) {
844                double yy = item.getValue().doubleValue();
845                this.minY = minIgnoreNaN(this.minY, yy);
846                this.maxY = minIgnoreNaN(this.maxY, yy);
847            }
848        }
849        else {
850            item = (TimeSeriesDataItem) item.clone();
851            this.data.add(-index - 1, item);
852            updateBoundsForAddedItem(item);
853
854            // check if this addition will exceed the maximum item count...
855            if (getItemCount() > this.maximumItemCount) {
856                TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0);
857                updateBoundsForRemovedItem(d);
858            }
859        }
860        removeAgedItems(false);  // remove old items if necessary, but
861                                 // don't notify anyone, because that
862                                 // happens next anyway...
863        fireSeriesChanged();
864        return overwritten;
865
866    }
867
868    /**
869     * Age items in the series.  Ensure that the timespan from the youngest to
870     * the oldest record in the series does not exceed maximumItemAge time
871     * periods.  Oldest items will be removed if required.
872     *
873     * @param notify  controls whether or not a {@link SeriesChangeEvent} is
874     *                sent to registered listeners IF any items are removed.
875     */
876    public void removeAgedItems(boolean notify) {
877        // check if there are any values earlier than specified by the history
878        // count...
879        if (getItemCount() > 1) {
880            long latest = getTimePeriod(getItemCount() - 1).getSerialIndex();
881            boolean removed = false;
882            while ((latest - getTimePeriod(0).getSerialIndex())
883                    > this.maximumItemAge) {
884                this.data.remove(0);
885                removed = true;
886            }
887            if (removed) {
888                findBoundsByIteration();
889                if (notify) {
890                    fireSeriesChanged();
891                }
892            }
893        }
894    }
895
896    /**
897     * Age items in the series.  Ensure that the timespan from the supplied
898     * time to the oldest record in the series does not exceed history count.
899     * oldest items will be removed if required.
900     *
901     * @param latest  the time to be compared against when aging data
902     *     (specified in milliseconds).
903     * @param notify  controls whether or not a {@link SeriesChangeEvent} is
904     *                sent to registered listeners IF any items are removed.
905     */
906    public void removeAgedItems(long latest, boolean notify) {
907        if (this.data.isEmpty()) {
908            return;  // nothing to do
909        }
910        // find the serial index of the period specified by 'latest'
911        long index = Long.MAX_VALUE;
912        try {
913            Method m = RegularTimePeriod.class.getDeclaredMethod(
914                    "createInstance", new Class[] {Class.class, Date.class,
915                    TimeZone.class});
916            RegularTimePeriod newest = (RegularTimePeriod) m.invoke(
917                    this.timePeriodClass, new Object[] {this.timePeriodClass,
918                            new Date(latest), TimeZone.getDefault()});
919            index = newest.getSerialIndex();
920        }
921        catch (NoSuchMethodException e) {
922            e.printStackTrace();
923        }
924        catch (IllegalAccessException e) {
925            e.printStackTrace();
926        }
927        catch (InvocationTargetException e) {
928            e.printStackTrace();
929        }
930
931        // check if there are any values earlier than specified by the history
932        // count...
933        boolean removed = false;
934        while (getItemCount() > 0 && (index
935                - getTimePeriod(0).getSerialIndex()) > this.maximumItemAge) {
936            this.data.remove(0);
937            removed = true;
938        }
939        if (removed) {
940            findBoundsByIteration();
941            if (notify) {
942                fireSeriesChanged();
943            }
944        }
945    }
946
947    /**
948     * Removes all data items from the series and sends a
949     * {@link SeriesChangeEvent} to all registered listeners.
950     */
951    public void clear() {
952        if (this.data.size() > 0) {
953            this.data.clear();
954            this.timePeriodClass = null;
955            this.minY = Double.NaN;
956            this.maxY = Double.NaN;
957            fireSeriesChanged();
958        }
959    }
960
961    /**
962     * Deletes the data item for the given time period and sends a
963     * {@link SeriesChangeEvent} to all registered listeners.  If there is no
964     * item with the specified time period, this method does nothing.
965     *
966     * @param period  the period of the item to delete (<code>null</code> not
967     *                permitted).
968     */
969    public void delete(RegularTimePeriod period) {
970        int index = getIndex(period);
971        if (index >= 0) {
972            TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.remove(
973                    index);
974            updateBoundsForRemovedItem(item);
975            if (this.data.isEmpty()) {
976                this.timePeriodClass = null;
977            }
978            fireSeriesChanged();
979        }
980    }
981
982    /**
983     * Deletes data from start until end index (end inclusive).
984     *
985     * @param start  the index of the first period to delete.
986     * @param end  the index of the last period to delete.
987     */
988    public void delete(int start, int end) {
989        delete(start, end, true);
990    }
991
992    /**
993     * Deletes data from start until end index (end inclusive).
994     *
995     * @param start  the index of the first period to delete.
996     * @param end  the index of the last period to delete.
997     * @param notify  notify listeners?
998     *
999     * @since 1.0.14
1000     */
1001    public void delete(int start, int end, boolean notify) {
1002        if (end < start) {
1003            throw new IllegalArgumentException("Requires start <= end.");
1004        }
1005        for (int i = 0; i <= (end - start); i++) {
1006            this.data.remove(start);
1007        }
1008        findBoundsByIteration();
1009        if (this.data.isEmpty()) {
1010            this.timePeriodClass = null;
1011        }
1012        if (notify) {
1013            fireSeriesChanged();
1014        }
1015    }
1016
1017    /**
1018     * Returns a clone of the time series.
1019     * <P>
1020     * Notes:
1021     * <ul>
1022     *   <li>no need to clone the domain and range descriptions, since String
1023     *     object is immutable;</li>
1024     *   <li>we pass over to the more general method clone(start, end).</li>
1025     * </ul>
1026     *
1027     * @return A clone of the time series.
1028     *
1029     * @throws CloneNotSupportedException not thrown by this class, but
1030     *         subclasses may differ.
1031     */
1032    public Object clone() throws CloneNotSupportedException {
1033        TimeSeries clone = (TimeSeries) super.clone();
1034        clone.data = (List) ObjectUtilities.deepClone(this.data);
1035        return clone;
1036    }
1037
1038    /**
1039     * Creates a new timeseries by copying a subset of the data in this time
1040     * series.
1041     *
1042     * @param start  the index of the first time period to copy.
1043     * @param end  the index of the last time period to copy.
1044     *
1045     * @return A series containing a copy of this times series from start until
1046     *         end.
1047     *
1048     * @throws CloneNotSupportedException if there is a cloning problem.
1049     */
1050    public TimeSeries createCopy(int start, int end)
1051            throws CloneNotSupportedException {
1052        if (start < 0) {
1053            throw new IllegalArgumentException("Requires start >= 0.");
1054        }
1055        if (end < start) {
1056            throw new IllegalArgumentException("Requires start <= end.");
1057        }
1058        TimeSeries copy = (TimeSeries) super.clone();
1059        copy.minY = Double.NaN;
1060        copy.maxY = Double.NaN;
1061        copy.data = new java.util.ArrayList();
1062        if (this.data.size() > 0) {
1063            for (int index = start; index <= end; index++) {
1064                TimeSeriesDataItem item
1065                        = (TimeSeriesDataItem) this.data.get(index);
1066                TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone();
1067                try {
1068                    copy.add(clone);
1069                }
1070                catch (SeriesException e) {
1071                    e.printStackTrace();
1072                }
1073            }
1074        }
1075        return copy;
1076    }
1077
1078    /**
1079     * Creates a new timeseries by copying a subset of the data in this time
1080     * series.
1081     *
1082     * @param start  the first time period to copy (<code>null</code> not
1083     *         permitted).
1084     * @param end  the last time period to copy (<code>null</code> not
1085     *         permitted).
1086     *
1087     * @return A time series containing a copy of this time series from start
1088     *         until end.
1089     *
1090     * @throws CloneNotSupportedException if there is a cloning problem.
1091     */
1092    public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end)
1093        throws CloneNotSupportedException {
1094
1095        if (start == null) {
1096            throw new IllegalArgumentException("Null 'start' argument.");
1097        }
1098        if (end == null) {
1099            throw new IllegalArgumentException("Null 'end' argument.");
1100        }
1101        if (start.compareTo(end) > 0) {
1102            throw new IllegalArgumentException(
1103                    "Requires start on or before end.");
1104        }
1105        boolean emptyRange = false;
1106        int startIndex = getIndex(start);
1107        if (startIndex < 0) {
1108            startIndex = -(startIndex + 1);
1109            if (startIndex == this.data.size()) {
1110                emptyRange = true;  // start is after last data item
1111            }
1112        }
1113        int endIndex = getIndex(end);
1114        if (endIndex < 0) {             // end period is not in original series
1115            endIndex = -(endIndex + 1); // this is first item AFTER end period
1116            endIndex = endIndex - 1;    // so this is last item BEFORE end
1117        }
1118        if ((endIndex < 0)  || (endIndex < startIndex)) {
1119            emptyRange = true;
1120        }
1121        if (emptyRange) {
1122            TimeSeries copy = (TimeSeries) super.clone();
1123            copy.data = new java.util.ArrayList();
1124            return copy;
1125        }
1126        return createCopy(startIndex, endIndex);
1127    }
1128
1129    /**
1130     * Tests the series for equality with an arbitrary object.
1131     *
1132     * @param obj  the object to test against (<code>null</code> permitted).
1133     *
1134     * @return A boolean.
1135     */
1136    public boolean equals(Object obj) {
1137        if (obj == this) {
1138            return true;
1139        }
1140        if (!(obj instanceof TimeSeries)) {
1141            return false;
1142        }
1143        TimeSeries that = (TimeSeries) obj;
1144        if (!ObjectUtilities.equal(getDomainDescription(),
1145                that.getDomainDescription())) {
1146            return false;
1147        }
1148        if (!ObjectUtilities.equal(getRangeDescription(),
1149                that.getRangeDescription())) {
1150            return false;
1151        }
1152        if (!ObjectUtilities.equal(this.timePeriodClass,
1153                that.timePeriodClass)) {
1154            return false;
1155        }
1156        if (getMaximumItemAge() != that.getMaximumItemAge()) {
1157            return false;
1158        }
1159        if (getMaximumItemCount() != that.getMaximumItemCount()) {
1160            return false;
1161        }
1162        int count = getItemCount();
1163        if (count != that.getItemCount()) {
1164            return false;
1165        }
1166        if (!ObjectUtilities.equal(this.data, that.data)) {
1167            return false;
1168        }
1169        return super.equals(obj);
1170    }
1171
1172    /**
1173     * Returns a hash code value for the object.
1174     *
1175     * @return The hashcode
1176     */
1177    public int hashCode() {
1178        int result = super.hashCode();
1179        result = 29 * result + (this.domain != null ? this.domain.hashCode()
1180                : 0);
1181        result = 29 * result + (this.range != null ? this.range.hashCode() : 0);
1182        result = 29 * result + (this.timePeriodClass != null
1183                ? this.timePeriodClass.hashCode() : 0);
1184        // it is too slow to look at every data item, so let's just look at
1185        // the first, middle and last items...
1186        int count = getItemCount();
1187        if (count > 0) {
1188            TimeSeriesDataItem item = getRawDataItem(0);
1189            result = 29 * result + item.hashCode();
1190        }
1191        if (count > 1) {
1192            TimeSeriesDataItem item = getRawDataItem(count - 1);
1193            result = 29 * result + item.hashCode();
1194        }
1195        if (count > 2) {
1196            TimeSeriesDataItem item = getRawDataItem(count / 2);
1197            result = 29 * result + item.hashCode();
1198        }
1199        result = 29 * result + this.maximumItemCount;
1200        result = 29 * result + (int) this.maximumItemAge;
1201        return result;
1202    }
1203
1204    /**
1205     * Updates the cached values for the minimum and maximum data values.
1206     *
1207     * @param item  the item added (<code>null</code> not permitted).
1208     *
1209     * @since 1.0.14
1210     */
1211    private void updateBoundsForAddedItem(TimeSeriesDataItem item) {
1212        Number yN = item.getValue();
1213        if (item.getValue() != null) {
1214            double y = yN.doubleValue();
1215            this.minY = minIgnoreNaN(this.minY, y);
1216            this.maxY = maxIgnoreNaN(this.maxY, y);
1217        }
1218    }
1219    
1220    /**
1221     * Updates the cached values for the minimum and maximum data values on
1222     * the basis that the specified item has just been removed.
1223     *
1224     * @param item  the item added (<code>null</code> not permitted).
1225     *
1226     * @since 1.0.14
1227     */
1228    private void updateBoundsForRemovedItem(TimeSeriesDataItem item) {
1229        Number yN = item.getValue();
1230        if (yN != null) {
1231            double y = yN.doubleValue();
1232            if (!Double.isNaN(y)) {
1233                if (y <= this.minY || y >= this.maxY) {
1234                    findBoundsByIteration();
1235                }
1236            }
1237        }
1238    }
1239
1240    /**
1241     * Finds the bounds of the x and y values for the series, by iterating
1242     * through all the data items.
1243     *
1244     * @since 1.0.14
1245     */
1246    private void findBoundsByIteration() {
1247        this.minY = Double.NaN;
1248        this.maxY = Double.NaN;
1249        Iterator iterator = this.data.iterator();
1250        while (iterator.hasNext()) {
1251            TimeSeriesDataItem item = (TimeSeriesDataItem) iterator.next();
1252            updateBoundsForAddedItem(item);
1253        }
1254    }
1255
1256    /**
1257     * A function to find the minimum of two values, but ignoring any
1258     * Double.NaN values.
1259     *
1260     * @param a  the first value.
1261     * @param b  the second value.
1262     *
1263     * @return The minimum of the two values.
1264     */
1265    private double minIgnoreNaN(double a, double b) {
1266        if (Double.isNaN(a)) {
1267            return b;
1268        }
1269        if (Double.isNaN(b)) {
1270            return a;
1271        }
1272        return Math.min(a, b);
1273    }
1274
1275    /**
1276     * A function to find the maximum of two values, but ignoring any
1277     * Double.NaN values.
1278     *
1279     * @param a  the first value.
1280     * @param b  the second value.
1281     *
1282     * @return The maximum of the two values.
1283     */
1284    private double maxIgnoreNaN(double a, double b) {
1285        if (Double.isNaN(a)) {
1286            return b;
1287        }
1288        if (Double.isNaN(b)) {
1289            return a;
1290        }
1291        else {
1292            return Math.max(a, b);
1293        }
1294    }
1295
1296
1297    /**
1298     * Creates a new (empty) time series with the specified name and class
1299     * of {@link RegularTimePeriod}.
1300     *
1301     * @param name  the series name (<code>null</code> not permitted).
1302     * @param timePeriodClass  the type of time period (<code>null</code> not
1303     *                         permitted).
1304     *
1305     * @deprecated As of 1.0.13, it is not necessary to specify the
1306     *     <code>timePeriodClass</code> as this will be inferred when the
1307     *     first data item is added to the dataset.
1308     */
1309    public TimeSeries(Comparable name, Class timePeriodClass) {
1310        this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
1311                timePeriodClass);
1312    }
1313
1314    /**
1315     * Creates a new time series that contains no data.
1316     * <P>
1317     * Descriptions can be specified for the domain and range.  One situation
1318     * where this is helpful is when generating a chart for the time series -
1319     * axis labels can be taken from the domain and range description.
1320     *
1321     * @param name  the name of the series (<code>null</code> not permitted).
1322     * @param domain  the domain description (<code>null</code> permitted).
1323     * @param range  the range description (<code>null</code> permitted).
1324     * @param timePeriodClass  the type of time period (<code>null</code> not
1325     *                         permitted).
1326     *
1327     * @deprecated As of 1.0.13, it is not necessary to specify the
1328     *     <code>timePeriodClass</code> as this will be inferred when the
1329     *     first data item is added to the dataset.
1330     */
1331    public TimeSeries(Comparable name, String domain, String range,
1332                      Class timePeriodClass) {
1333        super(name);
1334        this.domain = domain;
1335        this.range = range;
1336        this.timePeriodClass = timePeriodClass;
1337        this.data = new java.util.ArrayList();
1338        this.maximumItemCount = Integer.MAX_VALUE;
1339        this.maximumItemAge = Long.MAX_VALUE;
1340        this.minY = Double.NaN;
1341        this.maxY = Double.NaN;
1342    }
1343
1344}