| //+------------------------------------------------------------------+
|
| //| toyjson3.mqh |
|
| //| Copyright 2022-2025, MetaQuotes Ltd. |
|
| //| https://www.mql5.com |
|
| //+------------------------------------------------------------------+
|
|
|
| #include "..\\..\\..\\MQL5Book\\Include\\MapArray.mqh"
|
| #include "..\\..\\..\\MQL5Book\\Include\\AutoPtr.mqh"
|
| #include "..\\..\\..\\MQL5Book\\Include\\Defines.mqh"
|
| #include "..\\..\\..\\MQL5Book\\Include\\StringUtils.mqh"
|
|
|
| enum JS_TYPE
|
| {
|
| JS_OBJECT,
|
| JS_ARRAY,
|
| JS_PRIMITIVE,
|
| JS_NULL, // singleton object stub for undefined props
|
| JS_ERROR
|
| };
|
|
|
| enum JS_AFFINITY // for primitive type
|
| {
|
| JS_NONE = 0,
|
| JS_CONST = 1, // true, false, null
|
| JS_INT = 2,
|
| JS_FLOAT = 4,
|
| JS_STRING = 8
|
| };
|
|
|
| #define JS_EOF 26 // Ctrl-Z
|
| #ifndef JS_STACK_LIMIT
|
| #define JS_STACK_LIMIT 50
|
| #endif
|
|
|
| #ifndef ArrayContains
|
|
|
| template<typename T>
|
| int ArrayIndexOf(const T &a[], const T v)
|
| {
|
| for(int i = 0; i < ArraySize(a); i++)
|
| {
|
| if(a[i] == v) return i;
|
| }
|
| return -1;
|
| }
|
|
|
| #define ArrayContains(A,V) (ArrayIndexOf(A,(V)) > -1)
|
| #endif
|
|
|
| //+------------------------------------------------------------------+
|
| //| Main class to store single value (primitive, object, or array) |
|
| //+------------------------------------------------------------------+
|
| class JsValue
|
| {
|
| MapArray<string, int> properties;
|
| JsValue *objects[];
|
|
|
| public:
|
| const static JsValue null;
|
|
|
| const string s; // value is assigned once at construction
|
| const JS_TYPE t; // type and
|
| const JS_AFFINITY a; // affinity as detected
|
| JsValue(const string _s): s(_s), t(JS_PRIMITIVE), a(detect()) { }
|
|
|
| JsValue(const JS_TYPE _t = JS_OBJECT): t(_t), s(NULL), a(JS_NONE) { }
|
|
|
| JsValue(JsValue *that): s(that.s), t(that.t), a(that.a)
|
| {
|
| for(int i = 0; i < that.size(); i++)
|
| {
|
| if(that[i].t >= JS_PRIMITIVE)
|
| {
|
| this.put(t == JS_OBJECT ? that.props().getKey(i) : NULL, that[i].s);
|
| }
|
| else
|
| {
|
| this.put(t == JS_OBJECT ? that.props().getKey(i) : NULL, new JsValue(that[i]));
|
| }
|
| }
|
| }
|
|
|
| ~JsValue()
|
| {
|
| for(int i = 0; i < ArraySize(objects); i++)
|
| {
|
| FREE(objects[i]);
|
| }
|
| }
|
|
|
| bool merge(JsValue *that, const bool move = false)
|
| {
|
| if(this.t != that.t) return false;
|
| if(this.t >= JS_PRIMITIVE) return false;
|
| for(int i = 0; i < that.size(); i++)
|
| {
|
| if(that[i].t >= JS_PRIMITIVE)
|
| {
|
| if(move)
|
| {
|
| this.put(that.t == JS_OBJECT ? that.props().getKey(i) : NULL, that[i]);
|
| }
|
| else
|
| {
|
| this.put(that.t == JS_OBJECT ? that.props().getKey(i) : NULL, that[i].s);
|
| }
|
| }
|
| else
|
| {
|
| if(move)
|
| {
|
| this.put(that.t == JS_OBJECT ? that.props().getKey(i) : NULL, that[i]);
|
| }
|
| else
|
| {
|
| this.put(that.t == JS_OBJECT ? that.props().getKey(i) : NULL, move ? that[i] : new JsValue(that[i]));
|
| }
|
| }
|
| if(move) that.drop(i);
|
| }
|
| return true;
|
| }
|
|
|
| int length() const
|
| {
|
| return ArraySize(objects);
|
| }
|
|
|
| const MapArray<string, int> * const props() const
|
| {
|
| return &properties;
|
| }
|
|
|
| // simplistic state-machine approach
|
| static JS_AFFINITY isNumeric(const ushort c, const JS_AFFINITY state = 0)
|
| {
|
| if(c == '+' || c == '-')
|
| {
|
| if(state == JS_NONE || ((state & JS_FLOAT) == JS_FLOAT)) return state; // leading sign or in exponent
|
| }
|
| else if(c >= '0' && c <= '9') return JS_INT;
|
| else if(c == '.')
|
| {
|
| if(state < JS_FLOAT) return JS_FLOAT; // one point is a valid number
|
| }
|
| else if(c == 'e' || c == 'E') // PRB: multiple E's aren't catched as string
|
| {
|
| if(state >= JS_INT) return JS_FLOAT; // exponent is allowed provided digits exist
|
| }
|
| else if(c == ' ' || c == '\r' || c == '\n' || c == '\t') return JS_NONE;
|
| return JS_STRING; // also used for datetime, not existing in JSON
|
| }
|
|
|
| JS_AFFINITY detect() const
|
| {
|
| if(s == "true" || s == "false" || s == "null") return JS_CONST;
|
| if(s == "") return JS_STRING;
|
| JS_AFFINITY result = 0;
|
| for(int i = 0; i < StringLen(s) && result < JS_STRING; ++i)
|
| {
|
| result |= isNumeric(s[i], result);
|
| }
|
| return result;
|
| }
|
|
|
| string stringify(const uint allocate = 0, const int pretty = -1) const
|
| {
|
| if(t < JS_PRIMITIVE)
|
| {
|
| string buffer;
|
| if(allocate) StringReserve(buffer, allocate);
|
| stringify(buffer, pretty);
|
| return buffer;
|
| }
|
| if(a >= JS_STRING)
|
| {
|
| string ss = s;
|
| StringReplace(ss, "\"", "\\\"");
|
| return "\"" + ss + "\"";
|
| }
|
| if(a >= JS_FLOAT) return s; // FIXME: apply specific accuracy
|
| if(a >= JS_INT) return s;
|
| return s != NULL ? s : "null";
|
| }
|
|
|
| template<typename T>
|
| T get(const bool typechecks = true, T def = (T)NULL) const
|
| {
|
| if(typechecks)
|
| {
|
| if(t != JS_PRIMITIVE) return def;
|
| if(a >= JS_STRING && typename(T) != "string") return def;
|
| if(a == JS_CONST && typename(T) != "bool") return def;
|
| }
|
| // return (T)s;
|
| T r;
|
| cast(s, r);
|
| return r;
|
| }
|
|
|
| template<typename T>
|
| void cast(const string &in, T &out) const
|
| {
|
| out = (T)s;
|
| }
|
|
|
| void cast(const string &in, bool &out) const
|
| {
|
| out = !this.operator!();
|
| }
|
|
|
| bool operator!() const
|
| {
|
| if(t != JS_PRIMITIVE) return !(t < JS_PRIMITIVE); // NULL object is falsy
|
| if(a >= JS_STRING) return !(StringLen(s) > 0);
|
| if(a == JS_CONST) return !(s == "true");
|
| return !(double)(s);
|
| }
|
|
|
| template<typename T>
|
| bool operator==(const T value) const
|
| {
|
| if(t < JS_PRIMITIVE) return false;
|
| if(a >= JS_STRING && typename(T) != "string") return false;
|
| if(a == JS_CONST && typename(T) != "bool") return false;
|
| T temp = this.get<T>(false);
|
| return temp == value;
|
| }
|
|
|
| // check if 'that' is a subset of 'this',
|
| // 'fast' false means deep look for detailed changes and strict comparison
|
| bool match(const JsValue *that, const bool fast = true, JsValue *details = NULL)
|
| {
|
| bool result = true;
|
|
|
| if(this.t != that.t)
|
| {
|
| if(details)
|
| {
|
| details.put("(Type change)", StringFormat("(%s->%s)", EnumToString(this.t), EnumToString(that.t)));
|
| }
|
| return false;
|
| }
|
| if(this.t == JS_PRIMITIVE)
|
| {
|
| if(details && this.s != that.s)
|
| {
|
| details.put("(Value change)", StringFormat("(%s;%s->%s)", EnumToString(this.a), this.s, that.s));
|
| }
|
| return this.s == that.s;
|
| }
|
|
|
| if(this.t == JS_OBJECT)
|
| {
|
| if(fast && this.size() != that.size())
|
| {
|
| if(details)
|
| {
|
| details.put("(Object size change)", StringFormat("(%d->%d)", this.size(), that.size()));
|
| }
|
| return false;
|
| }
|
|
|
| static string keys1[], keys2[];
|
| that.getProperties(keys2);
|
|
|
| if(fast)
|
| {
|
| this.getProperties(keys1);
|
| if(ArrayCompare(keys1, keys2))
|
| {
|
| if(details)
|
| {
|
| details.put("(Object keys differ)", StringFormat("`[[%s],[%s]]", SubArrayCombine(keys1, ","), SubArrayCombine(keys2, ",")));
|
| }
|
| return false;
|
| }
|
| }
|
| else
|
| {
|
| for(int i = 0; i < ArraySize(keys2); i++)
|
| {
|
| if(this[keys2[i]].t == JS_NULL)
|
| {
|
| //if(details)
|
| //{
|
| // details.put("Key is missing", keys2[i]); // will be logged below
|
| //}
|
| result = false;
|
| }
|
| }
|
| }
|
| }
|
| else
|
| {
|
| if(fast && this.length() != that.length())
|
| {
|
| if(details)
|
| {
|
| details.put("(Array length change)", StringFormat("(%d->%d)", this.length(), that.length()));
|
| }
|
| return false;
|
| }
|
| }
|
|
|
| if(fast)
|
| {
|
| for(int i = 0; i < ArraySize(objects); i++)
|
| {
|
| JsValue *sub = details ? new JsValue() : NULL;
|
| if(!objects[i].match(that[i], fast, sub))
|
| {
|
| if(details)
|
| {
|
| if(sub.size()) details.put(this.t == JS_OBJECT ? properties.getKey(i) : StringFormat("%d", i), sub);
|
| else delete sub;
|
| }
|
| result = false;
|
| if(!details) return false;
|
| // for details keep looping and comparing
|
| }
|
|
|
| if(sub && !sub.size()) delete sub;
|
| }
|
| }
|
| else
|
| {
|
| int matches = 0;
|
| int foundinthis[];
|
| int foundinthat[];
|
|
|
| // collect changes preliminary
|
| for(int i = 0; i < that.length() /*&& matches < that.length()*/; i++)
|
| {
|
| const JsValue *other = that[i];
|
| //if(ArrayContains(foundinthat, i)) continue; // fast but incomplete
|
| for(int j = 0; j < ArraySize(objects); j++)
|
| {
|
| //if(ArrayContains(foundinthis, j)) continue; // fast but incomplete
|
| if(objects[j].match(other, fast, NULL))
|
| {
|
| //PUSH(foundinthis, j); // fast but incomplete
|
| //PUSH(foundinthat, i); // fast but incomplete
|
|
|
| bool update = true;
|
| int i1 = ArrayIndexOf(foundinthis, j);
|
| int i2 = ArrayIndexOf(foundinthat, i);
|
|
|
| // look at corresponding counterparts
|
| int ip = -1, jp = -1;
|
| if(i1 > -1) jp = foundinthat[i1];
|
| if(i2 > -1) ip = foundinthis[i2];
|
|
|
| if(jp > -1 && ip > -1 && fabs(ip - jp) <= fabs(i - j)) // speculation on closer changes
|
| {
|
| update = false;
|
| }
|
| if(update)
|
| {
|
| if(i1 > -1) foundinthis[i1] = -1;
|
| PUSH(foundinthis, j);
|
| if(i2 > -1) foundinthat[i2] = -1;
|
| PUSH(foundinthat, i);
|
| }
|
|
|
| matches++;
|
| }
|
| }
|
| }
|
|
|
| matches = 0; // that.length();
|
| for(int i = 0; i < ArraySize(foundinthat); i++)
|
| {
|
| if(foundinthat[i] != -1 && foundinthat[i] == foundinthis[i]) matches++;
|
| }
|
|
|
| if(matches < that.length())
|
| {
|
| if(details)
|
| {
|
| // collect details about differences
|
| for(int i = 0; i < that.length(); i++)
|
| {
|
| const int p = ArrayIndexOf(foundinthis, i); // ArrayIndexOf(foundinthis, i);
|
| if(p != -1 && foundinthat[p] == i) continue;
|
| int k = ArrayIndexOf(foundinthat, i);
|
| if(k > -1) k = foundinthis[k];
|
|
|
| if(k == -1 && i < this.length()) k = i; // speculation about change at the same i, modification
|
|
|
| if(k == -1)
|
| {
|
| details.put(that.t == JS_OBJECT ? that.props().getKey(i) : StringFormat("%d", i), "(new, missing in old)");
|
| continue;
|
| }
|
|
|
| // here we have 'that' element missing in 'this'
|
|
|
| JsValue *sub = new JsValue();
|
|
|
| objects[k].match(that[i], fast, sub);
|
|
|
| if(sub.size())
|
| {
|
| details.put(that.t == JS_OBJECT ? that.props().getKey(i) : StringFormat("%d", i), sub);
|
| }
|
| else
|
| {
|
| delete sub;
|
| matches++;
|
| }
|
| }
|
|
|
| #ifdef TOYJSON_SHOW_STATS
|
| if(matches < that.length())
|
| details.put("(Incomplete pattern match)", StringFormat("(%d of %d)", matches, that.length()));
|
| #endif
|
| }
|
| if(matches < that.length())
|
| {
|
| result = false;
|
| if(!details) return false;
|
| }
|
| }
|
|
|
| // uncomment this to make comparison symmetrical between 'this' and 'that'
|
| #ifdef TOYJSON_MIRROR_MATCH
|
| matches = 0; // that.length();
|
| for(int i = 0; i < ArraySize(foundinthis); i++)
|
| {
|
| if(foundinthis[i] != -1 && foundinthat[i] == foundinthis[i]) matches++;
|
| }
|
|
|
| if(matches < this.length())
|
| {
|
| if(details)
|
| {
|
| for(int j = 0; j < ArraySize(objects); j++)
|
| {
|
| const int p = ArrayIndexOf(foundinthis, j);
|
| if(p != -1 && foundinthat[p] == j) continue;
|
| int k = ArrayIndexOf(foundinthis, j);
|
| if(k > -1) k = foundinthat[k];
|
|
|
| if(k == -1 && j < that.length()) k = j; // speculation about change at the same j, modification
|
|
|
| if(k == -1)
|
| {
|
| details.put(this.t == JS_OBJECT ? this.props().getKey(j) : StringFormat("%d", j), "(old, missing in new)");
|
| continue;
|
| }
|
|
|
| // here we have 'this' element missing in 'that' (which can be normal if 'that' is a subset, "search pattern")
|
|
|
| JsValue *sub = new JsValue();
|
|
|
| objects[j].match(that[k], fast, sub);
|
|
|
| if(sub.size())
|
| {
|
| details.put(this.t == JS_OBJECT ? properties.getKey(j) : StringFormat("%d", j), sub);
|
| }
|
| else
|
| {
|
| delete sub;
|
| matches++;
|
| }
|
| }
|
|
|
| #ifdef TOYJSON_SHOW_STATS
|
| if(matches < this.length())
|
| details.put("(Coverage)", StringFormat("(%d of %d)", matches, this.length()));
|
| #endif
|
| }
|
| // we don't return false here, because 'that' is considered a search pattern (subset) for 'this',
|
| // hence some parts existing in 'this' but missing in 'that' does still mean that 'this' fulfills all props of 'that'
|
| // result = false;
|
| }
|
| #endif
|
| }
|
|
|
| return result;
|
| }
|
|
|
| int size() const
|
| {
|
| if(t == JS_OBJECT) return properties.getSize();
|
| else if(t == JS_ARRAY) return ArraySize(objects);
|
| return 0;
|
| }
|
|
|
| // object putters
|
|
|
| template<typename T>
|
| bool try_parse(const string key, const T value)
|
| {
|
| return false; // all except strings are non-parseable
|
| }
|
|
|
| bool try_parse(const string key, const string value)
|
| {
|
| if(value[0] == '`')
|
| {
|
| put(key, JsParser::jsonify(StringSubstr(value, 1)));
|
| return true;
|
| }
|
| return false;
|
| }
|
|
|
| template<typename T>
|
| void put(const string key, const T value)
|
| {
|
| if(try_parse(key, value)) return;
|
|
|
| put(key, new JsValue((string)value));
|
| }
|
|
|
| void put(const string key, const double value, const int digits = 8)
|
| {
|
| put(key, new JsValue(DoubleToString(value, digits)));
|
| }
|
|
|
| void put(const string key, JsValue *value)
|
| {
|
| if(t == JS_NULL) return; // show bulk errors?
|
| if(t != JS_OBJECT)
|
| {
|
| if(key != NULL)
|
| {
|
| PrintFormat("WARNING: Setting property '%s' for non-object", key);
|
| }
|
| }
|
| else if(key == NULL)
|
| {
|
| Print("WARNING: Setting NULL property for object");
|
| }
|
| int p = key == NULL ? -1 : properties.find(key);
|
| if(p == -1)
|
| {
|
| p = EXPAND(objects);
|
| if(key != NULL) properties.put(key, p);
|
| }
|
| else
|
| {
|
| FREE(objects[p]);
|
| }
|
| objects[p] = value;
|
| }
|
|
|
| // array putters
|
|
|
| template<typename T>
|
| void put(const T value)
|
| {
|
| put(NULL, value);
|
| }
|
|
|
| template<typename T>
|
| void replace(const int i, const T value)
|
| {
|
| if(t >= JS_PRIMITIVE) return;
|
| if(i < 0 || i >= ArraySize(objects)) return;
|
| FREE(objects[i]);
|
| objects[i] = new JsValue((string)value);
|
| }
|
|
|
| template<typename T>
|
| void put(const T &values[])
|
| {
|
| for(int i = 0; i < ArraySize(values); i++)
|
| {
|
| put(NULL, new JsValue((string)values[i]));
|
| }
|
| }
|
|
|
| void put(const double value, const int digits = 8)
|
| {
|
| put(NULL, new JsValue(DoubleToString(value, digits)));
|
| }
|
|
|
| void put(JsValue *value)
|
| {
|
| if(t != JS_ARRAY)
|
| {
|
| Print("WARNING: Setting indexed element for non-array: ", value.stringify());
|
| }
|
| put(NULL, value);
|
| }
|
|
|
| void replace(const int i, JsValue *value)
|
| {
|
| if(t >= JS_PRIMITIVE) return;
|
| /*
|
| if(t != JS_ARRAY)
|
| {
|
| PrintFormat("WARNING: Setting %d-th element for non-array (%s): %s", i, EnumToString(t), value.stringify());
|
| }
|
| */
|
| if(i < 0 || i >= ArraySize(objects)) return;
|
| FREE(objects[i]);
|
| objects[i] = value;
|
| }
|
|
|
| void drop(const int i)
|
| {
|
| if(t >= JS_PRIMITIVE) return;
|
| /*
|
| if(t != JS_ARRAY)
|
| {
|
| PrintFormat("WARNING: Dropping %d-th element of non-array (%s): %s", i, EnumToString(t), s);
|
| }
|
| */
|
| if(i < 0 || i >= ArraySize(objects)) return;
|
| objects[i] = (JsValue *)&null;
|
| }
|
|
|
| // indexed access
|
|
|
| JsValue *operator[](const string name) // const
|
| {
|
| const int p = properties.find(name);
|
| if(p == -1) return (JsValue *)&null;
|
| return objects[p];
|
| }
|
|
|
| JsValue *operator[](const int i) const
|
| {
|
| if(i >= ArraySize(objects)) return (JsValue *)&null;
|
| if(i < 0)
|
| {
|
| if(-i <= ArraySize(objects)) return objects[ArraySize(objects) + i];
|
| return (JsValue *)&null;
|
| }
|
| return objects[i];
|
| }
|
|
|
| bool getProperties(string &keys[]) const
|
| {
|
| if(t != JS_OBJECT) return false;
|
| ArrayResize(keys, properties.getSize());
|
| for(int i = 0; i < properties.getSize(); i++)
|
| {
|
| keys[i] = properties.getKey(i);
|
| }
|
| return true;
|
| }
|
|
|
| int indexOf(const string value)
|
| {
|
| if(t != JS_ARRAY) return -1;
|
| for(int i = 0; i < ArraySize(objects); i++)
|
| {
|
| if(objects[i].s == value) return i;
|
| }
|
| return -1;
|
| }
|
|
|
| // aux stuff
|
|
|
| void print(const int maxlevel = INT_MAX) const
|
| {
|
| int level = 0;
|
| print(level, maxlevel);
|
| }
|
|
|
| void print(int &level, const int maxlevel) const
|
| {
|
| const static string open[] = {"{", "[", "", "null", "ERROR: "};
|
| const static string close[] = {"}", "]", "", "", "!"};
|
| Print(StringFormat("%*s", level * 2, ""), open[t], s);
|
| ++level;
|
| if(level < maxlevel)
|
| {
|
| const string padding = StringFormat("%*s", level * 2, "");
|
| if(properties.getSize() >= ArraySize(objects))
|
| {
|
| for(int i = 0; i < properties.getSize(); ++i)
|
| {
|
| if(objects[i])
|
| {
|
| if(objects[i].s != NULL)
|
| {
|
| Print(padding, properties.getKey(i), " = ", objects[i].s);
|
| }
|
| else
|
| {
|
| Print(padding, properties.getKey(i), " = ");
|
| objects[i].print(level, maxlevel);
|
| }
|
| }
|
| else Print(padding, "<N/A>");
|
| }
|
| }
|
| else // if(t == JS_ARRAY)
|
| {
|
| for(int i = 0; i < ArraySize(objects); ++i)
|
| {
|
| if(objects[i])
|
| {
|
| if(objects[i].s != NULL)
|
| {
|
| Print(padding, objects[i].s);
|
| }
|
| else
|
| {
|
| objects[i].print(level, maxlevel);
|
| }
|
| }
|
| else Print(padding, "<N/A>");
|
| }
|
| }
|
| }
|
| --level;
|
| if(t < JS_PRIMITIVE) Print(StringFormat("%*s", level * 2, ""), close[t]);
|
| }
|
|
|
| void stringify(string &buffer, const int pretty = 0) const
|
| {
|
| const static string open[] = {"{", "[", "", "null", "ERROR: "};
|
| const static string close[] = {"}", "]", "", "", "!"};
|
| const string padding = pretty && t <= JS_ARRAY ? StringFormat("\n%*s", fmax(pretty, 0) * 2, "") : "";
|
| const string inner = pretty && t <= JS_ARRAY ? " " : "";
|
| const bool inprocess = StringLen(buffer);
|
| if(StringLen(buffer) << 1 > (int)StringBufferLen(buffer)) StringReserve(buffer, StringBufferLen(buffer) << 1);
|
| StringAdd(buffer, (inprocess && size() ? padding : "") + open[t]);
|
| for(int i = 0; i < ArraySize(objects); ++i)
|
| {
|
| if(i > 0) StringAdd(buffer, ", ");
|
| if(t != JS_ARRAY) StringAdd(buffer, padding + inner + "\"" + properties.getKey(i) + "\" : ");
|
|
|
| if(objects[i].s != NULL)
|
| {
|
| StringAdd(buffer, (!i && t == JS_ARRAY ? (padding + inner) : "") + objects[i].stringify());
|
| }
|
| else
|
| {
|
| objects[i].stringify(buffer, pretty ? fmax(pretty, 0) + 1 : pretty);
|
| }
|
| }
|
| // TODO: single value json produces empty output!
|
| if(s != NULL) StringAdd(buffer, padding + inner + s);
|
| StringAdd(buffer, (ArraySize(objects) ? padding : "") + close[t]);
|
| }
|
|
|
| string flatten(const bool compact = true, const string prefix = "@")
|
| {
|
| string buffer;
|
| flatten(&this, buffer, compact, prefix);
|
| return buffer;
|
| }
|
|
|
| static void flatten(JsValue *element, string &buffer, const bool compact = true, const string prefix = "@")
|
| {
|
| if(element.t == JS_OBJECT)
|
| {
|
| if(!compact)
|
| buffer += prefix + " = {..." + "};\n"; // 0x2026
|
| }
|
| else if(element.t == JS_ARRAY)
|
| {
|
| if(!compact)
|
| buffer += prefix + " = [..." + "];\n";
|
| }
|
| else
|
| {
|
| buffer += prefix + "=" + element.s + ";\n";
|
| return;
|
| }
|
|
|
| for(int i = 0; i < element.size(); i++)
|
| {
|
| string key = (element.t == JS_OBJECT) ? element.props().getKey(i) : "";
|
| if(!StringLen(key)) key = "[" + (string)i + "]";
|
| else key = "[" + key + "]";
|
|
|
| flatten(element[i], buffer, compact, prefix + key);
|
| }
|
| }
|
|
|
| static JsValue *toUniformCase(JsValue *source, const bool upper = false)
|
| {
|
| if(source.t == JS_PRIMITIVE)
|
| {
|
| if(source.a == JS_STRING)
|
| {
|
| string s = source.s;
|
| if(upper) StringToUpper(s);
|
| else StringToLower(s);
|
| return new JsValue(s);
|
| }
|
| return new JsValue(source);
|
| }
|
| else
|
| {
|
| JsValue *result = new JsValue(source.t);
|
| if(source.t == JS_OBJECT)
|
| {
|
| string keys[];
|
| source.getProperties(keys);
|
| for(int i = 0; i < source.size(); i++)
|
| {
|
| result.put(keys[i], toUniformCase(source[i], upper));
|
| }
|
| }
|
| else
|
| {
|
| for(int i = 0; i < source.length(); i++)
|
| {
|
| result.put(toUniformCase(source[i], upper));
|
| }
|
| }
|
| return result;
|
| }
|
| }
|
|
|
| };
|
|
|
| /* Can't do this due to MQL5 limitation (pointers vs values mixture)
|
| class JsObject: public JsValue
|
| {
|
| public:
|
| JsObject() : JsValue(JS_OBJECT) { }
|
| };
|
|
|
| class JsArray: public JsValue
|
| {
|
| public:
|
| JsArray() : JsValue(JS_ARRAY) { }
|
| };
|
| */
|
|
|
| //+------------------------------------------------------------------+
|
| //| Helper classes for stack overflow control |
|
| //+------------------------------------------------------------------+
|
|
|
| class JsCallController
|
| {
|
| public:
|
| int counter;
|
| JsCallController(): counter(0) { };
|
| };
|
|
|
| class JsCallCounter
|
| {
|
| JsCallController *owner;
|
| public:
|
| JsCallCounter(JsCallController *controller): owner(controller)
|
| {
|
| owner.counter++;
|
| }
|
| ~JsCallCounter()
|
| {
|
| owner.counter--;
|
| }
|
| };
|
|
|
| //+------------------------------------------------------------------+
|
| //| Lightweight pseudo-string proxies on top of entire input text |
|
| //+------------------------------------------------------------------+
|
| struct JsToken
|
| {
|
| int offset; // points to data, control delimiter is at -1 (if not left trimmed)
|
| int length;
|
| ushort c;
|
| };
|
|
|
| //+------------------------------------------------------------------+
|
| //| JSON parser class |
|
| //+------------------------------------------------------------------+
|
| class JsParser
|
| {
|
| public:
|
| int cursor;
|
| JsToken token2[];
|
| int offsets[][2]; // [offset][line No]
|
| bool error;
|
| int error_cursor;
|
| JsCallController stack;
|
| string copy; // input document as is
|
|
|
| bool isWhitespace(const ushort c)
|
| {
|
| return c == ' ' || c == '\r' || c == '\n' || c == '\t';
|
| }
|
|
|
| void tokenTrimLeft(const int i)
|
| {
|
| while(isWhitespace(copy[token2[i].offset]) && token2[i].offset < (i < ArraySize(token2) - 1 ? token2[i + 1].offset : StringLen(copy)))
|
| {
|
| token2[i].offset++;
|
| token2[i].length--;
|
| }
|
| }
|
|
|
| void tokenTrimRight(const int i)
|
| {
|
| while(isWhitespace(copy[token2[i].offset + token2[i].length - 1]) && token2[i].length > 0)
|
| token2[i].length--;
|
| }
|
|
|
| string token(const int i) const // show token with leading delimiter
|
| {
|
| return ((token2[i].c != 1 && token2[i].c != JS_EOF) || token2[i].length ? ShortToString(token2[i].c) + StringSubstr(copy, token2[i].offset, token2[i].length) : "");
|
| }
|
|
|
| void tokenize()
|
| {
|
| //copy = context;
|
| ArrayResize(token2, 0);
|
| ArrayResize(offsets, 0);
|
|
|
| // lookup table for syntax chars
|
| static const bool tbl[256] = {0,0,0,0,0,0,0,0,0,0,/*1*/0,0,0,/*1*/0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
|
| // static const ushort ctrl[] = {'{', '}', '[', ']', ',', ':', '\r', '\n'};
|
|
|
| bool quotes = false;
|
|
|
| JsToken _t = {0, 0, (ushort)1}; // start with optional whitespace
|
| PUSH(token2, _t);
|
|
|
| int m = EXPAND(offsets);
|
| offsets[m][0] = 0;
|
|
|
| for(int i = 0; i < StringLen(copy); i++)
|
| {
|
| if(copy[i] == '"')
|
| {
|
| quotes = !quotes;
|
|
|
| m = EXPAND(offsets);
|
| offsets[m][0] = i;
|
| JsToken t = {i + 1, 0, '"'};
|
| PUSH(token2, t);
|
| token2[m - 1].length = i - token2[m - 1].offset;
|
| }
|
| else
|
| {
|
| if(quotes) // inside quotes
|
| {
|
| if(copy[i] == '\\' && copy[i + 1] == '"')
|
| {
|
| i++; // skip next escaped quote
|
| }
|
| }
|
| else
|
| {
|
| if(copy[i] < 256 && tbl[copy[i]])
|
| {
|
| m = EXPAND(offsets);
|
| offsets[m][0] = i;
|
| JsToken t = {i + 1, 0, copy[i]};
|
| PUSH(token2, t);
|
| token2[m - 1].length = i - token2[m - 1].offset;
|
| }
|
| }
|
| }
|
| }
|
|
|
| if(ArraySize(token2))
|
| {
|
| token2[ArraySize(token2) - 1].length = StringLen(copy) - token2[ArraySize(token2) - 1].offset;
|
| }
|
|
|
| for(int i = 0; i < ArraySize(token2); i++)
|
| {
|
| if(token2[i].c != '"') tokenTrimLeft(i);
|
| if(token2[i].c != '"') tokenTrimRight(i);
|
| }
|
|
|
| int lineno = 1;
|
| for(int i = 0; i < ArrayRange(offsets, 0); ++i)
|
| {
|
| int position = offsets[i][0];
|
| offsets[i][1] = lineno;
|
| // CRLF \r\n - Windows
|
| // LF \n - Linux/Unix
|
| // CR \r - Mac
|
| if(copy[position] == '\r') lineno++;
|
| else if(copy[position] == '\n' && position && copy[position - 1] != '\r') lineno++;
|
| }
|
| // end of file
|
| JsToken t = {StringLen(copy), 0, (ushort)(JS_EOF)};
|
| PUSH(token2, t);
|
| m = EXPAND(offsets);
|
| offsets[m][0] = StringLen(copy);
|
| offsets[m][1] = lineno;
|
| }
|
|
|
| int next() // pre-increment safeguard
|
| {
|
| if(cursor < ArraySize(token2) - 1) ++cursor;
|
| return cursor;
|
| }
|
|
|
| int post() // post-increment safeguard
|
| {
|
| const int prev = cursor;
|
| next();
|
| return prev;
|
| }
|
|
|
| string parse_key()
|
| {
|
| if(token2[cursor].c == '"')
|
| {
|
| const JsToken t = token2[post()];
|
| const string result = StringSubstr(copy, t.offset, t.length);
|
| if(token2[cursor].c != '"')
|
| {
|
| if(!error) {PrintFormat("Closing '\"' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| return NULL;
|
| }
|
| next();
|
| return result;
|
| }
|
| else
|
| {
|
| if(!error) {PrintFormat("Opening '\"' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| return NULL;
|
| }
|
| }
|
|
|
| bool stack_guard()
|
| {
|
| if(stack.counter > JS_STACK_LIMIT)
|
| {
|
| if(!error) { Print("Too many nested structures, the limit exceeded: ", JS_STACK_LIMIT); error_cursor = cursor; }
|
| error = true;
|
| return false;
|
| }
|
| return true;
|
| }
|
|
|
| JsValue *parse_value(const bool skip = false)
|
| {
|
| JsCallCounter cnt(&stack);
|
| if(!stack_guard()) return NULL;
|
|
|
| if(token2[cursor].c == '"')
|
| {
|
| const JsToken t = token2[post()];
|
| string v = StringSubstr(copy, t.offset, t.length);
|
| while((token2[cursor].c != '"'
|
| || (copy[ token2[cursor - 1].offset + token2[cursor - 1].length - 1 ] == '\\'))
|
| && token2[cursor].c != JS_EOF)
|
| {
|
| const JsToken w = token2[post()];
|
| v += StringSubstr(copy, w.offset, w.length);
|
| }
|
| StringReplace(v, "\\\"", "\"");
|
| // TODO: unescape more chars? \u0000
|
| // static const ushort specs[] = {'"', '\\', '\/', '\r', '\n', '\t', '\b', '\f'};
|
|
|
| if(token2[cursor].c != '"')
|
| {
|
| if(!error) { PrintFormat("Unclosed string: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| }
|
| else if(token2[cursor].length > 0)
|
| {
|
| tokenTrimRight(cursor);
|
| if(token2[cursor].length > 0)
|
| {
|
| if(!error) { PrintFormat("Garbage after closing quote: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| }
|
| }
|
| next();
|
| return new JsValue(v);
|
| }
|
| else if(token2[cursor].c == '{')
|
| {
|
| return parse_object();
|
| }
|
| else if(token2[cursor].c == '[' && !skip)
|
| {
|
| return parse_array();
|
| }
|
| else // primitive, should be ":123.456", ":true", ",123", "\"text\"", "[null"
|
| {
|
| JsValue *result = token2[cursor].length ? new JsValue(StringSubstr(copy, token2[cursor].offset, token2[cursor].length)) : new JsValue(NULL);
|
| if(result.a == JS_NONE)
|
| {
|
| if(!error) { PrintFormat("Value expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| }
|
| else if(result.a == JS_STRING && token2[cursor].c != '"')
|
| {
|
| if(!error) { PrintFormat("Unexpected string, no quotes: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| }
|
| next();
|
| return result;
|
| }
|
| }
|
|
|
| JsValue *parse_array()
|
| {
|
| JsCallCounter cnt(&stack);
|
| if(!stack_guard()) return NULL;
|
|
|
| JsValue *current = NULL;
|
| if(token2[cursor].c == '[')
|
| {
|
| current = new JsValue(JS_ARRAY);
|
| const int i = cursor; // prevent reentrancy from parse_value
|
| do
|
| {
|
| if((token2[cursor].c == '[' || token2[cursor].c == ',') && !token2[cursor].length) // skip leading '[' or iterative ','
|
| {
|
| next();
|
| if(token2[cursor].c == ',')
|
| {
|
| if(!error) { PrintFormat("Extra comma: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| break;
|
| }
|
| }
|
| if(token2[cursor].c == ']')
|
| {
|
| if(token2[cursor].length == 0) break; // valid array close-up, including empty one
|
| if(!error) { PrintFormat("Delimiter after array expected, got some data: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| break;
|
|
|
| }
|
| current.put(parse_value(i == cursor)); // parse_value increments cursor internally
|
|
|
| if(!(token2[cursor].c == ']' && !token2[cursor].length) && token2[cursor].c != ',')
|
| {
|
| if(!error) { PrintFormat("'],' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| return current;
|
| }
|
| }
|
| while(token2[cursor].c != ']' && token2[cursor].c != JS_EOF);
|
| next();
|
| }
|
| else
|
| {
|
| if(!error) { PrintFormat("'[' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| return NULL;
|
| }
|
| return current;
|
| }
|
|
|
| JsValue *parse_object()
|
| {
|
| JsCallCounter cnt(&stack);
|
| if(!stack_guard()) return NULL;
|
|
|
| JsValue *current = NULL;
|
| if(token2[cursor].c == '{')
|
| {
|
| current = new JsValue(JS_OBJECT);
|
| const int i = cursor;
|
| do
|
| {
|
| if((token2[cursor].c == '{' || token2[cursor].c == ',') && !token2[cursor].length) // equivalent mod of toyjson2 ?
|
| {
|
| next();
|
| }
|
| if(token2[cursor].c == '}' && !token2[cursor].length && i == cursor - 1)
|
| {
|
| break; // empty object
|
| }
|
| string key = parse_key(); // parse_key increments cursor internally
|
| if(token2[cursor].c != ':')
|
| {
|
| if(!error) { PrintFormat("':' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| return current;
|
| }
|
| if(token2[cursor].length == 0) next(); // can be ":<whitespace>" (if a string or object is next) or ":<whitespace>value"
|
| current.put(key, parse_value()); // parse_value increments cursor internally
|
|
|
| if(!(token2[cursor].c == '}' && !token2[cursor].length) && token2[cursor].c != ',')
|
| {
|
| if(!error) { PrintFormat("'},' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| return current;
|
| }
|
| }
|
| while(token2[cursor].c != '}' && token2[cursor].c != JS_EOF);
|
| next();
|
| }
|
| else
|
| {
|
| if(!error) { PrintFormat("'{' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
|
| error = true;
|
| return NULL;
|
| }
|
| return current;
|
| }
|
|
|
| public:
|
| JsParser(): cursor(0), error_cursor(0) { }
|
| JsValue *parse(const string &text)
|
| {
|
| tokenize();
|
| // printTokens();
|
|
|
| cursor = 0;
|
| if(token2[cursor].c == (ushort)(1) && token2[cursor].length == 0) next(); // skip single start token (0x1) and whitespace, if present
|
| error = false;
|
|
|
| JsValue *result = parse_value();
|
| if(!error)
|
| {
|
| if(cursor != ArraySize(token2) - 1)
|
| {
|
| Print("Trailing garbage detected at the end: @", cursor);
|
| error_cursor = cursor;
|
| error = true;
|
| }
|
| }
|
| return result;
|
| }
|
|
|
| void printTokens(const bool context = false) const
|
| {
|
| if(context)
|
| {
|
| PrintFormat("Line No: %d, Offset: 0x%X/%d bytes", offsets[error_cursor][1], offsets[error_cursor][0], offsets[error_cursor][0]);
|
| string line = "";
|
| for(int i = error_cursor - 10; i <= error_cursor + 10; ++i)
|
| {
|
| if(i >= 0 && i < ArraySize(token2))
|
| {
|
| line += "[" + (string)i + "]:`" + token(i) + "` ";
|
| }
|
| }
|
| Print(line);
|
| }
|
| else
|
| {
|
| ArrayPrint(token2);
|
| }
|
| }
|
|
|
| static JsValue *jsonify(const string text)
|
| {
|
| JsParser parser;
|
| JsValue *result = parser.parse(text);
|
| if(parser.error)
|
| {
|
| parser.printTokens(true);
|
| JsValue *error = new JsValue(JS_ERROR);
|
| error.put("Bad JSON", result);
|
| return error;
|
| }
|
| return result;
|
| }
|
|
|
| bool isValid() const
|
| {
|
| return !error;
|
| }
|
| };
|
|
|
| const static JsValue JsValue::null(JS_NULL);
|
| //+------------------------------------------------------------------+
|