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 >= 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}