ABSTRACT
In the last post, we created a basic recursive-descent parser function that almost gives us what we want, but we still have some errors.
We had previously “dumbed-down” the errors to reduce the noise, but they are now so simple that they don’t give us much information.
TURN THE VOLUME UP
The first thing that we should do, is have the failure messages give us more information.
We also don’t really need to know when something works. We’re more interested in when it doesn’t work, so we’ll turn off the “Passed!” lines:
// Test Set 1 jsonInterpretations.forEach { let parsedResult = recursiveDescentParser($0) if whatWeWantToSee != parsedResult { print("ERROR! Bad Result for \($0)") } else { // print("\($0) Passes!") } }
Run the playground again, and you’ll get this:
ERROR! Bad Result for ["containerElement": <__nsarrayi 0x600003838000>( { arrayElement = ( { attributes = ( { index = 0; } ); }, { value = "Element Value 01"; } ); }, { arrayElement = ( { attributes = ( { index = 1; } ); }, { value = "Element Value 02"; } ); }, { arrayElement = ( { attributes = ( { index = 2; } ); }, { value = "Element Value 03"; } ); }, { arrayElement = ( { attributes = ( { index = 3; } ); }, { value = "Element Value 04"; } ); }, { arrayElement = ( { attributes = ( { index = 4; } ); }, { value = "Element Value 05"; } ); } ) ] · · · ERROR! Bad Result for ["containerElement": <__nsarrayi 0x60000383c480>( { arrayElement = { attributes = ( { row = 3; }, { value = "Element Value 04"; } ); }; }, { arrayElement = { attributes = ( { row = 2; }, { value = "Element Value 03"; } ); }; }, { arrayElement = { attributes = ( { row = 1; }, { value = "Element Value 02"; } ); }; }, { arrayElement = { attributes = ( { row = 4; }, { value = "Element Value 05"; } ); }; }, { arrayElement = { attributes = ( { row = 0; }, { value = "Element Value 01"; } ); }; } ) ]
We can see that there’s something these all have in common. They all have their String values wrapped in “value
” Dictionary elements.
That shouldn’t be a problem, as we don’t care what the element containers are called. Let’s look at each one a bit more closely:
This is the code that loads the first failure JSON:
jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": [{\"attributes\": [{\"index\": \"0\"}]}, {\"value\": \"Element Value 01\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"1\"}]}, {\"value\": \"Element Value 02\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"2\"}]}, {\"value\": \"Element Value 03\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"3\"}]}, {\"value\": \"Element Value 04\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"4\"}]}, {\"value\": \"Element Value 05\"}]}]}"))
What’s different? Maybe it’s that “index” attribute? Let’s look at the response, and see why it’s failing the test:
print("ERROR! Bad Result: \(parsedResult) for \($0)")
Run the playground again, and get:
ERROR! Bad Result: ["0", "Element Value 01", "1", "Element Value 02", "2", "Element Value 03", "3", "Element Value 04", "4", "Element Value 05"] for ["containerElement": <__nsarrayi 0x60000046b440>( { arrayElement = ( { attributes = ( { index = 0; } ); }, { value = "Element Value 01"; } ); }, · · ·
Before each “Element
,” you get an integer.
Yup. Looks like the index
is the problem. We need to ignore it.
It’s fairly straightforward to specifically add an ignore for the index
element:
func recursiveDescentParser(_ inDictionaryToBeParsed: [AnyHashable: Any]) -> [String] { var ret = [String]() inDictionaryToBeParsed.forEach { if let asString = $0.value as? String, let keyString = $0.key as? String, keyString != "index" { ret += [asString] } else if let asDictionary = $0.value as? [AnyHashable: Any] { ret += recursiveDescentParser(asDictionary) } else if let asDictionaryArray = $0.value as? [[AnyHashable: Any]] { asDictionaryArray.forEach { elem in ret += recursiveDescentParser(elem) } } else if let asArray = $0.value as? [String] { asArray.forEach { elem in ret += recursiveDescentParser(["": elem]) } } } return ret }
Run the playground again, and you get:
ERROR! Bad Result: ["0", "Element Value 01", "1", "Element Value 02", "2", "Element Value 03", "3", "Element Value 04", "4", "Element Value 05"] for ["containerElement": <__nsarrayi 0x600001990d80>( { arrayElement = { attributes = ( { row = 0; }, { value = "Element Value 01"; } ); }; }, { arrayElement = { attributes = ( { row = 1; }, { value = "Element Value 02"; } ); }; }, { arrayElement = { attributes = ( { row = 2; }, { value = "Element Value 03"; } ); }; }, { arrayElement = { attributes = ( { row = 3; }, { value = "Element Value 04"; } ); }; }, { arrayElement = { attributes = ( { row = 4; }, { value = "Element Value 05"; } ); }; } ) ] · · · ERROR! Bad Result: ["Element Value 04", "Element Value 03", "Element Value 02", "Element Value 05", "Element Value 01"] for ["containerElement": <__nsarrayi 0x60000199c100>( { arrayElement = { attributes = ( { row = 3; }, { value = "Element Value 04"; } ); }; }, { arrayElement = { attributes = ( { row = 2; }, { value = "Element Value 03"; } ); }; }, { arrayElement = { attributes = ( { row = 1; }, { value = "Element Value 02"; } ); }; }, { arrayElement = { attributes = ( { row = 4; }, { value = "Element Value 05"; } ); }; }, { arrayElement = { attributes = ( { row = 0; }, { value = "Element Value 01"; } ); }; } ) ]
D’oh! So close! Now, we see that we have “row
” values. Looks like we need to ignore them, as well. So we can do this:
func recursiveDescentParser(_ inDictionaryToBeParsed: [AnyHashable: Any]) -> [String] { var ret = [String]() inDictionaryToBeParsed.forEach { if let asString = $0.value as? String, let keyString = $0.key as? String, keyString != "index", keyString != "row" { ret += [asString] } else if let asDictionary = $0.value as? [AnyHashable: Any] { ret += recursiveDescentParser(asDictionary) } else if let asDictionaryArray = $0.value as? [[AnyHashable: Any]] { asDictionaryArray.forEach { elem in ret += recursiveDescentParser(elem) } } else if let asArray = $0.value as? [String] { asArray.forEach { elem in ret += recursiveDescentParser(["": elem]) } } } return ret }
Run the playground again, and you are left with one error:
ERROR! Bad Result: ["Element Value 04", "Element Value 03", "Element Value 02", "Element Value 05", "Element Value 01"] for ["containerElement": <__nsarrayi 0x600000218880>( { arrayElement = { attributes = ( { row = 3; }, { value = "Element Value 04"; } ); }; }, { arrayElement = { attributes = ( { row = 2; }, { value = "Element Value 03"; } ); }; }, { arrayElement = { attributes = ( { row = 1; }, { value = "Element Value 02"; } ); }; }, { arrayElement = { attributes = ( { row = 4; }, { value = "Element Value 05"; } ); }; }, { arrayElement = { attributes = ( { row = 0; }, { value = "Element Value 01"; } ); }; } ) ]
It’s fairly easy to see what’s going on here.
We are expecting to see this: ["Element Value 01", "Element Value 02", "Element Value 03", "Element Value 04", "Element Value 05"]
.
Instead, we are getting this: ["Element Value 04", "Element Value 03", "Element Value 02", "Element Value 05", "Element Value 01"]
The order is different.
That can be solved fairly easily:
func recursiveDescentParser(_ inDictionaryToBeParsed: [AnyHashable: Any]) -> [String] { var ret = [String]() inDictionaryToBeParsed.forEach { if let asString = $0.value as? String, let keyString = $0.key as? String, keyString != "index", keyString != "row" { ret += [asString] } else if let asDictionary = $0.value as? [AnyHashable: Any] { ret += recursiveDescentParser(asDictionary) } else if let asDictionaryArray = $0.value as? [[AnyHashable: Any]] { asDictionaryArray.forEach { elem in ret += recursiveDescentParser(elem) } } else if let asArray = $0.value as? [String] { asArray.forEach { elem in ret += recursiveDescentParser(["": elem]) } } } return ret.sorted() }
Run the playground again.
Ah… blessed silence. No console messages. All tests are passing.
SUCCESS!
W00t! That means we’re done, right?
Yeah…not really. Remember that we commented out a second set of tests. Uncomment them (also make the error report a bit more verbose, like the first set of tests):
// Test Set 2 jsonInterpretationsSecondaryTests.forEach { let parsedResult = recursiveDescentParser($0) if whatWeWantToSee2 != parsedResult { print("ERROR! Bad Result: \(parsedResult) for \($0)") } else { print("\($0) Passes!") } }
…and run the playground again:
ERROR! Bad Result: ["0123456789!@~*&^%$#.,;:?/\'", "12345.6789", "3.14"] for ["containerElement": <__nsarrayi 0x600001d6c8c0>( { arrayElement = { attributes = ( { row = 3; }, { value = 3; } ); }; }, { arrayElement = { attributes = ( { row = 2; }, { value = "3.14"; } ); }; }, { arrayElement = { attributes = ( { index = 1; }, { value = "0123456789!@~*&^%$#.,;:?/'"; } ); }; }, { arrayElement = { attributes = ( { row = 4; }, { value = 1234567890; } ); }; }, { arrayElement = { attributes = ( { index = 0; }, { value = "12345.6789"; } ); }; } ) ] ["containerElement": <__nsarrayi 0x600001d6ed80>( { arrayElement = 3; }, { arrayElement = "3.14"; }, { arrayElement = "0123456789!@~*&^%$#.,;:?/'"; }, { arrayElement = 1234567890; }, { arrayElement = "12345.6789"; } ) ] Passes! ERROR! Bad Result: ["0123456789!@~*&^%$#.,;:?/\'"] for ["containerElement": <__nsarrayi 0x600001d6b380>( { arrayElement = 3; }, { arrayElement = "3.14"; }, { arrayElement = "0123456789!@~*&^%$#.,;:?/'"; }, { arrayElement = 1234567890; }, { arrayElement = "12345.6789"; } ) ]
We’re getting some pretty strange results. Not all the tests are failing, but we do have two failures.
We’re expecting this: ["0123456789!@~*&^%$#.,;:?/'", "12345.6789", "1234567890", "3", "3.14"]
.
We’re getting this: ["0123456789!@~*&^%$#.,;:?/\'", "12345.6789", "3.14"]
, or this: ["0123456789!@~*&^%$#.,;:?/\'"]
.
That’s weird. What’s going on? Let’s look at the source:
jsonInterpretationsSecondaryTests.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": {\"attributes\": [{\"row\": 3}, {\"value\": 3}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 2}, {\"value\": \"3.14\"}]}}, {\"arrayElement\": {\"attributes\": [{\"index\": 1}, {\"value\": \"0123456789!@~*&^%$#.,;:?/'\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 4}, {\"value\": 1234567890}]}}, {\"arrayElement\": {\"attributes\": [{\"index\": 0}, {\"value\": \"12345.6789\"}]}}]}"))
and:
jsonInterpretationsSecondaryTests.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": 3}, {\"arrayElement\": 3.14}, {\"arrayElement\": \"0123456789!@~*&^%$#.,;:?/'\"}, {\"arrayElement\": 1234567890}, {\"arrayElement\": 12345.6789}]}"))
The middle one passes:
jsonInterpretationsSecondaryTests.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": \"3\"}, {\"arrayElement\": \"3.14\"}, {\"arrayElement\": \"0123456789!@~*&^%$#.,;:?/'\"}, {\"arrayElement\": \"1234567890\"}, {\"arrayElement\": \"12345.6789\"}]}"))
What is the difference between them?
Ah! It’s the way the JSON is specifying the numbers!
They have Int and Float values without quotes. This is perfectly valid JSON. However, it does mean that our String test fails. We’ll need to add another test for the numbers.
This isn’t quite as straightforward as you’d think. Integers are really floating-point numbers without fractional components; or, at least, that’s how a String parser would look at it.
We should parse for floats, and convert them to String. Remember that our output is an Array of String, so we’ll need to read them, and convert to String. Apple’s Foundation Framework has a NumberFormatter
class. We can use that to convert the floats to String, and not add things like “.0” after integers.
So we’ll need to add one more case to our tests (remember to add the test for the index/row):
func recursiveDescentParser(_ inDictionaryToBeParsed: [AnyHashable: Any]) -> [String] { var ret = [String]() inDictionaryToBeParsed.forEach { if let asFloat = ($0.value as? Double ?? Double($0.value as? String ?? "ERROR")) { let formatter = NumberFormatter() formatter.minimumIntegerDigits = 1 formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 20 if let keyString = $0.key as? String, keyString != "index", keyString != "row" { ret += [String(formatter.string(from: asFloat as NSNumber) ?? "")] } } else if let asString = $0.value as? String, let keyString = $0.key as? String, keyString != "index", keyString != "row" { ret += [asString] } else if let asDictionary = $0.value as? [AnyHashable: Any] { ret += recursiveDescentParser(asDictionary) } else if let asDictionaryArray = $0.value as? [[AnyHashable: Any]] { asDictionaryArray.forEach { elem in ret += recursiveDescentParser(elem) } } else if let asArray = $0.value as? [String] { asArray.forEach { elem in ret += recursiveDescentParser(["": elem]) } } } return ret.sorted() }
Woah…that’s some strange code. What did we just do?
Let’s break down that weird, question-mark-filled line:
let asFloat = ($0.value as? Double ?? Double($0.value as? String ?? "ERROR"))
What the heck does that all mean?
This is where Swift kinda rocks. It’s an optional chaining trick. Let’s walk through it:
let asFloat = ($0.value as? Double ?? Double($0.value as? String ?? "ERROR"))
OK. We try to assign a value to the “asFloat” contextual variable. If the JSON parser gave us an Int/Double/Float, then it’s sorted. No need to go any further. We need a Double for the NumberFormatter (not a Float or Int).
However, if the parser gave us a String, the assignment would fail. We use the double-question-mark failover to try a different assignment:
let asFloat = ($0.value as? Double ?? Double($0.value as? String ?? "ERROR"))
If the String assignment fails (for example, it’s an Array or a Dictionary), then we want the Double cast to completely bork, so we feed it “ERROR” (“NIN” would probably also work, but I’m not sure if the keyword would make a difference). There needs to be a String, going into the parser. If the parser can’t extract a Double from that String, it will return nil, which is what we want.
In this instance, the cast can fail (allowing us to move on to other tests). But it does allow us to have a one-line parser that can get an Int, Float, Double or String, and convert to a Double.
That’s pretty cool.
SUCCESS! (NO, REALLY, THIS TIME)
Let’s quieten the pass report, as well, so we get blessed silence. That was nice:
// Test Set 2 jsonInterpretationsSecondaryTests.forEach { let parsedResult = recursiveDescentParser($0) if whatWeWantToSee2 != parsedResult { print("ERROR! Bad Result: \(parsedResult) for \($0)") } else { // print("\($0) Passes!") } }
OK. Run the playground again. Lean back, and smugly enjoy the silence…
IMPROVEMENTS
OK. We’re passing all the tests. We’re done, right?
No, not really. We have some awkward code in there. We should probably do a bit of refactoring while we have the car up on blocks and the hood open.
In particular, the hardcoded tests for “index” and “row” are just downright ugly. We need to do something better.
We should probably add the capacity to pass in an arbitrary Array of String, containing “not allowed” Strings. The parser can check against that list, and simply not use anything on the blacklist.
We’ll need to make sure that this list gets passed down the recursion, so it will take a bit of coding. Not too bad:
func recursiveDescentParser(_ inDictionaryToBeParsed: [AnyHashable: Any], blackList inBlacklist: [String] = []) -> [String] { var ret = [String]() inDictionaryToBeParsed.forEach { if let asFloat = ($0.value as? Double ?? Double($0.value as? String ?? "ERROR")) { let formatter = NumberFormatter() formatter.minimumIntegerDigits = 1 formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 20 if let keyString = $0.key as? String, !inBlacklist.contains(keyString) { ret += [String(formatter.string(from: asFloat as NSNumber) ?? "")] } } else if let asString = $0.value as? String, let keyString = $0.key as? String, !inBlacklist.contains(keyString) { ret += [asString] } else if let asDictionary = $0.value as? [AnyHashable: Any] { ret += recursiveDescentParser(asDictionary, blackList: inBlacklist, blackList: inBlacklist) } } else if let asArray = $0.value as? [String] { asArray.forEach { elem in ret += recursiveDescentParser(["": elem], blackList: inBlacklist) } } } return ret.sorted() } // Test Set 1 jsonInterpretations.forEach { let parsedResult = recursiveDescentParser($0, blackList: ["index", "row"]) if whatWeWantToSee != parsedResult { print("ERROR! Bad Result: \(parsedResult) for \($0)") } else { // print("\($0) Passes!") } } // Test Set 2 jsonInterpretationsSecondaryTests.forEach { let parsedResult = recursiveDescentParser($0, blackList: ["index", "row"]) if whatWeWantToSee2 != parsedResult { print("ERROR! Bad Result: \(parsedResult) for \($0)") } else { // print("\($0) Passes!") } }
Run the playground again. Silence, still…
A nice thing about TDD, is that the tests will help to ensure that future work will not introduce new artifacts. We just did a fairly significant refactoring for a “nice to have” feature, and didn’t even break a sweat, because we knew the tests would tell us if there was an issue.
That said, you need to be careful. TDD tests are often “low-hanging fruit” tests. They only test the obvious or calculated issues that we thought of while designing. Most of us have had the experience where, on Day One of release, users find bugs that we NEVER would have thought of.
It’s not a panacea. There Ain’t No Such Thing As A Free Lunch. We need to test, beta-test, and be prepared to have bugs.
CONCLUSION
We’ve written a really tiny little function (about 25 lines of code) that has some pretty big juju. However, it could probably be refactored to be more efficient and flexible. For example, if we added a “whitelist,” it would be far more useful. You could use it to extract all the instances of some particular element; not just every element not on the blacklist.
It is also in a fairly small problem domain. Data exchange is a big, messy world, and things can get very, very ugly. In real life, you’d probably want to make this more flexible and robust; maybe add more error trapping and a larger suite of tests, as well as the ability to parse for more data types.
Nonetheless, it does its job quite nicely, and in a ridiculously small code footprint.
SAMPLE PLAYGROUND
/* <containerElement> <arrayElement>Element Value 01</arrayElement> <arrayElement>Element Value 02</arrayElement> <arrayElement>Element Value 03</arrayElement> <arrayElement>Element Value 04</arrayElement> <arrayElement>Element Value 05</arrayElement> </containerElement> <!-- {"containerElement": [ {"arrayElement": "Element Value 01"}, {"arrayElement": "Element Value 02"}, {"arrayElement": "Element Value 03"}, {"arrayElement": "Element Value 04"}, {"arrayElement": "Element Value 05"} ] } {"containerElement": {"arrayElement": [ "Element Value 01", "Element Value 02", "Element Value 03", "Element Value 04", "Element Value 05" ] } } {"containerElement": [ {"arrayElement": {"value": "Element Value 01"}}, {"arrayElement": {"value": "Element Value 02"}}, {"arrayElement": {"value": "Element Value 03"}}, {"arrayElement": {"value": "Element Value 04"}}, {"arrayElement": {"value": "Element Value 05"}} ] } --> <containerElement> <arrayElement> <value>Element Value 01</value> <value>Element Value 02</value> <value>Element Value 03</value> <value>Element Value 04</value> <value>Element Value 05</value> </arrayElement> </containerElement> <!-- {"containerElement": {"arrayElement": [ {"value": "Element Value 01"}, {"value": "Element Value 02"}, {"value": "Element Value 03"}, {"value": "Element Value 04"}, {"value": "Element Value 05"} ] } } {"containerElement": {"arrayElement": [ "Element Value 01", "Element Value 02", "Element Value 03", "Element Value 04", "Element Value 05" ] } } --> <containerElement> <arrayElement value="Element Value 01"/> <arrayElement value="Element Value 02"/> <arrayElement value="Element Value 03"/> <arrayElement value="Element Value 04"/> <arrayElement value="Element Value 05"/> </containerElement> <!-- {"containerElement": [ {"arrayElement": {"attributes": [{"value": "Element Value 01"}]}}, {"arrayElement": {"attributes": [{"value": "Element Value 02"}]}}, {"arrayElement": {"attributes": [{"value": "Element Value 03"}]}}, {"arrayElement": {"attributes": [{"value": "Element Value 04"}]}}, {"arrayElement": {"attributes": [{"value": "Element Value 05"}]}} ] } {"containerElement": [ {"arrayElement": {"attributes": {"value": "Element Value 01"}}}, {"arrayElement": {"attributes": {"value": "Element Value 02"}}}, {"arrayElement": {"attributes": {"value": "Element Value 03"}}}, {"arrayElement": {"attributes": {"value": "Element Value 04"}}}, {"arrayElement": {"attributes": {"value": "Element Value 05"}}} ] } --> <containerElement> <arrayElement index="0">Element Value 01</arrayElement> <arrayElement index="1">Element Value 02</arrayElement> <arrayElement index="2">Element Value 03</arrayElement> <arrayElement index="3">Element Value 04</arrayElement> <arrayElement index="4">Element Value 05</arrayElement> </containerElement> <!-- {"containerElement": [ {"arrayElement": [{"attributes": [{"index": 0}]}, {"value": "Element Value 01"}]}, {"arrayElement": [{"attributes": [{"index": 1}]}, {"value": "Element Value 02"}]}, {"arrayElement": [{"attributes": [{"index": 2}]}, {"value": "Element Value 03"}]}, {"arrayElement": [{"attributes": [{"index": 3}]}, {"value": "Element Value 04"}]}, {"arrayElement": [{"attributes": [{"index": 4}]}, {"value": "Element Value 05"}]} ] } {"containerElement": [ {"arrayElement": [{"attributes": [{"index": "0"}]}, {"value": "Element Value 01"}]}, {"arrayElement": [{"attributes": [{"index": "1"}]}, {"value": "Element Value 02"}]}, {"arrayElement": [{"attributes": [{"index": "2"}]}, {"value": "Element Value 03"}]}, {"arrayElement": [{"attributes": [{"index": "3"}]}, {"value": "Element Value 04"}]}, {"arrayElement": [{"attributes": [{"index": "4"}]}, {"value": "Element Value 05"}]} ] } {"containerElement": [ {"arrayElement": [{"attributes": {"index": "0"}}, {"value": "Element Value 01"}]}, {"arrayElement": [{"attributes": {"index": "1"}}, {"value": "Element Value 02"}]}, {"arrayElement": [{"attributes": {"index": "2"}}, {"value": "Element Value 03"}]}, {"arrayElement": [{"attributes": {"index": "3"}}, {"value": "Element Value 04"}]}, {"arrayElement": [{"attributes": {"index": "4"}}, {"value": "Element Value 05"}]} ] } {"containerElement": [ {"arrayElement": [{"attributes": [{"index": "0"}, {"value": "Element Value 01"}]}]}, {"arrayElement": [{"attributes": [{"index": "1"}, {"value": "Element Value 02"}]}]}, {"arrayElement": [{"attributes": [{"index": "2"}, {"value": "Element Value 03"}]}]}, {"arrayElement": [{"attributes": [{"index": "3"}, {"value": "Element Value 04"}]}]}, {"arrayElement": [{"attributes": [{"index": "4"}, {"value": "Element Value 05"}]}]} ] } --> <containerElement> <arrayElement row="0" value="Element Value 01"/> <arrayElement row="1" value="Element Value 02"/> <arrayElement row="2" value="Element Value 03"/> <arrayElement row="3" value="Element Value 04"/> <arrayElement row="4" value="Element Value 05"/> </containerElement> <!-- {"containerElement": [ {"arrayElement": {"attributes": [{"row": 0}, {"value": "Element Value 01"}]}}, {"arrayElement": {"attributes": [{"row": 1}, {"value": "Element Value 02"}]}}, {"arrayElement": {"attributes": [{"row": 2}, {"value": "Element Value 03"}]}}, {"arrayElement": {"attributes": [{"row": 3}, {"value": "Element Value 04"}]}}, {"arrayElement": {"attributes": [{"row": 4}, {"value": "Element Value 05"}]}} ] } {"containerElement": [ {"arrayElement": [{"attributes": [{"row": 0}, {"value": "Element Value 01"}]}]}, {"arrayElement": [{"attributes": [{"row": 1}, {"value": "Element Value 02"}]}]}, {"arrayElement": [{"attributes": [{"row": 2}, {"value": "Element Value 03"}]}]}, {"arrayElement": [{"attributes": [{"row": 3}, {"value": "Element Value 04"}]}]}, {"arrayElement": [{"attributes": [{"row": 4}, {"value": "Element Value 05"}]}]} ] } {"containerElement": [ {"arrayElement": {"attributes": [{"row": "0"}, {"value": "Element Value 01"}]}}, {"arrayElement": {"attributes": [{"row": "1"}, {"value": "Element Value 02"}]}}, {"arrayElement": {"attributes": [{"row": "2"}, {"value": "Element Value 03"}]}}, {"arrayElement": {"attributes": [{"row": "3"}, {"value": "Element Value 04"}]}}, {"arrayElement": {"attributes": [{"row": "4"}, {"value": "Element Value 05"}]}} ] } {"containerElement": [ {"arrayElement": [{"attributes": [{"row": "0"}, {"value": "Element Value 01"}]}]}, {"arrayElement": [{"attributes": [{"row": "1"}, {"value": "Element Value 02"}]}]}, {"arrayElement": [{"attributes": [{"row": "2"}, {"value": "Element Value 03"}]}]}, {"arrayElement": [{"attributes": [{"row": "3"}, {"value": "Element Value 04"}]}]}, {"arrayElement": [{"attributes": [{"row": "4"}, {"value": "Element Value 05"}]}]} ] } --> */ import Foundation func convertJSONToDictionary(_ inJSON: String) -> [String: Any] { do { if let stringData = inJSON.data(using: .utf8), let jsonDict = try JSONSerialization.jsonObject(with: stringData) as? [String: Any] { return jsonDict } } catch(let error) { print(String(describing: error)) } return [:] } var jsonInterpretations: [[String: Any]] = [] var jsonInterpretationsSecondaryTests: [[String: Any]] = [] func loadJSONIntoDictionaries() { // Initial test set jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": \"Element Value 01\"}, {\"arrayElement\": \"Element Value 02\"}, {\"arrayElement\": \"Element Value 03\"}, {\"arrayElement\": \"Element Value 04\"}, {\"arrayElement\": \"Element Value 05\"}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": {\"arrayElement\": [\"Element Value 01\", \"Element Value 02\", \"Element Value 03\", \"Element Value 04\", \"Element Value 05\"]}}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": {\"value\": \"Element Value 01\"}}, {\"arrayElement\": {\"value\": \"Element Value 02\"}}, {\"arrayElement\": {\"value\": \"Element Value 03\"}}, {\"arrayElement\": {\"value\": \"Element Value 04\"}}, {\"arrayElement\": {\"value\": \"Element Value 05\"}}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": {\"arrayElement\": [{\"value\": \"Element Value 01\"}, {\"value\": \"Element Value 02\"}, {\"value\": \"Element Value 03\"}, {\"value\": \"Element Value 04\"}, {\"value\": \"Element Value 05\"}]}}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": {\"arrayElement\": [\"Element Value 01\", \"Element Value 02\", \"Element Value 03\", \"Element Value 04\", \"Element Value 05\"]}}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": {\"attributes\": [{\"value\": \"Element Value 01\"}]}}, {\"arrayElement\": {\"attributes\": [{\"value\": \"Element Value 02\"}]}}, {\"arrayElement\": {\"attributes\": [{\"value\": \"Element Value 03\"}]}}, {\"arrayElement\": {\"attributes\": [{\"value\": \"Element Value 04\"}]}}, {\"arrayElement\": {\"attributes\": [{\"value\": \"Element Value 05\"}]}}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": {\"attributes\": {\"value\": \"Element Value 01\"}}}, {\"arrayElement\": {\"attributes\": {\"value\": \"Element Value 02\"}}}, {\"arrayElement\": {\"attributes\": {\"value\": \"Element Value 03\"}}}, {\"arrayElement\": {\"attributes\": {\"value\": \"Element Value 04\"}}}, {\"arrayElement\": {\"attributes\": {\"value\": \"Element Value 05\"}}}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": [{\"attributes\": [{\"index\": 0}]}, {\"value\": \"Element Value 01\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": 1}]}, {\"value\": \"Element Value 02\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": 2}]}, {\"value\": \"Element Value 03\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": 3}]}, {\"value\": \"Element Value 04\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": 4}]}, {\"value\": \"Element Value 05\"}]}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": [{\"attributes\": [{\"index\": \"0\"}]}, {\"value\": \"Element Value 01\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"1\"}]}, {\"value\": \"Element Value 02\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"2\"}]}, {\"value\": \"Element Value 03\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"3\"}]}, {\"value\": \"Element Value 04\"}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"4\"}]}, {\"value\": \"Element Value 05\"}]}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": [{\"attributes\": {\"index\": \"0\"}}, {\"value\": \"Element Value 01\"}]}, {\"arrayElement\": [{\"attributes\": {\"index\": \"1\"}}, {\"value\": \"Element Value 02\"}]}, {\"arrayElement\": [{\"attributes\": {\"index\": \"2\"}}, {\"value\": \"Element Value 03\"}]}, {\"arrayElement\": [{\"attributes\": {\"index\": \"3\"}}, {\"value\": \"Element Value 04\"}]}, {\"arrayElement\": [{\"attributes\": {\"index\": \"4\"}}, {\"value\": \"Element Value 05\"}]}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": [{\"attributes\": [{\"index\": \"0\"}, {\"value\": \"Element Value 01\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"1\"}, {\"value\": \"Element Value 02\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"2\"}, {\"value\": \"Element Value 03\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"3\"}, {\"value\": \"Element Value 04\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"index\": \"4\"}, {\"value\": \"Element Value 05\"}]}]}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": {\"attributes\": [{\"row\": 0}, {\"value\": \"Element Value 01\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 1}, {\"value\": \"Element Value 02\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 2}, {\"value\": \"Element Value 03\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 3}, {\"value\": \"Element Value 04\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 4}, {\"value\": \"Element Value 05\"}]}}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": [{\"attributes\": [{\"row\": 0}, {\"value\": \"Element Value 01\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"row\": 1}, {\"value\": \"Element Value 02\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"row\": 2}, {\"value\": \"Element Value 03\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"row\": 3}, {\"value\": \"Element Value 04\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"row\": 4}, {\"value\": \"Element Value 05\"}]}]}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": {\"attributes\": [{\"row\": \"0\"}, {\"value\": \"Element Value 01\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": \"1\"}, {\"value\": \"Element Value 02\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": \"2\"}, {\"value\": \"Element Value 03\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": \"3\"}, {\"value\": \"Element Value 04\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": \"4\"}, {\"value\": \"Element Value 05\"}]}}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": [{\"attributes\": [{\"row\": \"0\"}, {\"value\": \"Element Value 01\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"row\": \"1\"}, {\"value\": \"Element Value 02\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"row\": \"2\"}, {\"value\": \"Element Value 03\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"row\": \"3\"}, {\"value\": \"Element Value 04\"}]}]}, {\"arrayElement\": [{\"attributes\": [{\"row\": \"4\"}, {\"value\": \"Element Value 05\"}]}]}]}")) // Extra Credit jsonInterpretations.append(convertJSONToDictionary("{\"arrayElement\": [{\"value\": \"Element Value 01\"}, {\"value\": \"Element Value 02\"}, {\"value\": \"Element Value 03\"}, {\"value\": \"Element Value 04\"}, {\"value\": \"Element Value 05\"}]}")) jsonInterpretations.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": {\"attributes\": [{\"row\": 3}, {\"value\": \"Element Value 04\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 2}, {\"value\": \"Element Value 03\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 1}, {\"value\": \"Element Value 02\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 4}, {\"value\": \"Element Value 05\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 0}, {\"value\": \"Element Value 01\"}]}}]}")) // Differing Data jsonInterpretationsSecondaryTests.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": {\"attributes\": [{\"row\": 3}, {\"value\": 3}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 2}, {\"value\": \"3.14\"}]}}, {\"arrayElement\": {\"attributes\": [{\"index\": 1}, {\"value\": \"0123456789!@~*&^%$#.,;:?/'\"}]}}, {\"arrayElement\": {\"attributes\": [{\"row\": 4}, {\"value\": 1234567890}]}}, {\"arrayElement\": {\"attributes\": [{\"index\": 0}, {\"value\": \"12345.6789\"}]}}]}")) jsonInterpretationsSecondaryTests.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": \"3\"}, {\"arrayElement\": \"3.14\"}, {\"arrayElement\": \"0123456789!@~*&^%$#.,;:?/'\"}, {\"arrayElement\": \"1234567890\"}, {\"arrayElement\": \"12345.6789\"}]}")) jsonInterpretationsSecondaryTests.append(convertJSONToDictionary("{\"containerElement\": [{\"arrayElement\": 3}, {\"arrayElement\": 3.14}, {\"arrayElement\": \"0123456789!@~*&^%$#.,;:?/'\"}, {\"arrayElement\": 1234567890}, {\"arrayElement\": 12345.6789}]}")) } loadJSONIntoDictionaries() let whatWeWantToSee: [String] = ["Element Value 01", "Element Value 02", "Element Value 03", "Element Value 04", "Element Value 05"] let whatWeWantToSee2: [String] = ["0123456789!@~*&^%$#.,;:?/'", "12345.6789", "1234567890", "3", "3.14"] func recursiveDescentParser(_ inDictionaryToBeParsed: [AnyHashable: Any], blackList inBlacklist: [String] = []) -> [String] { var ret = [String]() inDictionaryToBeParsed.forEach { if let asFloat = ($0.value as? Double ?? Double($0.value as? String ?? "ERROR")) { let formatter = NumberFormatter() formatter.minimumIntegerDigits = 1 formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 20 if let keyString = $0.key as? String, !inBlacklist.contains(keyString) { ret += [String(formatter.string(from: asFloat as NSNumber) ?? "")] } } else if let asString = $0.value as? String, let keyString = $0.key as? String, !inBlacklist.contains(keyString) { ret += [asString] } else if let asDictionary = $0.value as? [AnyHashable: Any] { ret += recursiveDescentParser(asDictionary, blackList: inBlacklist) } else if let asDictionaryArray = $0.value as? [[AnyHashable: Any]] { asDictionaryArray.forEach { elem in ret += recursiveDescentParser(elem, blackList: inBlacklist) } } else if let asArray = $0.value as? [String] { asArray.forEach { elem in ret += recursiveDescentParser(["": elem], blackList: inBlacklist) } } } return ret.sorted() } // Test Set 1 jsonInterpretations.forEach { let parsedResult = recursiveDescentParser($0, blackList: ["index", "row"]) if whatWeWantToSee != parsedResult { print("ERROR! Bad Result: \(parsedResult) for \($0)") } else { // print("\($0) Passes!") } } // Test Set 2 jsonInterpretationsSecondaryTests.forEach { let parsedResult = recursiveDescentParser($0, blackList: ["index", "row"]) if whatWeWantToSee2 != parsedResult { print("ERROR! Bad Result: \(parsedResult) for \($0)") } else { // print("\($0) Passes!") } }