Swiftでの改行( )の扱いでハマったこと

先日、とある事情からSwiftで文字列の処理を書いていました。

その中で盛大にハマった事があったので忘れないようにメモ。

環境

Swift 5.0.1

改行

ここで取り扱う改行というのは以下の三つを指します。

表現説明補足
\n改行LF : UNIX系OS
\r復帰CR : 旧Mac
\r\n改行 + 復帰CR+LF : Windows

改行の処理

とある事情から、文字列を改行ごとに分割する処理を書いていました。

extension String {
    public subscript (_ i: Int) -> Character {
        return self[self.index(self.startIndex, offsetBy: i)]
    }
    public subscript (_ start:Int,end:Int) -> String {
        return String(self.prefix(end).dropFirst(start))
    }

    public func splitlines(_ keepends:Bool=false) -> [String] {
        let lineTokens = "\n\r"
        var len = self.count, i = 0, j = 0, eol = 0
        var result:[String] = []
        while  i < len {
            while i < len && !lineTokens.contains(self[i]) {
                i += 1
            }
            eol = i
            if i < len {
                if self[i] == "\r" && (i + 1) < len && self[i + 1] == "\n" {
                    i += 2
                } else {
                    i += 1
                }
                if keepends {
                    eol = i
                }
            }
            result.append(self[j,eol])
            j = i;
        }
        if j < len {
            result.append(self[j,eol])
        }
        return result
    }
}

上記のコードに対して以下のようなテストを行いました。

XCTAssertEqual("abc\nabc".splitlines(), ["abc","abc"])
XCTAssertEqual("abc\nabc\r".splitlines(true), ["abc\n","abc\r"])
XCTAssertEqual("abc\r\nabc\n".splitlines(), ["abc","abc"])
XCTAssertEqual("abc\r\nabc\n".splitlines(true), ["abc\r\n","abc\n"])

すると

error : XCTAssertEqual failed: ("["abc\r\nabc"]") is not equal to ("["abc", "abc"]")
error : XCTAssertEqual failed: ("["abc\r\nabc\n"]") is not equal to ("["abc\r\n", "abc\n"]")

というような警告を受けた。

\r\nでうまく分割されない…

デバッガを利用して、調べているとそもそも\r"abc\r\nabc\n"の中に入っていない判定を頂きました。

もしや、と思いSwiftの対話コンソールで確認してみると

  1> "\r\n".count
$R1: Int = 1

なんと\r\nが一文字の判定になっている!!!

なるほど、確かにこっちの方が色々と都合が良いのかもしれない…

それを踏まえて書き直したのがこちら

    public func splitlines(_ keepends:Bool=false) -> [String] {
        let lineTokens = "\n\r\r\n" /* \r\n を追加 */
        var len = self.count, i = 0, j = 0, eol = 0
        var result:[String] = []
        while  i < len {
            while i < len && !lineTokens.contains(self[i]) {
                i += 1
            }
            eol = i
            if i < len {
                i += 1
                if keepends {
                    eol = i
                }
            }
            result.append(self[j,eol])
            j = i;
        }
        if j < len {
            result.append(self[j,eol])
        }
        return result
    }
}

普段C++やらPythonを触っている筆者的には若干違和感を感じざるを得ないですが、\r\nを特殊な扱いをしなくても処理できるのは素晴らしい…でも最初に言って欲しかったなぁ

テストで気付けたからよかったけど気づかなかったらと思うと恐ろしい…

まとめ

  • SwiftのStringでは\r\nの改行は一文字として扱われる
  • テスト大事