    package flare.util
    {
        import flash.utils.Dictionary;
        import flare.util.Arrays;
        import flare.util.Property;

        /**
         * Utility class for computing statistics for a collection of values.
         */
        public class Stats
        {
            /** Constant indicating numerical values. */
            public static const NUMBER:int = 0;
            /** Constant indicating date/time values. */
            public static const DATE:int   = 1;
            /** Constant indicating arbitrary object values. */
            public static const OBJECT:int = 2;

            private var _type:int = -1;
            private var _comp:Function = null;

            private var _num:Number = 0;
            private var _distinct:Number = 0;
            private var _elm:Array = null;

            private var _minObject:Object = null;
            private var _maxObject:Object = null;

            private var _min:Number = Number.MAX_VALUE;
            private var _max:Number = Number.MIN_VALUE;
            private var _sum:Number = 0;
            private var _stdev:Number = 0;

            /** The data type of the collection, one of NUMBER, DATE, or OBJECT. */
            public function get dataType():int { return _type; }
            /** A sorted array of all the values. */
            public function get values():Array { return _elm; }
            /** A sorted array of all unique values in the collection. */
            public function get distinctValues():Array {
                // get array with only unique items
                var dists:Array = [];
                if (_elm==null || _elm.length == 0) return dists;
                dists.push(_elm[0]);
                for (var i:int=1, j:int=0; i<_num; ++i) {
                    if (!equal(_elm[i], dists[j])) {
                        dists.push(_elm[i]); ++j;
                    }
                }
                return dists;
            }
            /** The minimum value (for numerical data). */
            public function get minimum():Number { return _min; }
            /** The maximum value (for numerical data). */
            public function get maximum():Number { return _max; }
            /** The sum of all the values (for numerical data). */
            public function get sum():Number { return _sum; }
            /** The average of all the values (for numerical data). */
            public function get average():Number { return _sum / _num; }
            /** The standard deviation of all the values (for numerical data). */
            public function get stddev():Number { return _stdev; }
            /** The standard error of all the values (for numerical data). */
            public function get stderr():Number { return stddev / Math.sqrt(_num); }
            /** The total number of values. */
            public function get count():Number { return _num; }
            /** The total number of distinct values. */
            public function get distinct():Number { return _distinct; }

            /** The minimum value (for date/time values). */
            public function get minDate():Date { return _minObject as Date; }
            /** The maximum value (for date/time values). */
            public function get maxDate():Date { return _maxObject as Date; }

            /** The minimum value (for arbitrary objects). */
            public function get minObject():Object { return _minObject; }
            /** The maximum value (for arbitrary objects). */
            public function get maxObject():Object { return _maxObject; }

            // --------------------------------------------------------------------

            /**
             * Creates a new Stats instance using the given input data. If the
             * field argument is null, it is assumed that the input data array
             * directly contains the values to analyze. If the field argument is
             * non-null, values will be extracted from the objects in the input
             * array using the specified property name.
             * @param data an input data array. The data to analyze may be
             *  contained directly in the array, or may be properties of the
             *  objects contained in the array.
             * @param field a property name. This property will be used to extract
             *  data values from the objects in the data array. If null, the
             *  objects in the data array will be used directly.
             * @param comparator the comparator function to use to sort the data.
             *  If null, the natural sort order will be used.
             * @param copy flag indicating if the input data array should be
             *  copied to a new array. This flag only applied when the field
             *  argument is null. In false, the input data array will be sorted.
             *  If true, the array will be copied before being sorted. The default
             *  behavior is to make a copy.
             */
            public function Stats(data:Array, field:String=null,
                                  comparator:Function=null, copy:Boolean=true)
            {
                _comp = comparator;
                init(data, field, copy);
            }

            private function init(data:Array, field:String, copy:Boolean):void
            {
                // INVARIANT: properties must be set to default values
                // TODO: how to handle null values?

                // collect all values into element array
                _num = data.length; if (_num==0) return;
                _elm = (field==null ? (copy ? Arrays.copy(data) : data)
                                    : new Array(_num));
                if (field != null) {
                    var p:Property = Property.$(field);
                    for (var i:uint=0; i<_num; ++i) {
                        _elm[i] = p.getValue(data[i]);
                    }
                }

                // determine data type
                for (i=0; i<_num; ++i) {
                    var v:Object = _elm[i], type:int;
                    type = v is Date ? DATE : (v is Number ? NUMBER : OBJECT);

                    if (_type == -1) {
                        _type = type; // seed type
                    } else if (type != _type) {
                        _type = OBJECT; // punt if no match
                        break;
                    }
                }

                // sort data values
                var opt:int = (_type==OBJECT ? 0 : Array.NUMERIC);
                if (_comp==null) _elm.sort(opt); else _elm.sort(_comp, opt);

                // count unique values
                _distinct = 1; var j:uint = 0;
                for (i=1; i<_num; ++i) {
                    if (!equal(_elm[i], _elm[j])) { ++_distinct; j=i;    }
                }

                // populate stats
                var N:int = _num-1;
                if (_type == NUMBER)
                {
                    _min = (_minObject = _elm[0]) as Number;
                    _max = (_maxObject = _elm[N]) as Number;

                    var ss:Number = 0, x:Number;
                    for each (x in _elm) {
                        _sum += x;
                        ss += x*x;
                    }
                    _stdev = Math.sqrt(ss/_num - average*average);
                }
                else
                {
                    _minObject = _elm[0];
                    _maxObject = _elm[N];
                }
            }

            /**
             * Tests for equality between two values. This method is necessary
             * because the <code>==</code> operator checks object equality and
             * not value equality for <code>Date</code> instances.
             * @param a the first object to compare
             * @param b the second object to compare
             * @returns true if the object values are equal, false otherwise
             */
            public static function equal(a:*, b:*):Boolean
            {
                if (a is Date && b is Date) {
                    return (a as Date).time == (b as Date).time;
                } else {
                    return a == b;
                }
            }

        } // end of class Stats
    }