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