Advertisement
Guest User

Untitled

a guest
Sep 19th, 2019
191
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.10 KB | None | 0 0
  1. //
  2. // TextReader.swift
  3. //
  4. // Created by Sebastian Boettcher on 05.09.19.
  5. // Copyright © 2019 Sebastian Boettcher. All rights reserved.
  6. //
  7.  
  8. import Foundation
  9.  
  10. private func isReadable (fileHandle: FileHandle) -> Bool {
  11. let flags = fcntl(fileHandle.fileDescriptor, F_GETFL)
  12. return flags != -1 && ((flags & O_ACCMODE == O_RDONLY) || (flags & O_ACCMODE == O_RDWR))
  13. }
  14.  
  15. /// A `TextReader` can read single characters, full lines or complete texts from an `URL` or file
  16. /// with an arbitrary `String.Encoding` and line delimiter.
  17. ///
  18. /// Most creation methods for `TextReader` cause the text reader object to take ownership of the associated file handle.
  19. /// This means that the text reader object both creates the file handle and is responsible for closing it later, usually
  20. /// when the text reader object itself is deallocated. If you want to use a text reader with a file handle that you
  21. /// created, use the `init(fileHandle:closeOnDealloc:encoding:delimiter:)` method. Pass `false` for the `closeOnDealloc`
  22. /// parameter if you want to deallocate the file handle object by yourself.
  23. ///
  24. /// When you supply your own file handle, it must be readable or else a precondition check will fail when creating
  25. /// the text reader.
  26. ///
  27. /// Characterwise seeking is supported, albeit it is very slow due to the fact that there may be no one-to-one
  28. /// relationship inbetween the offset in the file and the offset of a character in the text. Thus seeking requires
  29. /// scanning the file. Don't do it on large files.
  30.  
  31. public class TextReader {
  32.  
  33. private let _chunkSize: Int = 4096
  34. private let _fileHandle: FileHandle
  35. private var _dataBuffer: Data
  36. private let _dataDelimiter: Data
  37. private var _lineBuffer: String = ""
  38. private let _closeOnDealloc: Bool
  39.  
  40. /// Gets the preferred encoding.
  41.  
  42. public let encoding: String.Encoding
  43.  
  44. /// Gets the delimiter used to determine line boundaries.
  45.  
  46. public let delimiter: String
  47.  
  48. /// Gets the current position in the text.
  49.  
  50. private(set) var offsetInText: UInt64 = 0
  51.  
  52. /// Uses an existing file handle for reading.
  53. ///
  54. /// - Parameters:
  55. /// - fileHandle: The file handle to use. If the handle is not readable, a precondition check if fail.
  56. /// - closeOnDealloc: If set to `true`, the text reader takes ownership of the the fileHandle and closes
  57. /// it when the text reader object itself gets deallocated.
  58. /// - encoding: The desired encoding, defaults to `String.Encoding.utf8`.
  59. /// - delimiter: The delimiter to separate the lines, defaults to "\n".
  60. ///
  61. /// - Precondition: `fileHandle` must be readable.
  62. /// - Returns: A `TextReader` that can read text.
  63.  
  64. init (fileHandle: FileHandle, closeOnDealloc: Bool, encoding: String.Encoding = .utf8, delimiter: String = "\n") {
  65.  
  66. precondition(isReadable(fileHandle: fileHandle), "file handle is not readable")
  67.  
  68. self.encoding = encoding
  69. self.delimiter = delimiter
  70.  
  71. self._fileHandle = fileHandle
  72. self._closeOnDealloc = closeOnDealloc
  73. self._dataBuffer = Data(capacity: self._chunkSize)
  74. self._dataDelimiter = delimiter.data(using: encoding)!
  75. }
  76.  
  77. /// Opens an URL for reading.
  78. ///
  79. /// - Parameters:
  80. /// - url: The url to open for reading.
  81. /// - encoding: The desired encoding, defaults to `String.Encoding.utf8`.
  82. /// - delimiter: The delimiter to separate the lines, defaults to "\n".
  83. ///
  84. /// - Throws: `NSError` if the underlying `FileHandle` fails to open the file.
  85. /// - Returns: A `TextReader` that can read text.
  86.  
  87. convenience init (from url: URL, encoding: String.Encoding = .utf8, delimiter: String = "\n") throws {
  88.  
  89. let file = try FileHandle(forReadingFrom: url)
  90. self.init(fileHandle: file, closeOnDealloc: true, encoding: encoding, delimiter: delimiter)
  91. }
  92.  
  93. /// Opens a file for reading.
  94. ///
  95. /// - Parameters:
  96. /// - path: The path to the file to open for reading.
  97. /// - encoding: The desired encoding, defaults to `String.Encoding.utf8`.
  98. /// - delimiter: The delimiter to separate the lines, defaults to "\n".
  99. ///
  100. /// - Throws: `NSError` if the underlying `FileHandle` fails to open the file.
  101. /// - Returns: A `TextReader` that can read text.
  102.  
  103. convenience init (atPath path: String, encoding: String.Encoding = .utf8, delimiter: String = "\n") throws {
  104.  
  105. let file = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
  106. self.init(fileHandle: file, closeOnDealloc: true, encoding: encoding, delimiter: delimiter)
  107. }
  108.  
  109. deinit {
  110. if self._closeOnDealloc {
  111. self._fileHandle.closeFile()
  112. }
  113. }
  114.  
  115. private func readLineInternal () -> String? {
  116.  
  117. while true {
  118. if let range = self._dataBuffer.range(of: self._dataDelimiter) {
  119. let line = String(data: self._dataBuffer.subdata(in: 0..<range.lowerBound), encoding: self.encoding)!
  120. self._dataBuffer.removeSubrange(0..<range.upperBound)
  121. return line
  122. }
  123.  
  124. let buffer = self._fileHandle.readData(ofLength: self._chunkSize)
  125. if !buffer.isEmpty {
  126. self._dataBuffer.append(buffer)
  127. continue
  128. }
  129.  
  130. if !_dataBuffer.isEmpty {
  131. let line = String(data: self._dataBuffer, encoding: self.encoding)
  132. self._dataBuffer.removeAll()
  133. return line
  134. }
  135.  
  136. return nil
  137. }
  138. }
  139.  
  140. private func readInternal (consume: Bool) -> Character? {
  141.  
  142. // serves the request out of the line buffer and replenishes it from the data buffer if needed
  143. while true {
  144. if let char = self._lineBuffer.first {
  145. if consume {
  146. self._lineBuffer = String(self._lineBuffer.dropFirst())
  147. self.offsetInText += 1
  148. }
  149. return char
  150. }
  151.  
  152. guard let line = self.readLineInternal() else {
  153. return nil
  154. }
  155.  
  156. self._lineBuffer = line + self.delimiter
  157. }
  158. }
  159.  
  160. /// Gets the next available `Character` to be read without changing the current file location.
  161. /// - Returns: The next available `Character` or `nil` if no such `Character` exists.
  162.  
  163. func peek () -> Character? {
  164. return self.readInternal(consume: false)
  165. }
  166.  
  167. /// Reads the next available `Character`.
  168. /// - Returns: The next available `Character` or `nil` if no such `Character` exists.
  169.  
  170. func read () -> Character? {
  171. return self.readInternal(consume: true)
  172. }
  173.  
  174. /// Reads the next available line.
  175. /// - Returns: The next available line without delimiter as a `String` or `nil` if no such line exists.
  176.  
  177. func readLine () -> String? {
  178.  
  179. // first serve the request out of the line buffer, if it is not empty
  180. if !self._lineBuffer.isEmpty {
  181. let line = String(self._lineBuffer.dropLast())
  182. self._lineBuffer.removeAll()
  183. self.offsetInText += UInt64(line.count + self.delimiter.count)
  184. return line
  185. }
  186.  
  187. // then serves the request out of the data buffer and replenishes it from the file
  188. if let line = self.readLineInternal() {
  189. self.offsetInText += UInt64(line.count + self.delimiter.count)
  190. return line
  191. }
  192.  
  193. return nil
  194. }
  195.  
  196. /// Reads the remainder of the file.
  197. /// - Returns: A `String` containing the remainder of the file or `nil` if nothing could be read.
  198.  
  199. func readToEndOfFile () -> String? {
  200.  
  201. var line: String?
  202.  
  203. // first serves the request from the line buffer (if not empty)
  204. if !self._lineBuffer.isEmpty {
  205. line = self._lineBuffer
  206. self.offsetInText += UInt64(self._lineBuffer.count + self.delimiter.count)
  207. self._lineBuffer.removeAll()
  208. }
  209.  
  210. // then serves the request from the data buffer (if not empty)
  211. if !self._dataBuffer.isEmpty, let rest = String(data: self._dataBuffer, encoding: self.encoding) {
  212. line = (line ?? "") + rest
  213. self.offsetInText += UInt64(rest.count)
  214. }
  215.  
  216. // finally serves the request from the remainder of the file
  217. if let rest = String(data: self._fileHandle.readDataToEndOfFile(), encoding: self.encoding) {
  218. line = (line ?? "") + rest
  219. self.offsetInText += UInt64(rest.count)
  220. }
  221.  
  222. return line
  223. }
  224.  
  225. /// Rewinds the file to the beginning.
  226.  
  227. func rewind () {
  228.  
  229. self.offsetInText = 0
  230. self._lineBuffer.removeAll()
  231. self._dataBuffer.removeAll()
  232. self._fileHandle.seek(toFileOffset: 0)
  233. }
  234.  
  235. /// Seeks to the given character offset in the text.
  236. /// This is painfully slow, since the file needs to be scanned again.
  237. /// - Parameters:
  238. /// - offset: the offset of the character in the text we want to seek to.
  239.  
  240. func seek (toTextOffset offset: UInt64) {
  241.  
  242. if offset == self.offsetInText {
  243. return
  244. }
  245. if offset < self.offsetInText {
  246. self.rewind()
  247. }
  248. while self.offsetInText < offset && self.read() != nil { } // a call to read() updates self.offsetInText
  249. }
  250. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement