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