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