Custom jSON - escaco95/Charcoal GitHub Wiki

// Copyright (c) 2019 μ΅œμ„ μ›…
// Distributed under the BSD License, Version 1.0.
using System.Collections.Generic;
using System.Text;
//==========================================================================================
// μ½”λ“œ λ³Έλ¬Έ
//==========================================================================================
#region [    Base CSON Element    ]
/// <summary>
/// <see cref="CSONElement"/>κ°€ μ·¨ν•  수 μžˆλŠ” ν˜•νƒœμ˜ μ’…λ₯˜λ₯Ό λ‚˜νƒ€λƒ…λ‹ˆλ‹€.
/// </summary>
public enum CSONElementType {
    Unknown,
    /// <summary>
    /// Key(string), Value(<see cref="CSONElement"/>)의 쌍으둜 이루어진 <see cref="CSONElement"/>의 νŒŒμƒ ν˜•νƒœ 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€.
    /// </summary>
    Object,
    /// <summary>
    /// Index(int), Value(<see cref="CSONElement"/>)의 쌍으둜 이루어진 <see cref="CSONElement"/>의 νŒŒμƒ ν˜•νƒœ 쀑 ν•˜λ‚˜μž…λ‹ˆλ‹€.
    /// <para>λ°°μ—΄μ˜ 각 μš”μ†ŒλŠ” 리슀트의 ν˜•νƒœλ‘œ λ‹€λ£¨μ–΄μ§‘λ‹ˆλ‹€.</para>
    /// </summary>
    Array,
    /// <summary>
    /// 배열에 μ†ν•œ μš”μ†Œμ˜ μœ„μΉ˜κ°€ 연속적이지 μ•Šμ€ 자료ꡬ쑰λ₯Ό 닀루기에 μ ν•©ν•œ νŒŒμƒ ν˜•νƒœμž…λ‹ˆλ‹€.
    /// <para>λ°°μ—΄μ˜ μ˜λ―Έμ— 맞게 각 μš”μ†Œκ°€ λ‹€λ£¨μ–΄μ§‘λ‹ˆλ‹€.</para>
    /// </summary>
    SparseArray,
    /// <summary>
    /// μ›μ‹œμ μΈ 데이터 νƒ€μž… <see cref="string"/>의 ν˜•νƒœλ₯Ό μ·¨ν•˜κ³  μžˆλŠ” <see cref="CSONElement"/>μž…λ‹ˆλ‹€.
    /// </summary>
    String,
    /// <summary>
    /// μ›μ‹œμ μΈ 데이터 νƒ€μž… <see cref="int"/>의 ν˜•νƒœλ₯Ό μ·¨ν•˜κ³  μžˆλŠ” <see cref="CSONElement"/>μž…λ‹ˆλ‹€.
    /// </summary>
    Integer,
    // TODO: CSON FIxedReal κ΅¬ν˜„
    FixedReal,
    // TODO: CSON FloatReal κ΅¬ν˜„
    FloatReal
}
/// <summary>
/// Custom JSON 자료ꡬ쑰λ₯Ό κ΅¬μ„±ν•˜λŠ” λ‹¨μœ„, Elementμž…λ‹ˆλ‹€.
/// </summary>
public abstract class CSONElement
{
    /// <summary>
    /// ν•΄λ‹Ή <see cref="CSONElement"/>κ°€ μ·¨ν•˜κ³  μžˆλŠ” μžλ£Œν˜•μž…λ‹ˆλ‹€.
    /// </summary>
    public CSONElementType Type { get; protected set; }
}
#endregion
#region[    CSON Object{}    ]
/// <summary>
/// Key(string), Value(<see cref="CSONElement"/>) 쌍으둜 이루어진 <see cref="CSONElement"/>듀을 κ°–λŠ” μžλ£Œν˜•μž…λ‹ˆλ‹€.
/// </summary>
public class CSONObject : CSONElement
{
    public Dictionary<string, CSONElement> Elements { get; protected set; } = new Dictionary<string, CSONElement>();
    public CSONObject(){ Type = CSONElementType.Object;}
    public bool Contains(string key) { return Elements.ContainsKey(key); }
    public void Remove(string key) { Elements.Remove(key); }
    public void Clear() { Elements.Clear(); }
    public CSONElement this[string key] { get { return Elements[key]; } set { Elements[key] = value; } }
    public void Add(string key, CSONElement Element){Elements.Add(key, Element); }
    public void Add(string key, string value) { Elements.Add(key, new CSONString() { Value = value }); }
    public void Add(string key, int value) { Elements.Add(key, new CSONInteger() { Value = value }); }
    public override string ToString()
    {
        string result = "{";
        bool first = true;
        foreach (var Element in Elements)
        {
            if (!first)
                result += ", ";
            result += $"\"{Element.Key}\": {Element.Value.ToString()}";
            first = false;
        }
        result += "}";
        return result;
    }
}
#endregion
#region [    CSON Sparse Array<>    ]
public class CSONSparseArray : CSONElement
{
    public Dictionary<int, CSONElement> Items { get; protected set; } = new Dictionary<int, CSONElement>();
    public CSONSparseArray() { Type = CSONElementType.SparseArray; }
    public bool Contains(int index) { return Items.ContainsKey(index); }
    public void Remove(int index) { Items.Remove(index); }
    public void Clear() { Items.Clear(); }
    public CSONElement this[int index] { get { return Items[index]; } set { Items[index] = value; } }
    public void Add(int index, CSONElement Element) { Items.Add(index, Element); }
    public void Add(int index, string value) { Items.Add(index, new CSONString() { Value = value }); }
    public void Add(int index, int value) { Items.Add(index, new CSONInteger() { Value = value }); }
    public override string ToString()
    {
        string result = "<";
        bool first = true;
        foreach (var item in Items)
        {
            if (!first)
                result += ", ";
            result += $"{item.Key}: {item.Value.ToString()}";
            first = false;
        }
        result += ">";
        return result;
    }
}
#endregion
#region [    CSON Array[]    ]
public class CSONArray : CSONElement
{
    public List<CSONElement> Items { get; protected set; } = new List<CSONElement>();
    public CSONArray(){Type = CSONElementType.Array;}
    public bool Contains(CSONElement Element) { return Items.Contains(Element); }
    public void RemoveAt(int index) { Items.RemoveAt(index); }
    public void Clear() { Items.Clear(); }
    public CSONElement this[int index] { get { return Items[index]; } set { Items[index] = value; } }
    public void Add(CSONElement Element) { Items.Add(Element); }
    public void Add(string value) { Items.Add( new CSONString() { Value = value }); }
    public void Add(int value) { Items.Add( new CSONInteger() { Value = value }); }
    public override string ToString()
    {
        string result = "[";
        bool first = true;
        foreach (var element in Items)
        {
            if (!first)
                result += ", ";
            result += element.ToString();
            first = false;
        }
        result += "]";
        return result;
    }
}
#endregion
#region [    Primitive CSON Elements    ]
public class CSONString : CSONElement
{
    public string Value;
    public CSONString() { Type = CSONElementType.String; }
    public override string ToString(){ return $"\"{Value}\""; }
}
public class CSONInteger : CSONElement
{
    public int Value;
    public CSONInteger() { Type = CSONElementType.Integer; }
    public override string ToString() { return $"{Value}"; }
}
public class CSONReal : CSONElement
{
    public double Value;
    public CSONReal() { Type = CSONElementType.FixedReal; }
    public override string ToString() { return $"{Value}"; }
}
#endregion
//==========================================================================================
// Parser
//==========================================================================================
#region [    CSON Parser    ]
public class CSONParser
{
    private enum TokenType
    {
        Default, String, Integer, Real, CommentReady, Comment, CommentEndReady,
        Seperator, Parameter, ObjectOpen, ArrayOpen, SparseOpen, ObjectClose, ArrayClose, SparseClose
    }
    private class ParseToken
    {
        public TokenType Type;
        public string Value;
    }
    private List<ParseToken> Tokenize(char[] CSONCharArray)
    {
        var tokens = new List<ParseToken>();
        TokenType currentToken = TokenType.Default;
        StringBuilder tokenBuilder = new StringBuilder();
        string tokenString;
        for(int ci=0;ci<CSONCharArray.Length;ci++)
        {
            var c = CSONCharArray[ci];
            switch (currentToken)
            {
                case TokenType.Integer:
                case TokenType.Real:
                    switch (c)
                    {
                        case '.':
                            if (currentToken == TokenType.Real)
                                throw new System.Exception($"μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 숫자 μ„œμ‹.");
                            currentToken = TokenType.Real;
                            tokenBuilder.Append(c);
                            break;
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                        case '6':
                        case '7':
                        case '8':
                        case '9':
                            tokenBuilder.Append(c);
                            break;
                        default:
                            tokenString = tokenBuilder.ToString();
                            tokens.Add(new ParseToken() { Type = currentToken, Value = tokenString });
                            tokenBuilder.Clear();
                            //
                            currentToken = TokenType.Default; //....?
                            ci--;
                            break;
                    }
                    break;
                case TokenType.String:
                    switch (c)
                    {
                        case '"':
                            tokenString = tokenBuilder.ToString();
                            tokens.Add(new ParseToken() { Type = TokenType.String, Value = tokenString });
                            tokenBuilder.Clear();
                            //
                            currentToken = TokenType.Default; //....?
                            break;
                        default:
                            tokenBuilder.Append(c);
                            break;
                    }
                    break;
                case TokenType.CommentEndReady:
                case TokenType.Comment:
                    switch (c)
                    {
                        case '*':
                            currentToken = TokenType.CommentEndReady;
                            break;
                        case '/':
                            if (currentToken == TokenType.CommentEndReady)
                            {
                                currentToken = TokenType.Default; //....?
                            }
                            break;
                    }
                    break;
                case TokenType.CommentReady:
                case TokenType.Default:
                    switch (c)
                    {
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                        case '6':
                        case '7':
                        case '8':
                        case '9':
                            // λ””ν΄νŠΈκ°€ μ•„λ‹Œ 상황 = μ•žμ„œ acceptν•œ λ¬Έμžκ°€ μžˆμ—ˆμŒ.
                            if (currentToken != TokenType.Default)
                                throw new System.Exception($"μ˜ˆμƒν•˜μ§€ λͺ»ν•œ λ¬Έμžκ°€ 선행됨.");
                            currentToken = TokenType.Integer;
                            tokenBuilder.Append(c);
                            break;
                        case '"':
                            // λ””ν΄νŠΈκ°€ μ•„λ‹Œ 상황 = μ•žμ„œ acceptν•œ λ¬Έμžκ°€ μžˆμ—ˆμŒ.
                            if (currentToken != TokenType.Default)
                                throw new System.Exception($"μ˜ˆμƒν•˜μ§€ λͺ»ν•œ λ¬Έμžκ°€ 선행됨.");
                            currentToken = TokenType.String;
                            break;
                        case '/':
                            // λ””ν΄νŠΈκ°€ μ•„λ‹Œ 상황 = μ•žμ„œ acceptν•œ λ¬Έμžκ°€ μžˆμ—ˆμŒ.
                            if (currentToken != TokenType.Default)
                                throw new System.Exception($"μ˜ˆμƒν•˜μ§€ λͺ»ν•œ λ¬Έμžκ°€ 선행됨.");
                            // μ½”λ©˜νŠΈ μ€€λΉ„ μ‹œμž‘
                            currentToken = TokenType.CommentReady;
                            break;
                        case ' ':
                        case '\t':
                            // λ””ν΄νŠΈκ°€ μ•„λ‹Œ 상황 = μ•žμ„œ acceptν•œ λ¬Έμžκ°€ μžˆμ—ˆμŒ.
                            if (currentToken != TokenType.Default)
                                throw new System.Exception($"μ˜ˆμƒν•˜μ§€ λͺ»ν•œ λ¬Έμžκ°€ 선행됨.");
                            // 기본적으둜 곡백은 λ¬΄μ‹œν•¨
                            break;
                        case '*':
                            // μ½”λ©˜νŠΈ μ€€λΉ„
                            if (currentToken == TokenType.CommentReady)
                            {
                                currentToken = TokenType.Comment;
                                break;
                            }
                            throw new System.Exception($"μ˜ˆμƒν•˜μ§€ λͺ»ν•œ 문자 {c}.");
                        case '{':
                            tokens.Add(new ParseToken() { Type = TokenType.ObjectOpen });
                            break;
                        case '[':
                            tokens.Add(new ParseToken() { Type = TokenType.ArrayOpen });
                            break;
                        case '<':
                            tokens.Add(new ParseToken() { Type = TokenType.SparseOpen });
                            break;
                        case '>':
                            tokens.Add(new ParseToken() { Type = TokenType.SparseClose });
                            break;
                        case ']':
                            tokens.Add(new ParseToken() { Type = TokenType.ArrayClose });
                            break;
                        case '}':
                            tokens.Add(new ParseToken() { Type = TokenType.ObjectClose });
                            break;
                        case ':':
                            tokens.Add(new ParseToken() { Type = TokenType.Parameter });
                            break;
                        case ',':
                            tokens.Add(new ParseToken() { Type = TokenType.Seperator });
                            break;
                        default:
                            throw new System.Exception($"μ˜ˆμƒν•˜μ§€ λͺ»ν•œ 문자 {c}.");
                    }
                    break;
            }
        }
        if (currentToken != TokenType.Default)
            throw new System.Exception($"CSON이 μ˜¬λ°”λ₯΄κ²Œ μ’…κ²°λ˜μ§€ λͺ»ν•¨.");
        return tokens;
    }
    private CSONElement ParseRecur(List<ParseToken> tokens, ref int readPtr)
    {
        CSONElement baseElement = null;
        var token = tokens[readPtr];
        readPtr++;
        switch (token.Type)
        {
            case TokenType.String:
                baseElement = new CSONString() { Value = token.Value };
                break;
            case TokenType.Integer:
                baseElement = new CSONInteger() { Value = int.Parse(token.Value) };
                break;
            case TokenType.Real:
                baseElement = new CSONReal() { Value = double.Parse(token.Value) };
                break;
            case TokenType.ObjectOpen:
                baseElement = new CSONObject();
                while (readPtr < tokens.Count)
                {
                    var finish = false;
                    var subtoken = tokens[readPtr];
                    readPtr++;
                    switch (subtoken.Type)
                    {
                        case TokenType.Seperator:/*Do Nothing*/break;
                        case TokenType.String:
                            {
                                if(tokens[readPtr].Type != TokenType.Parameter)
                                    throw new System.Exception($"예기치 λͺ»ν•œ {subtoken.Type}.");
                                var paraToken = tokens[readPtr + 1];
                                readPtr += 2;
                                switch (paraToken.Type)
                                {
                                    case TokenType.String:
                                        ((CSONObject)baseElement).Add(subtoken.Value, new CSONString() { Value = paraToken.Value });
                                        break;
                                    case TokenType.Integer:
                                        ((CSONObject)baseElement).Add(subtoken.Value, new CSONInteger() { Value = int.Parse(paraToken.Value) });
                                        break;
                                    case TokenType.Real:
                                        ((CSONObject)baseElement).Add(subtoken.Value, new CSONReal() { Value = double.Parse(paraToken.Value) });
                                        break;
                                    case TokenType.ObjectOpen:
                                    case TokenType.SparseOpen:
                                    case TokenType.ArrayOpen:
                                        readPtr--;
                                        ((CSONObject)baseElement).Add(subtoken.Value, ParseRecur(tokens, ref readPtr));
                                        break;
                                }
                            }
                            break;
                        case TokenType.ObjectClose:
                            finish = true;
                            break;
                        default:
                            throw new System.Exception($"예기치 λͺ»ν•œ {subtoken.Type}.");
                    }
                    if (finish)
                        break;
                }
                break;
            case TokenType.SparseOpen:
                baseElement = new CSONSparseArray();
                while (readPtr < tokens.Count)
                {
                    var finish = false;
                    var subtoken = tokens[readPtr];
                    readPtr++;
                    switch (subtoken.Type)
                    {
                        case TokenType.Seperator:/*Do Nothing*/break;
                        case TokenType.Integer:
                            {
                                if (tokens[readPtr].Type != TokenType.Parameter)
                                    throw new System.Exception($"예기치 λͺ»ν•œ {subtoken.Type}.");
                                var paraToken = tokens[readPtr + 1];
                                readPtr += 2;
                                switch (paraToken.Type)
                                {
                                    case TokenType.String:
                                        ((CSONSparseArray)baseElement).Add(int.Parse(subtoken.Value), new CSONString() { Value = paraToken.Value });
                                        break;
                                    case TokenType.Integer:
                                        ((CSONSparseArray)baseElement).Add(int.Parse(subtoken.Value), new CSONInteger() { Value = int.Parse(paraToken.Value) });
                                        break;
                                    case TokenType.Real:
                                        ((CSONSparseArray)baseElement).Add(int.Parse(subtoken.Value), new CSONReal() { Value = double.Parse(paraToken.Value) });
                                        break;
                                    case TokenType.ObjectOpen:
                                    case TokenType.SparseOpen:
                                    case TokenType.ArrayOpen:
                                        readPtr--;
                                        ((CSONSparseArray)baseElement).Add(int.Parse(subtoken.Value), ParseRecur(tokens, ref readPtr));
                                        break;
                                }
                            }
                            break;
                        case TokenType.ArrayClose:
                            finish = true;
                            break;
                        default:
                            throw new System.Exception($"예기치 λͺ»ν•œ {subtoken.Type}.");
                    }
                    if (finish)
                        break;
                }
                break;
            case TokenType.ArrayOpen:
                baseElement = new CSONArray();
                while (readPtr < tokens.Count)
                {
                    var finish = false;
                    var subtoken = tokens[readPtr];
                    readPtr++;
                    switch (subtoken.Type)
                    {
                        case TokenType.Seperator:/*Do Nothing*/break;
                        case TokenType.String:
                            ((CSONArray)baseElement).Add(new CSONString() { Value = subtoken.Value });
                            break;
                        case TokenType.Integer:
                            ((CSONArray)baseElement).Add(new CSONInteger() { Value = int.Parse(subtoken.Value) });
                            break;
                        case TokenType.Real:
                            ((CSONArray)baseElement).Add(new CSONReal() { Value = double.Parse(subtoken.Value) });
                            break;
                        case TokenType.ObjectOpen:
                        case TokenType.SparseOpen:
                        case TokenType.ArrayOpen:
                            readPtr--;
                            ((CSONArray)baseElement).Add(ParseRecur(tokens, ref readPtr));
                            break;
                        case TokenType.ArrayClose:
                            finish = true;
                            break;
                        default:
                            throw new System.Exception($"예기치 λͺ»ν•œ {subtoken.Type}.");
                    }
                    if (finish)
                        break;
                }
                break;
        }
        return baseElement;
    }
    public CSONElement Parse(string CSONString)
    {
        CSONString += " "; //곡백을 μΆ”κ°€ν•΄μ„œ λΉ„-블둝 ꡬ쑰의 데이터가 μ•Œκ³ λ¦¬μ¦˜ 쀑간에 λŠκΈ°λŠ” 일이 없도둝 함
        // 주석은 배제되고 λ¬Έμžμ—΄μ΄ 토큰 ν˜•νƒœλ‘œ 묢인 리슀트λ₯Ό λ°˜ν™˜λ°›μŠ΅λ‹ˆλ‹€.
        List<ParseToken> tokens = Tokenize(CSONString.ToCharArray());
        int readPtr = 0;
        return ParseRecur(tokens, ref readPtr);
    }
}
#endregion
⚠️ **GitHub.com Fallback** ⚠️