From: Sasa M. <sa...@us...> - 2004-11-18 13:28:07
|
Update of /cvsroot/jrobin/src/org/jrobin/core In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv782/org/jrobin/core Modified Files: FetchData.java RpnCalculator.java Util.java Added Files: FetchDataStats.java Log Message: Improved FetchData statistics. Class RpnCalculator is now public - can be used as a general purpose RPN calculation tool. Index: RpnCalculator.java =================================================================== RCS file: /cvsroot/jrobin/src/org/jrobin/core/RpnCalculator.java,v retrieving revision 1.4 retrieving revision 1.5 diff -C2 -d -r1.4 -r1.5 *** RpnCalculator.java 20 May 2004 10:29:32 -0000 1.4 --- RpnCalculator.java 18 Nov 2004 13:27:58 -0000 1.5 *************** *** 27,346 **** import java.util.StringTokenizer; ! class RpnCalculator { ! static final String VAR_PLACEHOLDER = "value"; ! private static final byte TOK_VAR = 0; ! private static final byte TOK_NUM = 1; ! private static final byte TOK_PLUS = 2; ! private static final byte TOK_MINUS = 3; ! private static final byte TOK_MULT = 4; ! private static final byte TOK_DIV = 5; ! private static final byte TOK_MOD = 6; ! private static final byte TOK_SIN = 7; ! private static final byte TOK_COS = 8; ! private static final byte TOK_LOG = 9; ! private static final byte TOK_EXP = 10; ! private static final byte TOK_FLOOR = 11; ! private static final byte TOK_CEIL = 12; ! private static final byte TOK_ROUND = 13; ! private static final byte TOK_POW = 14; ! private static final byte TOK_ABS = 15; ! private static final byte TOK_SQRT = 16; ! private static final byte TOK_RANDOM = 17; ! private static final byte TOK_LT = 18; ! private static final byte TOK_LE = 19; ! private static final byte TOK_GT = 20; ! private static final byte TOK_GE = 21; ! private static final byte TOK_EQ = 22; ! private static final byte TOK_IF = 23; ! private static final byte TOK_MIN = 24; ! private static final byte TOK_MAX = 25; ! private static final byte TOK_LIMIT = 26; ! private static final byte TOK_DUP = 27; ! private static final byte TOK_EXC = 28; ! private static final byte TOK_POP = 29; ! private static final byte TOK_UN = 30; ! private static final byte TOK_UNKN = 31; ! // private static final byte TOK_NOW = 32; ! // private static final byte TOK_TIME = 33; ! private static final byte TOK_PI = 34; ! private static final byte TOK_E = 35; ! private static final byte TOK_AND = 36; ! private static final byte TOK_OR = 37; ! private static final byte TOK_XOR = 38; ! private String[] tokens; ! private byte[] tokenCodes; ! private double[] parsedDoubles; private RpnStack stack = new RpnStack(); private String rpnExpression; - private double value; - // private long timestamp; ! RpnCalculator(String rpnExpression) throws RrdException { this.rpnExpression = rpnExpression; createTokens(); } ! void setValue(double value) { ! this.value = value; ! } ! ! /* not supported yet ! public void setTimestamp(long timestamp) { ! this.timestamp = timestamp; ! } ! */ ! ! private void createTokens() throws RrdException { ! StringTokenizer st = new StringTokenizer(rpnExpression, ","); ! int count = st.countTokens(); ! tokens = new String[count]; ! tokenCodes = new byte[count]; ! parsedDoubles = new double[count]; for(int i = 0; st.hasMoreTokens(); i++) { ! String token = st.nextToken(); ! tokens[i] = token; ! byte tokenCode = findTokenCode(token); ! tokenCodes[i] = tokenCode; ! if(tokenCode == TOK_NUM) { ! parsedDoubles[i] = Double.parseDouble(token); ! } } } ! private byte findTokenCode(String token) throws RrdException { ! if(isVariable(token)) { ! return TOK_VAR; ! } ! else if(isNumber(token)) { ! return TOK_NUM; } ! else if(token.equals("+")) { ! return TOK_PLUS; } ! else if(token.equals("-")) { ! return TOK_MINUS; } ! else if(token.equals("*")) { ! return TOK_MULT; } ! else if(token.equals("/")) { ! return TOK_DIV; } ! else if(token.equals("%")) { ! return TOK_MOD; } ! else if(token.equals("SIN")) { ! return TOK_SIN; } ! else if(token.equals("COS")) { ! return TOK_COS; } ! else if(token.equals("LOG")) { ! return TOK_LOG; } ! else if(token.equals("EXP")) { ! return TOK_EXP; } ! else if(token.equals("FLOOR")) { ! return TOK_FLOOR; } ! else if(token.equals("CEIL")) { ! return TOK_CEIL; } ! else if(token.equals("ROUND")) { ! return TOK_ROUND; } ! else if(token.equals("POW")) { ! return TOK_POW; } ! else if(token.equals("ABS")) { ! return TOK_ABS; } ! else if(token.equals("SQRT")) { ! return TOK_SQRT; } ! else if(token.equals("RANDOM")) { ! return TOK_RANDOM; } ! else if(token.equals("LT")) { ! return TOK_LT; } ! else if(token.equals("LE")) { ! return TOK_LE; } ! else if(token.equals("GT")) { ! return TOK_GT; } ! else if(token.equals("GE")) { ! return TOK_GE; } ! else if(token.equals("EQ")) { ! return TOK_EQ; } ! else if(token.equals("IF")) { ! return TOK_IF; } ! else if(token.equals("MIN")) { ! return TOK_MIN; } ! else if(token.equals("MAX")) { ! return TOK_MAX; } ! else if(token.equals("LIMIT")) { ! return TOK_LIMIT; } ! else if(token.equals("DUP")) { ! return TOK_DUP; } ! else if(token.equals("EXC")) { ! return TOK_EXC; } ! else if(token.equals("POP")) { ! return TOK_POP; } ! else if(token.equals("UN")) { ! return TOK_UN; } ! else if(token.equals("UNKN")) { ! return TOK_UNKN; } ! ! /* not supported yet ! else if(token.equals("NOW")) { ! return TOK_NOW; } ! else if(token.equals("TIME")) { ! return TOK_TIME; } ! */ ! else if(token.equals("PI")) { ! return TOK_PI; } ! else if(token.equals("E")) { ! return TOK_E; } ! else if(token.equals("AND")) { ! return TOK_AND; } ! else if(token.equals("OR")) { ! return TOK_OR; } ! else if(token.equals("XOR")) { ! return TOK_XOR; } else { ! throw new RrdException("Unknown RPN token encountered: " + token); } } ! private static boolean isNumber(String token) { ! try { ! Double.parseDouble(token); ! return true; ! } ! catch(NumberFormatException nfe) { ! return false; ! } } ! private static boolean isVariable(String token) { ! return token.equals(VAR_PLACEHOLDER); } ! double calculate() throws RrdException { ! resetCalculator(); ! for(int i = 0; i < tokenCodes.length; i++) { ! byte tokenCode = tokenCodes[i]; double x1, x2, x3; ! switch(tokenCode) { ! case TOK_NUM: ! push(parsedDoubles[i]); break; ! case TOK_VAR: ! push(value); break; ! case TOK_PLUS: ! push(pop() + pop()); break; ! case TOK_MINUS: ! x2 = pop(); x1 = pop(); push(x1 - x2); break; ! case TOK_MULT: ! push(pop() * pop()); break; ! case TOK_DIV: ! x2 = pop(); x1 = pop(); push(x1 / x2); break; ! case TOK_MOD: ! x2 = pop(); x1 = pop(); push(x1 % x2); break; ! case TOK_SIN: ! push(Math.sin(pop())); break; ! case TOK_COS: ! push(Math.cos(pop())); break; ! case TOK_LOG: ! push(Math.log(pop())); break; ! case TOK_EXP: ! push(Math.exp(pop())); break; ! case TOK_FLOOR: ! push(Math.floor(pop())); break; ! case TOK_CEIL: ! push(Math.ceil(pop())); break; ! case TOK_ROUND: ! push(Math.round(pop())); break; ! case TOK_POW: ! x2 = pop(); x1 = pop(); push(Math.pow(x1, x2)); break; ! case TOK_ABS: ! push(Math.abs(pop())); break; ! case TOK_SQRT: ! push(Math.sqrt(pop())); break; ! case TOK_RANDOM: ! push(Math.random()); break; ! case TOK_LT: ! x2 = pop(); x1 = pop(); push(x1 < x2? 1: 0); break; ! case TOK_LE: ! x2 = pop(); x1 = pop(); push(x1 <= x2? 1: 0); break; ! case TOK_GT: ! x2 = pop(); x1 = pop(); push(x1 > x2? 1: 0); break; ! case TOK_GE: ! x2 = pop(); x1 = pop(); push(x1 >= x2? 1: 0); break; ! case TOK_EQ: ! x2 = pop(); x1 = pop(); push(x1 == x2? 1: 0); break; ! case TOK_IF: ! x3 = pop(); x2 = pop(); x1 = pop(); push(x1 != 0? x2: x3); break; ! case TOK_MIN: ! push(Math.min(pop(), pop())); break; ! case TOK_MAX: ! push(Math.max(pop(), pop())); break; ! case TOK_LIMIT: x3 = pop(); x2 = pop(); x1 = pop(); ! push(x1 < x2 || x1 > x3? Double.NaN: x1); break; ! case TOK_DUP: ! x1 = pop(); push(x1); push(x1); break; ! case TOK_EXC: ! x2 = pop(); x1 = pop(); push(x2); push(x1); break; ! case TOK_POP: ! pop(); break; ! case TOK_UN: ! push(Double.isNaN(pop())? 1: 0); break; ! case TOK_UNKN: ! push(Double.NaN); break; ! /* not supported yet ! case TOK_NOW: ! push(Util.getTime()); break; ! case TOK_TIME: ! push(timestamp); break; ! */ ! case TOK_PI: ! push(Math.PI); break; ! case TOK_E: ! push(Math.E); break; ! case TOK_AND: ! x2 = pop(); x1 = pop(); push((x1 != 0 && x2 != 0)? 1: 0); break; ! case TOK_OR: ! x2 = pop(); x1 = pop(); push((x1 != 0 || x2 != 0)? 1: 0); break; ! case TOK_XOR: x2 = pop(); x1 = pop(); ! push(((x1 != 0 && x2 == 0) || (x1 == 0 && x2 != 0))? 1: 0); break; default: throw new RrdException("Unexpected RPN token encountered [" + ! tokenCode + "]"); } } --- 27,437 ---- import java.util.StringTokenizer; + import java.util.Map; + import java.util.HashMap; ! /** ! * Class which implements simple RPN calculator (RRDTool-like). <p> ! * To calculate the value of expression: ! * <pre> ! * square_root[(x + y) * (x-y)] ! * </pre> ! * for <code>x=5, y=4</code>, than for <code>x=6, y=4</code>, use the following code: ! * <pre> ! * RpnCalculator c = new RpnCalculator("x,y,+,x,y,-,*,SQRT"); ! * c.setValue("x", 5); ! * c.setValue("y", 4); ! * System.out.println(c.calculate()); ! * // change the value of "x", and leave "y" as before ! * c.setValue("x", 6); ! * System.out.println(c.calculate()); ! * </pre> ! * Notes:<p> ! * <ul> ! * <li>If you call the {@link #setValue(double)} method with just one double argument, ! * it will set the value of variable named "value" by default. ! * <li>The method {@link #setTimestamp(long)} will set the value of variable "timestamp". ! * This special variable can be referenced in the RPN expression by using the token TIME. ! * <li>Once set, variable values remain preserved between consecutive {@link #calculate()} calls. You can overwrite ! * this values by calling the {@link #setValue(String, double)} method again. To get rid of all variable values, ! * use method {@link #clearValues()}. ! * </ul> ! * ! */ ! public class RpnCalculator { ! /** Default variable name for the {@link #setValue(double)} method ("value") */ ! public static final String VALUE_PLACEHOLDER = "value"; ! /** Default variable name for the {@link #setTimestamp(long)} method ("timestamp") */ ! public static final String TIMESTAMP_PLACEHOLDER = "timestamp"; ! private static final byte TKN_VAR = 0; ! private static final byte TKN_NUM = 1; ! private static final byte TKN_PLUS = 2; ! private static final byte TKN_MINUS = 3; ! private static final byte TKN_MULT = 4; ! private static final byte TKN_DIV = 5; ! private static final byte TKN_MOD = 6; ! private static final byte TKN_SIN = 7; ! private static final byte TKN_COS = 8; ! private static final byte TKN_LOG = 9; ! private static final byte TKN_EXP = 10; ! private static final byte TKN_FLOOR = 11; ! private static final byte TKN_CEIL = 12; ! private static final byte TKN_ROUND = 13; ! private static final byte TKN_POW = 14; ! private static final byte TKN_ABS = 15; ! private static final byte TKN_SQRT = 16; ! private static final byte TKN_RANDOM = 17; ! private static final byte TKN_LT = 18; ! private static final byte TKN_LE = 19; ! private static final byte TKN_GT = 20; ! private static final byte TKN_GE = 21; ! private static final byte TKN_EQ = 22; ! private static final byte TKN_IF = 23; ! private static final byte TKN_MIN = 24; ! private static final byte TKN_MAX = 25; ! private static final byte TKN_LIMIT = 26; ! private static final byte TKN_DUP = 27; ! private static final byte TKN_EXC = 28; ! private static final byte TKN_POP = 29; ! private static final byte TKN_UN = 30; ! private static final byte TKN_UNKN = 31; ! private static final byte TKN_NOW = 32; ! private static final byte TKN_TIME = 33; ! private static final byte TKN_PI = 34; ! private static final byte TKN_E = 35; ! private static final byte TKN_AND = 36; ! private static final byte TKN_OR = 37; ! private static final byte TKN_XOR = 38; ! private Map values = new HashMap(); ! private Token[] tokens; private RpnStack stack = new RpnStack(); private String rpnExpression; ! /** ! * Creates new RpnCalculator. RpnCalculator objects may be safely reused to calculate as many ! * expression values (for different variable values) as needed. ! * @param rpnExpression RPN expression to be used. RPN tokens should be comma (",") ! * or space (" ") delimited. ! */ ! public RpnCalculator(String rpnExpression) { this.rpnExpression = rpnExpression; createTokens(); } ! private void createTokens() { ! StringTokenizer st = new StringTokenizer(rpnExpression, ", "); ! tokens = new Token[st.countTokens()]; for(int i = 0; st.hasMoreTokens(); i++) { ! tokens[i] = createToken(st.nextToken()); } } ! private Token createToken(String str) { ! Token token = new Token(str); ! if(Util.isDouble(str)) { ! token.id = TKN_NUM; ! token.number = Util.parseDouble(str); } ! else if(str.equals("+")) { ! token.id = TKN_PLUS; } ! else if(str.equals("-")) { ! token.id = TKN_MINUS; } ! else if(str.equals("*")) { ! token.id = TKN_MULT; } ! else if(str.equals("/")) { ! token.id = TKN_DIV; } ! else if(str.equals("%")) { ! token.id = TKN_MOD; } ! else if(str.equals("SIN")) { ! token.id = TKN_SIN; } ! else if(str.equals("COS")) { ! token.id = TKN_COS; } ! else if(str.equals("LOG")) { ! token.id = TKN_LOG; } ! else if(str.equals("EXP")) { ! token.id = TKN_EXP; } ! else if(str.equals("FLOOR")) { ! token.id = TKN_FLOOR; } ! else if(str.equals("CEIL")) { ! token.id = TKN_CEIL; } ! else if(str.equals("ROUND")) { ! token.id = TKN_ROUND; } ! else if(str.equals("POW")) { ! token.id = TKN_POW; } ! else if(str.equals("ABS")) { ! token.id = TKN_ABS; } ! else if(str.equals("SQRT")) { ! token.id = TKN_SQRT; } ! else if(str.equals("RANDOM")) { ! token.id = TKN_RANDOM; } ! else if(str.equals("LT")) { ! token.id = TKN_LT; } ! else if(str.equals("LE")) { ! token.id = TKN_LE; } ! else if(str.equals("GT")) { ! token.id = TKN_GT; } ! else if(str.equals("GE")) { ! token.id = TKN_GE; } ! else if(str.equals("EQ")) { ! token.id = TKN_EQ; } ! else if(str.equals("IF")) { ! token.id = TKN_IF; } ! else if(str.equals("MIN")) { ! token.id = TKN_MIN; } ! else if(str.equals("MAX")) { ! token.id = TKN_MAX; } ! else if(str.equals("LIMIT")) { ! token.id = TKN_LIMIT; } ! else if(str.equals("DUP")) { ! token.id = TKN_DUP; } ! else if(str.equals("EXC")) { ! token.id = TKN_EXC; } ! else if(str.equals("POP")) { ! token.id = TKN_POP; } ! else if(str.equals("UN")) { ! token.id = TKN_UN; } ! else if(str.equals("UNKN")) { ! token.id = TKN_UNKN; } ! else if(str.equals("NOW")) { ! token.id = TKN_NOW; } ! else if(str.equals("TIME")) { ! token.id = TKN_TIME; } ! else if(str.equals("PI")) { ! token.id = TKN_PI; } ! else if(str.equals("E")) { ! token.id = TKN_E; } ! else if(str.equals("AND")) { ! token.id = TKN_AND; } ! else if(str.equals("OR")) { ! token.id = TKN_OR; } ! else if(str.equals("XOR")) { ! token.id = TKN_XOR; } else { ! token.id = TKN_VAR; } + return token; } ! /** ! * Sets the value for the default variable if RPN expression ("value"). ! * @param value Value to be used in calculation ! */ ! public void setValue(double value) { ! setValue(VALUE_PLACEHOLDER, value); } ! /** ! * Sets the timestamp to be used in evaluation of the RPN expression. To use this ! * value in the RPN expression, use token TIME. ! * @param timestamp The value which will be used if token TIME is found in the RPN expression ! */ ! public void setTimestamp(long timestamp) { ! setValue(TIMESTAMP_PLACEHOLDER, timestamp); } ! /** ! * Sets new value for a variable in the RPN expression. ! * @param name Variable name ! * @param value Variable value ! */ ! public void setValue(String name, double value) { ! values.put(name, new Double(value)); ! } ! ! /** ! * Clears all values specified for variables in the RPN expression ! */ ! public void clearValues() { ! values.clear(); ! } ! ! /** ! * Evaluates RPN expression, by replacing variable placeholders with specified values. You are free ! * to call this method as many times as needed, with the same or modified variable values. ! * @return The value of the RPN expression ! * @throws RrdException Thrown if some variable values are not specified before this method is called, or if the ! * RPN expression is not valid. ! */ ! public double calculate() throws RrdException { ! resetStack(); ! for(int i = 0; i < tokens.length; i++) { ! Token token = tokens[i]; double x1, x2, x3; ! switch(token.id) { ! case TKN_NUM: ! push(token.number); ! break; ! case TKN_VAR: ! push(getValue(token.str)); ! break; ! case TKN_PLUS: ! push(pop() + pop()); ! break; ! case TKN_MINUS: ! x2 = pop(); x1 = pop(); ! push(x1 - x2); ! break; ! case TKN_MULT: ! push(pop() * pop()); ! break; ! case TKN_DIV: ! x2 = pop(); x1 = pop(); ! push(x1 / x2); ! break; ! case TKN_MOD: ! x2 = pop(); x1 = pop(); ! push(x1 % x2); ! break; ! case TKN_SIN: ! push(Math.sin(pop())); ! break; ! case TKN_COS: ! push(Math.cos(pop())); ! break; ! case TKN_LOG: ! push(Math.log(pop())); ! break; ! case TKN_EXP: ! push(Math.exp(pop())); ! break; ! case TKN_FLOOR: ! push(Math.floor(pop())); ! break; ! case TKN_CEIL: ! push(Math.ceil(pop())); ! break; ! case TKN_ROUND: ! push(Math.round(pop())); ! break; ! case TKN_POW: ! x2 = pop(); x1 = pop(); ! push(Math.pow(x1, x2)); ! break; ! case TKN_ABS: ! push(Math.abs(pop())); ! break; ! case TKN_SQRT: ! push(Math.sqrt(pop())); ! break; ! case TKN_RANDOM: ! push(Math.random()); ! break; ! case TKN_LT: ! x2 = pop(); x1 = pop(); ! push(x1 < x2? 1: 0); ! break; ! case TKN_LE: ! x2 = pop(); x1 = pop(); ! push(x1 <= x2? 1: 0); ! break; ! case TKN_GT: ! x2 = pop(); x1 = pop(); ! push(x1 > x2? 1: 0); ! break; ! case TKN_GE: ! x2 = pop(); x1 = pop(); ! push(x1 >= x2? 1: 0); ! break; ! case TKN_EQ: ! x2 = pop(); x1 = pop(); ! push(x1 == x2? 1: 0); ! break; ! case TKN_IF: x3 = pop(); x2 = pop(); x1 = pop(); ! push(x1 != 0? x2: x3); ! break; ! case TKN_MIN: ! push(Math.min(pop(), pop())); ! break; ! case TKN_MAX: ! push(Math.max(pop(), pop())); ! break; ! case TKN_LIMIT: ! x3 = pop(); x2 = pop(); x1 = pop(); ! push(x1 < x2 || x1 > x3? Double.NaN: x1); ! break; ! case TKN_DUP: ! push(peek()); ! break; ! case TKN_EXC: x2 = pop(); x1 = pop(); ! push(x2); ! push(x1); ! break; ! case TKN_POP: ! pop(); ! break; ! case TKN_UN: ! push(Double.isNaN(pop())? 1: 0); ! break; ! case TKN_UNKN: ! push(Double.NaN); ! break; ! case TKN_NOW: ! push(Util.getTime()); ! break; ! case TKN_TIME: ! push(getValue(TIMESTAMP_PLACEHOLDER)); ! break; ! case TKN_PI: ! push(Math.PI); ! break; ! case TKN_E: ! push(Math.E); ! break; ! case TKN_AND: ! x2 = pop(); x1 = pop(); ! push((x1 != 0 && x2 != 0)? 1: 0); ! break; ! case TKN_OR: ! x2 = pop(); x1 = pop(); ! push((x1 != 0 || x2 != 0)? 1: 0); ! break; ! case TKN_XOR: ! x2 = pop(); x1 = pop(); ! push(((x1 != 0 && x2 == 0) || (x1 == 0 && x2 != 0))? 1: 0); ! break; default: throw new RrdException("Unexpected RPN token encountered [" + ! token.id + "," + token.str + "]"); } } *************** *** 348,374 **** if(!isStackEmpty()) { throw new RrdException("Stack not empty at the end of calculation. " + ! "Probably bad RPN expression"); } return retVal; } ! void push(double x) throws RrdException { stack.push(x); } ! double pop() throws RrdException { return stack.pop(); } ! void resetCalculator() { stack.reset(); } ! boolean isStackEmpty() { return stack.isEmpty(); } ! class RpnStack { ! static final int MAX_STACK_SIZE = 1000; private double[] stack = new double[MAX_STACK_SIZE]; private int pos = 0; --- 439,476 ---- if(!isStackEmpty()) { throw new RrdException("Stack not empty at the end of calculation. " + ! "Probably bad RPN expression [" + rpnExpression + "]"); } return retVal; } ! private double getValue(String varName) throws RrdException { ! if(values.containsKey(varName)) { ! return ((Double) values.get(varName)).doubleValue(); ! } ! throw new RrdException("Value of variable [" + varName + "] not specified"); ! } ! ! private void push(double x) throws RrdException { stack.push(x); } ! private double pop() throws RrdException { return stack.pop(); } ! private double peek() throws RrdException { ! return stack.peek(); ! } ! ! private void resetStack() { stack.reset(); } ! private boolean isStackEmpty() { return stack.isEmpty(); } ! private class RpnStack { ! private static final int MAX_STACK_SIZE = 1000; private double[] stack = new double[MAX_STACK_SIZE]; private int pos = 0; *************** *** 376,381 **** void push(double x) throws RrdException { if(pos >= MAX_STACK_SIZE) { ! throw new RrdException( ! "PUSH failed, RPN stack full [" + MAX_STACK_SIZE + "]"); } stack[pos++] = x; --- 478,482 ---- void push(double x) throws RrdException { if(pos >= MAX_STACK_SIZE) { ! throw new RrdException("PUSH failed, RPN stack full [" + MAX_STACK_SIZE + "]"); } stack[pos++] = x; *************** *** 389,392 **** --- 490,500 ---- } + double peek() throws RrdException { + if(pos <= 0) { + throw new RrdException("PEEK failed, RPN stack is empty "); + } + return stack[pos - 1]; + } + void reset() { pos = 0; *************** *** 394,407 **** boolean isEmpty() { ! return pos == 0; } } ! /* public static void main(String[] args) throws RrdException { ! RpnCalculator c = new RpnCalculator("2,3,/,value,+"); ! c.setValue(5); System.out.println(c.calculate()); } - */ } --- 502,527 ---- boolean isEmpty() { ! return pos <= 0; } } ! private class Token { ! byte id; ! String str; ! double number; ! ! Token(String str) { ! this.str = str; ! } ! } ! ! /** Just a small, trivial demo */ public static void main(String[] args) throws RrdException { ! RpnCalculator c = new RpnCalculator("x,y,+,x,y,-,*,SQRT"); ! c.setValue("x", 5); ! c.setValue("y", 4); ! System.out.println(c.calculate()); ! c.setValue("x", 6); System.out.println(c.calculate()); } } Index: FetchData.java =================================================================== RCS file: /cvsroot/jrobin/src/org/jrobin/core/FetchData.java,v retrieving revision 1.10 retrieving revision 1.11 diff -C2 -d -r1.10 -r1.11 *** FetchData.java 21 Sep 2004 08:42:09 -0000 1.10 --- FetchData.java 18 Nov 2004 13:27:58 -0000 1.11 *************** *** 53,56 **** --- 53,58 ---- * all values for the i-th datasource. Returned datasource values correspond to * the values returned with {@link #getTimestamps() getTimestamps()} method.<p> + * + * Use {@link #getStats(String, String)} method to calculate aggregates for the fetched data<p> */ public class FetchData implements RrdDataSet, ConsolFuns { *************** *** 64,68 **** this.matchingArchive = matchingArchive; this.dsNames = request.getFilter(); ! if(this.dsNames == null) { this.dsNames = matchingArchive.getParentDb().getDsNames(); } --- 66,70 ---- this.matchingArchive = matchingArchive; this.dsNames = request.getFilter(); ! if (this.dsNames == null) { this.dsNames = matchingArchive.getParentDb().getDsNames(); } *************** *** 81,94 **** * Returns the number of rows fetched from the corresponding RRD. * Each row represents datasource values for the specific timestamp. * @return Number of rows. */ ! public int getRowCount() { return timestamps.length; } ! /** * Returns the number of columns fetched from the corresponding RRD. * This number is always equal to the number of datasources defined * in the RRD. Each column represents values of a single datasource. * @return Number of columns (datasources). */ --- 83,98 ---- * Returns the number of rows fetched from the corresponding RRD. * Each row represents datasource values for the specific timestamp. + * * @return Number of rows. */ ! public int getRowCount() { return timestamps.length; } ! /** * Returns the number of columns fetched from the corresponding RRD. * This number is always equal to the number of datasources defined * in the RRD. Each column represents values of a single datasource. + * * @return Number of columns (datasources). */ *************** *** 100,111 **** * Returns the number of rows fetched from the corresponding RRD. * Each row represents datasource values for the specific timestamp. * @param rowIndex Row index. * @return FetchPoint object which represents datasource values for the ! * specific timestamp. */ public FetchPoint getRow(int rowIndex) { int numCols = getColumnCount(); FetchPoint point = new FetchPoint(timestamps[rowIndex], getColumnCount()); ! for(int dsIndex = 0; dsIndex < numCols; dsIndex++) { point.setValue(dsIndex, values[dsIndex][rowIndex]); } --- 104,117 ---- * Returns the number of rows fetched from the corresponding RRD. * Each row represents datasource values for the specific timestamp. + * * @param rowIndex Row index. * @return FetchPoint object which represents datasource values for the ! * specific timestamp. ! * @deprecated The usage of FetchPoint object is deprecated. */ public FetchPoint getRow(int rowIndex) { int numCols = getColumnCount(); FetchPoint point = new FetchPoint(timestamps[rowIndex], getColumnCount()); ! for (int dsIndex = 0; dsIndex < numCols; dsIndex++) { point.setValue(dsIndex, values[dsIndex][rowIndex]); } *************** *** 116,119 **** --- 122,126 ---- * Returns an array of timestamps covering the whole range specified in the * {@link FetchRequest FetchReguest} object. + * * @return Array of equidistant timestamps. */ *************** *** 124,127 **** --- 131,135 ---- /** * Returns the step with which this data was fetched. + * * @return Step as long. */ *************** *** 134,137 **** --- 142,146 ---- * Returned values correspond to timestamps * returned with {@link #getTimestamps() getTimestamps()} method. + * * @param dsIndex Datasource index. * @return Array of single datasource values. *************** *** 145,148 **** --- 154,158 ---- * Returned values correspond to timestamps * returned with {@link #getTimestamps() getTimestamps()} method. + * * @return Two-dimensional aray of all datasource values. */ *************** *** 150,158 **** return values; } ! /** * Returns all archived values for a single datasource. * Returned values correspond to timestamps * returned with {@link #getTimestamps() getTimestamps()} method. * @param dsName Datasource name. * @return Array of single datasource values. --- 160,169 ---- return values; } ! /** * Returns all archived values for a single datasource. * Returned values correspond to timestamps * returned with {@link #getTimestamps() getTimestamps()} method. + * * @param dsName Datasource name. * @return Array of single datasource values. *************** *** 160,165 **** */ public double[] getValues(String dsName) throws RrdException { ! for(int dsIndex = 0; dsIndex < getColumnCount(); dsIndex++) { ! if(dsName.equals(dsNames[dsIndex])) { return getValues(dsIndex); } --- 171,176 ---- */ public double[] getValues(String dsName) throws RrdException { ! for (int dsIndex = 0; dsIndex < getColumnCount(); dsIndex++) { ! if (dsName.equals(dsNames[dsIndex])) { return getValues(dsIndex); } *************** *** 170,173 **** --- 181,185 ---- /** * Returns {@link FetchRequest FetchRequest} object used to create this FetchData object. + * * @return Fetch request object. */ *************** *** 176,181 **** } ! /** * Returns the first timestamp in this FetchData object. * @return The smallest timestamp. */ --- 188,194 ---- } ! /** * Returns the first timestamp in this FetchData object. + * * @return The smallest timestamp. */ *************** *** 186,189 **** --- 199,203 ---- /** * Returns the last timestamp in this FecthData object. + * * @return The biggest timestamp. */ *************** *** 196,199 **** --- 210,214 ---- * timestamps specified in the fetch request. All datasource values are obtained * from round robin archives belonging to this archive. + * * @return Matching archive. */ *************** *** 206,209 **** --- 221,225 ---- * was filtered (data was fetched only for selected datasources), only datasources selected * for fetching are returned. + * * @return Array of datasource names. */ *************** *** 211,217 **** return dsNames; } ! /** * Retrieve the table index number of a datasource by name. Names are case sensitive. * @param dsName Name of the datasource for which to find the index. * @return Index number of the datasources in the value table. --- 227,234 ---- return dsNames; } ! /** * Retrieve the table index number of a datasource by name. Names are case sensitive. + * * @param dsName Name of the datasource for which to find the index. * @return Index number of the datasources in the value table. *************** *** 219,226 **** public int getDsIndex(String dsName) { // Let's assume the table of dsNames is always small, so it is not necessary to use a hashmap for lookups ! for (int i = 0; i < dsNames.length; i++) ! if ( dsNames[i].equals(dsName) ) return i; ! return -1; // Datasource not found ! } --- 236,244 ---- public int getDsIndex(String dsName) { // Let's assume the table of dsNames is always small, so it is not necessary to use a hashmap for lookups ! for (int i = 0; i < dsNames.length; i++) { ! if (dsNames[i].equals(dsName)) { return i; ! } ! } return -1; // Datasource not found ! } *************** *** 230,234 **** */ public void dump() { ! for(int i = 0; i < getRowCount(); i++) { System.out.println(getRow(i).dump()); } --- 248,252 ---- */ public void dump() { ! for (int i = 0; i < getRowCount(); i++) { System.out.println(getRow(i).dump()); } *************** *** 237,240 **** --- 255,259 ---- /** * Returns string representing fetched data in a RRDTool-like form. + * * @return Fetched data as a string in a rrdfetch-like output form. */ *************** *** 245,258 **** buff.append(padWithBlanks("", 10)); buff.append(" "); ! for(int i = 0; i < dsNames.length; i++) { buff.append(padWithBlanks(dsNames[i], 18)); } buff.append("\n \n"); ! for(int i = 0; i < timestamps.length; i++) { buff.append(padWithBlanks("" + timestamps[i], 10)); buff.append(":"); ! for(int j = 0; j < dsNames.length; j++) { double value = values[j][i]; ! String valueStr = Double.isNaN(value)? "nan": df.format(value); buff.append(padWithBlanks(valueStr, 18)); } --- 264,277 ---- buff.append(padWithBlanks("", 10)); buff.append(" "); ! for (int i = 0; i < dsNames.length; i++) { buff.append(padWithBlanks(dsNames[i], 18)); } buff.append("\n \n"); ! for (int i = 0; i < timestamps.length; i++) { buff.append(padWithBlanks("" + timestamps[i], 10)); buff.append(":"); ! for (int j = 0; j < dsNames.length; j++) { double value = values[j][i]; ! String valueStr = Double.isNaN(value) ? "nan" : df.format(value); buff.append(padWithBlanks(valueStr, 18)); } *************** *** 265,269 **** StringBuffer buff = new StringBuffer(""); int diff = width - input.length(); ! while(diff-- > 0) { buff.append(' '); } --- 284,288 ---- StringBuffer buff = new StringBuffer(""); int diff = width - input.length(); ! while (diff-- > 0) { buff.append(' '); } *************** *** 274,284 **** /** * Returns aggregated value from the fetched data for a single datasource. ! * @param dsName Datasource name * @param consolFun Consolidation function to be applied to fetched datasource values. ! * Valid consolidation functions are "MIN", "MAX", "LAST" and "AVERAGE" ! * (these string constants are conveniently defined in the {@link ConsolFuns} class). * @return MIN, MAX, LAST or AVERAGE value calculated from the fetched data ! * for the given datasource name * @throws RrdException Thrown if the given datasource name cannot be found in fetched data. */ public double getAggregate(String dsName, String consolFun) throws RrdException { --- 293,305 ---- /** * Returns aggregated value from the fetched data for a single datasource. ! * ! * @param dsName Datasource name * @param consolFun Consolidation function to be applied to fetched datasource values. ! * Valid consolidation functions are "MIN", "MAX", "LAST" and "AVERAGE" ! * (these string constants are conveniently defined in the {@link ConsolFuns} class). * @return MIN, MAX, LAST or AVERAGE value calculated from the fetched data ! * for the given datasource name * @throws RrdException Thrown if the given datasource name cannot be found in fetched data. + * @deprecated Use {@link #getStats(String) getStats(dsName)} method instead. */ public double getAggregate(String dsName, String consolFun) throws RrdException { *************** *** 289,318 **** * Returns aggregated value from the fetched data for a single datasource. * Before applying aggrregation functions, specified RPN expression is applied to fetched ! * data. For example, if you have a GAUGE datasource named 'foots' but you wont to * find the maximum fetched value in meters use something like:</p> ! * <code>getAggregate("foots", ConsolFuns.MAX, "value,0.3048,*");</code> * Note that 'value' in the RPN expression is a reserved word and stands for the ! * original value (value fetched from RRD)</p> ! * @param dsName Datasource name * @param consolFun Consolidation function to be applied to fetched datasource values. ! * Valid consolidation functions are "MIN", "MAX", "LAST" and "AVERAGE" ! * (these string constants are conveniently defined in the {@link ConsolFuns} class) ! * @return MIN, MAX, LAST or AVERAGE value calculated from the fetched data ! * for the given datasource name * @throws RrdException Thrown if the given datasource name cannot be found in fetched data. */ public double getAggregate(String dsName, String consolFun, String rpnExpression) ! throws RrdException { ! if(consolFun.equals(CF_MAX)) { ! return getMax(dsName, rpnExpression); } ! else if(consolFun.equals(CF_MIN)) { ! return getMin(dsName, rpnExpression); } ! else if(consolFun.equals(CF_LAST)) { ! return getLast(dsName, rpnExpression); } ! else if(consolFun.equals(CF_AVERAGE)) { ! return getAverage(dsName, rpnExpression); } else { --- 310,345 ---- * Returns aggregated value from the fetched data for a single datasource. * Before applying aggrregation functions, specified RPN expression is applied to fetched ! * data. For example, if you have a GAUGE datasource named 'feet' but you want to * find the maximum fetched value in meters use something like:</p> ! * <code>getAggregate("feet", ConsolFuns.MAX, "value,0.3048,*");</code> * Note that 'value' in the RPN expression is a reserved word and stands for the ! * original value (value fetched from the RRD)</p> ! * ! * @param dsName Datasource name * @param consolFun Consolidation function to be applied to fetched datasource values. ! * Valid consolidation functions are "MIN", "MAX", "LAST", "AVERAGE" and "TOTAL" ! * (these string constants are conveniently defined in the {@link ConsolFuns} class) ! * @return MIN, MAX, LAST, AVERAGE or TOTAL value calculated from the fetched data ! * for the given datasource name * @throws RrdException Thrown if the given datasource name cannot be found in fetched data. + * @deprecated Use {@link #getStats(String, String) getStats(dsName, rpnExpression)} method instead. */ public double getAggregate(String dsName, String consolFun, String rpnExpression) ! throws RrdException { ! FetchDataStats stats = getStats(dsName, rpnExpression); ! if (consolFun.equals(CF_MAX)) { ! return stats.getMax(); } ! else if (consolFun.equals(CF_MIN)) { ! return stats.getMin(); } ! else if (consolFun.equals(CF_LAST)) { ! return stats.getLast(); } ! else if (consolFun.equals(CF_AVERAGE)) { ! return stats.getAverage(); ! } ! else if (consolFun.equals(CF_TOTAL)) { ! return stats.getTotal(); } else { *************** *** 321,398 **** } ! private double getMax(String dsName, String rpnExpression) throws RrdException { ! RpnCalculator rpnCalculator = null; ! if(rpnExpression != null) { ! rpnCalculator = new RpnCalculator(rpnExpression); ! } ! double vals[] = getValues(dsName), max = Double.NaN; ! for(int i = 0; i < vals.length - 1; i++) { ! double value = vals[i + 1]; ! if(rpnCalculator != null) { ! rpnCalculator.setValue(value); ! value = rpnCalculator.calculate(); ! } ! max = Util.max(max, value); ! } ! return max; ! } ! ! private double getMin(String dsName, String rpnExpression) throws RrdException { ! RpnCalculator rpnCalculator = null; ! if(rpnExpression != null) { ! rpnCalculator = new RpnCalculator(rpnExpression); ! } ! double vals[] = getValues(dsName), min = Double.NaN; ! for(int i = 0; i < vals.length - 1; i++) { ! double value = vals[i + 1]; ! if(rpnCalculator != null) { ! rpnCalculator.setValue(value); ! value = rpnCalculator.calculate(); ! } ! min = Util.min(min, value); ! } ! return min; ! } ! ! private double getLast(String dsName, String rpnExpression) throws RrdException { ! RpnCalculator rpnCalculator = null; ! if(rpnExpression != null) { ! rpnCalculator = new RpnCalculator(rpnExpression); ! } ! double vals[] = getValues(dsName); ! double value = vals[vals.length - 1]; ! if(rpnCalculator != null) { ! rpnCalculator.setValue(value); ! value = rpnCalculator.calculate(); ! } ! return value; } ! private double getAverage(String dsName, String rpnExpression) throws RrdException { RpnCalculator rpnCalculator = null; ! if(rpnExpression != null) { rpnCalculator = new RpnCalculator(rpnExpression); } ! double vals[] = getValues(dsName); ! double totalVal = 0; ! long totalSecs = 0; ! for(int i = 0; i < vals.length - 1; i++) { long t1 = Math.max(request.getFetchStart(), timestamps[i]); long t2 = Math.min(request.getFetchEnd(), timestamps[i + 1]); ! double value = vals[i + 1]; ! if(rpnCalculator != null) { rpnCalculator.setValue(value); value = rpnCalculator.calculate(); } ! if(!Double.isNaN(value)) { ! totalSecs += (t2 - t1); ! totalVal += (t2 - t1) * value; } } ! return totalSecs > 0? totalVal / totalSecs: Double.NaN; } /** * Dumps fetch data to output stream in XML format. * @param outputStream Output stream to dump fetch data to * @throws IOException Thrown in case of I/O error --- 348,416 ---- } ! /** ! * Returns all aggregated values from the fetched data for a single datasource. ! * ! * @param dsName Datasource name ! * @return Object containing MIN, MAX, LAST, AVERAGE and TOTAL values calculated from the fetched data ! * for the given datasource name ! * @throws RrdException Thrown if the given datasource name cannot be found in fetched data. ! */ ! public FetchDataStats getStats(String dsName) throws RrdException { ! return getStats(dsName, null); } ! /** ! * Returns all aggregated values from the fetched data for a single datasource. ! * Before applying aggrregation functions, specified RPN expression is applied to fetched ! * data. For example, if you have a GAUGE datasource named 'feet' but you want to ! * calculate aggregates in meters use something like:<p> ! * <pre> ! * getStats("feet", "value,0.3048,*"); ! * </pre> ! * Note that the placeholder 'value' in the RPN expression is a reserved word and stands for the ! * original value (value fetched from the RRD) ! * ! * @param dsName Datasource name ! * @return Object containing MIN, MAX, LAST, AVERAGE and TOTAL values calculated from the fetched data ! * for the given datasource name and a given RPN expression ! * @throws RrdException Thrown if the given datasource name cannot be found in fetched data ! */ ! public FetchDataStats getStats(String dsName, String rpnExpression) throws RrdException { ! double[] values = getValues(dsName); ! long totalSecs = 0; ! double totalValue = 0.0, min = Double.NaN, max = Double.NaN, last = Double.NaN; RpnCalculator rpnCalculator = null; ! if (rpnExpression != null) { rpnCalculator = new RpnCalculator(rpnExpression); } ! for (int i = 0; i < values.length - 1; i++) { long t1 = Math.max(request.getFetchStart(), timestamps[i]); long t2 = Math.min(request.getFetchEnd(), timestamps[i + 1]); ! double value = values[i + 1]; ! if (rpnCalculator != null) { rpnCalculator.setValue(value); + rpnCalculator.setTimestamp(t2); value = rpnCalculator.calculate(); } ! if (!Double.isNaN(value)) { ! totalSecs += (t2 - t1); ! totalValue += (t2 - t1) * value; } + min = Util.min(min, value); + max = Util.max(max, value); + last = value; } ! FetchDataStats stats = new FetchDataStats(); ! stats.setLast(last); ! stats.setMax(max); ! stats.setMin(min); ! stats.setSeconds(totalSecs); ! stats.setTotal(totalValue); ! return stats; } /** * Dumps fetch data to output stream in XML format. + * * @param outputStream Output stream to dump fetch data to * @throws IOException Thrown in case of I/O error *************** *** 411,425 **** writer.closeTag(); // request writer.startTag("datasources"); ! for(int i = 0; i < dsNames.length; i++) { writer.writeTag("name", dsNames[i]); } writer.closeTag(); // datasources writer.startTag("data"); ! for(int i = 0; i < timestamps.length; i++) { writer.startTag("row"); writer.writeComment(Util.getDate(timestamps[i])); ! writer.writeTag("timestamp", timestamps[i]); writer.startTag("values"); ! for(int j = 0; j < dsNames.length; j++) { writer.writeTag("v", values[j][i]); } --- 429,443 ---- writer.closeTag(); // request writer.startTag("datasources"); ! for (int i = 0; i < dsNames.length; i++) { writer.writeTag("name", dsNames[i]); } writer.closeTag(); // datasources writer.startTag("data"); ! for (int i = 0; i < timestamps.length; i++) { writer.startTag("row"); writer.writeComment(Util.getDate(timestamps[i])); ! writer.writeTag("timestamp", timestamps[i]); writer.startTag("values"); ! for (int j = 0; j < dsNames.length; j++) { writer.writeTag("v", values[j][i]); } *************** *** 434,437 **** --- 452,456 ---- /** * Dumps fetch data to file in XML format. + * * @param filepath Path to destination file * @throws IOException Thrown in case of I/O error *************** *** 444,448 **** } finally { ! if(outputStream != null) { outputStream.close(); } --- 463,467 ---- } finally { ! if (outputStream != null) { outputStream.close(); } *************** *** 452,455 **** --- 471,475 ---- /** * Dumps fetch data in XML format. + * * @return String containing XML formatted fetch data * @throws IOException Thrown in case of I/O error Index: Util.java =================================================================== RCS file: /cvsroot/jrobin/src/org/jrobin/core/Util.java,v retrieving revision 1.26 retrieving revision 1.27 diff -C2 -d -r1.26 -r1.27 *** Util.java 8 Nov 2004 11:18:49 -0000 1.26 --- Util.java 18 Nov 2004 13:27:58 -0000 1.27 *************** *** 236,239 **** --- 236,254 ---- /** + * Checks if a string can be parsed as double. + * @param s Input string + * @return <code>true</code> if the string can be parsed as double, <code>false</code> otherwise + */ + public static boolean isDouble(String s) { + try { + Double.parseDouble(s); + return true; + } + catch(NumberFormatException nfe) { + return false; + } + } + + /** * Parses input string as a boolean value. The parser is case insensitive. * @param valueStr String representing boolean value *************** *** 545,551 **** /** ! * Compares two doubles, but returns true if x=y=Double.NaN (by default Double.NaN != Double.NaN) ! * @param x first value ! * @param y second value * @return <code>true</code> if x and y are both equal to Double.NaN, or if x == y. <code>false</code> otherwise */ --- 560,567 ---- /** ! * Compares two doubles but treats all NaNs as equal. ! * In Java (by default) Double.NaN == Double.NaN always returns <code>false</code> ! * @param x the first value ! * @param y the second value * @return <code>true</code> if x and y are both equal to Double.NaN, or if x == y. <code>false</code> otherwise */ --- NEW FILE: FetchDataStats.java --- /* ============================================================ * JRobin : Pure java implementation of RRDTool's functionality * ============================================================ * * Project Info: http://www.jrobin.org * Project Lead: Sasa Markovic (sa...@jr...); * * (C) Copyright 2003, by Sasa Markovic. * * Developers: Sasa Markovic (sa...@jr...) * Arne Vandamme (cob...@jr...) * * This library is free software; you can redistribute it and/or modify it under the terms * of the GNU Lesser General Public License as published by the Free Software Foundation; * either version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with this * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, * Boston, MA 02111-1307, USA. */ package org.jrobin.core; /** * Dumb class which contains statistics (AVERAGE, MIN, MAX, LAST and TOTAL) calculated from the * {@link FetchData#getStats(String, String) FetchData} returned by a single {@link FetchRequest}. */ public class FetchDataStats implements ConsolFuns { private long seconds; private double total, min, max, last; FetchDataStats() { // just to prohibit explicit creation } void setSeconds(long seconds) { this.seconds = seconds; } void setTotal(double total) { this.total = total; } void setMin(double min) { this.min = min; } void setMax(double max) { this.max = max; } void setLast(double last) { this.last = last; } /** * Returns TOTAL of the fetched data * @return TOTAL of the fetched data */ public double getTotal() { return total; } /** * Returns MIN of the fetched data * @return MIN of the fetched data */ public double getMin() { return min; } /** * Returns MAX of the fetched data * @return MAX of the fetched data */ public double getMax() { return max; } /** * Returns LAST of the fetched data * @return LAST of the fetched data */ public double getLast() { return last; } /** * Returns AVERAGE of the fetched data * @return AVERAGE of the fetched data */ public double getAverage() { return total / (double) seconds; } /** * Returns aggregated value of the fetch data * @param consolFun Consolidation function to be used * (AVERAGE, MIN, MAX, LAST and TOTAL) * @return Aggregated value * @throws RrdException Thrown if invalid consolidation function is supplied */ public double getAggregate(String consolFun) throws RrdException { if (consolFun.equals(CF_MAX)) { return getMax(); } else if (consolFun.equals(CF_MIN)) { return getMin(); } else if (consolFun.equals(CF_LAST)) { return getLast(); } else if (consolFun.equals(CF_AVERAGE)) { return getAverage(); } else if (consolFun.equals(CF_TOTAL)) { return getTotal(); } else { throw new RrdException("Unsupported consolidation function [" + consolFun + "]"); } } /** * Dumps all aggregated values in a human-readable form. * @return A string containing all aggregated values. */ public String dump() { return "AVERAGE: " + Util.formatDouble(getAverage(), true) + "\n" + "MIN: " + Util.formatDouble(getMin(), true) + "\n" + "MAX: " + Util.formatDouble(getMax(), true) + "\n" + "LAST: " + Util.formatDouble(getLast(), true) + "\n" + "TOTAL: " + Util.formatDouble(getTotal(), true); } } |