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