001    package org.hd.d.pg2k.svrCore.vars;
002    
003    import java.text.DecimalFormat;
004    import java.text.DecimalFormatSymbols;
005    import java.text.FieldPosition;
006    import java.util.Iterator;
007    import java.util.Map;
008    
009    import org.hd.d.pg2k.svrCore.I18NTools;
010    
011    /**Immutable simple stats derived from a NUMBER globalMap variable.
012     * Has a toString() method directly usable on a JSP page.
013     * <p>
014     * Captures a reference to the value it was constructed from.
015     * <p>
016     * For most combination operators such as getSum(),
017     * values are manipulated as double (as risk of precision loss) to:
018     * <ul>
019     * <li>Have the maximum-possible dynamic range.
020     * <li>Represent integer and floating values.
021     * <li>Preserve special IEEE floating-point behaviour, such as for NaN.
022     * </ul>
023     */
024    public final class SimpleNumberStats extends SimpleVarStats
025        {
026        /**One-off initialisation of symbols for gallery-default locale; not null. */
027        private static final DecimalFormatSymbols symbols = new DecimalFormatSymbols(I18NTools.DEFAULT_SYSTEM_LOCALE);
028    
029        /**Construct a new instance, filtering variable globalMap members by age.
030         * The Gallery-default locale is used.
031         *
032         * @param originalVar  the variable to compute; must be non-null
033         *     TYPE_NUMBER (and getValue() must return non-null value)
034         * @param oldestAllowed  any globalMap entry than this will
035         *     be ignored, which allows only up-to-date information
036         *     to be included
037         * @param decimalFormatPattern  numeric formatting string to
038         *     be used for display of most quantities;
039         *     null if toString() is not going to be used
040         *     or String.valueOf() formatting is acceptable
041         * @param sysID  identifies this system so we can select the local
042         *     value of a global variable if available; can be null
043         */
044        public SimpleNumberStats(final SimpleVariableValue originalVar,
045                                 final long oldestAllowed,
046                                 final InstanceID sysID,
047                                 final String decimalFormatPattern)
048            {
049            super(originalVar, oldestAllowed, sysID);
050    
051            // Post-hoc veto non-numeric or null values.
052            if((originalVar == null) ||
053               (originalVar.getDef().getType() != SimpleVariableDefinition.TYPE_NUMBER) ||
054               (originalVar.getValue() == null))
055                { throw new IllegalArgumentException("bad NUMBER argument"); }
056    
057            pattern = decimalFormatPattern;
058    
059            // Create the formatting object (at most) once to save time.
060            format = (null == pattern) ? null : new DecimalFormat(decimalFormatPattern, symbols);
061            }
062    
063        /**The pattern String; null if toString() is not going to be used. */
064        private final String pattern;
065    
066        /**Get the pattern String; null if toString() is not going to be used. */
067        public String getPattern()
068            {
069            return(pattern);
070            }
071    
072        /**Our computed formatting object; never null.
073         * Not immutable, nor immutable, so must not be passed outside the class.
074         * <p>
075         * All access to this value is synchronized.
076         */
077        private final transient DecimalFormat format;
078    
079        /**Gets the total of the non-null values.
080         * If there are no usable globalMap values,
081         * then this returns the main value, which may be null.
082         * <p>
083         * Will attempt to use integer computations for efficiency
084         * if all component values are Integer/Short/Byte,
085         * and the result fits in an int,
086         * else will promote sum/result to Long or Double.
087         */
088        public Number getSum()
089            {
090            assert(filteredVar != null);
091            final Map<InstanceID,SimpleVariableValue> globalMap = filteredVar.getGlobalMap();
092            if((globalMap == null) || (getNonNullValueCount() < 1))
093                { return((Number) filteredVar.getValue()); }
094    
095            long lResult = 0;
096            final Iterator<SimpleVariableValue> it = globalMap.values().iterator();
097            while(it.hasNext())
098                {
099                final SimpleVariableValue svv = it.next();
100                final Number value = (Number) svv.getValue();
101                if(null == value) { continue; }
102                // We expect Integer to be the most common value type in use.
103                final Class<? extends Number> valueClass = value.getClass();
104                if((Integer.class != valueClass) && (Short.class != valueClass) && (Byte.class != valueClass))
105                    {
106                    // Convert sum so far to, and sum remainder as, double...
107                    double dResult = ((double) lResult) + value.doubleValue();
108                    while(it.hasNext())
109                        {
110                        final SimpleVariableValue svvD = it.next();
111                        final Number valueD = (Number) svvD.getValue();
112                        if(null == valueD) { continue; }
113                        dResult += valueD.doubleValue();
114                        }
115                    return(Double.valueOf(dResult));
116                    }
117    
118                lResult += value.intValue();
119                }
120            if((lResult < Integer.MIN_VALUE) || (lResult > Integer.MAX_VALUE))
121                { return(Long.valueOf(lResult)); }
122            return(Integer.valueOf((int) lResult));
123            }
124    
125        /**Gets the maximum of the non-null values.
126         * If there are no usable globalMap values,
127         * then this returns the main value, which may be null.
128         */
129        public Number getMax()
130            {
131            final Map<InstanceID,SimpleVariableValue> globalMap = filteredVar.getGlobalMap();
132            if((globalMap == null) || (getNonNullValueCount() < 1))
133                { return((Number) filteredVar.getValue()); }
134    
135            Number best = null;
136            for(final SimpleVariableValue svv : globalMap.values())
137                {
138                final Number value = (Number) svv.getValue();
139                if(value != null)
140                    {
141                    if((best == null) ||
142                       (value.doubleValue() > best.doubleValue()))
143                        { best = value; }
144                    }
145                }
146            return(best);
147            }
148    
149        /**Gets the minimum of the non-null values.
150         * If there are no usable globalMap values,
151         * then this returns the main value, which may be null.
152         */
153        public Number getMin()
154            {
155            final Map<InstanceID,SimpleVariableValue> globalMap = filteredVar.getGlobalMap();
156            if((globalMap == null) || (getNonNullValueCount() < 1))
157                { return((Number) filteredVar.getValue()); }
158    
159            Number best = null;
160            for(final SimpleVariableValue svv : globalMap.values())
161                {
162                final Number value = (Number) svv.getValue();
163                if(value != null)
164                    {
165                    if((best == null) ||
166                       (value.doubleValue() < best.doubleValue()))
167                        { best = value; }
168                    }
169                }
170            return(best);
171            }
172    
173        /**Format value and append to buffer.
174         * If using the format then thread-safety is ensured by synchronising on it.
175         */
176        private void fmt(final Object o, final StringBuffer sb)
177            {
178            if(null != format) { synchronized(format) { format.format(o, sb, new FieldPosition(0)); } }
179            else { sb.append(String.valueOf(o)); }
180            }
181    
182        /**Returns HTML- and human- friendly summary of numerical stats.
183         * This does <em>not</em> include the variable name.
184         * <p>
185         * If the count is zero
186         * then this just returns a formatted version of the main value.
187         */
188        @Override
189        public String toString()
190            {
191            final int n = getNonNullValueCount();
192            final StringBuffer sb = new StringBuffer((n > 1) ? 64 : 32);
193    
194            fmt(getMainValue(), sb);
195    
196            if(n > 0)
197                {
198                sb.append(", n=").append(n);
199                if(n > 1)
200                    {
201                    // Order stats values from smallest to largest...
202                    sb.append(", min="); fmt(getMin(), sb);
203                    sb.append(", max="); fmt(getMax(), sb);
204                    sb.append(", sum="); fmt(getSum(), sb);
205                    }
206                }
207    
208            return(sb.toString());
209            }
210        }