001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * ---------------
028 * TimeSeries.java
029 * ---------------
030 * (C) Copyright 2001-2007, by Object Refinery Limited.
031 *
032 * Original Author: David Gilbert (for Object Refinery Limited);
033 * Contributor(s): Bryan Scott;
034 * Nick Guenther;
035 *
036 * Changes
037 * -------
038 * 11-Oct-2001 : Version 1 (DG);
039 * 14-Nov-2001 : Added listener mechanism (DG);
040 * 15-Nov-2001 : Updated argument checking and exceptions in add() method (DG);
041 * 29-Nov-2001 : Added properties to describe the domain and range (DG);
042 * 07-Dec-2001 : Renamed TimeSeries --> BasicTimeSeries (DG);
043 * 01-Mar-2002 : Updated import statements (DG);
044 * 28-Mar-2002 : Added a method add(TimePeriod, double) (DG);
045 * 27-Aug-2002 : Changed return type of delete method to void (DG);
046 * 04-Oct-2002 : Added itemCount and historyCount attributes, fixed errors
047 * reported by Checkstyle (DG);
048 * 29-Oct-2002 : Added series change notification to addOrUpdate() method (DG);
049 * 28-Jan-2003 : Changed name back to TimeSeries (DG);
050 * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented
051 * Serializable (DG);
052 * 01-May-2003 : Updated equals() method (see bug report 727575) (DG);
053 * 14-Aug-2003 : Added ageHistoryCountItems method (copied existing code for
054 * contents) made a method and added to addOrUpdate. Made a
055 * public method to enable ageing against a specified time
056 * (eg now) as opposed to lastest time in series (BS);
057 * 15-Oct-2003 : Added fix for setItemCount method - see bug report 804425.
058 * Modified exception message in add() method to be more
059 * informative (DG);
060 * 13-Apr-2004 : Added clear() method (DG);
061 * 21-May-2004 : Added an extra addOrUpdate() method (DG);
062 * 15-Jun-2004 : Fixed NullPointerException in equals() method (DG);
063 * 29-Nov-2004 : Fixed bug 1075255 (DG);
064 * 17-Nov-2005 : Renamed historyCount --> maximumItemAge (DG);
065 * 28-Nov-2005 : Changed maximumItemAge from int to long (DG);
066 * 01-Dec-2005 : New add methods accept notify flag (DG);
067 * ------------- JFREECHART 1.0.x ---------------------------------------------
068 * 24-May-2006 : Improved error handling in createCopy() methods (DG);
069 * 01-Sep-2006 : Fixed bugs in removeAgedItems() methods - see bug report
070 * 1550045 (DG);
071 * 22-Mar-2007 : Simplified getDataItem(RegularTimePeriod) - see patch 1685500
072 * by Nick Guenther (DG);
073 * 31-Oct-2007 : Implemented faster hashCode() (DG);
074 * 21-Nov-2007 : Fixed clone() method (bug 1832432) (DG);
075 *
076 */
077
078 package org.jfree.data.time;
079
080 import java.io.Serializable;
081 import java.lang.reflect.InvocationTargetException;
082 import java.lang.reflect.Method;
083 import java.util.Collection;
084 import java.util.Collections;
085 import java.util.Date;
086 import java.util.List;
087 import java.util.TimeZone;
088
089 import org.jfree.data.general.Series;
090 import org.jfree.data.general.SeriesChangeEvent;
091 import org.jfree.data.general.SeriesException;
092 import org.jfree.util.ObjectUtilities;
093
094 /**
095 * Represents a sequence of zero or more data items in the form (period, value).
096 */
097 public class TimeSeries extends Series implements Cloneable, Serializable {
098
099 /** For serialization. */
100 private static final long serialVersionUID = -5032960206869675528L;
101
102 /** Default value for the domain description. */
103 protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time";
104
105 /** Default value for the range description. */
106 protected static final String DEFAULT_RANGE_DESCRIPTION = "Value";
107
108 /** A description of the domain. */
109 private String domain;
110
111 /** A description of the range. */
112 private String range;
113
114 /** The type of period for the data. */
115 protected Class timePeriodClass;
116
117 /** The list of data items in the series. */
118 protected List data;
119
120 /** The maximum number of items for the series. */
121 private int maximumItemCount;
122
123 /**
124 * The maximum age of items for the series, specified as a number of
125 * time periods.
126 */
127 private long maximumItemAge;
128
129 /**
130 * Creates a new (empty) time series. By default, a daily time series is
131 * created. Use one of the other constructors if you require a different
132 * time period.
133 *
134 * @param name the series name (<code>null</code> not permitted).
135 */
136 public TimeSeries(Comparable name) {
137 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
138 Day.class);
139 }
140
141 /**
142 * Creates a new (empty) time series with the specified name and class
143 * of {@link RegularTimePeriod}.
144 *
145 * @param name the series name (<code>null</code> not permitted).
146 * @param timePeriodClass the type of time period (<code>null</code> not
147 * permitted).
148 */
149 public TimeSeries(Comparable name, Class timePeriodClass) {
150 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
151 timePeriodClass);
152 }
153
154 /**
155 * Creates a new time series that contains no data.
156 * <P>
157 * Descriptions can be specified for the domain and range. One situation
158 * where this is helpful is when generating a chart for the time series -
159 * axis labels can be taken from the domain and range description.
160 *
161 * @param name the name of the series (<code>null</code> not permitted).
162 * @param domain the domain description (<code>null</code> permitted).
163 * @param range the range description (<code>null</code> permitted).
164 * @param timePeriodClass the type of time period (<code>null</code> not
165 * permitted).
166 */
167 public TimeSeries(Comparable name, String domain, String range,
168 Class timePeriodClass) {
169 super(name);
170 this.domain = domain;
171 this.range = range;
172 this.timePeriodClass = timePeriodClass;
173 this.data = new java.util.ArrayList();
174 this.maximumItemCount = Integer.MAX_VALUE;
175 this.maximumItemAge = Long.MAX_VALUE;
176 }
177
178 /**
179 * Returns the domain description.
180 *
181 * @return The domain description (possibly <code>null</code>).
182 *
183 * @see #setDomainDescription(String)
184 */
185 public String getDomainDescription() {
186 return this.domain;
187 }
188
189 /**
190 * Sets the domain description and sends a <code>PropertyChangeEvent</code>
191 * (with the property name <code>Domain</code>) to all registered
192 * property change listeners.
193 *
194 * @param description the description (<code>null</code> permitted).
195 *
196 * @see #getDomainDescription()
197 */
198 public void setDomainDescription(String description) {
199 String old = this.domain;
200 this.domain = description;
201 firePropertyChange("Domain", old, description);
202 }
203
204 /**
205 * Returns the range description.
206 *
207 * @return The range description (possibly <code>null</code>).
208 *
209 * @see #setRangeDescription(String)
210 */
211 public String getRangeDescription() {
212 return this.range;
213 }
214
215 /**
216 * Sets the range description and sends a <code>PropertyChangeEvent</code>
217 * (with the property name <code>Range</code>) to all registered listeners.
218 *
219 * @param description the description (<code>null</code> permitted).
220 *
221 * @see #getRangeDescription()
222 */
223 public void setRangeDescription(String description) {
224 String old = this.range;
225 this.range = description;
226 firePropertyChange("Range", old, description);
227 }
228
229 /**
230 * Returns the number of items in the series.
231 *
232 * @return The item count.
233 */
234 public int getItemCount() {
235 return this.data.size();
236 }
237
238 /**
239 * Returns the list of data items for the series (the list contains
240 * {@link TimeSeriesDataItem} objects and is unmodifiable).
241 *
242 * @return The list of data items.
243 */
244 public List getItems() {
245 return Collections.unmodifiableList(this.data);
246 }
247
248 /**
249 * Returns the maximum number of items that will be retained in the series.
250 * The default value is <code>Integer.MAX_VALUE</code>.
251 *
252 * @return The maximum item count.
253 *
254 * @see #setMaximumItemCount(int)
255 */
256 public int getMaximumItemCount() {
257 return this.maximumItemCount;
258 }
259
260 /**
261 * Sets the maximum number of items that will be retained in the series.
262 * If you add a new item to the series such that the number of items will
263 * exceed the maximum item count, then the FIRST element in the series is
264 * automatically removed, ensuring that the maximum item count is not
265 * exceeded.
266 *
267 * @param maximum the maximum (requires >= 0).
268 *
269 * @see #getMaximumItemCount()
270 */
271 public void setMaximumItemCount(int maximum) {
272 if (maximum < 0) {
273 throw new IllegalArgumentException("Negative 'maximum' argument.");
274 }
275 this.maximumItemCount = maximum;
276 int count = this.data.size();
277 if (count > maximum) {
278 delete(0, count - maximum - 1);
279 }
280 }
281
282 /**
283 * Returns the maximum item age (in time periods) for the series.
284 *
285 * @return The maximum item age.
286 *
287 * @see #setMaximumItemAge(long)
288 */
289 public long getMaximumItemAge() {
290 return this.maximumItemAge;
291 }
292
293 /**
294 * Sets the number of time units in the 'history' for the series. This
295 * provides one mechanism for automatically dropping old data from the
296 * time series. For example, if a series contains daily data, you might set
297 * the history count to 30. Then, when you add a new data item, all data
298 * items more than 30 days older than the latest value are automatically
299 * dropped from the series.
300 *
301 * @param periods the number of time periods.
302 *
303 * @see #getMaximumItemAge()
304 */
305 public void setMaximumItemAge(long periods) {
306 if (periods < 0) {
307 throw new IllegalArgumentException("Negative 'periods' argument.");
308 }
309 this.maximumItemAge = periods;
310 removeAgedItems(true); // remove old items and notify if necessary
311 }
312
313 /**
314 * Returns the time period class for this series.
315 * <p>
316 * Only one time period class can be used within a single series (enforced).
317 * If you add a data item with a {@link Year} for the time period, then all
318 * subsequent data items must also have a {@link Year} for the time period.
319 *
320 * @return The time period class (never <code>null</code>).
321 */
322 public Class getTimePeriodClass() {
323 return this.timePeriodClass;
324 }
325
326 /**
327 * Returns a data item for the series.
328 *
329 * @param index the item index (zero-based).
330 *
331 * @return The data item.
332 *
333 * @see #getDataItem(RegularTimePeriod)
334 */
335 public TimeSeriesDataItem getDataItem(int index) {
336 return (TimeSeriesDataItem) this.data.get(index);
337 }
338
339 /**
340 * Returns the data item for a specific period.
341 *
342 * @param period the period of interest (<code>null</code> not allowed).
343 *
344 * @return The data item matching the specified period (or
345 * <code>null</code> if there is no match).
346 *
347 * @see #getDataItem(int)
348 */
349 public TimeSeriesDataItem getDataItem(RegularTimePeriod period) {
350 int index = getIndex(period);
351 if (index >= 0) {
352 return (TimeSeriesDataItem) this.data.get(index);
353 }
354 else {
355 return null;
356 }
357 }
358
359 /**
360 * Returns the time period at the specified index.
361 *
362 * @param index the index of the data item.
363 *
364 * @return The time period.
365 */
366 public RegularTimePeriod getTimePeriod(int index) {
367 return getDataItem(index).getPeriod();
368 }
369
370 /**
371 * Returns a time period that would be the next in sequence on the end of
372 * the time series.
373 *
374 * @return The next time period.
375 */
376 public RegularTimePeriod getNextTimePeriod() {
377 RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
378 return last.next();
379 }
380
381 /**
382 * Returns a collection of all the time periods in the time series.
383 *
384 * @return A collection of all the time periods.
385 */
386 public Collection getTimePeriods() {
387 Collection result = new java.util.ArrayList();
388 for (int i = 0; i < getItemCount(); i++) {
389 result.add(getTimePeriod(i));
390 }
391 return result;
392 }
393
394 /**
395 * Returns a collection of time periods in the specified series, but not in
396 * this series, and therefore unique to the specified series.
397 *
398 * @param series the series to check against this one.
399 *
400 * @return The unique time periods.
401 */
402 public Collection getTimePeriodsUniqueToOtherSeries(TimeSeries series) {
403
404 Collection result = new java.util.ArrayList();
405 for (int i = 0; i < series.getItemCount(); i++) {
406 RegularTimePeriod period = series.getTimePeriod(i);
407 int index = getIndex(period);
408 if (index < 0) {
409 result.add(period);
410 }
411 }
412 return result;
413
414 }
415
416 /**
417 * Returns the index for the item (if any) that corresponds to a time
418 * period.
419 *
420 * @param period the time period (<code>null</code> not permitted).
421 *
422 * @return The index.
423 */
424 public int getIndex(RegularTimePeriod period) {
425 if (period == null) {
426 throw new IllegalArgumentException("Null 'period' argument.");
427 }
428 TimeSeriesDataItem dummy = new TimeSeriesDataItem(
429 period, Integer.MIN_VALUE);
430 return Collections.binarySearch(this.data, dummy);
431 }
432
433 /**
434 * Returns the value at the specified index.
435 *
436 * @param index index of a value.
437 *
438 * @return The value (possibly <code>null</code>).
439 */
440 public Number getValue(int index) {
441 return getDataItem(index).getValue();
442 }
443
444 /**
445 * Returns the value for a time period. If there is no data item with the
446 * specified period, this method will return <code>null</code>.
447 *
448 * @param period time period (<code>null</code> not permitted).
449 *
450 * @return The value (possibly <code>null</code>).
451 */
452 public Number getValue(RegularTimePeriod period) {
453
454 int index = getIndex(period);
455 if (index >= 0) {
456 return getValue(index);
457 }
458 else {
459 return null;
460 }
461
462 }
463
464 /**
465 * Adds a data item to the series and sends a
466 * {@link org.jfree.data.general.SeriesChangeEvent} to all registered
467 * listeners.
468 *
469 * @param item the (timeperiod, value) pair (<code>null</code> not
470 * permitted).
471 */
472 public void add(TimeSeriesDataItem item) {
473 add(item, true);
474 }
475
476 /**
477 * Adds a data item to the series and sends a
478 * {@link org.jfree.data.general.SeriesChangeEvent} to all registered
479 * listeners.
480 *
481 * @param item the (timeperiod, value) pair (<code>null</code> not
482 * permitted).
483 * @param notify notify listeners?
484 */
485 public void add(TimeSeriesDataItem item, boolean notify) {
486 if (item == null) {
487 throw new IllegalArgumentException("Null 'item' argument.");
488 }
489 if (!item.getPeriod().getClass().equals(this.timePeriodClass)) {
490 StringBuffer b = new StringBuffer();
491 b.append("You are trying to add data where the time period class ");
492 b.append("is ");
493 b.append(item.getPeriod().getClass().getName());
494 b.append(", but the TimeSeries is expecting an instance of ");
495 b.append(this.timePeriodClass.getName());
496 b.append(".");
497 throw new SeriesException(b.toString());
498 }
499
500 // make the change (if it's not a duplicate time period)...
501 boolean added = false;
502 int count = getItemCount();
503 if (count == 0) {
504 this.data.add(item);
505 added = true;
506 }
507 else {
508 RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
509 if (item.getPeriod().compareTo(last) > 0) {
510 this.data.add(item);
511 added = true;
512 }
513 else {
514 int index = Collections.binarySearch(this.data, item);
515 if (index < 0) {
516 this.data.add(-index - 1, item);
517 added = true;
518 }
519 else {
520 StringBuffer b = new StringBuffer();
521 b.append("You are attempting to add an observation for ");
522 b.append("the time period ");
523 b.append(item.getPeriod().toString());
524 b.append(" but the series already contains an observation");
525 b.append(" for that time period. Duplicates are not ");
526 b.append("permitted. Try using the addOrUpdate() method.");
527 throw new SeriesException(b.toString());
528 }
529 }
530 }
531 if (added) {
532 // check if this addition will exceed the maximum item count...
533 if (getItemCount() > this.maximumItemCount) {
534 this.data.remove(0);
535 }
536
537 removeAgedItems(false); // remove old items if necessary, but
538 // don't notify anyone, because that
539 // happens next anyway...
540 if (notify) {
541 fireSeriesChanged();
542 }
543 }
544
545 }
546
547 /**
548 * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
549 * to all registered listeners.
550 *
551 * @param period the time period (<code>null</code> not permitted).
552 * @param value the value.
553 */
554 public void add(RegularTimePeriod period, double value) {
555 // defer argument checking...
556 add(period, value, true);
557 }
558
559 /**
560 * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
561 * to all registered listeners.
562 *
563 * @param period the time period (<code>null</code> not permitted).
564 * @param value the value.
565 * @param notify notify listeners?
566 */
567 public void add(RegularTimePeriod period, double value, boolean notify) {
568 // defer argument checking...
569 TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
570 add(item, notify);
571 }
572
573 /**
574 * Adds a new data item to the series and sends
575 * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered
576 * listeners.
577 *
578 * @param period the time period (<code>null</code> not permitted).
579 * @param value the value (<code>null</code> permitted).
580 */
581 public void add(RegularTimePeriod period, Number value) {
582 // defer argument checking...
583 add(period, value, true);
584 }
585
586 /**
587 * Adds a new data item to the series and sends
588 * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered
589 * listeners.
590 *
591 * @param period the time period (<code>null</code> not permitted).
592 * @param value the value (<code>null</code> permitted).
593 * @param notify notify listeners?
594 */
595 public void add(RegularTimePeriod period, Number value, boolean notify) {
596 // defer argument checking...
597 TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
598 add(item, notify);
599 }
600
601 /**
602 * Updates (changes) the value for a time period. Throws a
603 * {@link SeriesException} if the period does not exist.
604 *
605 * @param period the period (<code>null</code> not permitted).
606 * @param value the value (<code>null</code> permitted).
607 */
608 public void update(RegularTimePeriod period, Number value) {
609 TimeSeriesDataItem temp = new TimeSeriesDataItem(period, value);
610 int index = Collections.binarySearch(this.data, temp);
611 if (index >= 0) {
612 TimeSeriesDataItem pair = (TimeSeriesDataItem) this.data.get(index);
613 pair.setValue(value);
614 fireSeriesChanged();
615 }
616 else {
617 throw new SeriesException(
618 "TimeSeries.update(TimePeriod, Number): period does not exist."
619 );
620 }
621
622 }
623
624 /**
625 * Updates (changes) the value of a data item.
626 *
627 * @param index the index of the data item.
628 * @param value the new value (<code>null</code> permitted).
629 */
630 public void update(int index, Number value) {
631 TimeSeriesDataItem item = getDataItem(index);
632 item.setValue(value);
633 fireSeriesChanged();
634 }
635
636 /**
637 * Adds or updates data from one series to another. Returns another series
638 * containing the values that were overwritten.
639 *
640 * @param series the series to merge with this.
641 *
642 * @return A series containing the values that were overwritten.
643 */
644 public TimeSeries addAndOrUpdate(TimeSeries series) {
645 TimeSeries overwritten = new TimeSeries("Overwritten values from: "
646 + getKey(), series.getTimePeriodClass());
647 for (int i = 0; i < series.getItemCount(); i++) {
648 TimeSeriesDataItem item = series.getDataItem(i);
649 TimeSeriesDataItem oldItem = addOrUpdate(item.getPeriod(),
650 item.getValue());
651 if (oldItem != null) {
652 overwritten.add(oldItem);
653 }
654 }
655 return overwritten;
656 }
657
658 /**
659 * Adds or updates an item in the times series and sends a
660 * {@link org.jfree.data.general.SeriesChangeEvent} to all registered
661 * listeners.
662 *
663 * @param period the time period to add/update (<code>null</code> not
664 * permitted).
665 * @param value the new value.
666 *
667 * @return A copy of the overwritten data item, or <code>null</code> if no
668 * item was overwritten.
669 */
670 public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
671 double value) {
672 return this.addOrUpdate(period, new Double(value));
673 }
674
675 /**
676 * Adds or updates an item in the times series and sends a
677 * {@link org.jfree.data.general.SeriesChangeEvent} to all registered
678 * listeners.
679 *
680 * @param period the time period to add/update (<code>null</code> not
681 * permitted).
682 * @param value the new value (<code>null</code> permitted).
683 *
684 * @return A copy of the overwritten data item, or <code>null</code> if no
685 * item was overwritten.
686 */
687 public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
688 Number value) {
689
690 if (period == null) {
691 throw new IllegalArgumentException("Null 'period' argument.");
692 }
693 TimeSeriesDataItem overwritten = null;
694
695 TimeSeriesDataItem key = new TimeSeriesDataItem(period, value);
696 int index = Collections.binarySearch(this.data, key);
697 if (index >= 0) {
698 TimeSeriesDataItem existing
699 = (TimeSeriesDataItem) this.data.get(index);
700 overwritten = (TimeSeriesDataItem) existing.clone();
701 existing.setValue(value);
702 removeAgedItems(false); // remove old items if necessary, but
703 // don't notify anyone, because that
704 // happens next anyway...
705 fireSeriesChanged();
706 }
707 else {
708 this.data.add(-index - 1, new TimeSeriesDataItem(period, value));
709
710 // check if this addition will exceed the maximum item count...
711 if (getItemCount() > this.maximumItemCount) {
712 this.data.remove(0);
713 }
714
715 removeAgedItems(false); // remove old items if necessary, but
716 // don't notify anyone, because that
717 // happens next anyway...
718 fireSeriesChanged();
719 }
720 return overwritten;
721
722 }
723
724 /**
725 * Age items in the series. Ensure that the timespan from the youngest to
726 * the oldest record in the series does not exceed maximumItemAge time
727 * periods. Oldest items will be removed if required.
728 *
729 * @param notify controls whether or not a {@link SeriesChangeEvent} is
730 * sent to registered listeners IF any items are removed.
731 */
732 public void removeAgedItems(boolean notify) {
733 // check if there are any values earlier than specified by the history
734 // count...
735 if (getItemCount() > 1) {
736 long latest = getTimePeriod(getItemCount() - 1).getSerialIndex();
737 boolean removed = false;
738 while ((latest - getTimePeriod(0).getSerialIndex())
739 > this.maximumItemAge) {
740 this.data.remove(0);
741 removed = true;
742 }
743 if (removed && notify) {
744 fireSeriesChanged();
745 }
746 }
747 }
748
749 /**
750 * Age items in the series. Ensure that the timespan from the supplied
751 * time to the oldest record in the series does not exceed history count.
752 * oldest items will be removed if required.
753 *
754 * @param latest the time to be compared against when aging data
755 * (specified in milliseconds).
756 * @param notify controls whether or not a {@link SeriesChangeEvent} is
757 * sent to registered listeners IF any items are removed.
758 */
759 public void removeAgedItems(long latest, boolean notify) {
760
761 // find the serial index of the period specified by 'latest'
762 long index = Long.MAX_VALUE;
763 try {
764 Method m = RegularTimePeriod.class.getDeclaredMethod(
765 "createInstance", new Class[] {Class.class, Date.class,
766 TimeZone.class});
767 RegularTimePeriod newest = (RegularTimePeriod) m.invoke(
768 this.timePeriodClass, new Object[] {this.timePeriodClass,
769 new Date(latest), TimeZone.getDefault()});
770 index = newest.getSerialIndex();
771 }
772 catch (NoSuchMethodException e) {
773 e.printStackTrace();
774 }
775 catch (IllegalAccessException e) {
776 e.printStackTrace();
777 }
778 catch (InvocationTargetException e) {
779 e.printStackTrace();
780 }
781
782 // check if there are any values earlier than specified by the history
783 // count...
784 boolean removed = false;
785 while (getItemCount() > 0 && (index
786 - getTimePeriod(0).getSerialIndex()) > this.maximumItemAge) {
787 this.data.remove(0);
788 removed = true;
789 }
790 if (removed && notify) {
791 fireSeriesChanged();
792 }
793 }
794
795 /**
796 * Removes all data items from the series and sends a
797 * {@link SeriesChangeEvent} to all registered listeners.
798 */
799 public void clear() {
800 if (this.data.size() > 0) {
801 this.data.clear();
802 fireSeriesChanged();
803 }
804 }
805
806 /**
807 * Deletes the data item for the given time period and sends a
808 * {@link SeriesChangeEvent} to all registered listeners. If there is no
809 * item with the specified time period, this method does nothing.
810 *
811 * @param period the period of the item to delete (<code>null</code> not
812 * permitted).
813 */
814 public void delete(RegularTimePeriod period) {
815 int index = getIndex(period);
816 if (index >= 0) {
817 this.data.remove(index);
818 fireSeriesChanged();
819 }
820 }
821
822 /**
823 * Deletes data from start until end index (end inclusive).
824 *
825 * @param start the index of the first period to delete.
826 * @param end the index of the last period to delete.
827 */
828 public void delete(int start, int end) {
829 if (end < start) {
830 throw new IllegalArgumentException("Requires start <= end.");
831 }
832 for (int i = 0; i <= (end - start); i++) {
833 this.data.remove(start);
834 }
835 fireSeriesChanged();
836 }
837
838 /**
839 * Returns a clone of the time series.
840 * <P>
841 * Notes:
842 * <ul>
843 * <li>no need to clone the domain and range descriptions, since String
844 * object is immutable;</li>
845 * <li>we pass over to the more general method clone(start, end).</li>
846 * </ul>
847 *
848 * @return A clone of the time series.
849 *
850 * @throws CloneNotSupportedException not thrown by this class, but
851 * subclasses may differ.
852 */
853 public Object clone() throws CloneNotSupportedException {
854 TimeSeries clone = (TimeSeries) super.clone();
855 clone.data = (List) ObjectUtilities.deepClone(this.data);
856 return clone;
857 }
858
859 /**
860 * Creates a new timeseries by copying a subset of the data in this time
861 * series.
862 *
863 * @param start the index of the first time period to copy.
864 * @param end the index of the last time period to copy.
865 *
866 * @return A series containing a copy of this times series from start until
867 * end.
868 *
869 * @throws CloneNotSupportedException if there is a cloning problem.
870 */
871 public TimeSeries createCopy(int start, int end)
872 throws CloneNotSupportedException {
873
874 if (start < 0) {
875 throw new IllegalArgumentException("Requires start >= 0.");
876 }
877 if (end < start) {
878 throw new IllegalArgumentException("Requires start <= end.");
879 }
880 TimeSeries copy = (TimeSeries) super.clone();
881
882 copy.data = new java.util.ArrayList();
883 if (this.data.size() > 0) {
884 for (int index = start; index <= end; index++) {
885 TimeSeriesDataItem item
886 = (TimeSeriesDataItem) this.data.get(index);
887 TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone();
888 try {
889 copy.add(clone);
890 }
891 catch (SeriesException e) {
892 e.printStackTrace();
893 }
894 }
895 }
896 return copy;
897 }
898
899 /**
900 * Creates a new timeseries by copying a subset of the data in this time
901 * series.
902 *
903 * @param start the first time period to copy.
904 * @param end the last time period to copy.
905 *
906 * @return A time series containing a copy of this time series from start
907 * until end.
908 *
909 * @throws CloneNotSupportedException if there is a cloning problem.
910 */
911 public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end)
912 throws CloneNotSupportedException {
913
914 if (start == null) {
915 throw new IllegalArgumentException("Null 'start' argument.");
916 }
917 if (end == null) {
918 throw new IllegalArgumentException("Null 'end' argument.");
919 }
920 if (start.compareTo(end) > 0) {
921 throw new IllegalArgumentException(
922 "Requires start on or before end.");
923 }
924 boolean emptyRange = false;
925 int startIndex = getIndex(start);
926 if (startIndex < 0) {
927 startIndex = -(startIndex + 1);
928 if (startIndex == this.data.size()) {
929 emptyRange = true; // start is after last data item
930 }
931 }
932 int endIndex = getIndex(end);
933 if (endIndex < 0) { // end period is not in original series
934 endIndex = -(endIndex + 1); // this is first item AFTER end period
935 endIndex = endIndex - 1; // so this is last item BEFORE end
936 }
937 if (endIndex < 0) {
938 emptyRange = true;
939 }
940 if (emptyRange) {
941 TimeSeries copy = (TimeSeries) super.clone();
942 copy.data = new java.util.ArrayList();
943 return copy;
944 }
945 else {
946 return createCopy(startIndex, endIndex);
947 }
948
949 }
950
951 /**
952 * Tests the series for equality with an arbitrary object.
953 *
954 * @param object the object to test against (<code>null</code> permitted).
955 *
956 * @return A boolean.
957 */
958 public boolean equals(Object object) {
959 if (object == this) {
960 return true;
961 }
962 if (!(object instanceof TimeSeries) || !super.equals(object)) {
963 return false;
964 }
965 TimeSeries s = (TimeSeries) object;
966 if (!ObjectUtilities.equal(
967 getDomainDescription(), s.getDomainDescription()
968 )) {
969 return false;
970 }
971
972 if (!ObjectUtilities.equal(
973 getRangeDescription(), s.getRangeDescription()
974 )) {
975 return false;
976 }
977
978 if (!getClass().equals(s.getClass())) {
979 return false;
980 }
981
982 if (getMaximumItemAge() != s.getMaximumItemAge()) {
983 return false;
984 }
985
986 if (getMaximumItemCount() != s.getMaximumItemCount()) {
987 return false;
988 }
989
990 int count = getItemCount();
991 if (count != s.getItemCount()) {
992 return false;
993 }
994 for (int i = 0; i < count; i++) {
995 if (!getDataItem(i).equals(s.getDataItem(i))) {
996 return false;
997 }
998 }
999 return true;
1000 }
1001
1002 /**
1003 * Returns a hash code value for the object.
1004 *
1005 * @return The hashcode
1006 */
1007 public int hashCode() {
1008 int result = super.hashCode();
1009 result = 29 * result + (this.domain != null ? this.domain.hashCode()
1010 : 0);
1011 result = 29 * result + (this.range != null ? this.range.hashCode() : 0);
1012 result = 29 * result + (this.timePeriodClass != null
1013 ? this.timePeriodClass.hashCode() : 0);
1014 // it is too slow to look at every data item, so let's just look at
1015 // the first, middle and last items...
1016 int count = getItemCount();
1017 if (count > 0) {
1018 TimeSeriesDataItem item = getDataItem(0);
1019 result = 29 * result + item.hashCode();
1020 }
1021 if (count > 1) {
1022 TimeSeriesDataItem item = getDataItem(count - 1);
1023 result = 29 * result + item.hashCode();
1024 }
1025 if (count > 2) {
1026 TimeSeriesDataItem item = getDataItem(count / 2);
1027 result = 29 * result + item.hashCode();
1028 }
1029 result = 29 * result + this.maximumItemCount;
1030 result = 29 * result + (int) this.maximumItemAge;
1031 return result;
1032 }
1033
1034 }