Last active
May 23, 2022 00:32
-
-
Save kristopherjohnson/56c2f8606a3a6b264576c305e9f92e86 to your computer and use it in GitHub Desktop.
Iterators for reading bytes or lines from FileHandle objects as sequences
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
extension FileHandle { | |
/// Return an iterator over the bytes in the file. | |
/// | |
/// - returns: An iterator for UInt8 elements. | |
public func bytes() -> FileHandleByteIterator { | |
return FileHandleByteIterator(fileHandle: self) | |
} | |
/// Return an iterator over the lines in the file. | |
/// | |
/// A "line" is a String ending in "\n" or the specified delimiter, | |
/// unless at the end of file. | |
/// | |
/// - parameter encoding: Encoding of the text. Defaults to UTF8. | |
/// - parameter delimiter: Byte value that marks end of line. Defaults to LF. | |
/// | |
/// - returns: An iterator for Strings. | |
public func lines(encoding: String.Encoding = .utf8, delimiter: UInt8 = 10) -> FileHandleLineIterator { | |
return FileHandleLineIterator(fileHandle: self, encoding: encoding, delimiter: delimiter) | |
} | |
/// Return an iterator over fixed-length blocks read from the file. | |
/// | |
/// - parameter bytesPerBlock: Length of each block. | |
/// | |
/// - returns: An iterator for Data elements. | |
public func blocks(ofLength bytesPerBlock: Int) -> FileHandleBlockIterator { | |
return FileHandleBlockIterator(fileHandle: self, bytesPerBlock: bytesPerBlock) | |
} | |
} | |
/// An iterator over the bytes read from a FileHandle. | |
public struct FileHandleByteIterator: IteratorProtocol, Sequence { | |
let fileHandle: FileHandle | |
public init(fileHandle: FileHandle) { | |
self.fileHandle = fileHandle | |
} | |
/// Read next byte. | |
/// | |
/// - returns: Byte, or nil if at end of file. | |
public func next() -> UInt8? { | |
return fileHandle.readData(ofLength: 1).first | |
} | |
} | |
/// An iterator over the lines read from a FileHandle. | |
/// | |
/// A "line" is a String ending with a LF byte or specified byte value. | |
/// Each returned line includes the delimiter, unless end-of-file was reached. | |
public struct FileHandleLineIterator: IteratorProtocol, Sequence { | |
let fileHandle: FileHandle | |
let encoding: String.Encoding | |
let delimiter: UInt8 | |
/// Constructor | |
/// | |
/// - parameter fileHandle: Open file handle. | |
/// - parameter encoding: Encoding to be used to convert byte sequences to Strings. | |
/// - parameter delimiter: Byte value that marks end of line. | |
public init(fileHandle: FileHandle, encoding: String.Encoding = .utf8, delimiter: UInt8 = 10) { | |
self.fileHandle = fileHandle | |
self.encoding = encoding | |
self.delimiter = delimiter | |
} | |
/// Read next line from file. | |
/// | |
/// - returns: Next line, or nil if at end of file. | |
public func next() -> String? { | |
var readBuffer = readOneByte() | |
if readBuffer.count == 0 { | |
return nil | |
} | |
var lineData = readBuffer | |
var byte = readBuffer.first! | |
while byte != delimiter { | |
readBuffer = readOneByte() | |
if readBuffer.count == 0 { | |
break | |
} | |
lineData.append(readBuffer) | |
byte = readBuffer.first! | |
} | |
return String(data: lineData, encoding: encoding) | |
} | |
/// Read next byte. | |
/// | |
/// - returns: Data containing one byte, or empty Data if at end of file. | |
/// | |
/// - TODO: This implementation uses `FileHandle.readData(ofLength: 1)` to read a byte at a time. There may be more performant ways to read the data. | |
private func readOneByte() -> Data { | |
return fileHandle.readData(ofLength: 1) | |
} | |
} | |
/// An iterator that reads fixed-length blocks of data from a FileHandle. | |
public struct FileHandleBlockIterator: IteratorProtocol, Sequence { | |
let fileHandle: FileHandle | |
let bytesPerBlock: Int | |
/// Constructor. | |
/// | |
/// - parameter fileHandle: Open file handle. | |
/// - parameter bytesPerBlock: Number of bytes in each fixed-size block. | |
public init(fileHandle: FileHandle, bytesPerBlock: Int) { | |
assert(bytesPerBlock > 0) | |
self.fileHandle = fileHandle | |
self.bytesPerBlock = bytesPerBlock | |
} | |
/// Read next block. | |
/// | |
/// - returns: Data, or nil if at end of file. | |
public func next() -> Data? { | |
var block = fileHandle.readData(ofLength: bytesPerBlock) | |
if block.count == bytesPerBlock { | |
return block | |
} | |
else if block.count == 0 { | |
return nil | |
} | |
// Keep reading until we fill the block or hit end of file. | |
var nextData = fileHandle.readData(ofLength: bytesPerBlock - block.count) | |
while nextData.count > 0 { | |
block.append(nextData) | |
if block.count == bytesPerBlock { | |
return block | |
} | |
nextData = fileHandle.readData(ofLength: bytesPerBlock - block.count) | |
} | |
return block | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I consider it to be public domain.
If "public domain" isn't meaningful for a particular jurisdiction or use, then the MIT license terms are fine. I'm not going to enforce any sort of copyright terms for this.