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