| 1 | import Foundation |
| 2 | |
| 3 | struct Coordinate: Hashable, Sendable { |
| 4 | let r: Int |
| 5 | let c: Int |
| 6 | var ref: String { |
| 7 | var c = c |
| 8 | var label = "" |
| 9 | repeat { |
| 10 | label = String(UnicodeScalar(65 + c % 26)!) + label |
| 11 | c = c / 26 - 1 |
| 12 | } while c >= 0 |
| 13 | return label + String(r + 1) |
| 14 | } |
| 15 | static func parse(_ text: String) -> Coordinate? { |
| 16 | let text = text.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() |
| 17 | let letters = text.prefix { $0.isASCII && $0.isLetter } |
| 18 | guard !letters.isEmpty, letters.count <= 3, let row = Int(text.dropFirst(letters.count)), |
| 19 | row > 0, row <= 5000 |
| 20 | else { return nil } |
| 21 | let col = letters.utf8.reduce(0) { $0 * 26 + Int($1) - 64 } |
| 22 | guard col <= 2000 else { return nil } |
| 23 | return Coordinate(r: row - 1, c: col - 1) |
| 24 | } |
| 25 | } |
| 26 | struct CellRange: Equatable, Sendable { |
| 27 | let start: Coordinate |
| 28 | let end: Coordinate |
| 29 | init(_ a: Coordinate, _ b: Coordinate) { |
| 30 | start = Coordinate(r: min(a.r, b.r), c: min(a.c, b.c)) |
| 31 | end = Coordinate(r: max(a.r, b.r), c: max(a.c, b.c)) |
| 32 | } |
| 33 | static func parse(_ text: String) -> CellRange? { |
| 34 | let parts = text.split(separator: ":", omittingEmptySubsequences: false) |
| 35 | guard (1...2).contains(parts.count), let a = Coordinate.parse(String(parts[0])), |
| 36 | let b = Coordinate.parse(String(parts.last!)) |
| 37 | else { return nil } |
| 38 | return CellRange(a, b) |
| 39 | } |
| 40 | var coordinates: [Coordinate] { |
| 41 | (start.r...end.r).flatMap { r in (start.c...end.c).map { Coordinate(r: r, c: $0) } } |
| 42 | } |
| 43 | var count: Int { (end.r - start.r + 1) * (end.c - start.c + 1) } |
| 44 | func contains(_ p: Coordinate) -> Bool { |
| 45 | (start.r...end.r).contains(p.r) && (start.c...end.c).contains(p.c) |
| 46 | } |
| 47 | } |
| 48 | |