Download this file
| 1 | import Foundation |
| 2 | |
| 3 | // Quoting matches spreadsheets: tabs/newlines/quotes round-trip as cell content. |
| 4 | enum TSV { |
| 5 | static func encode(_ rows: [[String]]) -> String { |
| 6 | rows.map { |
| 7 | $0.map { text in |
| 8 | text.contains(where: { |
| 9 | $0 == "\t" || $0 == "\r" || $0 == "\n" || $0 == "\r\n" || $0 == "\"" |
| 10 | }) ? "\"" + text.replacingOccurrences(of: "\"", with: "\"\"") + "\"" : text |
| 11 | }.joined(separator: "\t") |
| 12 | }.joined(separator: "\n") |
| 13 | } |
| 14 | static func decode(_ text: String) -> [[String]] { |
| 15 | let chars = Array(text) |
| 16 | var rows: [[String]] = [] |
| 17 | var row: [String] = [] |
| 18 | var cell = "" |
| 19 | var quoted = false |
| 20 | var i = 0 |
| 21 | while i < chars.count { |
| 22 | let ch = chars[i] |
| 23 | if ch == "\"" { |
| 24 | if quoted && i + 1 < chars.count && chars[i + 1] == "\"" { |
| 25 | cell.append(ch) |
| 26 | i += 1 |
| 27 | } else if quoted || cell.isEmpty { |
| 28 | quoted.toggle() |
| 29 | } else { |
| 30 | cell.append(ch) |
| 31 | } |
| 32 | } else if !quoted && ch == "\t" { |
| 33 | row.append(cell) |
| 34 | cell = "" |
| 35 | } else if !quoted && (ch == "\n" || ch == "\r\n" || ch == "\r") { |
| 36 | row.append(cell) |
| 37 | rows.append(row) |
| 38 | row = [] |
| 39 | cell = "" |
| 40 | } else { |
| 41 | cell.append(ch) |
| 42 | } |
| 43 | i += 1 |
| 44 | } |
| 45 | row.append(cell) |
| 46 | rows.append(row) |
| 47 | if rows.last == [""] && (text.last == "\n" || text.last == "\r\n" || text.last == "\r") { |
| 48 | rows.removeLast() |
| 49 | } |
| 50 | return rows |
| 51 | } |
| 52 | } |
| 53 | |