Skip to content

Instantly share code, notes, and snippets.

@xplorld
Last active February 18, 2017 15:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xplorld/0e11c18a4e2d02505c5d5efca1335c30 to your computer and use it in GitHub Desktop.
Save xplorld/0e11c18a4e2d02505c5d5efca1335c30 to your computer and use it in GitHub Desktop.
a JSON parser in Swift without Cocoa or Regular Expressions.
//
// 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