I've txt file with content:
("All our dreams can come true, if we have the courage to pursue them.","Walt Disney")
("The secret of getting ahead is getting started","Mark Twain")
I want to get array of tuples from it with type [(String, String)]. I try to use code:
do {
if let path = Bundle.main.path(forResource: "quotes", ofType: "txt"){
let data = try String(contentsOfFile: path, encoding: .utf8)
let arrayOfStrings = data.components(separatedBy: "\n")
print(arrayOfStrings[0])
}
} catch let err as NSError {
// do something with Error
print(err)
}
But with it I cannot get tuple values. How I can get array of tuples from txt file with Swift?
As already mentioned in comments by Larme it would be better to properly format your text. If you can't change the text format you woill need to manually parse its contents:
let data = """
("All our dreams can come true, if we have the courage to pursue them.","Walt Disney")
("The secret of getting ahead is getting started","Mark Twain")
"""
let tuples = data.split(whereSeparator: \.isNewline)
.compactMap { line -> (Substring,Substring)? in
let comps = line.components(separatedBy: #"",""#)
guard comps.count == 2,
let lhs = comps.first?.dropFirst(2),
let rhs = comps.last?.dropLast(2) else { return nil }
return (lhs,rhs)
}
for tuple in tuples {
print(tuple.0)
print(tuple.1)
}
This will print:
All our dreams can come true, if we have the courage to pursue them.
Walt Disney
The secret of getting ahead is getting started
Mark Twain
Related
How to Convert this one. "{\n ID = \"d9a7c7bf-781d-47b3-bb4e-e1022ec4ce1b\";\n Name = Headquarters;\n}"; To this format {
"ID": "d9a7c7bf-781d-47b3-bb4e-e1022ec4ce1b",
"Name": "Headquarters"
}
if let jsonString = text as? String {
let objectData = jsonString.data(using: String.Encoding.utf8)
do {
let json = try JSONSerialization.jsonObject(with: objectData!, options: .allowFragments) as! [String:Any] //try JSONSerialization.jsonObject(with: objectData!, options: JSONSerialization.ReadingOptions.mutableContainers)
print(String(describing: json))
return json
} catch {
// Handle error
print(error)
}
}
Blockquote
First of all and already mentioned the string format is clearly not JSON.
It's the string format which is returned when calling the description property of a Foundation collection type (NSArray / NSDictionary).
For example a print statement calls description and this format appears also in output of Terminal.app.
However there is a solution: This string format is called openStep (an OpenStep / NeXt legacy format) and is available in PropertyListSerialization
This code reads the format:
let string = "{\n ID = \"d9a7c7bf-781d-47b3-bb4e-e1022ec4ce1b\";\n Name = Headquarters;\n}"
let data = Data(string.utf8)
do {
let dictionary = try PropertyListSerialization.propertyList(from: data, format: nil)
print(dictionary)
} catch { print(error) }
Note:
I'm pretty sure that the original data format is not openStep and somewhere you created the string unnecessarily with the String(describing initializer like in the question.
your json format is incorrect. If you try it with jsonformatter it will throw this error:
so first you need to replace ; with ,. The second is that Strings should be wrapped in double quotes, replace Name = Headquarters with Name = "Headquarters".
This is the right form
{\n ID = \"d9a7c7bf-781d-47b3-bb4e-e1022ec4ce1b\",
\n Name = "Headquarters"\n}
This question already has answers here:
Read and write a String from text file
(21 answers)
Closed 5 years ago.
I have a Test.txt file saved in Documents Directory of an app. There are several names saved one by one in each line in the Test.txt file, like this:
Tom
Jack
Jerry
Jennifer
Lynn
I would like to add these names to an array, like this:
var nameList = ["Tom", "Jack", "Jerry", "Jennifer", "Lynn"]
Is there any way to get it work?
I have the following codes, but it will consider the names as one string.
if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let archiveURL = dir.appendingPathComponent("Test").appendingPathExtension("txt")
do {
try Data(textView.text.utf8).write(to: archiveURL)
}
catch {
print("error")
}
do {
namesPool = try! [String(contentsOf: archiveURL, encoding: .utf8)]
}
}
The above codes will get the following array:
var nameList = ["Tom\nJack\nJerry\nJennifer\nLynn\n"]
Any suggestions would be appreciated.
You are missing one last step, separate it using:
let namesTogether = try! String(contentsOf: archiveURL, encoding: .utf8)
namesPool = namesTogether.components(separatedBy: "\n")
Swift 4
// String with multiple names separated by end of line
let nameStr = try! String(contentsOf: archiveURL, encoding: .utf8)
// array of Substrings
let nameArray = nameStr.split(separator: "\n")
// first name as String
let name = String(nameArray[0])
More about handling strings:Strings Cheat Sheet
I want to add some text like "Hi;Test;Pen" into my .csv file.
I have used the CSVImporter to insert the text of the .csv into my Xcode project. Here is the code, I have used for it:
guard let VokabelPath = Bundle.main.path(forResource: ResourceExample, ofType:"csv") else {
debugPrint(ResourceExample + " not found")
return
}
let importer = CSVImporter<[String]>(path: VokabelPath, delimiter: ";")
let importedRecords = importer.importRecords { $0 }
for record in importedRecords {
self.Vokabeln.insert(record, at: self.Vokabelzähler)
self.Vokabelzähler += 1
}
But for some reasons I want to
add something to the file
Change something in the file (e.g. in Line 4)
Delete the text in the file (e.g. filetext = ""
I have tried to use this Tutorial and I have made this code:
let fileName = "Tasks.csv"
let path = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
var csvText = "Test;Example;Apple;Pen\n"
do {
try csvText.write(to: path!, atomically: true, encoding: String.Encoding.utf8)
} catch {
print("Failed to create file")
print("\(error)")
}
guard let VokabelPath = Bundle.main.path(forResource: "Tasks", ofType:"csv") else {
debugPrint("Tasks not found")
return
}
let importer = CSVImporter<[String]>(path: VokabelPath, delimiter: ";")
let importedRecords = importer.importRecords { $0 }
for record in importedRecords {
self.Vokabeln.insert(record, at: self.Vokabelzähler)
self.Vokabelzähler += 1
print(record)
}
but he prints only "Tasks not found". What is my mistake?
I hope you can help me,
Thank you.
EDIT:
The person who made the first answer has written, I can't save files in the bundle.
He has written to me I should use NSSearchPathForDirectoriesInDomainsso I have updated my code and this works:
let DownloadPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
let VokabelPath = DownloadPath.appendingPathComponent("Tasks.csv")
let InhaltData = Inhalt.data(using: .utf8)
print(VokabelPath)
FileManager.default.createFile(atPath: VokabelPath, contents: InhaltData, attributes: nil)
let importer = CSVImporter<[String]>(path: VokabelPath, delimiter: ";")
let importedRecords = importer.importRecords { $0 }
for record in importedRecords {
self.Vokabeln.insert(record, at: self.Vokabelzähler)
self.Vokabelzähler += 1
}
But I have one further question:
I hav some datas in my Xcode project and I can get them with Bundle.main. But I want to use only one function, so is it possible, to get them with NSSearchPathForDirectoriesInDomainstoo?
In order to change a file, you would need to import the data in, make the necessary changes to the data and then write the data back to the same file, overwriting the previous content. That is how you would make changes generally - you don't make changes directly to the file.
Does that make sense?
If it does, then you could easily modify the tutorial you pointed to (the one for creating and exporting CSV) and write back the data that you read from the original file after you make the modifications.
As for your question about not wanting to use a library, you can always write your own CSV parser. It is not very difficult since all you have to do is read the text from the file a line at a time and then separate out the values by comma. A simple parser is straightforward but allowing for more complicated cases could take a bit more effort. It would all depend on your use-cases and the type of data you'd be handling. The easiest way still would be to use your importing library and then output via custom code.
Regarding your additional questions, if the following code fails:
guard let VokabelPath = Bundle.main.path(forResource: "Tasks", ofType:"csv") else {
debugPrint("Tasks not found")
return
}
Then there is either an issue with the file name (or extension) or you have not included the file as part of your Resources. The above code simply gets the path for a given file in your Resources.
The above code is different from what I was talking about with regards to NSSearchPathForDirectoriesInDomains - that was for writing a file from your app. What I meant was that you could not write a file back to your Resources - instead, you would have to write the modified file to your Documents folder. Hope that clarifies things :)
I'm looking for a fast, optimized way to trim log files on iOS. I want to specify that my log files have a maximum number of lines (e.g., 10,000). Appending new lines to the end of a text file seems relatively simple. However, I haven't yet found a fast way to trim lines at the beginning of the file. Here's the (slow) code I came up with.
guard let fileURL = self.fileURL else {
return
}
guard let path = fileURL.path else {
return
}
guard let fileHandle = NSFileHandle(forUpdatingAtPath: path) else {
return
}
fileHandle.seekToEndOfFile()
fileHandle.writeData(message.dataUsingEncoding(NSUTF8StringEncoding)!)
fileHandle.writeData("\n".dataUsingEncoding(NSUTF8StringEncoding)!)
currentLineCount += 1
// TODO: This could probably use some major optimization
if currentLineCount >= maxLineCount {
if let fileString = try? NSString(contentsOfURL: fileURL, encoding: NSUTF8StringEncoding) {
var lines = fileString.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())
lines.removeFirst()
let newData = lines.joinWithSeparator("\n")
fileHandle.seekToFileOffset(0)
fileHandle.writeData(newData.dataUsingEncoding(NSUTF8StringEncoding)!)
}
}
fileHandle.closeFile()
There are two aspects of your question. First, your code removes a single
line from the log file. Therefore, once the limit is reached, every new
log message causes the entire file to be read, shortened, and be re-written.
It would be more effective to use a "high-water mark" and a "low-water mark". For example, if you want the last 10.000 lines to be preserved,
let the log file grow until it has 15.000 lines, and only then truncate
it to 10.000 lines. This reduces the number of "trim actions"
considerably.
The second part is about the truncating itself. Your code loads the
file into an NSString, which requires the conversion of UTF-8 data
to Unicode characters
(and fails if there is a single invalid byte in the log file).
Then the string is split into an array, one array element removed,
the array concatenated to a string again, and then written back
to the file, which converts the Unicode characters to UTF-8.
I haven't done performance tests, but I can imagine that it could be
faster to operate on binary data only, without the conversions to
NSString, Array and back. Here is a possible implementation
which removes a given number of lines from the start of a file:
func removeLinesFromFile(fileURL: NSURL, numLines: Int) {
do {
let data = try NSData(contentsOfURL: fileURL, options: .DataReadingMappedIfSafe)
let nl = "\n".dataUsingEncoding(NSUTF8StringEncoding)!
var lineNo = 0
var pos = 0
while lineNo < numLines {
// Find next newline character:
let range = data.rangeOfData(nl, options: [], range: NSMakeRange(pos, data.length - pos))
if range.location == NSNotFound {
return // File has less than `numLines` lines.
}
lineNo++
pos = range.location + range.length
}
// Now `pos` is the position where line number `numLines` begins.
let trimmedData = data.subdataWithRange(NSMakeRange(pos, data.length - pos))
trimmedData.writeToURL(fileURL, atomically: true)
} catch let error as NSError {
print(error.localizedDescription)
}
}
I have updated Martin R answer to Swift 3, and I also changed it so that we can pass the number of lines to keep instead of the number of lines to remove:
func removeLinesFromFile(fileURL: URL, linesToKeep numLines: Int) {
do {
let data = try Data(contentsOf: fileURL, options: .dataReadingMapped)
let nl = "\n".data(using: String.Encoding.utf8)!
var lineNo = 0
var pos = data.count-1
while lineNo <= numLines {
// Find next newline character:
guard let range = data.range(of: nl, options: [ .backwards ], in: 0..<pos) else {
return // File has less than `numLines` lines.
}
lineNo += 1
pos = range.lowerBound
}
let trimmedData = data.subdata(in: pos..<data.count)
try trimmedData.write(to: fileURL)
} catch let error as NSError {
print(error.localizedDescription)
}
}
Instead of writing the new line to the log, and processing the appended content afterwards, do both write and trimming in one step:
let fileString = (try? NSString(contentsOfURL: fileURL, encoding: NSUTF8StringEncoding)) as NSString? ?? ""
var lines = fileString.characters.split("\n").map{String($0)}
lines.append(message)
// this also more generic as it will remove any number of extra lines
lines.removeFirst(max(currentLineCount - maxLineCount), 0))
let newLogContents = lines.joinWithSeparator("\n")
(newLogContents as NSString).writeToURL(fileURL, atomically: true, encoding: NSUTF8StringEncoding)
I have an function to write some numbers to a file
fun writeNumToFile -> Void {
//get Documents’ path
let docPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).last as? String
let filePath = docPath.stringByAppendingPathComponent(“myFlie.txt”)
//the count is NOT the count of elements in the array below.
//think it as an independent constant.
let count = 10
//Write count to file
String(count).writeToFile(filePath, atomically: false, encoding: NSUTF8StringEncoding, error: nil);
//Write an array of numbers to file
for idx in [1,2,3] {
String(idx as! String).writeToFile(filePath, atomically: false, encoding: NSUTF8StringEncoding, error: nil);
}
}
Now I want to read the numbers back from file, I know I can read the content of file by:
let fileContent = String(contentsOfFile: filePath, encoding: NSUTF8StringEncoding, error: nil)
but how can I get count & [1,2,3] array back once I get the content?
You are writing code as if you are using low-level file i/o. You're not. The writeToFile:atomically: methods that you are using overwrite a file with new contents, not append data to an existing file. your second write deletes the contents of your first write.
NSArray supports the writeToFile:atomically: method, and a [String] array should be inter-operable with NSArray.
You should be able to simply say:
let array = [1, 2, 3]
let ok = array .writeToFile(filePath, atomically: false)
Then later,
let array = NSArray.contentsOfFile(filePath)
I say "should be able to" because I am still learning the subtleties of interaction between Swift and the Foundation classes.
EDIT:
If you need to save multiple discrete things into a file, create a dictionary:
let someValue = 42
let anArray = [1, 2, 3]
let aDictionary = [
"someValue": someValue,
"array": anArray]
let ok = aDictionary.writeToFile(filePath, atomically: false)
and to read it:
let aDictionary = NSDictionary(contentsOfFile: filePath)
let someValue = aDictionary["someValue"] as! Int
let anArray = aDictionary["array"] as! [Int]
There is no need to save the number of items in the array separately. The array is able to reconstitute itself from the file contents, including the correct count of elements.
EDIT #2:
Note that iOS includes the C file i/o library, which should be callable from Swift. If you are glutton for punishment you could do what you are trying to do using fopen(), fseek(), fwrite(), etc. (But don't. It's much more work, much more error-prone, and a non-standard way of doing it in iOS or Mac OS.)