Last active
June 7, 2024 01:38
-
-
Save sndwch/b9092e08c910a6ea3f98 to your computer and use it in GitHub Desktop.
For MetaTrader 4. Creates JSON objects from MQL types
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#property copyright "erdeszt" | |
#property version "1.00" | |
#property library | |
#include <Object.mqh> | |
#include <Arrays/ArrayObj.mqh> | |
#include <Arrays/ArrayString.mqh> | |
#include <mqlcommon/Erdeszt/Map.mqh> | |
#include <mqlcommon/Erdeszt/SmartPtr.mqh> | |
#include <mqlcommon/Erdeszt/Utils.mqh> | |
const ushort CHAR_TAB = 9; | |
const ushort CHAR_LF = 10; | |
const ushort CHAR_CR = 13; | |
const ushort CHAR_SPACE = 32; | |
const ushort CHAR_QUOTE = 34; | |
const ushort CHAR_COMMA = 44; | |
const ushort CHAR_DOT = 46; | |
const ushort CHAR_COLON = 58; | |
const ushort CHAR_BACKSLASH = 92; | |
const ushort CHAR_LBRACKET = 91; | |
const ushort CHAR_RBRACKET = 93; | |
const ushort CHAR_LBRACE = 123; | |
const ushort CHAR_RBRACE = 125; | |
enum JsonType | |
{ JsonNull | |
, JsonBool | |
, JsonNumber | |
, JsonString | |
, JsonArray | |
, JsonObject | |
}; | |
enum BoolTrue { True }; | |
enum BoolFalse { False }; | |
string jsonTypeToString(const JsonType type) { | |
switch (type) { | |
case JsonNull: return "Null"; | |
case JsonBool: return "Bool"; | |
case JsonNumber: return "Number"; | |
case JsonString: return "String"; | |
case JsonArray: return "Array"; | |
case JsonObject: return "Object"; | |
} | |
exit("Invalid json type: " + (string)type); | |
return ""; | |
} | |
class JsonValue : CObject { | |
private: | |
const JsonType mType; | |
const bool mBoolValue; | |
const double mNumberValue; | |
const string mStringValue; | |
const CArrayObj* mArrayValue; | |
Map* mObjectValue; | |
public: | |
// Constructors/Destructor | |
JsonValue() : mType(JsonNull) {} | |
JsonValue(const BoolTrue _) : mType(JsonBool) , mBoolValue(true) {} | |
JsonValue(const BoolFalse _) : mType(JsonBool) , mBoolValue(false) {} | |
JsonValue(const double number) : mType(JsonNumber), mNumberValue(number) {} | |
JsonValue(const string str) : mType(JsonString), mStringValue(str) {} | |
JsonValue(const CArrayObj* array) : mType(JsonArray) , mArrayValue(array) {} | |
JsonValue(Map* map) : mType(JsonObject), mObjectValue(map) {} | |
~JsonValue() { delete mArrayValue; delete mObjectValue; } | |
// Accessors | |
const JsonType type() const { return mType; } | |
const bool boolValue() const { | |
assert(mType == JsonBool, "JsonValue::boolValue() called on non bool value!"); | |
return mBoolValue; | |
} | |
const string stringValue() const { | |
assert(mType == JsonString, "JsonValue::stringValue() called on non string value!"); | |
return mStringValue; | |
} | |
const double numberValue() const { | |
assert(mType == JsonNumber, "JsonValue::numberValue() called on non number value!"); | |
return mNumberValue; | |
} | |
const CArrayObj* arrayValue() const { | |
assert(mType == JsonArray, "JsonValue::arrayValue() called on non array value!"); | |
return mArrayValue; | |
} | |
const Map* objectValue() const { | |
assert(mType == JsonObject, "JsonValue::objectValue() called on non object value!"); | |
return mObjectValue; | |
} | |
// Shortcuts | |
const bool isTrue() const { return boolValue(); } | |
const bool isFalse() const { return !boolValue(); } | |
const string str() const { return stringValue(); } | |
const JsonValue* at(const int idx) const { | |
assert(mType == JsonArray, "JsonValue::at() called on non array value!"); | |
assert(idx < mArrayValue.Total() - 1 && idx >= 0, "JsonValue::at() called with index larger than the length!"); | |
return (JsonValue*)mArrayValue.At(idx); | |
} | |
JsonValue* get(const string key) const { | |
assert(mType == JsonObject, "JsonValue::get() called on non object value!"); | |
return (JsonValue*)mObjectValue.lookup(key); | |
} | |
CArrayString* keys() const { | |
assert(mType == JsonObject, "JsonValue::keys() called on non object value!"); | |
return mObjectValue.keys(); | |
} | |
}; | |
class JsonParseResult : CObject { | |
private: | |
JsonValue *mValue; | |
const string mRest; | |
const bool mSuccessful; | |
const string mError; | |
public: | |
JsonParseResult(JsonValue *value, const string rest) | |
: mValue(value), mRest(rest), mSuccessful(true) {} | |
JsonParseResult(const string error, const string rest) | |
: mError(error), mRest(rest), mSuccessful(false) {} | |
bool isSuccessful() const { return mSuccessful; } | |
JsonValue* value() { return mValue; } | |
string rest() const { return mRest; } | |
string error() const { return mError; } | |
JsonParseResult* clone() { | |
return mSuccessful | |
? new JsonParseResult(mValue, mRest) | |
: new JsonParseResult(mError, mRest); | |
} | |
}; | |
class JsonParser : CObject { | |
private: | |
bool _isPrefix(const string prefix, const string str) const { | |
return prefix == StringSubstr(str, 0, StringLen(prefix)); | |
} | |
bool _isNumAt(const string str, const int idx) const { | |
ushort chr = StringGetCharacter(str, idx); | |
return chr >= 48 && chr <= 57; | |
} | |
bool _isAlphaAt(const string str, const int idx) const { | |
ushort chr = StringGetCharacter(str, idx); | |
return (chr >= 65 && chr <= 90) || (chr >= 97 && chr <= 122); | |
} | |
bool _isSpaceAt(const string str, const int idx) const { | |
const ushort charCode = StringGetCharacter(str, idx); | |
return charCode == CHAR_TAB | |
|| charCode == CHAR_LF | |
|| charCode == CHAR_CR | |
|| charCode == CHAR_SPACE; | |
} | |
string _trimLeft(const string str) const { | |
const int strLen = StringLen(str); | |
int idx = 0; | |
if (!_isSpaceAt(str, idx)) { | |
return str; | |
} | |
while (idx < strLen && _isSpaceAt(str, idx++)) {} | |
return StringSubstr(str, idx - 1); | |
} | |
JsonParseResult* _tryParseNull(const string str) const { | |
if (_isPrefix("null", str)) { | |
return new JsonParseResult(new JsonValue(), StringSubstr(str, 4)); | |
} | |
return new JsonParseResult("Expceted `null`", str); | |
} | |
JsonParseResult* _tryParseBool(const string str) const { | |
if (_isPrefix("true", str)) { | |
return new JsonParseResult(new JsonValue(True), StringSubstr(str, 4)); | |
} | |
if (_isPrefix("false", str)) { | |
return new JsonParseResult(new JsonValue(False), StringSubstr(str, 5)); | |
} | |
return new JsonParseResult("Expceted `true` or `false`", str); | |
} | |
JsonParseResult* _tryParseNumber(const string str) const { | |
int idx = -1; | |
int strLen = StringLen(str); | |
while (idx < strLen && _isNumAt(str, ++idx)) {} | |
if (idx == 0) { | |
return new JsonParseResult("Numeric value expected", str); | |
} | |
if (StringGetCharacter(str, idx) == CHAR_DOT) { | |
idx++; | |
while (idx < strLen && _isNumAt(str, ++idx)) {} | |
return new JsonParseResult(new JsonValue(StringToDouble(StringSubstr(str, 0, idx))), StringSubstr(str, idx)); | |
} | |
return new JsonParseResult(new JsonValue(StringToDouble(StringSubstr(str, 0, idx))), StringSubstr(str, idx)); | |
} | |
JsonParseResult* _tryParseString(const string str) const { | |
if (StringGetCharacter(str, 0) != CHAR_QUOTE) { | |
return new JsonParseResult("Quoted string expected", str); | |
} | |
const int strLen = StringLen(str); | |
int idx = 0; | |
bool escaped = false; | |
while (++idx < strLen) { | |
if (StringGetCharacter(str, idx) == CHAR_BACKSLASH && !escaped) { | |
escaped = true; | |
continue; | |
} | |
if (StringGetCharacter(str, idx) == CHAR_QUOTE && !escaped) { | |
return new JsonParseResult(new JsonValue(StringSubstr(str, 1, idx - 1)), StringSubstr(str, idx + 1)); | |
} | |
escaped = false; | |
} | |
return new JsonParseResult("Unexpected", str); | |
} | |
JsonParseResult* _tryParseArray(const string str) const { | |
// Open bracket | |
if (StringGetCharacter(str, 0) != CHAR_LBRACKET) { | |
return new JsonParseResult("Left bracket expected!", str); | |
} | |
CArrayObj* items = new CArrayObj(); | |
string rest = _trimLeft(StringSubstr(str, 1)); | |
bool empty = true; | |
while (true) { | |
SmartPtr pItemResult = parse(rest); | |
JsonParseResult* itemResult = pItemResult.value(); | |
rest = _trimLeft(itemResult.rest()); | |
// Try to parse an item | |
if (!itemResult.isSuccessful()) { | |
// If we already have items then we should have one here | |
if (!empty) { | |
delete items; | |
return new JsonParseResult("Json value expected(Array)!", rest); | |
} | |
// Or a closing bracket | |
if (StringGetCharacter(rest, 0) != CHAR_RBRACKET) { | |
delete items; | |
return new JsonParseResult("Right bracket expected!", rest); | |
} | |
return new JsonParseResult(new JsonValue(items), _trimLeft(StringSubstr(rest, 1))); | |
} | |
if (empty) { empty = false; } | |
items.Add(itemResult.value()); | |
// Parse a comma | |
if (StringGetCharacter(rest, 0) != CHAR_COMMA) { | |
// Or a closing bracket | |
if (StringGetCharacter(rest, 0) != CHAR_RBRACKET) { | |
delete items; | |
return new JsonParseResult("Right bracket or comma expected!", rest); | |
} | |
return new JsonParseResult(new JsonValue(items), _trimLeft(StringSubstr(rest, 1))); | |
} | |
rest = _trimLeft(StringSubstr(rest, 1)); | |
} | |
return new JsonParseResult("This line in JsonParser::_tryParseArray should never be reached!", str); | |
} | |
JsonParseResult* _tryParseObject(const string str) const { | |
// Open curly brace | |
if (StringGetCharacter(str, 0) != CHAR_LBRACE) { | |
return new JsonParseResult("Left curly brace expected!", str); | |
} | |
Map* objs = new Map(); | |
string rest = _trimLeft(StringSubstr(str, 1)); | |
bool empty = true; | |
while (true) { | |
SmartPtr pKeyResult(_tryParseString(rest)); | |
JsonParseResult* keyResult = pKeyResult.value(); | |
rest = _trimLeft(keyResult.rest()); | |
// Trty to parse a key | |
if (!keyResult.isSuccessful()) { | |
// If the object is not empty we need a value here | |
if (!empty) { | |
delete objs; | |
return new JsonParseResult("Json value expected(Object:1)", rest); | |
} | |
// Or a closing brace | |
if (StringGetCharacter(rest, 0) != CHAR_RBRACE) { | |
delete objs; | |
return new JsonParseResult("Right curly brace expected!", rest); | |
} | |
return new JsonParseResult(new JsonValue(objs), _trimLeft(StringSubstr(rest, 1))); | |
} | |
const string key = keyResult.value().str(); | |
delete keyResult.value(); | |
rest = _trimLeft(rest); | |
// Parse a colon | |
if (StringGetCharacter(rest, 0) != CHAR_COLON) { | |
delete objs; | |
return new JsonParseResult("Colon expected!", rest); | |
} | |
rest = _trimLeft(StringSubstr(rest, 1)); | |
// Parse the value | |
SmartPtr pValueResult(parse(rest)); | |
JsonParseResult* valueResult = pValueResult.value(); | |
rest = _trimLeft(valueResult.rest()); | |
if (!valueResult.isSuccessful()) { | |
delete objs; | |
return new JsonParseResult("Json value expected(Object:2)!", rest); | |
} | |
if (empty) { empty = false; } | |
objs.insert(key, valueResult.value()); | |
// Parse a comma | |
if (StringGetCharacter(rest, 0) != CHAR_COMMA) { | |
// Or a brace | |
if (StringGetCharacter(rest, 0) != CHAR_RBRACE) { | |
delete objs; | |
return new JsonParseResult("Right brace expected!", rest); | |
} | |
return new JsonParseResult(new JsonValue(objs), _trimLeft(StringSubstr(rest, 1))); | |
} | |
rest = _trimLeft(StringSubstr(rest, 1)); | |
} | |
return new JsonParseResult("Object parsing is not implemented yet", str); | |
} | |
public: | |
JsonParser() {} | |
JsonParseResult* parse(const string jsonString) const { | |
SmartPtr pParseResults(new CArrayObj()); | |
CArrayObj* parseResults = pParseResults.value(); | |
JsonParseResult* bestResult = NULL; | |
parseResults.Add(_tryParseNull(jsonString)); | |
parseResults.Add(_tryParseBool(jsonString)); | |
parseResults.Add(_tryParseNumber(jsonString)); | |
parseResults.Add(_tryParseString(jsonString)); | |
parseResults.Add(_tryParseArray(jsonString)); | |
parseResults.Add(_tryParseObject(jsonString)); | |
for (int i = 0, l = parseResults.Total(); i < l; i++) { | |
JsonParseResult* result = parseResults.At(i); | |
if (result.isSuccessful()) { | |
return result.clone(); | |
} | |
if (null(bestResult)) { | |
bestResult = result; | |
} | |
if (StringLen(bestResult.rest()) > StringLen(result.rest())) { | |
bestResult = result; | |
} | |
} | |
return bestResult.clone(); | |
} | |
}; | |
class JsonPrinter : CObject { | |
public: | |
JsonPrinter() {} | |
string stringify(JsonValue* json) const { | |
switch (json.type()) { | |
case JsonNull: return "null"; | |
case JsonBool: return json.boolValue() ? "true" : "false"; | |
case JsonString: return "\"" + json.stringValue() + "\""; | |
case JsonNumber: return (string)json.numberValue(); | |
case JsonArray: { | |
SmartPtr pItems(new CArrayString()); | |
CArrayString* items = pItems.value(); | |
const CArrayObj* arrayItems = json.arrayValue(); | |
for (int i = 0, l = arrayItems.Total(); i < l; i++) { | |
items.Add(stringify((JsonValue*)arrayItems.At(i))); | |
} | |
return "[" + join(items) + "]"; | |
} | |
case JsonObject: { | |
SmartPtr pKeys(json.keys()); | |
CArrayString* keys = pKeys.value(); | |
SmartPtr pItems(new CArrayString()); | |
CArrayString* items = pItems.value(); | |
for (int i = 0, l = keys.Total(); i < l; i++) { | |
items.Add("\"" + keys.At(i) + "\": " + stringify(json.get(keys.At(i)))); | |
} | |
return "{ " + join(items) + " }"; | |
} | |
} | |
exit("Invalid json type: " + (string)json.type()); | |
return ""; | |
} | |
}; | |
class Json : CObject { | |
private: | |
const JsonParser mParser; | |
const JsonPrinter mPrinter; | |
public: | |
Json() {} | |
JsonParseResult* parse(const string jsonString) const { return mParser.parse(jsonString); } | |
string stringify(JsonValue* json) const { return mPrinter.stringify(json); } | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#property copyright "erdeszt" | |
#property version "1.00" | |
#property library | |
#include <Object.mqh> | |
#include <Arrays/ArrayString.mqh> | |
#include <mqlcommon/Erdeszt/SmartPtr.mqh> | |
#include <mqlcommon/Erdeszt/Utils.mqh> | |
class Node { | |
private: | |
Node* mLeft; | |
Node* mRight; | |
string mKey; | |
CObject* mValue; | |
public: | |
Node(const string key, CObject* value, Node* left, Node* right) | |
: mKey(key) | |
, mValue(value) | |
, mLeft(left) | |
, mRight(right) | |
{} | |
Node(const string key, CObject* &value) | |
: mKey(key) | |
, mValue(value) | |
{} | |
string key() const { return mKey; } | |
void setKey(const string key) { mKey = key; } | |
CObject* value() const { return mValue; } | |
void setValue(CObject* value) { mValue = value; } | |
Node* left() const { return mLeft; } | |
void setLeft(Node* left) { mLeft = left; } | |
Node* right() const { return mRight; } | |
void setRight(Node* right) { mRight = right; } | |
}; | |
enum NodePosition { | |
NP_EMPTY_TREE = -1, | |
NP_IN_PLACE = 0, | |
NP_LEFT = 1, | |
NP_RIGHT = 2 | |
}; | |
class NodeWithPosition : CObject { | |
private: | |
Node* mNode; | |
NodePosition mPosition; | |
public: | |
NodeWithPosition(Node* node, NodePosition position) | |
: mNode(node) | |
, mPosition(position) | |
{} | |
Node* node() const { return mNode; } | |
NodePosition position() const { return mPosition; } | |
}; | |
class Map { | |
private: | |
Node* mRoot; | |
const bool mFreeNodeValueOnRemove; | |
uint mSize; | |
NodeWithPosition* _findNodeOrPlace(const string key) const { | |
if (null(mRoot)) { | |
return new NodeWithPosition(NULL, NP_EMPTY_TREE); | |
} | |
Node* node = mRoot; | |
// This loop should never terminate normally, returns should break out of the function at appropriate times. | |
// When we found the key, or we have to go left/right but there's no left/right node. | |
// These invariants should prevent node to become null. | |
while (notNull(node)) { | |
if (StringCompare(node.key(), key) == EQ) { | |
return new NodeWithPosition(node, NP_IN_PLACE); | |
} | |
if (StringCompare(node.key(), key) == LT) { | |
if (null(node.right())) { | |
return new NodeWithPosition(node, NP_RIGHT); | |
} | |
node = node.right(); | |
} | |
else { | |
if (null(node.left())) { | |
return new NodeWithPosition(node, NP_LEFT); | |
} | |
node = node.left(); | |
} | |
} | |
Print("ERROR|The impossible happened, this line should never be reached in Map::_findKeyOrPlace"); | |
return NULL; | |
} | |
void _deleteNode(Node* node) { | |
if (mFreeNodeValueOnRemove && !null(node)) { | |
delete node.value(); | |
} | |
if (notNull(node)) { | |
if (notNull(node.left())) { _deleteNode(node.left()); } | |
if (notNull(node.right())) { _deleteNode(node.right()); } | |
} | |
delete node; | |
mSize--; | |
} | |
Node* _newNode(const string key, CObject* value) { | |
mSize++; | |
return new Node(key, value); | |
} | |
void _copyNodeAndDeleteSource(Node* source, Node* dest) { | |
dest.setKey(source.key()); | |
dest.setValue(source.value()); | |
dest.setLeft(source.left()); | |
dest.setRight(source.right()); | |
_deleteNode(source); | |
} | |
public: | |
Map(const bool freeNodeValueOnRemove = true) | |
: mSize(0) | |
, mFreeNodeValueOnRemove(freeNodeValueOnRemove) | |
{} | |
~Map(void) { _deleteNode(mRoot); } | |
uint size(void) const { return mSize; } | |
void insert(const string key, CObject* value) { | |
SmartPtr pNodePosition(_findNodeOrPlace(key)); | |
NodePosition position = ((NodeWithPosition*)pNodePosition.value()).position(); | |
Node* node = ((NodeWithPosition*)pNodePosition.value()).node(); | |
if (position == NP_EMPTY_TREE) { | |
mRoot = _newNode(key, value); | |
} | |
else if (position == NP_IN_PLACE) { | |
node.setValue(value); | |
} | |
else if (position == NP_RIGHT) { | |
node.setRight(_newNode(key, value)); | |
} | |
else { | |
node.setLeft(_newNode(key, value)); | |
} | |
} | |
CObject* lookup(const string key) const { | |
SmartPtr pNodePosition(_findNodeOrPlace(key)); | |
if (((NodeWithPosition*)pNodePosition.value()).position() == NP_IN_PLACE) { | |
return ((NodeWithPosition*)pNodePosition.value()).node().value(); | |
} | |
return NULL; | |
} | |
bool has(const string key) const { | |
SmartPtr pNodePosition(_findNodeOrPlace(key)); | |
return ((NodeWithPosition*)pNodePosition.value()).position() == NP_IN_PLACE; | |
} | |
void remove(const string key) { | |
SmartPtr pNodePosition(_findNodeOrPlace(key)); | |
Node* node = ((NodeWithPosition*)pNodePosition.value()).node(); | |
if (((NodeWithPosition*)pNodePosition.value()).position() != NP_IN_PLACE) { | |
return; | |
} | |
// No children, just delete | |
if (null(node.left()) && null(node.right())) { | |
_deleteNode(node); | |
} | |
// Only right chlid replace node with right child | |
else if (null(node.left())) { | |
_copyNodeAndDeleteSource(node.right(), node); | |
} | |
// Only left child, replace node with left child | |
else if (null(node.right())) { | |
_copyNodeAndDeleteSource(node.left(), node); | |
} | |
// Two children | |
else { | |
Node* min = node.right(); | |
// Node to delete | |
Node* target; | |
// Find smallest(leftmost) child in the right subtree of the node | |
// This value is bigger then all children in node's left subtree and | |
// smaller then all children in node' right subtree | |
while (notNull(min.left())) { | |
min = min.left(); | |
} | |
// Replace node with min node | |
node.setValue(min.value()); | |
node.setKey(min.key()); | |
target = min; | |
// If node had right child(can't have left because it's leftmost) | |
// then move it to the min node's place | |
if (notNull(min.right())) { | |
min = min.right(); | |
} | |
_deleteNode(target); | |
} | |
} | |
CArrayString* keys(void) { | |
return keys(mRoot, new CArrayString()); | |
} | |
CArrayString* keys(Node* node, CArrayString* _keys) { | |
if (node == NULL) { | |
return _keys; | |
} | |
keys(node.left(), _keys); | |
_keys.Add(node.key()); | |
keys(node.right(), _keys); | |
return _keys; | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#property copyright "erdeszt" | |
#property version "1.00" | |
#property library | |
#include <Object.mqh> | |
class SmartPtr : CObject { | |
private: | |
CObject* mValue; | |
public: | |
SmartPtr(CObject* value) : mValue(value) {} | |
~SmartPtr() { delete mValue; } | |
CObject* value() const { return mValue; } | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#property copyright "erdeszt" | |
#property version "1.00" | |
#property library | |
#include <Arrays/ArrayString.mqh> | |
enum Ordering { | |
LT = -1, | |
EQ = 0, | |
GT = 1 | |
}; | |
template<typename T> | |
bool null(const T* ptr) { | |
return ptr == NULL || CheckPointer(ptr) == POINTER_INVALID; | |
} | |
template<typename T> | |
bool notNull(const T* ptr) { | |
return !null(ptr); | |
} | |
void exit(const string msg) { | |
Print("UNRECOVERABLE ERROR OCCURED: " + msg); | |
Print("LAST ERROR CODE: " + (string)GetLastError()); | |
ExpertRemove(); | |
} | |
bool assert(const bool predicate, const string msg) { | |
if (!predicate) { | |
exit(msg); | |
} | |
return true; | |
} | |
string join(CArrayString* strs, const string sep = ", ") { | |
const int strsLen = strs.Total(); | |
if (strsLen == 0) { return ""; } | |
string result = strs.At(0); | |
for (int i = 1; i < strsLen; i++) { | |
StringAdd(result, sep + strs.At(i)); | |
} | |
return result; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment