Last active
February 18, 2017 15:02
-
-
Save xplorld/0e11c18a4e2d02505c5d5efca1335c30 to your computer and use it in GitHub Desktop.
a JSON parser in Swift without Cocoa or Regular Expressions.
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
// | |
// main.swift | |
// RYJSON | |
// | |
// Created by Xplorld on 2017/2/18. | |
// Copyright © 2017年 xplorld. All rights reserved. | |
// | |
import Foundation | |
//a JSON parser | |
//target: | |
//no use of regex | |
//try to make friends with Swift.String (who turned out to be a fool guy) | |
//have fun with pattern matching | |
//no speed expectations | |
indirect enum JSON { | |
//string is not cared for now | |
//should escape: \" \\ \/ \b \f \n \r \t \u[hex]{4} | |
case string(String) | |
case number(Double) | |
case object([String:JSON]) | |
case array([JSON]) | |
case bool(Bool) | |
case null // {} is a .object, not a .null | |
case fail //FAIL!!! | |
} | |
//interface | |
extension JSON { | |
init(string:String) { | |
self = JSONParser(string).parse() | |
} | |
func serialize() -> String { | |
switch self { | |
case .string(let s): //or escaped? | |
return "\"\(s)\"" | |
case .number(let d): //or formatted? | |
return "\(d)" | |
case .bool(let b): | |
return b ? "true" : "false" | |
case .null: | |
return "null" | |
case .object(let dict): | |
return "{" + | |
dict | |
.map { "\"\($0.key)\":\($0.value.serialize())" } | |
.joined(separator: ",") | |
+ "}" | |
case .array(let arr): | |
return "[" + | |
arr | |
.map { $0.serialize() } | |
.joined(separator: ",") | |
+ "]" | |
default: | |
//fail? are you serious? | |
return "<FAIL>" | |
} | |
} | |
} | |
private class JSONParser { | |
typealias Index = String.Index | |
private var string: String | |
init(_ str:String) { | |
string = str | |
} | |
let isNotWhitespace:(Character)->Bool = { !" \t\n".characters.contains($0) } | |
func omitWhitespace() { | |
guard let idx = string.characters.index(where: isNotWhitespace) | |
else { return } | |
string.removeSubrange(Range<Index>(uncheckedBounds: (lower: string.startIndex, upper: idx))) | |
} | |
func beginMatches(pattern: String) -> Bool { | |
omitWhitespace() | |
return string.hasPrefix(pattern) | |
} | |
func nextCharacter() -> Character? { | |
omitWhitespace() | |
return string.characters.first | |
} | |
func touch(_ pattern:String) -> Bool { | |
return beginMatches(pattern: pattern) | |
} | |
func expect(_ pattern: String) -> Bool { | |
let good = touch(pattern) | |
if good { | |
let length = pattern.characters.count | |
let range = Range<Index>(uncheckedBounds: (string.startIndex, string.index(string.startIndex, offsetBy: length))) | |
string.removeSubrange(range) | |
} | |
return good | |
} | |
//return index of next char in pattern | |
func touchIn(_ pattern:String) -> Index? { | |
guard let c = nextCharacter() else {return nil} | |
return pattern.characters.index(of: c) | |
} | |
func expectIn(_ pattern: String) -> Index? { | |
let good = touchIn(pattern) | |
if good != nil { | |
string.remove(at: string.startIndex) | |
} | |
return good | |
} | |
func parse() -> JSON { | |
guard let c = nextCharacter() else {return .fail} | |
switch c { | |
case "t", "f": | |
return parseBool() | |
case "n": | |
return parseNull() | |
case "\"": | |
return parseString() | |
case "{": | |
return parseObject() | |
case "[": | |
return parseArray() | |
default: | |
return parseNumber() | |
} | |
} | |
func parseBool() -> JSON { | |
if expect("true") { | |
return JSON.bool(true) | |
} | |
if expect("false") { | |
return JSON.bool(false) | |
} | |
return .fail | |
} | |
func parseNull() -> JSON { | |
if expect("null") { | |
return JSON.null | |
} | |
return .fail | |
} | |
//01234 -> (1234, 5) | |
//01000 -> (1000, 5) | |
func digits() -> (digits:Int,len:Int)? { | |
let digits = "0123456789" | |
var int:Int = 0 | |
var len:Int = 0 | |
while let digit = expectIn(digits) { | |
let digitInt = digits.distance(from: digits.startIndex, to: digit) | |
int = int * 10 + digitInt | |
len += 1 | |
} | |
if len == 0 { | |
//found no digits | |
return nil | |
} | |
return (int,len) | |
} | |
//number = int frac? exp? | |
//int = [1-9][0-9]* | 0 | |
//frac? = \. [0-9]+ | *nothing* | |
//exp? = [eE] [+-]? [0-9]+ | |
func parseNumber() -> JSON { | |
let negative = expect("-") | |
var value:Double | |
//ints | |
if expect("0") { | |
value = 0 | |
} else if let tuple = digits(), | |
case let (int, _) = tuple { | |
value = Double(int) | |
} else { | |
return .fail | |
} | |
//decimals | |
if expect(".") { | |
if let tuple = digits(), | |
case let (int, len) = tuple { | |
let decimals = Double(int) / pow(10, Double(len)) | |
value += decimals | |
} else { | |
return .fail | |
} | |
} | |
//exponents | |
var exp:Int | |
if (expectIn("Ee") != nil) { | |
let expNegative = expect("-") | |
if !expNegative { | |
let _ = expect("+") //no matter found or not | |
} | |
if let tuple = digits(), | |
case let (int, _) = tuple { | |
exp = int | |
} else { | |
return .fail | |
} | |
if expNegative { | |
exp = -exp | |
} | |
} else { | |
exp = 0 | |
} | |
//done, make number! | |
if negative { | |
value = -value | |
} | |
value = value * pow(10, Double(exp)) | |
return .number(value) | |
} | |
//todo: did not consider any escapes | |
func parseString() -> JSON { | |
guard expect("\"") else { return .fail } | |
guard let nextIndex = string.characters.index(of: "\"") else {return .fail} | |
let range = Range<Index>(uncheckedBounds: (string.startIndex, nextIndex)) | |
let literal = string[range] | |
string.removeSubrange(range) | |
guard expect("\"") else { return .fail } | |
return .string(literal) | |
} | |
func parseObject() -> JSON { | |
guard expect("{") else { return .fail } | |
var dict:[String:JSON] = [:] | |
if expect("}") { | |
return .object(dict) | |
} | |
while true { | |
let key:String | |
if case JSON.string(let s) = parseString() { | |
key = s | |
} else { return .fail } | |
guard expect(":") else { return .fail } | |
let value = parse() | |
if case JSON.fail = value { return .fail } | |
dict[key] = value | |
if expect(",") { | |
continue | |
} else if expect("}") { | |
return .object(dict) | |
} else { | |
return .fail | |
} | |
} | |
} | |
func parseArray() -> JSON { | |
guard expect("[") else { return .fail } | |
var arr:[JSON] = [] | |
if expect("]") { | |
return .array(arr) | |
} | |
while true { | |
let json = parse() | |
if case JSON.fail = json { return .fail } | |
arr.append(json) | |
if expect(",") { | |
continue | |
} else if expect("]") { | |
return .array(arr) | |
} else { | |
return .fail | |
} | |
} | |
} | |
} | |
//test | |
let str = "{\"widget\": {\n \"debug\": \"on\",\n \"window\": {\n \"title\": \"Sample Konfabulator Widget\",\n \"name\": \"main_window\",\n \"width\": 500,\n \"height\": 500\n },\n \"image\": { \n \"src\": \"Images/Sun.png\",\n \"name\": \"sun1\",\n \"hOffset\": 250,\n \"vOffset\": 250,\n \"alignment\": \"center\"\n },\n \"text\": {\n \"data\": \"Click Here\",\n \"size\": 36,\n \"style\": \"bold\",\n \"name\": \"text1\",\n \"hOffset\": 250,\n \"vOffset\": 100,\n \"alignment\": \"center\",\n \"onMouseUp\": \"sun1.opacity = (sun1.opacity / 100) * 90;\"\n }\n}} " | |
print(JSON(string: str).serialize()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment