1 package org.djutils.stats.summarizers; 2 3 import java.util.Calendar; 4 5 import org.djutils.exceptions.Throw; 6 7 /** 8 * The TimestampWeightedTally class defines a time-weighted tally based on timestamped data. The difference with a normal 9 * time-weighed tally is that the weight of a value is only known at the occurrence of the next timestamp. Furthermore, a last 10 * timestamp needs to be specified to determine the weight of the last value. Timestamps can be Number based or Calendar based. 11 * <p> 12 * Copyright (c) 2020-2023 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See 13 * for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>. The DSOL 14 * project is distributed under a three-clause BSD-style license, which can be found at 15 * <a href="https://simulation.tudelft.nl/dsol/3.0/license.html" target="_blank"> 16 * https://simulation.tudelft.nl/dsol/3.0/license.html</a>. 17 * </p> 18 * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank"> Alexander Verbraeck</a> 19 */ 20 public class TimestampWeightedTally extends WeightedTally 21 { 22 /** */ 23 private static final long serialVersionUID = 20200228L; 24 25 /** startTime defines the time of the first observation. Often equals to 0.0, but can also have other value. */ 26 private double startTime; 27 28 /** lastTimestamp stores the time of the last observation. Stored separately to avoid ulp rounding errors and allow ==. */ 29 private double lastTimestamp; 30 31 /** lastValue tracks the last value. */ 32 private double lastValue; 33 34 /** indicate whether the statistic is active or not (false before first event and after last event). */ 35 private boolean active; 36 37 /** 38 * constructs a new TimestampWeightedTally with a description. 39 * @param description String; the description of this TimestampWeightedTally 40 */ 41 public TimestampWeightedTally(final String description) 42 { 43 super(description); 44 } 45 46 /** {@inheritDoc} */ 47 @Override 48 public void initialize() 49 { 50 synchronized (super.semaphore) 51 { 52 super.initialize(); 53 this.startTime = Double.NaN; 54 this.lastTimestamp = Double.NaN; 55 this.lastValue = 0.0; 56 this.active = true; 57 } 58 } 59 60 /** 61 * Return whether the statistic is active (accepting observations) or not. 62 * @return boolean; whether the statistic is active (accepting observations) or not 63 */ 64 public boolean isActive() 65 { 66 return this.active; 67 } 68 69 /** 70 * End the observations and closes the last interval of observations. After ending, no more observations will be accepted. 71 * Calling this method will create an extra observation, and corresponding events for the EventBased implementations of this 72 * interface will be called. 73 * @param timestamp Number; the Number object representing the final timestamp 74 */ 75 public void endObservations(final Number timestamp) 76 { 77 register(timestamp, this.lastValue); 78 this.active = false; 79 } 80 81 /** 82 * End the observations and closes the last interval of observations. After ending, no more observations will be accepted. 83 * Calling this method will create an extra observation, and corresponding events for the EventBased implementations of this 84 * interface will be called. 85 * @param timestamp Calendar; the Calendar object representing the final timestamp 86 */ 87 public void endObservations(final Calendar timestamp) 88 { 89 register(timestamp, this.lastValue); 90 this.active = false; 91 } 92 93 /** 94 * Return the last observed value. 95 * @return double; the last observed value 96 */ 97 public double getLastValue() 98 { 99 return this.lastValue; 100 } 101 102 /** 103 * Process one observed Calender-based value. The time used will be the Calendar's time in milliseconds. Silently ignore 104 * when a value is registered, but tally is not active, i.e. when endObservations() has been called. 105 * @param timestamp Calendar; the Calendar object representing the timestamp 106 * @param value double; the value to process 107 * @return double; the value 108 * @throws NullPointerException when timestamp is null 109 * @throws IllegalArgumentException when value is NaN 110 * @throws IllegalArgumentException when given timestamp is before last timestamp 111 */ 112 public double register(final Calendar timestamp, final double value) 113 { 114 Throw.whenNull(timestamp, "timestamp object may not be null"); 115 return registerValue(timestamp.getTimeInMillis(), value); 116 } 117 118 /** 119 * Process one observed Number-based value. Silently ignore when a value is registered, but tally is not active, i.e. when 120 * endObservations() has been called. 121 * @param timestamp Number; the object representing the timestamp 122 * @param value double; the value to process 123 * @return double; the value 124 * @throws NullPointerException when timestamp is null 125 * @throws IllegalArgumentException when value is NaN or timestamp is NaN 126 * @throws IllegalArgumentException when given timestamp is before last timestamp 127 */ 128 public double register(final Number timestamp, final double value) 129 { 130 return registerValue(timestamp, value); 131 } 132 133 /** 134 * Explicit;y override the double value method signature of WeightedTally to call the right method.<br> 135 * Process one observed double value. Silently ignore when a value is registered, but tally is not active, i.e. when 136 * endObservations() has been called. 137 * @param timestamp Number; the object representing the timestamp 138 * @param value double; the value to process 139 * @return double; the value 140 * @throws NullPointerException when timestamp is null 141 * @throws IllegalArgumentException when value is NaN or timestamp is NaN 142 * @throws IllegalArgumentException when given timestamp is before last timestamp 143 */ 144 @Override 145 public double register(final double timestamp, final double value) 146 { 147 return registerValue(timestamp, value); 148 } 149 150 /** 151 * Process one observed Number-based value. Silently ignore when a value is registered, but tally is not active, i.e. when 152 * endObservations() has been called. 153 * @param timestamp Number; the object representing the timestamp 154 * @param value double; the value to process 155 * @return double; the value 156 * @throws NullPointerException when timestamp is null 157 * @throws IllegalArgumentException when value is NaN or timestamp is NaN 158 * @throws IllegalArgumentException when given timestamp is before last timestamp 159 */ 160 protected double registerValue(final Number timestamp, final double value) 161 { 162 Throw.whenNull(timestamp, "timestamp object may not be null"); 163 Throw.when(Double.isNaN(value), IllegalArgumentException.class, "value may not be NaN"); 164 double timestampDouble = timestamp.doubleValue(); 165 Throw.when(Double.isNaN(timestampDouble), IllegalArgumentException.class, "timestamp may not be NaN"); 166 Throw.when(timestampDouble < this.lastTimestamp, IllegalArgumentException.class, 167 "times not offered in ascending order. Last time was " + this.lastTimestamp + ", new timestamp was " 168 + timestampDouble); 169 170 synchronized (super.semaphore) 171 { 172 // only calculate anything when the time interval is larger than 0, and when the TimestampWeightedTally is active 173 if ((Double.isNaN(this.lastTimestamp) || timestampDouble > this.lastTimestamp) && this.active) 174 { 175 if (Double.isNaN(this.startTime)) 176 { 177 this.startTime = timestampDouble; 178 } 179 else 180 { 181 double deltaTime = Math.max(0.0, timestampDouble - this.lastTimestamp); 182 super.register(deltaTime, this.lastValue); 183 } 184 this.lastTimestamp = timestampDouble; 185 } 186 this.lastValue = value; 187 return value; 188 } 189 } 190 191 /** 192 * Return a string representing a header for a textual table with a monospaced font that can contain multiple statistics. 193 * @return String; header for the textual table. 194 */ 195 public static String reportHeader() 196 { 197 return "-".repeat(126) 198 + String.format("%n| %-48.48s | %6.6s | %10.10s | %10.10s | %10.10s | %10.10s | %10.10s |%n", 199 "Timestamp-based weighted Tally name", "n", "interval", "w.mean", "w.st.dev", "min obs", "max obs") 200 + "-".repeat(126); 201 } 202 203 /** {@inheritDoc} */ 204 @Override 205 public String reportLine() 206 { 207 return String.format("| %-48.48s | %6d | %s | %s | %s | %s | %s |", getDescription(), getN(), 208 formatFixed(this.lastTimestamp - this.startTime, 10), formatFixed(getWeightedPopulationMean(), 10), 209 formatFixed(getWeightedPopulationStDev(), 10), formatFixed(getMin(), 10), formatFixed(getMax(), 10)); 210 } 211 212 /** 213 * Return a string representing a footer for a textual table with a monospaced font that can contain multiple statistics. 214 * @return String; footer for the textual table 215 */ 216 public static String reportFooter() 217 { 218 return "-".repeat(126); 219 } 220 221 /** {@inheritDoc} */ 222 @Override 223 public String toString() 224 { 225 return "TimestampWeightedTally" + super.toString().substring("WeightedTally".length()); 226 } 227 228 }