449 行
13 KiB
MQL5
449 行
13 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| toyjson.mqh |
| |||
//| Copyright 2022, MetaQuotes Ltd. |
| |||
//| https://www.mql5.com |
| |||
//+------------------------------------------------------------------+
| |||
| |||
#include "MapArray.mqh"
| |||
#include "AutoPtr.mqh"
| |||
#include "Defines.mqh"
| |||
| |||
enum JS_TYPE
| |||
{
| |||
JS_OBJECT,
| |||
JS_ARRAY,
| |||
JS_PRIMITIVE,
| |||
JS_NULL // singleton object stub for undefined props
| |||
};
| |||
| |||
enum JS_AFFINITY // for primitive type
| |||
{
| |||
JS_NONE = 0,
| |||
JS_CONST = 1, // true, false, null
| |||
JS_INT = 2,
| |||
JS_FLOAT = 4,
| |||
JS_STRING = 8
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Main class to store single value (primitive, object, or array) |
| |||
//+------------------------------------------------------------------+
| |||
class JsValue
| |||
{
| |||
MapArray<string, int> properties;
| |||
AutoPtr<JsValue> objects[];
| |||
| |||
const static JsValue null;
| |||
static bool selfcall;
| |||
| |||
public:
| |||
const string s; // value is assigned once at construction
| |||
const JS_TYPE t; // type and
| |||
const JS_AFFINITY a; // affinity are 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) { }
| |||
| |||
int size() const
| |||
{
| |||
return ArraySize(objects);
| |||
}
| |||
| |||
static JS_AFFINITY isNumeric(const ushort c)
| |||
{
| |||
if((c >= '0' && c <= '9') || c == '+' || c == '-') return JS_INT;
| |||
else if(c == '.' || c == 'e' || c == 'E') return JS_FLOAT;
| |||
else if(c == ' ' || c == '\r' || c == '\n' || c == '\t') return JS_NONE;
| |||
return JS_STRING;
| |||
}
| |||
| |||
JS_AFFINITY detect() const
| |||
{
| |||
if(s == "true" || s == "false" || s == "null") return JS_CONST;
| |||
JS_AFFINITY result = 0;
| |||
for(int i = 0; i < StringLen(s) && result < JS_STRING; ++i)
| |||
{
| |||
result |= isNumeric(s[i]);
| |||
}
| |||
return result;
| |||
}
| |||
| |||
string stringify() const
| |||
{
| |||
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) const
| |||
{
| |||
if(typechecks)
| |||
{
| |||
if(t != JS_PRIMITIVE) return (T)NULL;
| |||
if(a >= JS_STRING && typename(T) != "string") return (T)NULL;
| |||
if(a == JS_CONST && typename(T) != "bool") return (T)NULL;
| |||
}
| |||
return (T)s;
| |||
}
| |||
| |||
template<typename T>
| |||
bool operator==(const T value)
| |||
{
| |||
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;
| |||
}
| |||
| |||
// object putters
| |||
| |||
template<typename T>
| |||
void put(const string key, const T value)
| |||
{
| |||
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, const JsValue *value)
| |||
{
| |||
if(t != JS_OBJECT && !selfcall)
| |||
{
| |||
PrintFormat("WARNING: Setting property '%s' for non-object", key);
| |||
}
| |||
int p = properties.find(key);
| |||
if(p == -1)
| |||
{
| |||
p = EXPAND(objects);
| |||
properties.put(key, p);
| |||
}
| |||
objects[p] = value;
| |||
}
| |||
| |||
// array putters
| |||
| |||
template<typename T>
| |||
void put(const T value)
| |||
{
| |||
put("[" + (string)ArraySize(objects) + "]", new JsValue((string)value));
| |||
}
| |||
| |||
void put(const double value, const int digits = 8)
| |||
{
| |||
put("[" + (string)ArraySize(objects) + "]", new JsValue(DoubleToString(value, digits)));
| |||
}
| |||
| |||
void put(const JsValue *value)
| |||
{
| |||
if(t != JS_ARRAY)
| |||
{
| |||
Print("WARNING: Setting indexed element for non-array: ", value.stringify());
| |||
}
| |||
selfcall = true;
| |||
put("[" + (string)ArraySize(objects) + "]", value);
| |||
selfcall = false;
| |||
}
| |||
| |||
// indexed access
| |||
| |||
JsValue *operator[](const string name)
| |||
{
| |||
const int p = properties.find(name);
| |||
if(p == -1) return (JsValue *)&null;
| |||
return objects[p][];
| |||
}
| |||
| |||
JsValue *operator[](const int i)
| |||
{
| |||
if(i < 0 || i >= ArraySize(objects)) return (JsValue *)&null;
| |||
return objects[i][];
| |||
}
| |||
| |||
// 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>"};
| |||
const static string close[] = {"}", "]", "", ""};
| |||
Print(StringFormat("%*s", level * 2, ""), open[t], s);
| |||
++level;
| |||
if(level < maxlevel)
| |||
{
| |||
const string padding = StringFormat("%*s", level * 2, "");
| |||
for(int i = 0; i < properties.getSize(); ++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);
| |||
}
| |||
}
| |||
}
| |||
--level;
| |||
if(t < JS_PRIMITIVE) Print(StringFormat("%*s", level * 2, ""), close[t]);
| |||
}
| |||
| |||
void stringify(string &buffer) const
| |||
{
| |||
const static string open[] = {"{", "[", "", "null"};
| |||
const static string close[] = {"}", "]", "", ""};
| |||
StringAdd(buffer, open[t]);
| |||
for(int i = 0; i < ArraySize(objects); ++i)
| |||
{
| |||
if(i > 0) StringAdd(buffer, ", ");
| |||
if(t != JS_ARRAY) StringAdd(buffer, "\"" + properties.getKey(i) + "\" : ");
| |||
if(objects[i][].s != NULL)
| |||
{
| |||
StringAdd(buffer, objects[i][].stringify());
| |||
}
| |||
else
| |||
{
| |||
objects[i][].stringify(buffer);
| |||
}
| |||
}
| |||
StringAdd(buffer, close[t]);
| |||
}
| |||
};
| |||
| |||
/* 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) { }
| |||
};
| |||
*/
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| JSON parser class |
| |||
//+------------------------------------------------------------------+
| |||
class JsParser
| |||
{
| |||
int cursor;
| |||
string tokens[];
| |||
bool error;
| |||
| |||
void tokenize(const string &context)
| |||
{
| |||
string copy = context;
| |||
StringReplace(copy, "{", ShortToString(1));
| |||
StringReplace(copy, "}", ShortToString(1));
| |||
StringReplace(copy, "[", ShortToString(1));
| |||
StringReplace(copy, "]", ShortToString(1));
| |||
StringReplace(copy, ShortToString('"'), ShortToString(1));
| |||
StringReplace(copy, ",", ShortToString(1));
| |||
StringReplace(copy, ":", ShortToString(1));
| |||
StringSplit(copy, 1, tokens);
| |||
int position = 0;
| |||
for(int i = 0; i < ArraySize(tokens); ++i)
| |||
{
| |||
int step = i > 0 ? StringLen(tokens[i]) + 1 : StringLen(tokens[i]);
| |||
tokens[i] = (i > 0 ? ShortToString(context[position]) : ShortToString(1)) + tokens[i];
| |||
position += step;
| |||
}
| |||
}
| |||
| |||
string parse_key()
| |||
{
| |||
if(tokens[cursor][0] == '"')
| |||
{
| |||
const string result = StringSubstr(tokens[cursor], 1);
| |||
if(tokens[++cursor][0] != '"')
| |||
{
| |||
PrintFormat("Closing '\"' expected, got: '%s' @ %d", tokens[cursor], cursor);
| |||
error = true;
| |||
return NULL;
| |||
}
| |||
++cursor;
| |||
return result;
| |||
}
| |||
else
| |||
{
| |||
PrintFormat("Opening '\"' expected, got: '%s' @ %d", tokens[cursor], cursor);
| |||
error = true;
| |||
return NULL;
| |||
}
| |||
}
| |||
| |||
JsValue *parse_value()
| |||
{
| |||
if(tokens[cursor][0] == '"')
| |||
{
| |||
string v = StringSubstr(tokens[cursor], 1);
| |||
while(tokens[++cursor][0] != '"'
| |||
|| (tokens[cursor - 1][StringLen(tokens[cursor - 1]) - 1] == '\\'))
| |||
{
| |||
v += tokens[cursor];
| |||
}
| |||
if(v != NULL)
| |||
{
| |||
StringReplace(v, "\\\"", "\"");
| |||
++cursor;
| |||
return new JsValue(v);
| |||
}
| |||
else
| |||
{
| |||
PrintFormat("Value expected, got: '%s' @ %d", tokens[cursor], cursor);
| |||
error = true;
| |||
}
| |||
return NULL;
| |||
}
| |||
else if(tokens[cursor][0] == '{')
| |||
{
| |||
return parse_object();
| |||
}
| |||
else if(tokens[cursor][0] == '[')
| |||
{
| |||
return parse_array();
| |||
}
| |||
else // primitive, should be ":123.456", ":true"
| |||
{
| |||
return new JsValue(StringSubstr(tokens[cursor++], 1));
| |||
}
| |||
}
| |||
| |||
JsValue *parse_array()
| |||
{
| |||
JsValue *current = NULL;
| |||
if(tokens[cursor][0] == '[')
| |||
{
| |||
current = new JsValue(JS_ARRAY);
| |||
StringSetCharacter(tokens[cursor], 0, ','); // prevent reentrancy from parse_value
| |||
do
| |||
{
| |||
string t = tokens[cursor];
| |||
StringTrimRight(t);
| |||
if(StringLen(t) == 1) ++cursor;
| |||
if(tokens[cursor][0] == ']') break; // empty array
| |||
current.put(parse_value()); // parse_value increments cursor internally
| |||
| |||
if(tokens[cursor][0] != ']' && tokens[cursor][0] != ',')
| |||
{
| |||
PrintFormat("'],' expected, got: '%s' @ %d", tokens[cursor], cursor);
| |||
error = true;
| |||
return current;
| |||
}
| |||
}
| |||
while(tokens[cursor][0] != ']');
| |||
if(cursor < ArraySize(tokens)) ++cursor;
| |||
}
| |||
else
| |||
{
| |||
PrintFormat("'[' expected, got: '%s' @ %d", tokens[cursor], cursor);
| |||
error = true;
| |||
return NULL;
| |||
}
| |||
return current;
| |||
}
| |||
| |||
JsValue *parse_object()
| |||
{
| |||
JsValue *current = NULL;
| |||
if(tokens[cursor][0] == '{')
| |||
{
| |||
current = new JsValue(JS_OBJECT);
| |||
do
| |||
{
| |||
++cursor;
| |||
string key = parse_key(); // parse_key increments cursor internally
| |||
if(tokens[cursor][0] == '}') break; // empty object
| |||
if(tokens[cursor][0] != ':')
| |||
{
| |||
PrintFormat("':' expected, got: '%s' @ %d", tokens[cursor], cursor);
| |||
error = true;
| |||
return NULL;
| |||
}
| |||
string t = tokens[cursor]; // can be ":<whitespace>" (if a string or object is next) or ":<whitespace>value"
| |||
StringTrimRight(t);
| |||
if(StringLen(t) == 1) ++cursor;
| |||
current.put(key, parse_value()); // parse_value increments cursor internally
| |||
| |||
if(tokens[cursor][0] != '}' && tokens[cursor][0] != ',')
| |||
{
| |||
PrintFormat("'},' expected, got: '%s' @ %d", tokens[cursor], cursor);
| |||
error = true;
| |||
return current;
| |||
}
| |||
}
| |||
while(tokens[cursor][0] != '}');
| |||
if(cursor < ArraySize(tokens)) ++cursor;
| |||
}
| |||
else
| |||
{
| |||
PrintFormat("'{' expected, got: '%s' @ %d", tokens[cursor], cursor);
| |||
error = true;
| |||
return NULL;
| |||
}
| |||
return current;
| |||
}
| |||
| |||
public:
| |||
JsValue *parse(const string &text)
| |||
{
| |||
if(StringLen(text) < 2) return NULL;
| |||
| |||
cursor = 1; // skip start token (0x1)
| |||
error = false;
| |||
tokenize(text);
| |||
// printTokens();
| |||
return tokens[cursor][0] == '[' ? parse_array() : parse_object();
| |||
}
| |||
| |||
void printTokens(const bool context = false) const
| |||
{
| |||
if(context)
| |||
{
| |||
string line = "";
| |||
for(int i = cursor - 10; i <= cursor + 10; ++i)
| |||
{
| |||
if(i >= 0 && i < ArraySize(tokens))
| |||
{
| |||
line += "[" + (string)i + "]:`" + tokens[i] + "' ";
| |||
}
| |||
}
| |||
Print(line);
| |||
}
| |||
else
| |||
{
| |||
ArrayPrint(tokens);
| |||
}
| |||
}
| |||
| |||
static JsValue *jsonify(const string text)
| |||
{
| |||
JsParser parser;
| |||
JsValue *result = parser.parse(text);
| |||
if(parser.error) parser.printTokens(true);
| |||
return result;
| |||
}
| |||
};
| |||
| |||
static bool JsValue::selfcall = false;
| |||
const static JsValue JsValue::null(JS_NULL);
| |||
//+------------------------------------------------------------------+
|