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