Created
July 29, 2019 18:50
-
-
Save yasar11732/b7fbd001867e208bbb7b2ebfa45356b0 to your computer and use it in GitHub Desktop.
JSON parser and stringifier implemented in C# (Recursive Descent Parser)
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
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
namespace MiniWebFramework | |
{ | |
public enum JSONValueType | |
{ | |
NULL, | |
TRUE, | |
FALSE, | |
NUMBER, | |
STRING, | |
ARRAY, | |
OBJECT | |
} | |
public class JSONValue | |
{ | |
public JSONValueType type { get; private set; } | |
public object value { get; private set; } | |
public JSONValue(JSONValueType t, object v) | |
{ | |
type = t; | |
value = v; | |
} | |
} | |
public static class JSON | |
{ | |
private static readonly char[] positive_digits = new char[] { '1', '2', '3', '4', '5', '6', '7', '8', '9' }; | |
private static readonly char[] all_digits = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; | |
private static readonly char[] white_space = new char[] { ' ', '\t', '\n', '\r' }; | |
private static readonly char[] string_breakers = new char[] { '"', '\\', '\r', '\n', '\t', '\b', '\f'}; | |
private static readonly char[] hexdigit = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', | |
'a', 'b', 'c', 'd', 'e', 'f', | |
'A', 'B', 'C', 'D', 'E', 'F' | |
}; | |
private class KVPair | |
{ | |
public string key; | |
public JSONValue value; | |
} | |
private class ParseContext | |
{ | |
private static readonly char EOT = '\x04'; | |
private char[] data; | |
int position; | |
int cap; | |
public ParseContext(char[] _data) | |
{ | |
data = _data; | |
cap = _data.Length; | |
position = 0; | |
} | |
public bool isEOT() | |
{ | |
return !(position < cap); | |
} | |
public char Peek() | |
{ | |
if(position < cap) | |
return data[position]; | |
return EOT; | |
} | |
public char Read() | |
{ | |
if (position < cap) | |
return data[position++]; | |
return EOT; | |
} | |
public int Tell() | |
{ | |
return position; | |
} | |
public void Seek(int p) | |
{ | |
position = p; | |
} | |
public string StringFrom(int start) | |
{ | |
int strlen = position - start; | |
char[] segment = new char[strlen]; | |
for(var i = 0; i < strlen; i++) | |
{ | |
segment[i] = data[start + i]; | |
} | |
return new string(segment); | |
} | |
} | |
private static bool parse_literal(ParseContext p, string literal) | |
{ | |
bool isMatch = true; | |
int current = p.Tell(); | |
foreach(var c in literal) | |
{ | |
if(c != p.Read()) | |
{ | |
isMatch = false; | |
break; | |
} | |
} | |
if (!isMatch) | |
p.Seek(current); | |
return isMatch; | |
} | |
private static bool parse_char(ParseContext p, char c) | |
{ | |
if(c == p.Peek()) | |
{ | |
p.Read(); | |
return true; | |
} | |
return false; | |
} | |
private static bool parse_any(ParseContext p, char[] options) | |
{ | |
bool isMatch = false; | |
char next_char = p.Peek(); | |
for(var i = 0; i < options.Length; i++) | |
{ | |
if(options[i] == next_char) | |
{ | |
isMatch = true; | |
p.Read(); | |
break; | |
} | |
} | |
return isMatch; | |
} | |
private static bool parse_ws(ParseContext p) | |
{ | |
int num_matches = 0; | |
while(parse_any(p, white_space)) | |
{ | |
num_matches++; | |
} | |
return num_matches > 0; | |
} | |
private static JSONValue parse_json_null(ParseContext p) | |
{ | |
if (parse_literal(p, "null")) | |
return new JSONValue(JSONValueType.NULL, null); | |
return null; | |
} | |
private static JSONValue parse_json_true(ParseContext p) | |
{ | |
if (parse_literal(p, "true")) | |
return new JSONValue(JSONValueType.TRUE, null); | |
return null; | |
} | |
private static JSONValue parse_json_false(ParseContext p) | |
{ | |
if (parse_literal(p, "false")) | |
return new JSONValue(JSONValueType.FALSE, null); | |
return null; | |
} | |
private static JSONValue parse_json_number(ParseContext p) | |
{ | |
int start = p.Tell(); | |
parse_char(p, '-'); | |
if(!parse_int(p)) | |
{ | |
p.Seek(start); | |
return null; | |
} | |
parse_frac(p); | |
parse_exponent(p); | |
return new JSONValue(JSONValueType.NUMBER, Convert.ToDouble(p.StringFrom(start))); | |
} | |
private static bool parse_exponent(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_any(p, new char[] { 'e', 'E' })) | |
return false; | |
parse_any(p, new char[] { '-', '+' }); | |
if (!parse_any(p, all_digits)) | |
{ | |
p.Seek(start); | |
return false; | |
} | |
while(parse_any(p, all_digits)) | |
{ | |
} | |
return true; | |
} | |
private static bool parse_frac(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_char(p, '.')) | |
return false; | |
if (!parse_any(p, all_digits)) | |
{ | |
p.Seek(start); | |
return false; | |
} | |
while(parse_any(p, all_digits)) | |
{ | |
} | |
return true; | |
} | |
private static bool parse_int(ParseContext p) | |
{ | |
if(parse_char(p, '0')) | |
{ | |
return true; | |
} | |
if(!parse_any(p, positive_digits)) | |
{ | |
return false; | |
} | |
while(parse_any(p, all_digits)) | |
{ | |
} | |
return true; | |
} | |
private static string parse_string_token(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_char(p, '\"')) | |
return null; | |
while (parse_normal_char(p) || parse_escaped_char(p)) | |
{ | |
continue; | |
} | |
if(p.Peek() != '"') | |
{ | |
p.Seek(start); | |
return null; | |
} | |
var result = p.StringFrom(start + 1); // +1 skip initial double quote | |
p.Read(); // consume ending double quote | |
return Regex.Unescape(result); | |
} | |
private static JSONValue parse_json_string(ParseContext p) | |
{ | |
string s; | |
if ((s = parse_string_token(p)) != null) | |
return new JSONValue(JSONValueType.STRING, s); | |
else | |
return null; | |
} | |
private static bool parse_escaped_char(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_char(p, '\\')) | |
return false; | |
if (p.isEOT()) | |
{ | |
p.Seek(start); | |
return false; | |
} | |
var c = p.Read(); | |
if (c != 'u') | |
return true; | |
if( | |
!parse_any(p, hexdigit) || | |
!parse_any(p, hexdigit) || | |
!parse_any(p, hexdigit) || | |
!parse_any(p, hexdigit) | |
) | |
{ | |
p.Seek(start); | |
return false; | |
} | |
return true; | |
} | |
private static bool parse_normal_char(ParseContext p) | |
{ | |
var c = p.Peek(); | |
if (!shouldescape(c)) | |
{ | |
p.Read(); | |
return true; | |
} else | |
{ | |
return false; | |
} | |
} | |
private static JSONValue parse_json_array(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_char(p, '[')) | |
return null; | |
parse_ws(p); // skip optional space | |
List<JSONValue> elems = parse_elems(p); | |
if (!parse_char(p, ']')) | |
{ | |
p.Seek(start); | |
return null; | |
} | |
return new JSONValue(JSONValueType.ARRAY, elems); | |
} | |
private static List<JSONValue> parse_elems(ParseContext p) | |
{ | |
var elems = new List<JSONValue>(); | |
var elem = parse_jsonvalue(p); | |
while(elem != null) | |
{ | |
elems.Add(elem); | |
if (!parse_char(p, ',')) | |
break; | |
elem = parse_jsonvalue(p); | |
} | |
return elems; | |
} | |
private static JSONValue parse_json_object(ParseContext p) | |
{ | |
int start = p.Tell(); | |
if (!parse_char(p, '{')) | |
return null; | |
parse_ws(p); // skip optional space | |
Dictionary<string, JSONValue> elems = parse_kvpairs(p); | |
if (!parse_char(p, '}')) | |
{ | |
p.Seek(start); | |
return null; | |
} | |
return new JSONValue(JSONValueType.OBJECT, elems); | |
} | |
private static Dictionary<string, JSONValue> parse_kvpairs(ParseContext p) | |
{ | |
KVPair pair; | |
Dictionary<string, JSONValue> pairs = new Dictionary<string, JSONValue>(); | |
while((pair = parse_kvpair(p)) != null) | |
{ | |
pairs[pair.key] = pair.value; | |
parse_ws(p); // skip space if any | |
if (!parse_char(p, ',')) | |
break; | |
} | |
return pairs; | |
} | |
private static KVPair parse_kvpair(ParseContext p) | |
{ | |
int start = p.Tell(); | |
KVPair pair = new KVPair(); | |
parse_ws(p); // skip white space | |
string s = parse_string_token(p); | |
if (s == null) | |
return null; | |
parse_ws(p); // skip whitespace | |
if(!parse_char(p, ':')) | |
{ | |
p.Seek(start); | |
return null; | |
} | |
JSONValue v = parse_jsonvalue(p); | |
if(v == null) | |
{ | |
p.Seek(start); | |
return null; | |
} | |
return new KVPair() | |
{ | |
key = s, | |
value = v | |
}; | |
} | |
private static JSONValue parse_jsonvalue(ParseContext p) | |
{ | |
JSONValue v; | |
parse_ws(p); | |
if ( | |
(v = parse_json_null(p) ) != null || | |
(v = parse_json_true(p) ) != null || | |
(v = parse_json_false(p) ) != null || | |
(v = parse_json_number(p)) != null || | |
(v = parse_json_string(p)) != null || | |
(v = parse_json_array(p) ) != null || | |
(v = parse_json_object(p)) != null | |
) | |
{ | |
} | |
parse_ws(p); | |
return v; | |
} | |
public static JSONValue parse(string s) | |
{ | |
return parse(s.ToCharArray()); | |
} | |
public static JSONValue parse(char[] s) | |
{ | |
ParseContext p = new ParseContext(s); | |
JSONValue v = parse_jsonvalue(p); | |
if (p.isEOT()) | |
return v; | |
else | |
return null; | |
} | |
public static string stringify() | |
{ | |
return "null"; | |
} | |
public static string stringify(bool b) | |
{ | |
if (b) | |
return "true"; | |
else | |
return "false"; | |
} | |
private static bool shouldescape(char c) | |
{ | |
return !(c >= ' ' && c != '"' && c != '\\'); | |
} | |
private static string escape(string s) | |
{ | |
using (var ms = new MemoryStream()) | |
using (var wr = new StreamWriter(ms)) | |
{ | |
foreach(char c in s) | |
{ | |
if(shouldescape(c)) | |
{ | |
int x = c; | |
wr.Write("\\u"); | |
wr.Write(x.ToString("X4")); | |
} else | |
{ | |
wr.Write(c); | |
} | |
} | |
wr.Flush(); | |
ms.Seek(0, SeekOrigin.Begin); | |
return Encoding.UTF8.GetString(ms.ToArray()); | |
} | |
} | |
public static string stringify(string s) | |
{ | |
return String.Format("\"{0}\"",escape(s)); | |
} | |
public static string stringify(double d) | |
{ | |
return d.ToString(); | |
} | |
public static string stringify(List<object> l) | |
{ | |
List<string> parts = new List<string>(); | |
foreach(var o in l) | |
{ | |
if (o == null) | |
{ | |
parts.Add("null"); | |
continue; | |
} | |
Type t = o.GetType(); | |
do | |
{ | |
if(t == typeof(bool)) | |
{ | |
if ((bool)o) | |
parts.Add("true"); | |
else | |
parts.Add("false"); | |
break; | |
} | |
if (t == typeof(string)) | |
{ | |
parts.Add(stringify((string)o)); | |
break; | |
} | |
if (t == typeof(int) || t == typeof(double) || t == typeof(float)) | |
{ | |
parts.Add(stringify(Convert.ToDouble(o))); | |
break; | |
} | |
if (t == typeof(List<object>)) | |
{ | |
parts.Add(stringify((List<object>)o)); | |
break; | |
} | |
if (t == typeof(Dictionary<string, object>)) | |
{ | |
parts.Add(stringify((Dictionary<string, object>)o)); | |
break; | |
} | |
throw new InvalidDataException(); | |
} while (false); | |
} | |
return "[" + String.Join(",", parts.ToArray()) + "]"; | |
} | |
public static string stringify(Dictionary<string, object> o) | |
{ | |
List<string> parts = new List<string>(); | |
foreach(KeyValuePair<string, object> kv in o) | |
{ | |
if (kv.Value == null) | |
{ | |
parts.Add(String.Format("{0}:null", stringify(kv.Key))); | |
continue; | |
} | |
Type t = kv.Value.GetType(); | |
do | |
{ | |
if (t == typeof(bool)) | |
{ | |
if ((bool)kv.Value) | |
parts.Add(String.Format("{0}:true", stringify(kv.Key))); | |
else | |
parts.Add(String.Format("{0}:false", stringify(kv.Key))); | |
break; | |
} | |
if (t == typeof(string)) | |
{ | |
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify((string)kv.Value))); | |
break; | |
} | |
if (t == typeof(int) || t == typeof(double) || t == typeof(float)) | |
{ | |
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify(Convert.ToDouble(kv.Value)))); | |
break; | |
} | |
if (t == typeof(List<object>)) | |
{ | |
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify((List<object>)kv.Value))); | |
break; | |
} | |
if (t == typeof(Dictionary<string, object>)) | |
{ | |
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify((Dictionary<string, object>)kv.Value))); | |
break; | |
} | |
throw new InvalidDataException(); | |
} while (false); | |
} | |
return "{" + String.Join(",", parts.ToArray()) + "}"; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How To Use
JSON.stringify()
method.JSON.parse()
method.JSON.parse()
method returns values of typeJSONValue
.JSONValue
has two readonly properties.JSONValue.type
is of typeenum JSONValueType
. It is defined as;Other readonly property is
JSONValue.value
. This property should be interpreted according totype
property. Iftype
property is one ofNULL
,TRUE
,FALSE
,value
property is not used. Iftype
property isNUMBER
,value
can be converted to double. Iftype
isSTRING
, value isstring
. Iftype
isARRAY
, value isList<JSONValue>
. Finally, iftype
isOBJECT
, value isDictionary<string, JSONValue>
.Parser and stringifier are standards-compliant as far as I can tell, but they are not tested throughly. Minor issues might exist.