001: package jimm.datavision.field;
002:
003: import jimm.datavision.*;
004: import java.util.*;
005:
006: interface AggregateFunction {
007: public double aggregate(double[] values, int numValues);
008: }
009:
010: /**
011: * An aggregate field represents a field's aggregated values, either {@link
012: * ColumnField} or {@link FormulaField}. It also may be associated with a
013: * group (assigned in {@link ReportReader#field} or when editing a report),
014: * meaning that the aggregate value is reset whenever the group's value
015: * changes. The value of an aggregate field holds the id of some other field
016: * whose value we are aggregating.
017: *
018: * @author Jim Menard, <a href="mailto:jimm@io.com">jimm@io.com</a>
019: */
020: public class AggregateField extends Field {
021:
022: protected static final int START_VALUES_LENGTH = 100;
023:
024: /** Maps function names to {@link AggregateFunction} objects. */
025: protected static HashMap functions;
026: /** A sorted array of the function names. */
027: protected static Object[] functionNames;
028:
029: // Initialize map from function names to functions.
030: static {
031: functions = new HashMap();
032: functions.put("sum", new AggregateFunction() {
033: public double aggregate(double[] values, int numValues) {
034: double total = 0;
035: for (int i = 0; i < numValues; ++i)
036: total += values[i];
037: return total;
038: }
039: });
040: functions.put("subtotal", functions.get("sum")); // Old name for "sum"
041: functions.put("min", new AggregateFunction() {
042: public double aggregate(double[] values, int numValues) {
043: double min = Double.MAX_VALUE;
044: for (int i = 0; i < numValues; ++i)
045: if (values[i] < min)
046: min = values[i];
047: return min;
048: }
049: });
050: functions.put("max", new AggregateFunction() {
051: public double aggregate(double[] values, int numValues) {
052: double max = Double.MIN_VALUE;
053: for (int i = 0; i < numValues; ++i)
054: if (values[i] > max)
055: max = values[i];
056: return max;
057: }
058: });
059: functions.put("count", new AggregateFunction() {
060: public double aggregate(double[] values, int numValues) {
061: return numValues;
062: }
063: });
064: functions.put("average", new AggregateFunction() {
065: public double aggregate(double[] values, int numValues) {
066: if (numValues == 0)
067: return 0;
068: double total = 0;
069: for (int i = 0; i < numValues; ++i)
070: total += values[i];
071: return total / numValues;
072: }
073: });
074: functions.put("stddev", new AggregateFunction() {
075: public double aggregate(double[] values, int numValues) {
076: if (numValues < 2)
077: return 0;
078: double average = ((AggregateFunction) functions
079: .get("average")).aggregate(values, numValues);
080: double sumOfSquares = 0;
081: for (int i = 0; i < numValues; ++i)
082: sumOfSquares += (values[i] - average)
083: * (values[i] - average);
084: return Math.sqrt(sumOfSquares / (numValues - 1));
085: }
086: });
087:
088: // Create a sorted list of function names. Don't include "select", which
089: // is the old name for "sum".
090: TreeSet withoutSelect = new TreeSet(functions.keySet());
091: withoutSelect.remove("select");
092: functionNames = withoutSelect.toArray();
093: }
094:
095: protected Group group; // Set by report creation; possibly null
096: protected String functionName;
097: protected AggregateFunction function;
098: protected double[] values; // Read-only
099: protected int valuesIndex;
100: protected Field fieldToAggregate;
101:
102: /**
103: * Returns <code>true</code> if <var>functionName</var> is a legal aggregate
104: * function name.
105: *
106: * @param functionName an aggregate function name (hopefully)
107: * @return <code>true</code> if it's a function name
108: */
109: public static boolean isAggregateFunctionName(String functionName) {
110: return functions.keySet().contains(functionName);
111: }
112:
113: /**
114: * Returns the list of function names as an array of objects.
115: *
116: * @return all possible function names
117: */
118: public static Object[] functionNameArray() {
119: return functionNames;
120: }
121:
122: /**
123: * Constructs a field with the specified id in the specified report
124: * section that aggregates the field with id <i>value</i>.
125: *
126: * @param id the new field's id
127: * @param report the report containing this element
128: * @param section the report section in which the field resides
129: * @param value the magic string
130: * @param visible show/hide flag
131: * @param functionName "xxx", where xxx is the aggregate function
132: */
133: public AggregateField(Long id, Report report, Section section,
134: Object value, boolean visible, String functionName) {
135: super (id, report, section, value, visible);
136: values = null;
137:
138: setFunction(functionName);
139:
140: // The reason I don't grab fieldToAggregate right now is that this
141: // aggregate field may be constructed before the field to which it
142: // refers.
143: }
144:
145: protected void finalize() throws Throwable {
146: if (fieldToAggregate != null)
147: fieldToAggregate.deleteObserver(this );
148: super .finalize();
149: }
150:
151: public String getFunction() {
152: return functionName;
153: }
154:
155: public void setFunction(String newFunctionName) {
156: newFunctionName = newFunctionName.toLowerCase();
157: if (functionName != newFunctionName
158: && (functionName == null || !functionName
159: .equals(newFunctionName))) {
160: functionName = newFunctionName;
161: function = (AggregateFunction) functions.get(functionName);
162: setChanged();
163: notifyObservers();
164: }
165: }
166:
167: /**
168: * Resets this aggregate. Called by the report once at the beginning of
169: * each run.
170: */
171: public void initialize() {
172: values = null;
173: }
174:
175: public String dragString() {
176: return typeString() + ":" + getField().getId();
177: }
178:
179: /**
180: * Returns the group over which this field is aggregating. May return
181: * <code>null</code> since not all aggregate fields are associated with
182: * a group.
183: *
184: * @return a group; <code>null</code> if this aggregate field is not
185: * associated with any group
186: */
187: public Group getGroup() {
188: return group;
189: }
190:
191: /**
192: * Sets the group this field is aggregate. The group may be
193: * <code>null</code>.
194: *
195: * @param newGroup a group
196: */
197: public void setGroup(Group newGroup) {
198: if (group != newGroup) {
199: group = newGroup;
200: setChanged();
201: notifyObservers();
202: }
203: }
204:
205: /**
206: * Returns the field over which we are aggregating. We lazily instantiate
207: * it because when we are constructed that field may not yet exist. We also
208: * start observing the field so we can notify our observers in turn.
209: *
210: * @return the field we are aggregating
211: */
212: public Field getField() {
213: if (fieldToAggregate == null) {
214: fieldToAggregate = getReport().findField(value);
215: fieldToAggregate.addObserver(this );
216: }
217: return fieldToAggregate;
218: }
219:
220: /**
221: * Returns the id of the field over which we are aggregating. The field
222: * itself may not yet exist.
223: *
224: * @return the id of the field we are aggregating
225: */
226: public Long getFieldId() {
227: return (value instanceof Long) ? (Long) value : new Long(value
228: .toString());
229: }
230:
231: /**
232: * Returns the current aggregate value.
233: *
234: * @return a doubleing point total
235: */
236: public double getAggregateValue() {
237: if (function == null)
238: return 0;
239: return function.aggregate(values, valuesIndex);
240: }
241:
242: public String typeString() {
243: return functionName;
244: }
245:
246: public String designLabel() {
247: return functionName + "(" + getField().designLabel() + ")";
248: }
249:
250: public String formulaString() {
251: return designLabel();
252: }
253:
254: public boolean refersTo(Field f) {
255: return getField() == f;
256: }
257:
258: public boolean refersTo(Formula f) {
259: return (getField() instanceof FormulaField)
260: && ((FormulaField) getField()).getFormula() == f;
261: }
262:
263: public boolean refersTo(UserColumn uc) {
264: return (getField() instanceof UserColumnField)
265: && ((UserColumnField) getField()).getUserColumn() == uc;
266: }
267:
268: public boolean refersTo(Parameter p) {
269: if ((getField() instanceof ParameterField)
270: && ((ParameterField) getField()).getParameter() == p)
271: return true;
272:
273: if ((getField() instanceof FormulaField)
274: && ((FormulaField) getField()).refersTo(p))
275: return true;
276:
277: return false;
278: }
279:
280: public boolean canBeAggregated() {
281: return true;
282: }
283:
284: /**
285: * Updates the aggregate value. Called by the report when a new
286: * line of data is retrieved.
287: */
288: public void updateAggregate() {
289: /*
290: * Our value field holds the id of some other field. Get that field's
291: * value, then convert it to a double.
292: */
293: Object obj = getField().getValue();
294: double value = 0;
295: if (obj != null) {
296: if (obj instanceof Number)
297: value = ((Number) obj).doubleValue();
298: else
299: value = Double.parseDouble(obj.toString());
300: }
301:
302: // If we are aggregating within a group and this is a new value,
303: // reset the aggregate value. If we have not yet collected any
304: // values, allocate space.
305: if (values == null || (group != null && group.isNewValue())) {
306: values = new double[START_VALUES_LENGTH];
307: valuesIndex = 0;
308: } else if (valuesIndex == values.length) { // Expand the values array
309: double[] newValues = new double[values.length * 2];
310: System.arraycopy(values, 0, newValues, 0, values.length);
311: values = newValues;
312: }
313: values[valuesIndex++] = value;
314: }
315:
316: /**
317: * Returns the value of this field: the aggregate as a Double.
318: *
319: * @return a Double
320: */
321: public Object getValue() {
322: return new Double(getAggregateValue());
323: }
324:
325: }
|