package org.djutils.stats.summarizers; import java.util.Calendar; import org.djutils.exceptions.Throw; /** * The TimestampWeightedTally class defines a time-weighted tally based on timestamped data. The difference with a normal * time-weighed tally is that the weight of a value is only known at the occurrence of the next timestamp. Furthermore, a last * timestamp needs to be specified to determine the weight of the last value. Timestamps can be Number based or Calendar based. *

* Copyright (c) 2020-2021 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See * for project information https://simulation.tudelft.nl. The DSOL * project is distributed under a three-clause BSD-style license, which can be found at * * https://simulation.tudelft.nl/dsol/3.0/license.html.
* @author Alexander Verbraeck */ public class TimestampWeightedTally implements TimestampTallyInterface { /** */ private static final long serialVersionUID = 20200228L; /** the wrapped weighted tally. */ private WeightedTally wrappedWeightedTally; /** startTime defines the time of the first observation. Often equals to 0.0, but can also have other value. */ private double startTime = Double.NaN; /** lastTimestamp stores the time of the last observation. Stored separately to avoid ulp rounding errors and allow ==. */ private double lastTimestamp = Double.NaN; /** lastValue tracks the last value. */ private double lastValue = Double.NaN; /** indicate whether the statistic is active or not (false before first event and after last event). */ private boolean active = false; /** * constructs a new TimestampWeightedTally with a description. * @param description String; the description of this TimestampWeightedTally */ public TimestampWeightedTally(final String description) { this.wrappedWeightedTally = new WeightedTally(description); initialize(); } /** {@inheritDoc} */ @Override public void initialize() { synchronized (this.wrappedWeightedTally.semaphore) { this.wrappedWeightedTally.initialize(); this.startTime = Double.NaN; this.lastTimestamp = Double.NaN; this.lastValue = 0.0; this.active = true; } } /** {@inheritDoc} */ @Override public final boolean isActive() { return this.active; } /** {@inheritDoc} */ @Override public final void endObservations(final Number timestamp) { ingest(timestamp, this.lastValue); this.active = false; } /** {@inheritDoc} */ @Override public void endObservations(final Calendar timestamp) { endObservations(timestamp.getTimeInMillis()); } /** * Return the last observed value. * @return double; the last observed value */ public double getLastValue() { return this.lastValue; } /** * Process one observed value. * @param timestamp Calendar; the Calendar object representing the timestamp * @param value double; the value to process * @return double; the value */ public double ingest(final Calendar timestamp, final double value) { Throw.whenNull(timestamp, "timestamp object may not be null"); return ingest(timestamp.getTimeInMillis(), value); } /** * Process one observed value. * @param timestamp Number; the object representing the timestamp * @param value double; the value to process * @return double; the value */ public double ingest(final Number timestamp, final double value) { Throw.whenNull(timestamp, "timestamp object may not be null"); Throw.when(Double.isNaN(value), IllegalArgumentException.class, "value may not be NaN"); double timestampDouble = timestamp.doubleValue(); Throw.when(Double.isNaN(timestampDouble), IllegalArgumentException.class, "timestamp may not be NaN"); Throw.when(timestampDouble < this.lastTimestamp, IllegalArgumentException.class, "times not offered in ascending order. Last time was " + this.lastTimestamp + ", new timestamp was " + timestampDouble); synchronized (this.wrappedWeightedTally.semaphore) { // only calculate anything when the time interval is larger than 0, and when the TimestampWeightedTally is active if ((Double.isNaN(this.lastTimestamp) || timestampDouble > this.lastTimestamp) && this.active) { if (Double.isNaN(this.startTime)) { this.startTime = timestampDouble; } else { double deltaTime = Math.max(0.0, timestampDouble - this.lastTimestamp); this.wrappedWeightedTally.ingest(deltaTime, this.lastValue); } this.lastTimestamp = timestampDouble; } this.lastValue = value; return value; } } /** {@inheritDoc} */ @Override public final String getDescription() { return this.wrappedWeightedTally.getDescription(); } /** {@inheritDoc} */ @Override public final long getN() { return this.wrappedWeightedTally.getN(); } /** {@inheritDoc} */ @Override public final double getMax() { return this.wrappedWeightedTally.getMax(); } /** {@inheritDoc} */ @Override public final double getMin() { return this.wrappedWeightedTally.getMin(); } /** {@inheritDoc} */ @Override public final double getWeightedSampleMean() { return this.wrappedWeightedTally.getWeightedSampleMean(); } /** {@inheritDoc} */ @Override public final double getWeightedSampleStDev() { return this.wrappedWeightedTally.getWeightedSampleStDev(); } /** {@inheritDoc} */ @Override public final double getWeightedPopulationStDev() { return this.wrappedWeightedTally.getWeightedPopulationStDev(); } /** {@inheritDoc} */ @Override public final double getWeightedSampleVariance() { return this.wrappedWeightedTally.getWeightedSampleVariance(); } /** {@inheritDoc} */ @Override public final double getWeightedPopulationVariance() { return this.wrappedWeightedTally.getWeightedPopulationVariance(); } /** {@inheritDoc} */ @Override public final double getWeightedSum() { return this.wrappedWeightedTally.getWeightedSum(); } /** {@inheritDoc} */ @Override @SuppressWarnings("checkstyle:designforextension") public String toString() { return this.wrappedWeightedTally.toString(); } }