//+------------------------------------------------------------------+ //| OLAPCommon.mqh | //| Copyright © 2016-2020, Marketeer | //| https://www.mql5.com/en/users/marketeer | //| Online Analytical Processing of trading hypercubes | //| https://www.mql5.com/en/articles/6602 | //| https://www.mql5.com/en/articles/6603 | //| https://www.mql5.com/en/articles/7535 | //| https://www.mql5.com/en/articles/7656 | //| rev. 25.02.2020 | //+------------------------------------------------------------------+ #include #include #include #include enum AGGREGATORS { AGGREGATOR_SUM, // SUM AGGREGATOR_AVERAGE, // AVERAGE AGGREGATOR_MAX, // MAX AGGREGATOR_MIN, // MIN AGGREGATOR_COUNT, // COUNT AGGREGATOR_PROFITFACTOR, // PROFIT FACTOR AGGREGATOR_PROGRESSIVE, // PROGRESSIVE TOTAL AGGREGATOR_IDENTITY, // IDENTITY AGGREGATOR_STDDEV // DEVIATION }; enum DATA_TYPES { DATA_TYPE_NONE, DATA_TYPE_NUMBER = 'd', DATA_TYPE_INTEGER = 'i', DATA_TYPE_TIME = 't', DATA_TYPE_STRING = 's' }; int MathSign(const double x) { return x > 0 ? +1 : (x < 0 ? -1 : 0); } class Record { private: double data[]; public: Record(const int length) { ArrayResize(data, length); ArrayInitialize(data, 0); } void set(const int index, double value) { data[index] = value; } double get(const int index) const { return data[index]; } virtual string legend(const int index) const { return NULL; } template static char datatype2(const int index) { return T::datatype(index); } template static string legendFromEnum(E e) { string text = EnumToString(e); // don't return NULL as a flag of overflow (custom fields not in enum) // better to show lengthy wording then empty space if(StringFind(text, "::") > 0) return text; StringToLower(text); StringReplace(text, "_", " "); const string field = "field "; if(StringFind(text, field) == 0) { return StringSubstr(text, StringLen(field)); } return text; } static char datatype(const int index) { return 0; } virtual void fillCustomFields() {/* does nothing */}; }; // single pass data reader class DataAdapter { public: virtual void reset() = 0; virtual Record *getNext() = 0; virtual int reservedSize() const = 0; virtual bool isOwner() const = 0; virtual int getFieldCount() const = 0; virtual int getCustomFieldCount() const { return 0; }; virtual int getCustomFields(string &names[]) const { return 0; }; }; interface Progress { void progress(const int cursor, const int total) const; }; class PackedEnum { public: static bool type; }; static bool PackedEnum::type = true; template int EnumToArray(E dummy, int &values[], const int start = 0, const int stop = INT_MAX) { string t = typename(E) + "::"; int length = StringLen(t); ArrayResize(values, 0); int count = 0; for(int i = start; i < stop && !IsStopped(); i++) { E e = (E)i; if(StringCompare(StringSubstr(EnumToString(e), 0, length), t) != 0) { ArrayResize(values, count + 1); values[count++] = i; } else if(PACKED::type) { break; } } return count; } template int EnumSize(E dummy, const int start = 0, const int stop = INT_MAX) { string t = typename(E) + "::"; int length = StringLen(t); int count = 0; for(int i = start; i < stop && !IsStopped(); i++) { E e = (E)i; if(StringCompare(StringSubstr(EnumToString(e), 0, length), t) != 0) { count++; } else if(PACKED::type) { break; } } return count; } // COMMON SELECTORS template class Selector { protected: E selector; string _typename; int size; static bool shortTitles; public: Selector(const E field): selector(field) { size = EnumSize(field); _typename = typename(this); } virtual void prepare(const Record *r) { // no action by default } // returns index of cell to store values from the record virtual bool select(const Record *r, int &index) const = 0; virtual int getRange() const = 0; virtual double getMin() const = 0; virtual double getMax() const = 0; virtual E getField() const { return selector; } virtual string getLabel(const int index) const = 0; virtual string getTitle() const { if(shortTitles) { return Record::legendFromEnum(selector); } return _typename + "(" + EnumToString(selector) + ")"; } virtual int getFieldSize() const { return size; } static void setShortTitles(const bool t) { shortTitles = t; } }; template static bool Selector::shortTitles = false; template class BaseSelector: public Selector { public: BaseSelector(const T field): Selector(field) { _typename = typename(this); } virtual bool select(const Record *r, int &index) const override { index = 0; return true; } virtual int getRange() const override { return 1; // this is a scalar by default, returns 1 value } virtual double getMin() const override { return 0; } virtual double getMax() const override { return (double)(getRange() - 1); } virtual string getLabel(const int index) const override { return EnumToString(selector) + "[" + (string)index + "]"; // "scalar" } }; template class FilterSelector: public BaseSelector { public: FilterSelector(const T field): BaseSelector(field) { _typename = typename(this); } virtual bool select(const Record *r, int &index) const override { index = -1; return true; } virtual int getRange() const override { return 0; } virtual double getMin() const override { return 0; } virtual double getMax() const override { return 0; } virtual string getLabel(const int index) const override { return EnumToString(selector); } }; template class DateTimeSelector: public BaseSelector { protected: int granularity; public: DateTimeSelector(const E field, const int gsize): BaseSelector(field), granularity(gsize) { _typename = typename(this); } virtual int getRange() const { return granularity; } }; template class MonthSelector: public DateTimeSelector { public: MonthSelector(const E f): DateTimeSelector(f, 12) { _typename = typename(this); } virtual bool select(const Record *r, int &index) const { double d = r.get(selector); datetime t = (datetime)d; index = TimeMonth(t) - 1; return true; } virtual string getLabel(const int index) const { static string months[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; return months[index]; } }; template class WeekDaySelector: public DateTimeSelector { public: WeekDaySelector(const E f): DateTimeSelector(f, 7) { _typename = typename(this); } virtual bool select(const Record *r, int &index) const { double d = r.get(selector); datetime t = (datetime)d; index = TimeDayOfWeek(t); return true; } virtual string getLabel(const int index) const { static string days[7] = {"7`Sunday", "1`Monday", "1`Tuesday", "3`Wednesday", "4`Thursday", "5`Friday", "6`Saturday"}; return days[index]; } }; template class WorkWeekDaySelector: public DateTimeSelector { public: WorkWeekDaySelector(const E f): DateTimeSelector(f, 5) { _typename = typename(this); } virtual bool select(const Record *r, int &index) const { double d = r.get(selector); datetime t = (datetime)d; index = TimeDayOfWeek(t) - 1; return (index >= 0 && index < 5); } virtual string getLabel(const int index) const { static string days[5] = {"1`Monday", "2`Tuesday", "3`Wednesday", "4`Thursday", "5`Friday"}; return days[index]; } }; template class DayHourSelector: public DateTimeSelector { public: DayHourSelector(const E f): DateTimeSelector(f, 24) { _typename = typename(this); } virtual bool select(const Record *r, int &index) const { double d = r.get(selector); datetime t = (datetime)d; index = TimeHour(t); return true; } virtual string getLabel(const int index) const { return StringFormat("%02d", index); } }; template class HourMinuteSelector: public DateTimeSelector { public: HourMinuteSelector(const E f): DateTimeSelector(f, 60) { _typename = typename(this); } virtual bool select(const Record *r, int &index) const { double d = r.get(selector); datetime t = (datetime)d; index = TimeMinute(t); return true; } virtual string getLabel(const int index) const { return StringFormat("%02d", index); } }; // aux class for QuantizationSelector below template class Vocabulary { protected: T index[]; public: int get(const T &text) const { int n = ArraySize(index); for(int i = 0; i < n; i++) { if(index[i] == text) return i; } return -(n + 1); } int add(const T text) { int n = get(text); if(n < 0) { n = -n; ArrayResize(index, n); index[n - 1] = text; return n - 1; } return n; } int size() const { return ArraySize(index); } T operator[](const int position) const { return index[position]; } void clear() { ArrayResize(index, 0); } }; template class QuantizationSelector: public BaseSelector { protected: Vocabulary quants; uint cell; double min, max; public: QuantizationSelector(const T field, const uint granularity = 0): BaseSelector(field), cell(granularity) { _typename = typename(this); min = DBL_MAX; max = -DBL_MAX; } virtual void prepare(const Record *r) override { double value = r.get(selector); if(cell != 0) value = MathSign(value) * MathFloor(MathAbs(value) / cell) * cell; quants.add(value); double abs = MathAbs(value); if(abs > max) max = abs; abs = NormalizeDouble(abs - MathFloor(abs), 8); if(abs > 0.0000001 && abs < min) min = abs; } virtual bool select(const Record *r, int &index) const override { double value = r.get(selector); if(cell != 0) value = MathSign(value) * MathFloor(MathAbs(value) / cell) * cell; index = quants.get(value); return (index >= 0); } virtual int getRange() const override { return quants.size(); } virtual string getLabel(const int index) const override { int minbase = 0; int maxbase = 0; if(min > 0 && min != DBL_MAX) { double b1 = NormalizeDouble(min, (int)MathRound(MathAbs(MathLog10(min)) + 0.5)); if(b1 > 0) minbase = (int)MathAbs(MathFloor(MathLog10(b1))); } if(max > 0) { maxbase = (int)MathFloor(MathLog10(max)); } string format; // %5.2f format = StringFormat("%c%d.%df", '%', ((int)MathAbs(minbase) + (int)MathAbs(maxbase) + 3), (int)MathAbs(minbase)); string result = StringFormat(format, quants[index]); return result; } }; template class SerialNumberSelector: public BaseSelector { public: SerialNumberSelector(const T field): BaseSelector(field) { _typename = typename(this); } virtual bool select(const Record *r, int &index) const override { index = (int)r.get(selector); return true; } virtual int getRange() const override { return R::getRecordCount(); } virtual string getLabel(const int index) const override { return (string)(index); } }; // FILTERS template class Filter { protected: Selector *selector; double filter; public: Filter(Selector &s, const double value): selector(&s), filter(value) { } virtual bool matches(const Record *r) const { int index; if(selector.select(r, index)) { if(index == -1) { if(dynamic_cast *>(selector) != NULL) { return r.get(selector.getField()) == filter; } } else { if(index == (int)filter) return true; } } return false; } Selector *getSelector() const { return selector; } virtual string getTitle() const { return "Filter::" + selector.getTitle() + "[" + (string)filter + "]"; } }; template class FilterRange: public Filter { protected: double filterMax; public: FilterRange(Selector &s, const double valueMin, const double valueMax): Filter(s, valueMin), filterMax(valueMax) { } virtual bool matches(const Record *r) const override { int index; if(selector.select(r, index)) { if(index == -1) { if(dynamic_cast *>(selector) != NULL) { const double v = r.get(selector.getField()); if(filterMax > filter) { return v >= filter && v < filterMax; // range [;) is included } else { return v >= filter || v < filterMax; // range [;) is excluded } } } else { if(filterMax > filter) { return index >= (int)filter && index < (int)filterMax; } else { return index >= (int)filter || index < (int)filterMax; } } } return false; } virtual string getTitle() const override { return "FilterRange::" + selector.getTitle() + "[" + (string)filter + " ... " + (string)filterMax + "]"; } }; enum SORT_BY // applicable only for 1-dimensional cubes { SORT_BY_NONE, // none SORT_BY_VALUE_ASCENDING, // value (ascending) SORT_BY_VALUE_DESCENDING, // value (descending) SORT_BY_LABEL_ASCENDING, // label (ascending) SORT_BY_LABEL_DESCENDING // label (descending) }; #define SORT_ASCENDING(A) (((A) & 1) != 0) #define SORT_VALUE(A) ((A) > 0 && (A) < 3) class MetaCube { protected: int dimensions[]; int offsets[]; double totals[]; string _typename; public: int getDimension() const { return ArraySize(dimensions); } int getDimensionRange(const int n) const { return dimensions[n]; } int getCubeSize() const { return ArraySize(totals); } virtual double getValue(const int &indices[]) const = 0; virtual string getMetaCubeTitle(const bool shortNames = false) const = 0; virtual string getDimensionTitle(const int d) const = 0; virtual string getDimensionIndexLabel(const int d, const int index) const = 0; virtual string getFilterTitles() const = 0; virtual bool getVector(const int dimension, const int &consts[], PairArray *&result, const SORT_BY sortby = SORT_BY_NONE) const = 0; virtual int getDimensionField(const int d) const = 0; virtual bool hasSpecialFormat() const { return false; } virtual string getValueFormatted(const double value) const { return (string)value; // stub } //virtual string getDimensionFormat(const int d) const = 0; virtual bool isSerial() const { return false; } }; template class Aggregator: public MetaCube { protected: const E field; const int selectorCount; const Selector *selectors[]; const int filterCount; const Filter *filters[]; string customNames[]; virtual int mixIndex(const int &k[]) const { int result = 0; for(int i = 0; i < selectorCount; i++) { result += k[i] * offsets[i]; } return result; } virtual bool decode(const int _input, int &k[]) const { int index = _input; ArrayResize(k, selectorCount); ArrayInitialize(k, 0); for(int i = selectorCount - 1; i >= 0; i--) { k[i] = index / offsets[i]; index -= k[i] * offsets[i]; } if(index != 0) { Print("Bad index decode: ", _input); ArrayPrint(k); return false; } return true; } virtual bool filter(const Record *data) const { int q = 0; for(; q < filterCount; q++) { if(!filters[q].matches(data)) { break; } } if(q < filterCount) return false; return true; } public: Aggregator(const E f, const Selector *&s[], const Filter *&t[]): field(f), selectorCount(ArraySize(s)), filterCount(ArraySize(t)) { ArrayResize(selectors, selectorCount); for(int i = 0; i < selectorCount; i++) { selectors[i] = s[i]; } ArrayResize(filters, filterCount); for(int i = 0; i < filterCount; i++) { filters[i] = t[i]; } _typename = typename(this); } virtual void setSelectorBounds(const int length = 0) { ArrayResize(dimensions, selectorCount); dimensions[0] = length == 0 ? selectors[0].getRange() : length; int total = dimensions[0]; for(int i = 1; i < selectorCount; i++) { dimensions[i] = selectors[i].getRange(); total *= dimensions[i]; } ArrayResize(totals, total); ArrayInitialize(totals, 0); ArrayResize(offsets, selectorCount); offsets[0] = 1; for(int i = 1; i < selectorCount; i++) { offsets[i] = dimensions[i - 1] * offsets[i - 1]; // 1, X, Y*X } } virtual int prepareSelectors(const Record *&data[]) { int n = ArraySize(data); int count = 0; for(int i = 0; i < n; i++) { if(!filter(data[i])) continue; for(int j = 0; j < selectorCount; j++) { Selector *s = (Selector *)selectors[j]; s.prepare(data[i]); } count++; } return count; } // build an array with number of dimensions equal to number of selectors virtual int calculate(const Record *&data[], const Progress *callback = NULL) { int k[]; int processed = 0; ArrayResize(k, selectorCount); int n = ArraySize(data); for(int i = 0; i < n && !IsStopped(); i++) { if(!filter(data[i])) continue; int j = 0; for(; j < selectorCount; j++) { int d; if(!selectors[j].select(data[i], d)) // record successfully mapped to a cell of selector? { break; // skip it, if not } k[j] = d; // save index in j-th dimension in array } if(j == selectorCount) // all coordinates are resolved { update(mixIndex(k), data[i].get(field)); // apply maths/stats processed++; } if(callback != NULL) callback.progress(i, n); } return processed; } double getValue(const int &indices[]) const override { return totals[mixIndex(indices)]; } virtual string getMetaCubeTitle(const bool shortNames = false) const override { const int size = EnumSize(field); if(field >= size && field - size < ArraySize(customNames)) { return _typename + " " + customNames[field - size]; } return _typename + " " + (shortNames ? Record::legendFromEnum(field) : EnumToString(field)); // return _typename + " " + EnumToString(field); } virtual string getDimensionTitle(const int d) const override { if(d >= ArraySize(selectors)) return "n/a"; string title = selectors[d].getTitle(); if(StringFind(title, "::") > 0) // not found in enum, assume dynamically added custom field { const int n = getDimensionField(d); const int m = selectors[d].getFieldSize(); if(n != -1 && n >= m && n - m < ArraySize(customNames)) { return customNames[n - m]; } } return title; } virtual int getDimensionField(const int d) const override { if(d >= ArraySize(selectors)) return -1; return (int)selectors[d].getField(); } /* virtual string getDimensionFormat(const int d) const override { if(d >= ArraySize(selectors)) return NULL; return selectors[d].format(); // there's no such thing like datetime format } */ virtual string getDimensionIndexLabel(const int d, const int index) const override { if(d >= ArraySize(selectors)) return "n/a"; return selectors[d].getLabel(index); } virtual string getFilterTitles() const override { string titles = ""; for(int i = 0; i < ArraySize(filters); i++) { titles += filters[i].getTitle() + ";"; } if(titles == "") titles = "no"; return titles; } virtual void update(const int index, const double value) = 0; virtual bool getVector(const int dimension, const int &consts[], PairArray *&result, const SORT_BY sortby = SORT_BY_NONE) const { const int n = getDimension(); if(dimension >= n || n < 0) return false; result = new PairArray; int indices[]; ArrayResize(indices, n); if(sortby != SORT_BY_NONE) { result.compareBy((SORT_ASCENDING(sortby) ? (Comparator *)(new Greater()) : (Comparator *)(new Lesser()))); } else { result.compareBy(NULL); } int m = getDimensionRange(dimension); result.allocate(m); int count = 0; for(int i = 0; i < m; i++) { ArrayCopy(indices, consts); indices[dimension] = i; double v = getValue(indices); if(SORT_VALUE(sortby)) { result.insert(count, v, getDimensionIndexLabel(dimension, i)); } else { result.insert(count, getDimensionIndexLabel(dimension, i), v); } count++; } result.allocate(count); return true; } virtual void assignCustomFields(const string &fields[]) { ArrayCopy(customNames, fields); } }; template class IdentityAggregator: public Aggregator { private: int size; protected: virtual int mixIndex(const int &k[/*0 - record number, 1 - field number*/]) const override { int result = 0; for(int i = 0; i < size; i++) { result += k[i] * offsets[i]; } return result; } virtual bool decode(const int _input, int &k[]) const override { int index = _input; ArrayResize(k, size); ArrayInitialize(k, 0); for(int i = 1; i >= 0; i--) { k[i] = index / offsets[i]; index -= k[i] * offsets[i]; } if(index != 0) { Print("Bad index decode: ", _input); ArrayPrint(k); return false; } return true; } public: IdentityAggregator(const E f, const Selector *&s[], const Filter *&t[]): Aggregator(f, s, t) { _typename = typename(this); } virtual void setSelectorBounds(const int length = 0) override { size = 1 + (selectorCount > 1); ArrayResize(dimensions, size); int total = length * selectorCount; dimensions[0] = length; if(selectorCount > 1) dimensions[1] = selectorCount; ArrayResize(totals, total); ArrayInitialize(totals, 0); ArrayResize(offsets, size); offsets[0] = 1; if(selectorCount > 1) { offsets[1] = dimensions[0]; } } virtual int calculate(const Record *&data[], const Progress *callback = NULL) override { int k[]; int processed = 0; ArrayResize(k, size); int n = ArraySize(data); for(int i = 0; i < n && !IsStopped(); i++) { if(!filter(data[i])) continue; k[0] = processed; // i; for(int j = 0; j < selectorCount; j++) { if(selectorCount > 1) k[1] = j; update(mixIndex(k), data[i].get(selectors[j].getField())); } processed++; if(callback != NULL) callback.progress(i, n); } return processed; } virtual void update(const int index, const double value) override { totals[index] = value; } virtual string getMetaCubeTitle(const bool shortNames = false) const override { return _typename + (field != FIELD_NONE ? " (field has no effect and ignored)" : ""); } virtual string getDimensionTitle(const int d) const override { if(d < 0) { return Aggregator::getDimensionTitle(-d - 1); } else if(d == 0) { if(selectorCount > 1) return "index"; else return selectors[0].getTitle() + " by index"; } else { string titles = ""; for(int i = 0; i < selectorCount; i++) { titles += (string)i + ":" + selectors[i].getTitle() + "; "; } return titles; } } virtual int getDimensionField(const int d) const override { return Aggregator::getDimensionField((d < 0 ? -d - 1 : d)); } virtual string getDimensionIndexLabel(const int d, const int index) const override { return "[" + (string)index + "]"; } virtual bool isSerial() const override { return true; } }; template class SumAggregator: public Aggregator { public: SumAggregator(const E f, const Selector *&s[], const Filter *&t[]): Aggregator(f, s, t) { _typename = typename(this); } virtual void update(const int index, const double value) override { totals[index] += value; } }; template class ProgressiveTotalAggregator: public Aggregator { private: double accumulators[]; public: ProgressiveTotalAggregator(const E f, const Selector *&s[], const Filter *&t[]): Aggregator(f, s, t) { _typename = typename(this); } virtual void setSelectorBounds(const int length = 0) override { Aggregator::setSelectorBounds(length); int dim = 1; for(int i = 1; i < selectorCount; i++) // except 1-st dimention, for which progressive total is calculated { dim *= dimensions[i]; } ArrayResize(accumulators, dim); ArrayInitialize(accumulators, 0); Converter converter; const double nan = converter[0x7FF8000000000000]; // quiet NaN ArrayInitialize(totals, nan); } virtual int calculate(const Record *&data[], const Progress *callback = NULL) override { int k[]; int processed = 0; int cursor = 0; ArrayResize(k, selectorCount); int n = ArraySize(data); for(int i = 0; i < n && !IsStopped(); i++) { if(!filter(data[i])) continue; int j = 0; for(; j < selectorCount; j++) { int d; if(!selectors[j].select(data[i], d)) // record successfully mapped to a cell of selector? { break; // skip it, if not } k[j] = j == 0 ? cursor : d; // save index in j-th dimension in array } if(j == selectorCount) // all coordinates are resolved { update(mixIndex(k), data[i].get(field)); // apply maths/stats processed++; } cursor++; if(callback != NULL) callback.progress(i, n); } return processed; } virtual void update(const int index, const double value) override { if(index < 0 || index >= ArraySize(totals)) { Print(__FUNCSIG__, ": Index out of bound: ", index, " size: ", ArraySize(totals)); } int subindex = 0; if(selectorCount > 1) { int k[]; decode(index, k); // eliminate 1-st dimension, special case of index mix for(int i = 1; i < selectorCount; i++) { subindex += k[i] * (offsets[i] / dimensions[0]); } } accumulators[subindex] += value; totals[index] = accumulators[subindex]; } virtual bool isSerial() const override { return true; } }; template class AverageAggregator: public Aggregator { protected: int counters[]; public: AverageAggregator(const E f, const Selector *&s[], const Filter *&t[]): Aggregator(f, s, t) { _typename = typename(this); } virtual void setSelectorBounds(const int length = 0) override { Aggregator::setSelectorBounds(); ArrayResize(counters, ArraySize(totals)); ArrayInitialize(counters, 0); } virtual void update(const int index, const double value) override { totals[index] = (totals[index] * counters[index] + value) / (counters[index] + 1); counters[index]++; } }; template class VarianceAggregator: public Aggregator { protected: int counters[]; double sumx[]; double sumx2[]; public: VarianceAggregator(const E f, const Selector *&s[], const Filter *&t[]): Aggregator(f, s, t) { _typename = typename(this); } virtual void setSelectorBounds(const int length = 0) override { Aggregator::setSelectorBounds(); ArrayResize(counters, ArraySize(totals)); ArrayResize(sumx, ArraySize(totals)); ArrayResize(sumx2, ArraySize(totals)); ArrayInitialize(counters, 0); ArrayInitialize(sumx, 0); ArrayInitialize(sumx2, 0); } /* union floats { double d1; struct f2 { float mean; float deviation; } ff; }; */ virtual void update(const int index, const double value) override { // TODO: probably replace with Welford's algorithm counters[index]++; sumx[index] += value; sumx2[index] += value * value; const int n = counters[index]; // const double mean = sumx[index] / n; const double variance = (sumx2[index] - sumx[index] * sumx[index] / n) / MathMax(n - 1, 1); /* floats f; f.ff.mean = (float)mean; f.ff.deviation = (float)MathSqrt(variance); totals[index] = f.d1; */ totals[index] = MathSqrt(variance); } /* virtual bool hasSpecialFormat() const override { return true; } virtual string getValueFormatted(const double value) const override { floats f; f.d1 = value; return (f.ff.mean > 0 ? "+" : "") + (string)f.ff.mean + ShortToString(0x0B1) + (string)f.ff.deviation; } */ }; template class ProfitFactorAggregator: public Aggregator { protected: double positives[]; double negatives[]; public: ProfitFactorAggregator(const E f, const Selector *&s[], const Filter *&t[]): Aggregator(f, s, t) { _typename = typename(this); } virtual void setSelectorBounds(const int length = 0) override { Aggregator::setSelectorBounds(); ArrayResize(positives, ArraySize(totals)); ArrayResize(negatives, ArraySize(totals)); ArrayInitialize(positives, 0); ArrayInitialize(negatives, 0); } virtual void update(const int index, const double value) override { if(value >= 0) positives[index] += value; else negatives[index] -= value; if(negatives[index] > 0) { totals[index] = positives[index] / negatives[index]; } else { Converter converter; totals[index] = converter[0x7FF0000000000000]; // infinity } } }; template class MaxAggregator: public Aggregator { public: MaxAggregator(const E f, const Selector *&s[], const Filter *&t[]): Aggregator(f, s, t) { _typename = typename(this); } virtual void update(const int index, const double value) override { totals[index] = MathMax(totals[index], value); } }; template class MinAggregator: public Aggregator { public: MinAggregator(const E f, const Selector *&s[], const Filter *&t[]): Aggregator(f, s, t) { _typename = typename(this); } virtual void setSelectorBounds(const int length = 0) override { Aggregator::setSelectorBounds(); ArrayInitialize(totals, DBL_MAX); } virtual void update(const int index, const double value) override { totals[index] = MathMin(totals[index], value); } }; template class CountAggregator: public Aggregator { public: CountAggregator(const E f, const Selector *&s[], const Filter *&t[]): Aggregator(f, s, t) { _typename = typename(this); } virtual void update(const int index, const double value) override { totals[index]++; } }; class Display { public: virtual void display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) = 0; }; class LogDisplay: public Display { private: string format; int digits; bool dropZeros; public: LogDisplay(const int w, const int d, const bool omitZeros = false) { digits = d; format = StringFormat("%%%d.%df", w, d); dropZeros = omitZeros; } virtual void display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override { int n = metaData.getDimension(); int indices[], cursors[]; ArrayResize(indices, n); ArrayResize(cursors, n); ArrayInitialize(cursors, 0); for(int i = 0; i < n; i++) { indices[i] = metaData.getDimensionRange(i); } if(metaData.getCubeSize() == 0) { Print("[empty]"); return; } bool sorting = (n == 1 && sortby != SORT_BY_NONE) || SORT_VALUE(sortby); if(n > 1 && !sorting) { Print("Sorting of multidimentional arrays is fully supported for values only, labels are sorted as is"); } PairArray *flat = NULL; if(sorting) { flat = new PairArray(metaData.getCubeSize(), (SORT_ASCENDING(sortby) ? (Comparator *)(new Greater()) : (Comparator *)(new Lesser()))); } string labels[]; string allLabels; ArrayResize(labels, n); bool looping = false; int count = 0; Converter converter; const double nan = converter[0x7FF8000000000000]; // quiet NaN do { allLabels = ""; for(int j = 0; j < n; j++) { labels[j] = metaData.getDimensionIndexLabel(j, cursors[j]); allLabels += (j > 0 ? ";\t" : "") + labels[j]; } if(!dropZeros || (metaData.getValue(cursors) != 0 && MathIsValidNumber(metaData.getValue(cursors)))) { if(sorting) { // sort single (first) dimension by sort_by if(SORT_VALUE(sortby)) { flat.insert(count++, metaData.getValue(cursors), allLabels); } else { flat.insert(count++, allLabels, metaData.getValue(cursors)); } } else { if(metaData.hasSpecialFormat()) { arrayPrint(metaData.getValueFormatted(metaData.getValue(cursors)), labels); } else { arrayPrint(StringFormat(format, metaData.getValue(cursors)), labels); } } } for(int i = 0; i < n; i++) { if(cursors[i] < indices[i] - 1) { looping = true; cursors[i]++; break; } else { cursors[i] = 0; } looping = false; } } while(looping && !IsStopped()); if(sorting) { flat.allocate(count); // shrink (if neccessary) if(metaData.hasSpecialFormat()) { for(int i = 0; i < ArraySize(flat.array); i++) { Print(i, ": ", flat.array[i].title, " ", metaData.getValueFormatted(flat.array[i].value)); } } else { ArrayPrint(flat.array, digits); } delete flat; } } }; template void arrayPrint(const string title, const T &V[]) { int n = ArraySize(V); string s; for(int i = 0; i < n; i++) { s = s + " " + (string)V[i]; } Print(title + ":" + s); } template class Analyst { private: DataAdapter *adapter; Record *data[]; Aggregator *aggregator; Display *output; bool dataOwner; public: Analyst(DataAdapter &a, Aggregator &g, Display &d): adapter(&a), aggregator(&g), output(&d) { ArrayResize(data, adapter.reservedSize()); dataOwner = false; } ~Analyst() { if(dataOwner) { const int n = ArraySize(data); for(int i = 0; i < n; i++) { if(CheckPointer(data[i]) == POINTER_DYNAMIC) delete data[i]; } } } void setDataOwner(const bool owner) { dataOwner = owner; } void acquireData() { Record *record; int i = 0; dataOwner = !adapter.isOwner(); adapter.reset(); while((record = adapter.getNext()) != NULL) { data[i++] = record; } ArrayResize(data, i); const int n = aggregator.prepareSelectors(data); if(aggregator.isSerial()) { aggregator.setSelectorBounds(n); } else { aggregator.setSelectorBounds(); } string names[]; if(adapter.getCustomFields(names) > 0) { aggregator.assignCustomFields(names); } } int build(const Progress *ptr = NULL) { return aggregator.calculate(data, ptr); } void display(const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) { output.display(aggregator, sortby, identity); } }; template class OLAPEngine { protected: DataAdapter *adapter; const Progress *progress; uint quantGranularity; bool shortTitles; virtual Selector *createSelector(const S selector, const T field) = 0; virtual void initialize() = 0; public: OLAPEngine(): adapter(NULL), progress(NULL), shortTitles(false) {} OLAPEngine(DataAdapter *ptr, const uint quant = 0, const Progress *show = NULL): adapter(ptr), quantGranularity(quant), progress(show), shortTitles(false) {} void setAdapter(DataAdapter *ptr) { adapter = ptr; } void setProgress(Progress *ptr) { progress = ptr; } void setQuant(const uint quant) { quantGranularity = quant; // FIXME: need to be selector property } void setShortTitles(const bool t) { shortTitles = t; Selector::setShortTitles(t); } int process( const S &selectorArray[], const T &selectorField[], const AGGREGATORS AggregatorType, const T AggregatorField, Display &display, const SORT_BY SortBy = SORT_BY_NONE, const double Filter1value1 = 0, const double Filter1value2 = 0) { int selectorCount = 0; for(int i = 0; i < MathMin(ArraySize(selectorArray), 3); i++) { selectorCount += selectorArray[i] != SELECTOR_NONE; } if(selectorCount == 0) { Alert("No selectors. Setup at least one of them."); return 0; } // filter section part.1 starts S Filter1 = SELECTOR_NONE; T Filter1Field = FIELD_NONE; if(ArraySize(selectorArray) > 3) { Filter1 = selectorArray[3]; } if(ArraySize(selectorField) > 3) { Filter1Field = selectorField[3]; } // filter section part.1 ends Selector *selectors[]; ArrayResize(selectors, selectorCount); for(int i = 0; i < selectorCount; i++) { selectors[i] = createSelector(selectorArray[i], selectorField[i]); if(selectors[i] == NULL) { Print("Selector ", i, " is empty. Setup selectors successively (don't leave a hole in-between), specify a field when required"); return 0; } } // filter section part.2 starts Filter *filters[]; if(Filter1 != SELECTOR_NONE) { ArrayResize(filters, 1); Selector *filterSelector = createSelector(Filter1, Filter1Field); if(Filter1value1 != Filter1value2) { filters[0] = new FilterRange(filterSelector, Filter1value1, Filter1value2); } else { filters[0] = new Filter(filterSelector, Filter1value1); } } // filter section part.2 ends Aggregator *aggregator; // MQL does not support a 'class info' metaclass. // Otherwise we could use an array of classes instead of the switch switch(AggregatorType) { case AGGREGATOR_SUM: aggregator = new SumAggregator(AggregatorField, selectors, filters); break; case AGGREGATOR_AVERAGE: aggregator = new AverageAggregator(AggregatorField, selectors, filters); break; case AGGREGATOR_MAX: aggregator = new MaxAggregator(AggregatorField, selectors, filters); break; case AGGREGATOR_MIN: aggregator = new MinAggregator(AggregatorField, selectors, filters); break; case AGGREGATOR_COUNT: aggregator = new CountAggregator(AggregatorField, selectors, filters); break; case AGGREGATOR_PROFITFACTOR: aggregator = new ProfitFactorAggregator(AggregatorField, selectors, filters); break; case AGGREGATOR_PROGRESSIVE: aggregator = new ProgressiveTotalAggregator(AggregatorField, selectors, filters); break; case AGGREGATOR_IDENTITY: aggregator = new IdentityAggregator(AggregatorField, selectors, filters); break; case AGGREGATOR_STDDEV: aggregator = new VarianceAggregator(AggregatorField, selectors, filters); break; } Analyst *analyst = new Analyst(adapter, aggregator, display); analyst.acquireData(); initialize(); Print("Aggregator: ", aggregator.getMetaCubeTitle(shortTitles), " [", aggregator.getCubeSize(), "]", " dimensions: ", aggregator.getDimension()); Print("Filters: ", aggregator.getFilterTitles()); Print("Selectors: ", selectorCount); for(int i = 0; i < aggregator.getDimension(); i++) { Print(CharToString((uchar)('X' + i)), ": ", aggregator.getDimensionTitle(i) /*display.getCustomDimensionTitle(aggregator, i)*/, " [", aggregator.getDimensionRange(i), "]"); } const int p = analyst.build(progress); Print("Processed records: ", p); if(p > 0) { analyst.display(SortBy, AggregatorType == AGGREGATOR_IDENTITY); } delete analyst; delete aggregator; for(int i = 0; i < selectorCount; i++) { delete selectors[i]; } for(int i = 0; i < ArraySize(filters); i++) { delete filters[i].getSelector(); delete filters[i]; } return p; } };