Skip to content

Commit f08a3a6

Browse files
feature: Adds ZincReader and tests
1 parent 76d47de commit f08a3a6

File tree

2 files changed

+572
-0
lines changed

2 files changed

+572
-0
lines changed
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import Foundation
2+
3+
public class ZincReader {
4+
let tokenizer: ZincTokenizer
5+
6+
var cur: ZincToken = .eof
7+
var curVal: any Val = null
8+
var curLine: Int = 0
9+
var peek: ZincToken = .eof
10+
var peekVal: any Val = null
11+
var peekLine: Int = 0
12+
13+
14+
public init(_ zinc: String) throws {
15+
tokenizer = ZincTokenizer(zinc)
16+
try consume()
17+
try consume()
18+
}
19+
20+
public func readVal() throws -> any Val {
21+
var val: any Val = null
22+
if cur == .id, curVal.equals("ver") {
23+
val = try parseGrid()
24+
} else {
25+
val = try parseVal()
26+
}
27+
try verify(.eof)
28+
return val
29+
}
30+
31+
public func readGrid() throws -> Grid {
32+
guard let grid = try readVal() as? Grid else {
33+
throw ZincReaderError.InputIsNotGrid
34+
}
35+
return grid
36+
}
37+
38+
private func parseVal() throws -> any Val {
39+
if cur == .id {
40+
guard let id = curVal as? String else {
41+
throw ZincReaderError.IdValueIsNotString(curVal)
42+
}
43+
try consume(.id)
44+
45+
// check for Coord or XStr
46+
if cur == .lparen {
47+
if peek == .num {
48+
return try parseCoord(id)
49+
} else {
50+
return try parseXStr(id)
51+
}
52+
}
53+
54+
switch id {
55+
case "T": return true
56+
case "F": return false
57+
case "N": return null
58+
case "M": return marker
59+
case "R": return remove
60+
case "NA": return na
61+
case "NaN": return Number(val: .nan)
62+
case "INF": return Number(val: .infinity)
63+
default:
64+
throw ZincReaderError.UnexpectedId(id)
65+
}
66+
}
67+
68+
if cur.isLiteral {
69+
return try parseLiteral()
70+
}
71+
72+
// -INF
73+
if cur == .minus, peek == .id, peekVal.equals("INF") {
74+
try consume(.minus)
75+
try consume(.id)
76+
return Number(val: -1.0 * .infinity)
77+
}
78+
79+
if cur == .lbracket {
80+
return try parseList()
81+
}
82+
if cur == .lbrace {
83+
return try parseDict()
84+
}
85+
if cur == .lt2 {
86+
return try parseGrid()
87+
}
88+
89+
throw ZincReaderError.UnexpectedToken(cur)
90+
}
91+
92+
private func parseCoord(_ id: String) throws -> Coord {
93+
guard id == "C" else {
94+
throw ZincReaderError.InvalidCoord
95+
}
96+
try consume(.lparen)
97+
let latitude = try consumeNumber()
98+
try consume(.comma)
99+
let longitude = try consumeNumber()
100+
try consume(.rparen)
101+
return Coord(lat: latitude.val, lng: longitude.val)
102+
}
103+
104+
private func parseXStr(_ id: String) throws -> XStr {
105+
guard (id.first?.isLowercase ?? false) else {
106+
throw ZincReaderError.InvalidXStr
107+
}
108+
try consume(.lparen)
109+
let val = try consumeString()
110+
try consume(.rparen)
111+
return XStr(type: id, val: val)
112+
}
113+
114+
private func parseLiteral() throws -> any Val {
115+
var val = self.curVal
116+
if cur == .ref, peek == .str {
117+
guard let refVal = curVal as? String, let dis = peekVal as? String else {
118+
throw ZincReaderError.InvalidRef
119+
}
120+
val = Ref(val: refVal, dis: dis)
121+
try consume(.ref)
122+
}
123+
try consume()
124+
return val
125+
}
126+
127+
private func parseList() throws -> List {
128+
var elements = [any Val]()
129+
while cur != .rbracket, cur != .eof {
130+
try elements.append(parseVal())
131+
guard cur == .comma else {
132+
break
133+
}
134+
try consume(.comma)
135+
}
136+
try consume(.rbracket)
137+
return List(elements)
138+
}
139+
140+
private func parseDict() throws -> Dict {
141+
var elements = [String: any Val]()
142+
let hasBraces = cur == .lbrace
143+
if hasBraces {
144+
try consume(.lbrace)
145+
}
146+
while cur == .id {
147+
let id = try consumeTagName()
148+
var val: any Val = marker
149+
if cur == .colon {
150+
try consume(.colon)
151+
val = try parseVal()
152+
}
153+
elements[id] = val
154+
}
155+
if hasBraces {
156+
try consume(.rbrace)
157+
}
158+
159+
return Dict(elements)
160+
}
161+
162+
private func parseGrid() throws -> Grid {
163+
let isNested = cur == .lt2
164+
if isNested {
165+
try consume(.lt2)
166+
if cur == .nl {
167+
try consume(.nl)
168+
}
169+
}
170+
171+
// Check version
172+
guard cur == .id, curVal.equals("ver") else {
173+
throw ZincReaderError.GridDoesNotBeginWithVersion(curVal)
174+
}
175+
try consume()
176+
try consume(.colon)
177+
let version = try consumeString()
178+
guard version == "3.0" else {
179+
throw ZincReaderError.UnsupportedZincVersion(version)
180+
}
181+
182+
// Metadata
183+
let builder = GridBuilder()
184+
if cur == .id {
185+
try builder.setMeta(parseDict().elements)
186+
}
187+
try consume(.nl)
188+
189+
// Columns
190+
var numCols = 0
191+
while cur == .id {
192+
numCols += 1
193+
let name = try consumeTagName()
194+
var colMeta: [String: any Val]? = nil
195+
if cur == .id {
196+
colMeta = try parseDict().elements
197+
}
198+
try builder.addCol(name: name, meta: colMeta)
199+
200+
guard cur == .comma else {
201+
break
202+
}
203+
try consume(.comma)
204+
}
205+
guard numCols > 0 else {
206+
throw ZincReaderError.GridHasNoColumns
207+
}
208+
try consume(.nl)
209+
210+
// Rows
211+
while true {
212+
if cur == .nl || cur == .eof || (isNested && cur == .gt2) {
213+
break
214+
}
215+
216+
var cells = [any Val]()
217+
for i in 0 ..< numCols {
218+
if cur == .comma || cur == .nl || cur == .eof {
219+
cells.append(null)
220+
} else {
221+
try cells.append(parseVal())
222+
}
223+
if i+1 < numCols {
224+
try consume(.comma)
225+
}
226+
}
227+
try builder.addRow(cells)
228+
229+
if cur == .eof || (isNested && cur == .gt2) {
230+
break
231+
}
232+
try consume(.nl)
233+
}
234+
235+
if cur == .nl {
236+
try consume(.nl)
237+
}
238+
if isNested {
239+
try consume(.gt2)
240+
}
241+
return builder.toGrid()
242+
}
243+
244+
// MARK: Token Reads
245+
246+
private func consumeTagName() throws -> String {
247+
try verify(.id)
248+
guard let id = curVal as? String else {
249+
throw ZincReaderError.IdValueIsNotString(curVal)
250+
}
251+
guard (id.first?.isLowercase ?? false) else {
252+
throw ZincReaderError.InvalidTagName
253+
}
254+
try consume(.id)
255+
return id
256+
}
257+
258+
private func consumeNumber() throws -> Number {
259+
try verify(.num)
260+
guard let number = curVal as? Number else {
261+
throw ZincReaderError.NumValueIsNotNumber(curVal)
262+
}
263+
try consume(.num)
264+
return number
265+
}
266+
267+
private func consumeString() throws -> String {
268+
try verify(.str)
269+
guard let number = curVal as? String else {
270+
throw ZincReaderError.StrValueIsNotString(curVal)
271+
}
272+
try consume(.str)
273+
return number
274+
}
275+
276+
private func consume(_ expected: ZincToken? = nil) throws {
277+
if let expected = expected {
278+
try verify(expected)
279+
}
280+
cur = peek;
281+
curVal = peekVal;
282+
curLine = peekLine;
283+
284+
peek = try tokenizer.next();
285+
peekVal = tokenizer.val;
286+
peekLine = tokenizer.line;
287+
}
288+
289+
private func verify(_ expected: ZincToken) throws {
290+
if cur != expected {
291+
throw ZincReaderError.ExpectedToken(expected, not: cur)
292+
}
293+
}
294+
}
295+
296+
enum ZincReaderError: Error {
297+
case ExpectedToken(ZincToken, not: ZincToken)
298+
case GridDoesNotBeginWithVersion(any Val)
299+
case GridHasNoColumns
300+
case IdValueIsNotString(any Val)
301+
case InvalidCoord
302+
case InvalidRef
303+
case InvalidTagName
304+
case InvalidXStr
305+
case NumValueIsNotNumber(any Val)
306+
case StrValueIsNotString(any Val)
307+
case UnexpectedId(String)
308+
case UnexpectedToken(ZincToken)
309+
case InputIsNotGrid
310+
case UnsupportedZincVersion(String)
311+
}

0 commit comments

Comments
 (0)