1263 lines
37 KiB
MQL5
1263 lines
37 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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;
|
|
|
|
// 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);
|
|
//+------------------------------------------------------------------+
|