package org.rrd4j.data;
import java.io.Serializable;
import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
import org.rrd4j.core.Util;
/**
* An abstract class to help extract single value from a set of value (VDEF in rrdtool)
*
* It can be used to add new fancy statistical calculation with rrd values
*
*/
public abstract class Variable {
/**
* This class store both the value and the time stamp
* It will be used by graph rendering legend
*/
public static final class Value {
public final double value;
public final long timestamp;
Value(long timestamp, double value) {
this.value = value;
this.timestamp = timestamp;
}
};
public static final Value INVALIDVALUE = new Value(0, Double.NaN);
private Value val = null;
/**
* Used to calculate the needed value from a source, this method call fill.
* @param s
* @param start
* @param end
*/
void calculate(Source s, long start, long end) {
long step = s.timestamps[1] - s.timestamps[0];
int first = -1;
int last = -1;
// Iterate over array, stop then end cursor reach start or when both start and end has been found
// It also stop if cursor cross other side boundary
for(int i = 0, j = s.timestamps.length - 1 ; ( last == -1 && j > first ) || ( first == -1 && ( last == -1 || i < last ) ) ; i++, j--) {
if(first == -1) {
long leftdown = Math.max(s.timestamps[i] - step, start);
long rightdown = Math.min(s.timestamps[i], end);
if(rightdown > leftdown) {
first = i;
}
}
if(last == -1) {
long leftup = Math.max(s.timestamps[j] - step, start);
long rightup = Math.min(s.timestamps[j], end);
if(rightup > leftup ) {
last = j;
}
}
}
if( first == -1 || last == -1) {
throw new RuntimeException("Invalid range");
}
if(s instanceof VDef) {
// Already a variable, just check if it fits
Value v = ((VDef) s).getValue();
// No time stamp, or not time stamped value, keep it
if(v.timestamp == 0) {
val = v;
}
else {
if(v.timestamp < end && v.timestamp > start) {
val = v;
}
else {
val = new Value(0, Double.NaN);
}
}
}
else {
long[] timestamps = new long[ last - first + 1];
System.arraycopy(s.timestamps, first, timestamps, 0, timestamps.length);
double[] values = new double[ last - first + 1];
System.arraycopy(s.getValues(), first, values, 0, values.length);
val = fill(timestamps, values, start, end);
}
}
public Value getValue() {
assert val != null : "Used before calculation";
return val;
}
/**
* This method is call with the needed values, extracted from the datasource to do the calculation.
*
* Value is to be filled with both the double value and a possible timestamp, when it's used to find
* a specific point
*
* @param timestamps the timestamps for the value
* @param values the actual values
* @param start the start of the period
* @param end the end of the period
* @return a filled Value object
*/
protected abstract Value fill(long timestamps[], double[] values, long start, long end);
/**
* Find the first valid data point and it's timestamp
*
*/
public static class FIRST extends Variable {
@Override
protected Value fill(long[] timestamps, double[] values, long start, long end) {
for(int i = 0; i < values.length; i++) {
if( timestamps[i] > start && timestamps[i] < end && ! Double.isNaN(values[i])) {
return new Value(timestamps[i], values[i]);
}
}
return new Value(0, Double.NaN);
}
}
/**
* Find the first last valid point and it's timestamp
*
*/
public static class LAST extends Variable {
@Override
protected Value fill(long[] timestamps, double[] values, long start, long end) {
for(int i = values.length - 1 ; i >=0 ; i--) {
if( ! Double.isNaN(values[i]) ) {
return new Value(timestamps[i], values[i]);
}
}
return new Value(0, Double.NaN);
}
}
/**
* The smallest of the data points and it's time stamp (the first one) is stored.
*
*/
public static class MIN extends Variable {
@Override
protected Value fill(long[] timestamps, double[] values, long start, long end) {
long timestamp = 0;
double value = Double.NaN;
for(int i = values.length -1 ; i >=0 ; i--) {
if(! Double.isNaN(values[i]) && Double.isNaN(value)) {
timestamp = timestamps[i];
value = values[i];
} else if( ! Double.isNaN(values[i]) && value > values[i]) {
timestamp = timestamps[i];
value = values[i];
}
}
return new Value(timestamp, value);
}
}
/**
* The biggest of the data points and it's time stamp (the first one) is stored.
*
*/
public static class MAX extends Variable {
@Override
protected Value fill(long[] timestamps, double[] values, long start, long end) {
long timestamp = 0;
double value = Double.NaN;
for(int i = values.length -1 ; i >=0 ; i--) {
if(! Double.isNaN(values[i]) && Double.isNaN(value)) {
timestamp = timestamps[i];
value = values[i];
} else if(!Double.isNaN(values[i]) && value < values[i]) {
timestamp = timestamps[i];
value = values[i];
}
}
return new Value(timestamp, value);
}
}
/**
* Calculate the sum of the data points.
*
*/
public static class TOTAL extends Variable {
@Override
protected Value fill(long[] timestamps, double[] values, long start, long end) {
double value = Double.NaN;
for(double tempVal: values) {
value = Util.sum(value, tempVal);
}
return new Value(0, value * (timestamps[1] - timestamps[0]) );
}
}
/**
* Calculate the average of the data points.
*
*/
public static class AVERAGE extends Variable {
@Override
protected Value fill(long[] timestamps, double[] values, long start, long end) {
double value = 0;
int count = 0;
for(int i = values.length - 1 ; i >= 0 ; i--) {
if( !Double.isNaN(values[i]) ) {
count++;
value = Double.isNaN(value) ? values[i] : values[i] + value;
}
}
if(! Double.isNaN(value)) {
value = value / count;
}
else {
value = Double.NaN;
}
return new Value(0, value);
}
}
/**
* Calculate the standard deviation for the data point.
*
*/
public static class STDDEV extends Variable {
@Override
protected Value fill(long[] timestamps, double[] values, long start, long end) {
double value = Double.NaN;
int count = 0;
double M = 0.0;
double S = 0.0;
// See Knuth TAOCP vol 2, 3rd edition, page 232 and http://www.johndcook.com/standard_deviation.html
for(double cursVal: values) {
if(Double.isNaN(cursVal))
continue;
count++;
if(count == 1) {
M = cursVal;
S = 0;
}
else {
double dM = cursVal - M;
M += dM/count;
S += dM * (cursVal - M);
}
}
if(count > 1) {
value = Math.sqrt( S/(count - 1) );
}
return new Value(0, value);
}
}
/**
* Store all the informations about a datasource point, for predictive and consistent sorting
*
*/
static final class PercentElem {
long timestamp;
double value;
PercentElem(int pos, long timestamp, double value) {
this.timestamp = timestamp;
this.value = value;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PercentElem other = (PercentElem) obj;
if (timestamp != other.timestamp)
return false;
return true;
}
@Override
public int hashCode() {
return Long.valueOf(timestamp).hashCode();
}
@Override
public String toString() {
return String.format("[%d, %f]", timestamp, value);
}
}
/**
* The sort used by rrdtool for percent, where NaN < -INF < finite values < INF
*
*/
static final class ComparPercentElemen implements Comparator<PercentElem>, Serializable {
@Override
public final int compare(PercentElem arg0, PercentElem arg1) {
if(Double.isNaN(arg0.value) && Double.isNaN(arg1.value))
return Long.signum(arg0.timestamp - arg1.timestamp);
else if(Double.isNaN(arg0.value))
return -1;
else if (Double.isNaN(arg1.value))
return +1;
else {
int compared = Double.compare(arg0.value, arg1.value);
if (compared == 0) {
compared = Long.signum(arg0.timestamp - arg1.timestamp);
}
return compared;
}
}
}
/**
* Find the point at the n-th percentile.
*
*/
public static class PERCENTILE extends Variable {
private final float percentile;
private final boolean withNaN;
protected PERCENTILE(float percentile, boolean withNaN) {
this.percentile = percentile;
this.withNaN = withNaN;
}
public PERCENTILE(double percentile) {
this((float) percentile, true);
}
public PERCENTILE(float percentile) {
this(percentile, true);
}
@Override
protected Value fill(long[] timestamps, double[] values, long start, long end) {
// valuesSet will be a set with NaN packet at the start
SortedSet<PercentElem> valuesSet = new TreeSet<PercentElem>(new ComparPercentElemen());
for(int i = 0 ; i < values.length ; i++) {
valuesSet.add(new PercentElem(i, timestamps[i], values[i]));
}
//If not with nan, just drop all nan (inferior to min value)
if( ! withNaN) {
valuesSet = valuesSet.tailSet(new PercentElem(0, 0, Double.NEGATIVE_INFINITY ));
}
PercentElem[] element = (PercentElem[]) valuesSet.toArray(new PercentElem[valuesSet.size()]);
int pos = Math.round(percentile * (element.length - 1) / 100);
// if we have anything left...
if (pos >= 0) {
double value = element[pos].value;
long timestamp = element[pos].timestamp;
return new Value(timestamp, value);
}
return new Value(0, Double.NaN);
}
}
public static class PERCENTILENAN extends PERCENTILE {
public PERCENTILENAN(float percentile) {
super(percentile, false);
}
public PERCENTILENAN(double percentile) {
super((float)percentile, false);
}
}
/**
* Calculate the slop of the least squares line.
*
*/
public static class LSLSLOPE extends Variable {
@Override
protected Value fill(long[] timestamps, double[] values, long start, long end) {
int cnt = 0;
int lslstep = 0;
double SUMx = 0.0;
double SUMy = 0.0;
double SUMxy = 0.0;
double SUMxx = 0.0;
double lslslope;
for(int i = 0; i < values.length; i++) {
double value = values[i];
if (!Double.isNaN(value)) {
cnt++;
SUMx += lslstep;
SUMxx += lslstep * lslstep;
SUMy += value;
SUMxy += lslstep * value;
}
lslstep++;
}
if(cnt > 0) {
/* Bestfit line by linear least squares method */
lslslope = (SUMx * SUMy - cnt * SUMxy) / (SUMx * SUMx - cnt * SUMxx);
return new Value(0, lslslope);
}
return new Value(0, Double.NaN);
}
}
/**
* Calculate the y-intercept of the least squares line.
*
*/
public static class LSLINT extends Variable {
@Override
protected Value fill(long[] timestamps, double[] values, long start, long end) {
int cnt = 0;
int lslstep = 0;
double SUMx = 0.0;
double SUMy = 0.0;
double SUMxy = 0.0;
double SUMxx = 0.0;
double lslslope;
double lslint;
for(int i = 0; i < values.length; i++) {
double value = values[i];
if (!Double.isNaN(value)) {
cnt++;
SUMx += lslstep;
SUMxx += lslstep * lslstep;
SUMy += value;
SUMxy += lslstep * value;
}
lslstep++;
}
if(cnt > 0) {
/* Bestfit line by linear least squares method */
lslslope = (SUMx * SUMy - cnt * SUMxy) / (SUMx * SUMx - cnt * SUMxx);
lslint = (SUMy - lslslope * SUMx) / cnt;
return new Value(0, lslint);
}
return new Value(0, Double.NaN);
}
}
/**
* Calculate the correlation coefficient of the least squares line.
*
*/
public static class LSLCORREL extends Variable {
@Override
protected Value fill(long[] timestamps, double[] values, long start, long end) {
int cnt = 0;
int lslstep = 0;
double SUMx = 0.0;
double SUMy = 0.0;
double SUMxy = 0.0;
double SUMxx = 0.0;
double SUMyy = 0.0;
double lslcorrel;
for(int i = 0; i < values.length; i++) {
double value = values[i];
if (!Double.isNaN(value)) {
cnt++;
SUMx += lslstep;
SUMxx += lslstep * lslstep;
SUMy += value;
SUMxy += lslstep * value;
SUMyy += value * value;
}
lslstep++;
}
if(cnt > 0) {
/* Bestfit line by linear least squares method */
lslcorrel =
(SUMxy - (SUMx * SUMy) / cnt) /
Math.sqrt((SUMxx - (SUMx * SUMx) / cnt) * (SUMyy - (SUMy * SUMy) / cnt));
return new Value(0, lslcorrel);
}
return new Value(0, Double.NaN);
}
}
}