I am reading in the following bilingual English <-> Japanese dictionary data from the iOS app bundle directory formatted as a json file having identical entries, but with different meanings (i.e. abandon) as shown in 'json data set 1' below:
{
"aardvark":"土豚 (つちぶた)",
"abacus":"算盤 (そろばん)",
"abacus":"十露盤 (そろばん)",
"abalone":"鮑 (あわび)",
"abandon":"乗り捨てる (のりすてる)(a ship or vehicle)",
"abandon":"取り下げる (とりさげる)(e.g. a lawsuit)",
"abandon":"捨て去る (すてさる)(ship)",
"abandon":"泣し沈む (なきしずむ)oneself to grief",
"abandon":"遺棄する (いき)",
"abandon":"握りつぶす (にぎりつぶす)",
"abandon":"握り潰す (にぎりつぶす)",
"abandon":"見限る (みかぎる)",
"abandon":"見切り (みきり)",
"abandon":"見捨てる (みすてる)",
"abandon":"突き放す[見捨てる (つきはなす)",
"abandon":"放り出す (ほうりだす)",
"abandon":"廃棄 (はいき)",
"abandon":"廃棄する (はいき)",
"abandon":"放棄する (ほうき)",
}
I am using the code snippet below to read in the data from the app.bundle directory:
var vocab:[String:String] = [:]
do {
let path = Bundle.main.path(forResource: "words_alpha", ofType: "json")!
let text = try! String(contentsOfFile: path, encoding: String.Encoding.utf8)
do {
vocab = try JSONDecoder().decode([String: String].self, from: Data(text.utf8))
print(text)
}
} catch {
print(error)
}
}
Question: Only the first entry of a duplicate entry is being read in whereas I would like to have all duplicate entries read in as multiple definitions for a single dictionary item/term.
One solution is to reformat duplicate entries in the json data as shown below by adding line returns between different definitions in 'json data set 2':
"abandon":"乗り捨てる (のりすてる)(a ship or vehicle)\n\n取り下げる (とりさげる)(e.g. a lawsuit)\n\n捨て去る (すてさる)(ship)\n\n 泣し沈む (なきしずむ)oneself to grief\n\n",
However, that is a huge amount of work editing a 30MB json data file to make the above changes for duplicate items so I am looking for a quick and dirty way to use swift json string manipulations to read in the data 'as is' using the native 'data set 1' format with each entry being on a line by itself as shown below:
{
"abandon":"乗り捨てる (のりすてる)(a ship or vehicle)",
"abandon":"取り下げる (とりさげる)(e.g. a lawsuit)",
}
Have tried a number of approaches, but none have worked so far. Any suggestions very much appreciated.
Dictionary can't have the same key, there is unicity.
If you use for instance JSONLint, you'll have a "Error: Duplicate key 'abacus'" (it stops at first error found).
However, that is a huge amount of work editing a 30MB json data file to make the above changes for duplicate items so I am looking for a quick and dirty way
Instead of thinking that way, let's pre-process the JSON, and fix it!
So you could write a little script beforehand to fix your JSON. You can do it in Swift!
In a quick & dirty way, you could do this:
All definitions must be one line (else you might have to fix manually for them)
Create a fixJSON.swift file (in Terminal.app: $>touch fixJSON.swift), make it executable ($ chmod +x fixJSON.swift), put that code inside it:
#!/usr/bin/swift
import Foundation
func fixingJSON(_ input: String) -> String? {
let lines = input.components(separatedBy: .newlines)
let regex = try! NSRegularExpression(pattern: "\"(\\w+)\":\"(.*?)\",", options: [])
let output = lines.reduce(into: [String: [String]]()) { partialResult, aLine in
var cleaned = aLine.trimmingCharacters(in: .whitespaces)
guard !cleaned.isEmpty else { return }
if !cleaned.hasSuffix(",") { //Articially add a ",", for the last line case
cleaned.append(",")
}
guard let match = regex.firstMatch(in: cleaned, options: [], range: NSRange(location: 0, length: cleaned.utf16.count)) else { return }
guard let wordRange = Range(match.range(at: 1), in: cleaned),
let definitionRange = Range(match.range(at: 2), in: cleaned) else { return }
partialResult[String(cleaned[wordRange]), default: [String]()] += [String(cleaned[definitionRange])]
}
// print(output)
do {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let asJSONData = try encoder.encode(output)
let asJSONString = String(data: asJSONData, encoding: .utf8)
// print(asJSONString!)
return asJSONString
} catch {
print("Error while encoding: \(error)")
return nil
}
}
func main() {
do {
//To change
let path = "translate.json"
let content = try String(contentsOfFile: path)
guard let output = fixingJSON(content) else { return }
//To change
let outputPath = "translate2.json"
try output.write(to: URL(fileURLWithPath: outputPath), atomically: true, encoding: .utf8)
} catch {
print("Oops, error while trying to read or write content of file:\n\(error)")
}
}
main()
Modify path/output path values, it's easier if you put it as the same place as the script file, then the path will be just the name of the file.
Then, in Terminal.app, just write $> ./fixJSON.swift
Okay, now, let's talk about what the script does.
As said, it's quick & dirty, and might have issues.
We read the content of the JSON with issue, I iterate over the lines, then used a regex, to find this:
"englishWord":"anything",
I artificially add a comma if there isn't (special case for the last entry of the JSON which shouldn't have one).
As to why, it's because there could be double quotes in a translation, so it could generate issues. It's just a quick & dirty fix. I might do better, but since it's a quick fix, spending more time to write beautiful code might be overkill for a one time use.
In the end, you'll have a [String: [String]] JSON.
This is fine JSON (except for the last comma, which isn't legal), but Swift's JSONDecoder can't handle it. JSON allows duplicate keys, but Swift doesn't. So you'll need to parse it by hand.
If your data is exactly as given, one record per line, with nothing "weird" (like embedded \" in the Strings), the easiest way to do that is to just parse it line by line, using simple String manipulation or NSRegularExpression.
If this is more arbitrary JSON, then you may want to use a full JSON parser that can handle this, such as RNJSON. Note that this is just a hobby project to build a JSON parser that exactly follows the spec, and as much intended as an example of how to write these things as a serious framework, but it can handle this JSON (as long as you get rid of that last , which is not legal).
import RNJSON
let keyValues = try JSONParser()
.parse(data: json)
.keyValues()
.lazy
.map({($0, [try $1.stringValue()])})
let translations = Dictionary(keyValues, uniquingKeysWith: +)
// [
"abandon": ["乗り捨てる (のりすてる)(a ship or vehicle)", "取り下げる (とりさげる)(e.g. a lawsuit)", "捨て去る (すてさる)(ship)", "泣し沈む (なきしずむ)oneself to grief", "遺棄する (いき)", "握りつぶす (にぎりつぶす)", "握り潰す (にぎりつぶす)", "見限る (みかぎる)", "見切り (みきり)", "見捨てる (みすてる)", "突き放す[見捨てる (つきはなす)", "放り出す (ほうりだす)", "廃棄 (はいき)", "廃棄する (はいき)", "放棄する (ほうき)"],
"aardvark": ["土豚 (つちぶた)"],
"abacus": ["算盤 (そろばん)", "十露盤 (そろばん)"],
"abalone": ["鮑 (あわび)"]
]
It's not that complicated a framework, so you could also adapt it to your own needs (making it accept that last non-standard comma, for example).
But, if possible, I'd personally just parse it line by line with simple String manipulation. That would be the easiest to implement using AsyncLineSequence, which would avoid pulling all 30MB into the memory before parsing.
I am parsing a csv file into core data. I am parsing the file and saving the data into CD at the same time in the one function. I am interested to get some feedback if this is a good approach? It does work, the data is saved. To make things easy for now all the entity attributes are strings. I am new to swift so am looking to make sure I am not making any newbie mistakes. Here is the function.
I do have one question. I am not sure now to save data from the file to an attribute in a different entity in the same data model that has a relationship to the Asteroids entity. A resources and amount attributes in a Resources entity for example.
func parseAsteroidCSVForCD(){
let context = getContext()
let entity = NSEntityDescription.entity(forEntityName: "Asteroid", in: context)
let manageObj = NSManagedObject(entity: entity!, insertInto: context)
// Path variable to the asteroids.csv file
let path = Bundle.main.path(forResource: "asteroids", ofType: "csv")!
// Parse the file in a do catch
do {
// Use the paser to pull out the rows
let csv = try CSV(contentsOfURL: path)
let rows = csv.rows
// Go through each row to store the id, name, size, distance and value
for row in rows {
let id = row["id"]!
let name = row["name"]!
let size = row["size"]!
let distance = row["distancefromEarth"]!
let value = row["value"]!
// load into Core Data here
manageObj.setValue(id, forKey: "id")
manageObj.setValue(name, forKey: "name")
manageObj.setValue(size, forKey: "size")
manageObj.setValue(value, forKey: "value")
manageObj.setValue(distance, forKey: "distance")
do {
try context.save()
print("New Asteroid has been created")
} catch {
print(error.localizedDescription)
}
}
} catch let err as NSError {
print(err.debugDescription)
}
}
With this logic, all CSV rows will not be inserted. Because you create only one "manageObj" and keep on setting the value of every row to it. So finally you will have only one "manageObj" with the last row value.
the entity creation should be within the for loop, so that for every row, a new entity will be created. And do not save within the for loop. Once for loop is finished, save it.. A single save..
I'm using the following code to extract weather information from the HTML of a website. The code tracks through the given URL and searches for content separated by the two phrases I've given as strings. The piece of information I need to extract is between two phrases but the first phrase has a line break in the HTML.
How do I represent this in the string? I've tried simply removing the line break and also using \n but this makes the fetch unsuccessful, as it can't find that phrase. I've represented the line break in my code as four asterisks.
I've also attached an image of the HTML I'm looking at. In this instance I'm trying to extract the time given in the HTML, but I also want to extract the 'Clear, cloudless sky' bit, which obviously will change regularly, as will some of the content preceding it.
The reason for needing the time is that I know this and the wind will change, and I want to ultimately extract the current conditions, so I'll have to tell the code to insert the correct time and wind to be able to fetch the current conditions.
if let url = attemptedURL {
let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) -> Void in
if let urlContent = data {
let webContent = NSString(data: urlContent, encoding: String.Encoding.utf8.rawValue)
let websiteArray = webContent?.components(separatedBy: "<span class=\"current_description\">****<span>")
if websiteArray!.count > 1 {
let conditionArray = websiteArray![1].components(separatedBy: "</span>")
if conditionArray.count > 1 {
self.wasConSuccessful = true
let extract = conditionArray[0].replacingOccurrences(of: "°", with: "º")
DispatchQueue.main.async(execute: { () -> Void in
print(extract)
})
}
}
if self.wasConSuccessful == false {
self.conditionsLabel.text = "!"
Using SwiftSoup
do{
let doc: Document = try SwiftSoup.parse("<span class=\"current_description\"><span>5:30</span><span>3 km/h</span></span>")
let current_desc = try doc.getElementsByClass("current_description").first()
try print(current_desc?.select("span").get(1).text())//"5:30"
}catch Exception.Error(let type, let message){
print(message)
}catch{
print("error")
}
I suspect that I am not quite grokking Swift 1.2, and I need to RTFM a bit more.
I'm working on a Swift app that reads JSON data from a URI.
If the JSON data is bad, or nonexistent, no issue. The JSON object never instantiates.
However, if the JSON data is good JSON, but not what I want, the object instantiates, but contains a structure that is not what I'm looking for, I get a runtime error.
I looked at using Swift's "RTTI" (dynamicType), but that always returns "<Swift.AnyObject>", no matter what the data is.
I want the JSON to be a specific format: An array of Dictionaries:
[[String:String]]! JSON: [{"key":"value"},{"key","value"},{"Key":"value"}]
If I feed it a single element:
{"Key":"value"}
The routine I have tries to cast it, and that fails.
I want to test the JSON object to make sure that it has a structure I want before casting.
if(nil != inData) {
let rawJSONObject: AnyObject? = NSJSONSerialization.JSONObjectWithData(inData, options: nil, error: nil)
println("Type:\(rawJSONObject.dynamicType)")
if(nil != rawJSONObject) {
// THE LINE BELOW BLOWS UP IF I FEED IT "BAD/GOOD" JSON:
let jsonObject: [[String:String]]! = rawJSONObject as! [[String:String]]!
// I NEED TO TEST IT BEFORE DOING THE CAST
if((nil != jsonObject) && (0 < jsonObject.count)) {
let jsonDictionary: [String:String] = jsonObject[0]
if("1" == jsonDictionary["semanticAdmin"]) { // We have to have the semantic admin flag set.
let testString: String! = jsonDictionary["versionInt"]
if(nil != testString) {
let version = testString.toInt()
if(version >= self.s_minServerVersion) { // Has to be a valid version for us to pay attention.
self.serverVersionAsInt = version!
}
}
}
}
}
}
My question is, is there a good way to test an NSJSONSerialization response for the structure of the JSON before uwinding/casting it?
I feel as if this question may be closer to what I need, but I am having trouble "casting" it to my current issue.
You can use safe unwrapping to test the type of your raw object, for example:
if let jsonObject = rawJSONObject as? [[String:String]] {
// jsonObject is an array of dictionaries
} else if let jsonObject = rawJSONObject as? [String:String] {
// jsonObject is a dictionary
// you can conform it as you wish, for example put it in an array
} else {
// fail, rawJSONObject is of another type
}
Currently, your code crashes because of the forced unwrapping, with !, of values that will be nil if the cast fails.
I'm using a web API that returns an array of dictionaries. For instance, it returns an array of 9 dictionaries, and inside each dictionary is a key named "title".
I've tried the following code and it crashes with an error of unwrapping an optional value:
for dict in returnedJson {
if let validTitle = dict["title"] as? String {
print(validTitle)
dataList.append(validTitle)
} else {
print("Optional title?")
}
}
The list of titles is printed in the console due to the print(validTitle) line, but the array I'm using to populate the tableview with doesn't seem to want to append it.
I know it's probably something really basic thats eluding me.
Thanks in advance!
You have most likely defined dataList as an implicitly unwrapped optional. That's why you don't need to use optional chaining when calling append on it but if the dataList is nil it would still throw an error since it is trying to unwrap it.
I had the same problem:
1)make sure dataList is a NSMutableArray
2)create a var of type string right before you append
example:
var recs:NSArray = jsonObj.objectForKey("pets")?.objectForKey("records") as! NSArray
for item in recs{
var arr:NSArray = item as! NSArray
var name:String = arr[1] as! String
println(arr[2] as! String)
self.recsAddr.addObject(name)
}
I agree with the others, with that error log I suspect you are using an implicitly unwrapped optional that happen to be nil. We'll need more code for this.
Generally speaking, this kind of problem fits well the Functional Programming Paradigm (so loved by Swift) and can be solved as follow.
let returnedJson : [NSDictionary] = [["title":"one"], ["no title here":""], [:], ["title":"four"], ["title":"five"], ["title":"six"]]
let titles = returnedJson
.map { return $0["title"] } // now we have a list of titles/nil [String?]
.filter { return $0 != nil} // now we have a list of titles (without nil)
.map { return $0! } // now we have this return type [String]
titles // -> ["one", "four", "five", "six"]
Hope this helps.