Skip to content

Commit 50245e7

Browse files
committed
qjson
1 parent d8de048 commit 50245e7

File tree

3 files changed

+293
-0
lines changed

3 files changed

+293
-0
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# `qjson.lua`
2+
3+
Tiny and Rapid JSON encoding/decoding in pure Lua.
4+
5+
(Running benchmark by [`rxi/json.lua`](https://github.com/rxi/json.lua))
6+
```
7+
Lua version : LuaJIT 2.1.0-beta3
8+
CPU name : 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
9+
../json.lua : 0.00494s [x7.06e+03] (min: 0.00452s, max 0.00526s)
10+
dkjson.lua : 0.0121s [x1.73e+04] (min: 0.0118s, max 0.0123s)
11+
jfjson.lua : 0.00993s [x1.42e+04] (min: 0.00954s, max 0.0103s)
12+
qjson_simple.lua : 4.3e-06s [x6.14] (min: 0s, max 1.6e-05s)
13+
qjson.lua : 7e-07s [x1] (min: 0s, max 1e-06s)
14+
```
15+
16+
## Features
17+
* Pure lua, should work on every version (5.1-5.4, JIT)
18+
* Incredibly fast: ~10000x faster than rxi/json at decoding.
19+
* Actually tiny, < 150 loc.
20+
* Decent error handling: `Expected : to follow key for object at char 39`
21+
22+
## Usage
23+
```lua
24+
local json = require "qjson"
25+
print(json.encode {
26+
hello = "world!",
27+
})
28+
29+
--[[
30+
{"qjson": ["fast","simple","tiny"],"hello": "world!"}
31+
]]
32+
33+
print(json.decode([[
34+
{ "foo": "bar" }
35+
]]))
36+
```
37+
38+
## Notes
39+
* `true`, `false` and `null` are represented as strings.
40+
41+
## qjson_simple
42+
43+
Smaller initial version of `qjson` without all of the optimizations that make it less readable.
44+
45+
Often runs worse than `qjson`, but can run at around the same speed.
46+
47+
Doesn't support escaping strings, exponential numbers, or `null`.

qjson.lua

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
local find, sub, gsub, tonumber, error = string.find, string.sub, string.gsub, tonumber, error
2+
local function decode(json --[[@param json string]]) ---@return table
3+
local ptr = 0
4+
local function consume(pattern --[[@param pattern string]]) ---@return string?
5+
local _, finish, match = find(json, pattern, ptr)
6+
if finish then
7+
ptr = finish + 1
8+
return match or true
9+
end
10+
return nil
11+
end
12+
13+
local repl = { -- escape characters
14+
["b"] = "\b", ["f"] = "\f", ["n"] = "\n", ["r"] = "\r",
15+
["t"] = "\t", ["\\"] = "\\", ["\""] = "\""
16+
}
17+
18+
local function slowstring() -- string parser that works with escapes. fallback at very far end of parsing order, since rare case.
19+
local _, start = find(json, "^%s*\"", ptr)
20+
if not start then return end
21+
ptr = start + 1
22+
23+
while true do
24+
local _, finish, escapes = find(json, "(\\*)\"", ptr)
25+
if finish then
26+
ptr = finish + 1
27+
if #escapes % 2 == 0 then
28+
return ( gsub( sub(json, start + 1, finish - 1), "\\([bfnrt\\\"])", repl) )
29+
end
30+
else
31+
error("Missing end quote for string at char " .. ptr)
32+
end
33+
end
34+
end
35+
36+
local object, array
37+
local function faststring() -- Parses a string without any escapes. Fastest and most common case.
38+
return consume("^%s*\"([^\"\\]*)\"")
39+
end
40+
41+
--- Parses a JSON value.
42+
--- Order is important for performance with common cases.
43+
local function value()
44+
return faststring()
45+
or object()
46+
or tonumber(consume("^%s*(%-?%d+%.%d+)") or consume("^%s*(%-?%d+)"))
47+
or consume("^%s*(true)") or consume("^%s*(false)") or consume("^%s*(null)")
48+
or array()
49+
or tonumber(consume("^%s*(%-?%d+[eE][%+%-]?%d+)")) -- Exponential number.
50+
or slowstring()
51+
end
52+
53+
function object()
54+
if consume("^%s*{") then
55+
local fields = {}
56+
if consume("^%s*}") then return fields end
57+
58+
repeat
59+
local key = faststring()
60+
if not key then
61+
error("Expected field for object at char " .. ptr)
62+
end
63+
64+
if not consume("^%s*:") then
65+
error("Expected : to follow key for object at char " .. ptr)
66+
end
67+
68+
local val = value()
69+
if val then
70+
fields[key] = val
71+
else
72+
error("Expected value for field " .. key .. " at char " .. ptr)
73+
end
74+
75+
consume("^%s*,")
76+
until consume("^%s*}")
77+
78+
return fields
79+
end
80+
end
81+
82+
function array()
83+
if consume("^%s*%[") then
84+
local values, nvalues = {}, 0
85+
if consume("^%s*%]") then return values end
86+
87+
repeat
88+
nvalues = nvalues + 1
89+
local value = value()
90+
if value then
91+
values[nvalues] = value
92+
else
93+
error("Expected value for field #" .. nvalues + 1 .. " at char " .. ptr)
94+
end
95+
consume("^%s*,")
96+
until consume("^%s*%]")
97+
98+
return values
99+
end
100+
end
101+
102+
return object()
103+
end
104+
105+
local concat, tostring, pairs = table.concat, tostring, pairs
106+
local function isarray(t)
107+
local i = 1
108+
for k in pairs(t) do
109+
if type(k) ~= "number" or k ~= i then
110+
return false
111+
end
112+
i = i + 1
113+
end
114+
return true
115+
end
116+
117+
local encode
118+
local function value(v)
119+
local t = type(v)
120+
if t == "table" then
121+
return encode(v)
122+
elseif t == "string" then
123+
return "\"" .. v .. "\""
124+
else
125+
return tostring(v)
126+
end
127+
end
128+
129+
function encode(tbl --[[@param tbl table]]) ---@return string
130+
if isarray(tbl) then
131+
local strs, len = {}, #tbl
132+
for i = 1, len do
133+
strs[i] = value(tbl[i])
134+
end
135+
return "[" .. concat(strs, ",", 1, len) .. "]"
136+
else
137+
local kvs, nkvs = {}, 0
138+
for k, v in pairs(tbl) do
139+
nkvs = nkvs + 1
140+
kvs[nkvs] = "\"" .. tostring(k) .. "\"" .. ": " .. value(v)
141+
end
142+
return "{" .. concat(kvs, ",", 1, nkvs) .. "}";
143+
end
144+
end
145+
146+
return {
147+
encode = encode,
148+
decode = decode
149+
}

qjson_simple.lua

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
local function decode(json --[[@param json string]]) ---@return table
2+
local ptr = 0
3+
local function consume(pattern --[[@param pattern string]]) ---@return string?
4+
local start, finish = json:find("^%s+", ptr)
5+
if start then ptr = finish + 1 end
6+
7+
local start, finish, match = json:find(pattern, ptr)
8+
if start then
9+
ptr = finish + 1
10+
return match or true
11+
end
12+
end
13+
14+
local table, list
15+
local function number() return tonumber(consume("^(%d+%.%d+)") or consume("^(%d+)")) end
16+
local function bool() return consume("^(true)") or consume("^(false)") end
17+
local function string() return consume("^\"([^\"]*)\"") end
18+
local function value() return number() or bool() or string() or table() or list() end
19+
20+
function table()
21+
if consume("^{") then
22+
local fields = {}
23+
if consume("^}") then return fields end
24+
25+
repeat
26+
local key = assert(string(), "Expected field for table")
27+
assert(consume("^:"))
28+
fields[key] = assert(value(), "Expected value for field " .. key)
29+
consume("^,")
30+
until consume("^}")
31+
32+
return fields
33+
end
34+
end
35+
36+
function list()
37+
if consume("^%[") then
38+
local values = {}
39+
if consume("^%]") then return values end
40+
41+
repeat
42+
values[#values + 1] = assert(value(), "Expected value for field #" .. #values + 1)
43+
consume("^,")
44+
until consume("^%]")
45+
46+
return values
47+
end
48+
end
49+
50+
return table()
51+
end
52+
53+
local concat, tostring, pairs = table.concat, tostring, pairs
54+
local function isarray(t)
55+
local i = 1
56+
for k in pairs(t) do
57+
if type(k) ~= "number" or k ~= i then
58+
return false
59+
end
60+
i = i + 1
61+
end
62+
return true
63+
end
64+
65+
local encode
66+
local function value(v)
67+
local t = type(v)
68+
if t == "table" then
69+
return encode(v)
70+
elseif t == "string" then
71+
return "\"" .. v .. "\""
72+
else
73+
return tostring(v)
74+
end
75+
end
76+
77+
function encode(tbl --[[@param tbl table]]) ---@return string
78+
if isarray(tbl) then
79+
local strs, len = {}, #tbl
80+
for i = 1, len do
81+
strs[i] = value(tbl[i])
82+
end
83+
return "[" .. concat(strs, ",", 1, len) .. "]"
84+
else
85+
local kvs, nkvs = {}, 0
86+
for k, v in pairs(tbl) do
87+
nkvs = nkvs + 1
88+
kvs[nkvs] = tostring(k) .. ": " .. value(v)
89+
end
90+
return "{" .. concat(kvs, ",", 1, nkvs) .. "}";
91+
end
92+
end
93+
94+
return {
95+
encode = encode,
96+
decode = decode
97+
}

0 commit comments

Comments
 (0)