Fast way to trim lines at the beginning of a log file on iOS - ios

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)

Related

Get array of tuples from txt file

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

iOS Swift replace or update specific line in a file using file handle?

I am writing a point cloud file and need to keep updating the file header with the total number of points in a file: vertexCount. I don't know when the points will stop coming, so I can not just keep accumulating the values and waiting to write it to file.
The vertexCount value is kept on line 3 of ascii file, which is newline terminated.
I only see examples and functions that append data to the end of the file using write(to: URL, options: .atomic)
How can I use FileHandle to replace a specific line in a file, or overwrite the entire header?
ply
format ascii 1.0
element vertex \(vertexCount)
I see this question about replacing file contents using an array. Due to the file having at least 400 thousand lines, I do not want to separate it into individual lines. I was thinking of separating it on the end_header keyword, and then generating a new header, but am not sure how efficient this is.
Well the issue you will face is that when the numbers of digits increase it will overwrite the characters after it. You will need to use a fixed numbers of digits to be able to write them exactly over it (something like 0000000001). The number of lines doesn't really matter because it will replace any new line character after the last digit.
extension FixedWidthInteger where Self: CVarArg {
func strZero(maxLength: Int) -> String {
String(format: "%0*d", maxLength, self)
}
func write(toPLYFile atURL: URL) throws {
let fileHandle = try FileHandle(forUpdating: atURL)
try fileHandle.seek(toOffset: 36)
try fileHandle.write(contentsOf: Data(strZero(maxLength: 10).utf8))
fileHandle.closeFile()
}
}
var vertexCount = 1
let text = """
ply
format ascii 1.0
element vertex \(vertexCount.strZero(maxLength: 10))
abcdefghijklmnopqrstuvwxyz
1234567890
"""
print(text)
print("=========")
let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
.appendingPathComponent("file.txt")
try Data(text.utf8).write(to: fileURL, options: .atomic)
let fileHandle = try FileHandle(forUpdating: fileURL)
try fileHandle.seek(toOffset: 36)
vertexCount = 12345
try fileHandle.write(contentsOf: Data(vertexCount.strZero(maxLength: 10).utf8))
fileHandle.closeFile()
let stringLoaded = try String(contentsOf: fileURL)
print(stringLoaded)
This will print
ply
format ascii 1.0
element vertex 0000000001
abcdefghijklmnopqrstuvwxyz
1234567890
=========
ply
format ascii 1.0
element vertex 0000012345
abcdefghijklmnopqrstuvwxyz
1234567890
Updated use:
do {
// filepath to PLY file that is being updated
let url = URL(fileURLWithPath: path)
let totalVertexCount = 12345
try totalVertexCount.write(toPLYFile: url)
} catch {
print("Error writing PLY! \(error)")
return
}
This is just something I've written really quick, definitely look into making it look and work a bit better - but should be enough to demonstrate the concept. Needs some testing to see if this is actually more efficient than separating line by line.
What it's essentially doing is it's reading the file byte by byte into an array until it finds the beginning of the third line. Then, it copies our new string into the buffer. After that, it's looking for the beginning of the fourth line and copying the rest of the file into the buffer.
I'm also calculating the total byte size so that I can trim the buffer at the end.
var finalFileByteLength = 0;
if let d = NSData(contentsOfFile: filePath) {
let newLine = "element vertex \(vertexCount)\n".data(using: .ascii)! as NSData
var buffer = [UInt8](repeating: 0, count: d.length + newLine.length)
var bytePosition = 0
var lineCount = 0
while(true) {
//Read one Byte
d.getBytes(&buffer+bytePosition, range: NSMakeRange(bytePosition, 1))
//If it's a new line character
if(buffer[bytePosition] == 10) {
lineCount += 1
//If it found the end of the second line, copy our new line
if lineCount == 2 {
newLine.getBytes(&buffer+(bytePosition+1), length: newLine.length)
bytePosition += 1
break
}
}
bytePosition += 1
finalFileByteLength+=1
}
var oldLine3Length = 0
finalFileByteLength+=newLine.length
//Find the start of the fourth line in the initial file
while(true) {
//Read one Byte
var char = UInt8()
d.getBytes(&char, range: NSMakeRange(bytePosition, 1))
//If it's a new line character
if(char == 10) {
//If it found the end of the third line, break so we have the start of the fourth line
bytePosition += 1
oldLine3Length += 1
break
}
bytePosition += 1
oldLine3Length += 1
}
//Header is now modified, copy the rest of the file
d.getBytes(&buffer+(bytePosition+newLine.length-oldLine3Length), range: NSMakeRange(bytePosition, d.length - bytePosition))
finalFileByteLength+=d.length - bytePosition + 1
let finalFileData = NSData(bytes: &buffer, length: finalFileByteLength)
//Print the result - this is probably where you'll write the entire String to a file
print(String(data: finalFileData as Data, encoding: .ascii))
}
EDIT: Managed to reduce to this:
if let d = NSData(contentsOfFile: filePath) {
let newLine = "element vertex \(vertexCount)".data(using: .ascii)! as NSData
var newLineBuffer = [UInt8](repeating: 0, count: newLine.length)
newLine.getBytes(&newLineBuffer, length: newLine.length)
var buffer = [UInt8](repeating: 0, count: d.length)
d.getBytes(&buffer, length: d.length)
var thirdIndex = buffer.firstIndex(of: 10)
thirdIndex = buffer[buffer.index(after: thirdIndex!)...].firstIndex(of: 10)
thirdIndex = buffer[buffer.index(after: thirdIndex!)...].firstIndex(of: 10)
var fourthIndex = buffer[buffer.index(after: thirdIndex!)...].firstIndex(of: 10)
buffer.removeSubrange(thirdIndex!+1..<fourthIndex!)
buffer.insert(contentsOf: newLineBuffer, at: thirdIndex!+1)
let finalFileData = NSData(bytes: buffer, length: buffer.count) as Data
print(String(data:finalFileData, encoding: .ascii))
}

CSV Parsing - Swift 4

I am trying to parse a CSV but i am getting some issues. Below is the code i used for parsing CSV:
let fileURL = Bundle.main.url(forResource: "test_application_data - Sheet 1", withExtension: "csv")
let content = try String(contentsOf: fileURL!, encoding: String.Encoding.utf8)
let parsedCSV: [[String]] = content.components(separatedBy: "\n").map{ $0.components(separatedBy: ",")}
And this is the data in the CSV i am parsing :
Item 9,Description 9,image url
"Item 10 Extra line 1 Extra line 2 Extra line 3",Description 10,image url
So by using above code i get correct response for first row i.e Item 9 but i am getting malformed response for Item 10
How can i correctly parse both rows?
The RFC for CSV: Common Format and MIME Type for Comma-Separated Values (CSV) Files(RFC-4180)
Not all CSV data or CSV processors conform to all descriptions of this RFC, but generally, fields enclosed within double-quotes can contain:
newlines
commas
escaped double-quotes ("" represents a single double-quote)
This code is a little bit simplified than RFC-4180, but handles all three cases above:
UPDATE This old code does not handle CRLF well. (Which is a valid newline in RFC-4180.) I added a new code at the bottom, please check it.
Thanks to Jay.
import Foundation
let csvText = """
Item 9,Description 9,image url
"Item 10
Extra line 1
Extra line 2
Extra line 3",Description 10,image url
"Item 11
Csv item can contain ""double quote"" and comma(,)", Description 11 ,image url
"""
let pattern = "[ \r\t]*(?:\"((?:[^\"]|\"\")*)\"|([^,\"\\n]*))[ \t]*([,\\n]|$)"
let regex = try! NSRegularExpression(pattern: pattern)
var result: [[String]] = []
var record: [String] = []
let offset: Int = 0
regex.enumerateMatches(in: csvText, options: .anchored, range: NSRange(0..<csvText.utf16.count)) {match, flags, stop in
guard let match = match else {fatalError()}
if match.range(at: 1).location != NSNotFound {
let field = csvText[Range(match.range(at: 1), in: csvText)!].replacingOccurrences(of: "\"\"", with: "\"")
record.append(field)
} else if match.range(at: 2).location != NSNotFound {
let field = csvText[Range(match.range(at: 2), in: csvText)!].trimmingCharacters(in: .whitespaces)
record.append(field)
}
let separator = csvText[Range(match.range(at: 3), in: csvText)!]
switch separator {
case "\n": //newline
result.append(record)
record = []
case "": //end of text
//Ignoring empty last line...
if record.count > 1 || (record.count == 1 && !record[0].isEmpty) {
result.append(record)
}
stop.pointee = true
default: //comma
break
}
}
print(result)
(Intended to test in a Playground.)
New code, CRLF ready.
import Foundation
let csvText = "Field0,Field1\r\n"
let pattern = "[ \t]*(?:\"((?:[^\"]|\"\")*)\"|([^,\"\r\\n]*))[ \t]*(,|\r\\n?|\\n|$)"
let regex = try! NSRegularExpression(pattern: pattern)
var result: [[String]] = []
var record: [String] = []
let offset: Int = 0
regex.enumerateMatches(in: csvText, options: .anchored, range: NSRange(0..<csvText.utf16.count)) {match, flags, stop in
guard let match = match else {fatalError()}
if let quotedRange = Range(match.range(at: 1), in: csvText) {
let field = csvText[quotedRange].replacingOccurrences(of: "\"\"", with: "\"")
record.append(field)
} else if let range = Range(match.range(at: 2), in: csvText) {
let field = csvText[range].trimmingCharacters(in: .whitespaces)
record.append(field)
}
let separator = csvText[Range(match.range(at: 3), in: csvText)!]
switch separator {
case "": //end of text
//Ignoring empty last line...
if record.count > 1 || (record.count == 1 && !record[0].isEmpty) {
result.append(record)
}
stop.pointee = true
case ",": //comma
break
default: //newline
result.append(record)
record = []
}
}
print(result) //->[["Field0", "Field1"]]
The problem is with this line of code:
content.components(separatedBy: "\n")
It separates your csv file into rows based on the newline character. There are newline characters in your "Item 10 Extra line 1 Extra line 2 Extra line 3" String so each extra line is getting treated as a different row, so in the end you get the wrong result.
I'd suggest escaping the newline characters in your multiline text column or getting rid of them altogether. You can also modyfy the input file so the newline delimeter isn't \n at the end of each row but something custom (a string that won't appear elsewhere in the csv file).

NSNetService dictionaryFromTXTRecord fails an assertion on invalid input

The input to dictionary(fromTXTRecord:) comes from the network, potentially from outside the app, or even the device. However, Apple's docs say:
... Fails an assertion if txtData cannot be represented as an NSDictionary object.
Failing an assertion leaves the programmer (me) with no way of handling the error, which seems illogic for a method that processes external data.
If I run this in Terminal on a Mac:
dns-sd -R 'My Service Name' _myservice._tcp local 4567 asdf asdf
my app, running in an iPhone, crashes.
dictionary(fromTXTRecord:) expects the TXT record data (asdf asdf) to be in key=val form. If, like above, a word doesn't contain any = the method won't be able to parse it and fail the assertion.
I see no way of solving this problem other than not using that method at all and implementing my own parsing, which feels wrong.
Am I missing something?
Here's a solution in Swift 4.2, assuming the TXT record has only strings:
/// Decode the TXT record as a string dictionary, or [:] if the data is malformed
public func dictionary(fromTXTRecord txtData: Data) -> [String: String] {
var result = [String: String]()
var data = txtData
while !data.isEmpty {
// The first byte of each record is its length, so prefix that much data
let recordLength = Int(data.removeFirst())
guard data.count >= recordLength else { return [:] }
let recordData = data[..<(data.startIndex + recordLength)]
data = data.dropFirst(recordLength)
guard let record = String(bytes: recordData, encoding: .utf8) else { return [:] }
// The format of the entry is "key=value"
// (According to the reference implementation, = is optional if there is no value,
// and any equals signs after the first are part of the value.)
// `ommittingEmptySubsequences` is necessary otherwise an empty string will crash the next line
let keyValue = record.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false)
let key = String(keyValue[0])
// If there's no value, make the value the empty string
switch keyValue.count {
case 1:
result[key] = ""
case 2:
result[key] = String(keyValue[1])
default:
fatalError()
}
}
return result
}
I'm still hoping there's something I'm missing here, but in the mean time, I ended up checking the data for correctness and only then calling Apple's own method.
Here's my workaround:
func dictionaryFromTXTRecordData(data: NSData) -> [String:NSData] {
let buffer = UnsafeBufferPointer<UInt8>(start: UnsafePointer(data.bytes), count: data.length)
var pos = 0
while pos < buffer.count {
let len = Int(buffer[pos])
if len > (buffer.count - pos + 1) {
return [:]
}
let subdata = data.subdataWithRange(NSRange(location: pos + 1, length: len))
guard let substring = String(data: subdata, encoding: NSUTF8StringEncoding) else {
return [:]
}
if !substring.containsString("=") {
return [:]
}
pos = pos + len + 1
}
return NSNetService.dictionaryFromTXTRecordData(data)
}
I'm using Swift 2 here. All contributions are welcome. Swift 3 versions, Objective-C versions, improvements, corrections.
I just ran into this one using Swift 3. In my case the problem only occurred when I used NetService.dictionary(fromTXTRecord:) but did not occur when I switched to Objective-C and called NSNetService dictionaryFromTXTRecord:. When the Objective-C call encounters an entry without an equal sign it creates a key containing the data and shoves it into the dictionary with an NSNull value. From what I can tell the Swift version then enumerates that dictionary and throws a fit when it sees the NSNull. My solution was to add an Objective-C file and a utility function that calls dictionaryFromTXTRecord: and cleans up the results before handing them back to my Swift code.

Can't figure out why I get a fatal error: unexpectedly found nil while unwrapping an Optional value

I keep getting this error :
fatal error: unexpectedly found nil while unwrapping an Optional value
and cannot figure out how to debug it!
Here's my code :
func readCSV() -> Array<String> {
// Creates a new array of strings
var csvArray : Array<String> = Array<String>()
if let url: NSURL = NSURL(string : "URLFROMCSV" ) {
// Creates an Input Stream that will load the datas from our URL
let data :NSData! = NSData(contentsOfURL: url)!
let stream : NSInputStream! = NSInputStream(data: data)
// Opens the receiving stream
stream.open()
// Sets a buffer with a given size size
let bufferSize = 1024
var buffer = Array <UInt8>(count: bufferSize, repeatedValue: 0)
// String variable initialization
var csvFullString : String = ""
// While the stream receives datas, parses datas, convert them into strings and then concatenate them into one big string
while (stream.hasBytesAvailable) {
let readSize = stream.read(&buffer, maxLength: bufferSize)
let csvRaw = NSString (bytes: &buffer, length: readSize, encoding: NSUTF8StringEncoding)
let csvString = csvRaw as String!
csvFullString = csvFullString + csvString
}
// Fills the array with each strings. Separation between strings is made when a Θ character is parsed
csvArray = csvFullString.componentsSeparatedByString("Θ")
// Delete each null string
for(var i = 0 ; i < csvArray.count; i++) {
if(csvArray[i] == "") {
csvArray.removeAtIndex(i)
}
}
}
return csvArray
}
After searching on the web, I'm pretty sure it has something to do with unwrapping elements but the fact is when I debug it, i don't get any nil value anywhere.
PS: Would like to upload a screen but can't because i don't have 10 reputation, so bad!
Thanks in advance!
EDIT : Line let data :NSData! = NSData(contentsOfURL: url)! got the error.
Terry
You're probably creating the error in one of these two lines (though it may show up later):
let data :NSData! = NSData(contentsOfURL: url)!
let stream : NSInputStream! = NSInputStream(data: data)
You're assigning an optional value to an implicitlyUnwrappedOptional type and then using it without checking if you have a valid value.
This is why if let exists. It's a little funny that you've started to indent as if you're using if let but aren't.
Try this instead:
if let url = NSURL(string : "http://gorillaapplications.com/etablissements.csv" ) {
// Creates an Input Stream that will load the datas from our URL
if let data = NSData(contentsOfURL: url) {
let stream = NSInputStream(data: data)
stream.open()
// rest of your code here
}
else {
println("Didn't get a data object")
}
}
else {
println("Didn't get a URL object")
}
You really need to grasp Optionals for Swift. I'd recommend reading my Optionals chapter in this iBook: https://itunes.apple.com/us/book/swift-optionals-generics-for/id943445214?mt=11&uo=4&at=11lMGu
Update:
Since you added a bit more in your comments above, you're saying you get the error on this line: let data: NSData! = NSData(contentsOfURL: url)!. This is because of the ! at the end, which tells Swift you're sure that this function will return a valid value, so just use it, without checking if it's nil first. In your case, the function is returning nil and so your app crashes. Using the sample code I've provided above, you'll see that you'll no longer get a crash, but your code will execute the "Didn't get a data object" line. You'll need to correctly handle the case where you can't load data from that URL.

Resources