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