[Swift] AtCoder を UnitTest しつつ解く

     
⌛️ 6 min.
AtCoder 向けテスト環境を構築する方法のメモ

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Ventura 13.4
  • Xcode 14.3.1

以前の記事

以前にも AtCoder 向け環境セットアップの記事を書いてます。

Swift [Swift] AtCoder で Swift を使って UnitTest しながら実装する方法

ですが、自分で読み直してみて わかりにくい部分があったので、改めて記事にしてみます。

AtCoder

サイトは、こちら
問題が出されて、その問題を解くためのコードを提出すると採点してくれるサイトです。Web 上のテキストフィールドにコードを入れると、実行して コードの採点をしてくれます。その時に、様々な言語を指定することができ、その中の1つに Swift があります。

MEMO

前回の記事を書いた頃に、個人的に盛り上がっていました。ですが Swift で使用できる 標準ライブラリ的なもの皆無であることもあり、数回参加してやめてしまいました。例えば、C++ は、STL に便利なメソッドが多く用意されているのと比較して、Swift は 自前で用意しなければいけないメソッドが多く 面倒になってしまいました。

ですが、先日なんとなく 確認してみたところ、Swift-Collections や Swift-algorithms を使用したコードが使えるようになるみたいです。(2023.5.31 時点では、いつから利用可能かはよくわかりませんでした)

ということで、再度(?) やる気になったので、環境を再構築しようと思ったのですが、(自分で書いたにも関わらず) 以前の記事は 暗黙の前提が多くわかりにくかったので この記事で あらためて まとめています。

Xcode での環境構築(UnitTest 含む)

AtCoder では、 Browser 上にコピペしたコードが、サーバー上でコンパイル/リンク等されて実行されます。そのため、UI を持つアプリではなく、コマンドラインツールとしてのコードになっている必要があります。
つまり、明示的な main から実行されるコードです。

CommandLineToolプロジェクト

Xcode では、”Command Line Tool” 向け プロジェクトテンプレートを使用してセットアップします。

CLT_project

あとは、適当に名前をつけて、保存場所を指定すればプロジェクトが作成されます。

標準では main.swift が作成されて、以下のような中身になっています。

//
//  main.swift
//
//  Created by : Tomoaki Yagishita on 2023/05/31
//  © 2023  SmallDeskSoftware
//

import Foundation

print("Hello, World!")

Swift コンパイラーは、main.swift があると、そのファイルから実行を開始します。
つまり、このプロジェクトをコンパイルして、作成されたバイナリを実行すると “Hello, World!” と表示されるということです。

MEMO

@main という annotation で 実行開始位置を指定することもできますが、main.swift ファイルの存在とは排他的なオプションになっているようです。

UnitTestBundle 追加

UnitTest を使って進めていきたいので、最初に、 Unit Testing Bundle を追加してしまいます。

  1. 新しいターゲットを追加します。
    AddUnitTest_1
  2. “Unit Testing Bundle” を選択します。
    AddUnitTest_2
  3. 適当な名前をつけます
    AddUnitTest_3

    Command Line Tool の UnitTest は、Target を設定することはできませんので、None ですすめます。

  4. Command Line Tool 自身を Target に設定できないため、コード(main.swift)を直接 UnitTest ターゲットに追加します。
    AddUnitTest_4

ここまでの操作で、ロジックを UnitTest しつつ、提出用コードを作る準備ができました。

SwiftPackage の追加

swift-collections, swift-algorithms を使用できるようになるようなので、プロジェクトに Swift Package を追加します。

  1. swift-algorithms の追加。URL: https://github.com/apple/swift-algorithms.git
    add_swift_algorithms
    add_swift_algorithms_2
  2. swift-collections の追加。URL: https://github.com/apple/swift-collections.git
    add_swift_collections_1
    add_swift_collections_2
注意

SwiftPackage の追加ターゲットは、Command Line Tool ではなく UnitTest です。
自環境では、UnitTest 時に参照できればよく、Command Line Tool としてコンパイルされることは、自環境上ではありません。

注意

執筆時点(2023.5.31) では、swift-collections, swift-algorithms いずれも AtCoder では使用できないので、将来的な準備でしかありません。

自分の環境としては、上記をセットアップした git リポジトリを用意しておいて、必要に応じて branch を作成して使用しています。

プロジェクトのテンプレートを作りたいと思ったのですが、情報が少なくできませんでした・・・orz

2つのファイルの使い分け

# 自分の備忘録として、実装する時の方向性もメモしておきます。

プロジェクト内には2つのファイルがあります。
– main.swift
– <UnitTestターゲット名>.swift

main.swift には、main 関数と、ロジック実装関数 Func を実装していきます。
<UnitTestターゲット名>.swift には、Func 関数の UnitTest を記述する形で進めます。

どちらに何を書くかという基準は、main.swift をまるごとコピーして、AtCoder のサイトに貼り付けることで提出できるかどうかです。(必要なものは、main.swift に入れる)

AtCoder の課題は、複数行の入力をもとに課題の解を求め、結果を標準出力に出力します。

標準入出力を使った関数は、UnitTest が手間なので、Func(_ lines:[String]) -> [String] なる関数を UnitTest 対象にしてテストを行い、以下のような main 関数と合わせて、AtCoder に提出することで動作させます。

以下は、こちらの問題を解いてみたものです。

//
//  main.swift
//
//  Created by : Tomoaki Yagishita on 2023/05/31
//  © 2023  SmallDeskSoftware
//

import Foundation

func main() {
    guard let line0 = readLine() else { fatalError("no input")}
    guard let line1 = readLine() else { fatalError("no input")}
    guard let line2 = readLine() else { fatalError("no input")}
    let lines = [line0, line1, line2]
    let results = Func(lines)
    for result in results {
        print(result)
    }
}

#if !DEBUG
main()
#endif

func Func(_ lines: [String]) -> [String] {
    let ones = ["1", "l"]
    let zeros = ["0", "o"]
    for pair in zip(lines[1], lines[2]) {
        guard (pair.0 == pair.1) ||
                (ones.contains(String(pair.0)) && ones.contains(String(pair.1))) ||
                (zeros.contains(String(pair.0)) && zeros.contains(String(pair.1))) else { return ["No"] }
    }
    return ["Yes"]
}

UnitTest 側のファイルは、以下のような形で使用します。

//
//  UnitTest.swift
//
//  Created by : Tomoaki Yagishita on 2023/05/31
//  © 2023  SmallDeskSoftware
//

import XCTest

final class UnitTest: XCTestCase {

    func testExample() throws {
        let input1 = """
                     3
                     l0w
                     1ow
                     """.split(separator: "\n").map{ String($0)}
        XCTAssertEqual(Func(input1), ["Yes"])
        let input2 = """
                     3
                     abc
                     arc
                     """.split(separator: "\n").map{ String($0)}
        XCTAssertEqual(Func(input2), ["No"])

        let input3 = """
                     4
                     nok0
                     n0ko
                     """.split(separator: "\n").map{ String($0)}
        XCTAssertEqual(Func(input3), ["Yes"])
    }

}

なんとなく(?) 1つの 関数で例題として与えられているケース全てを実行してしまっていますが、複数のテストに分割しても もちろん 問題ありません。

main.swift 中の main

標準入力から読み込み、[String] にして、ロジック関数に渡し、ロジック関数からの返り値を標準出力に出力することを行います。

AtCoder での入力は、固定行数/可変行数 と大別できます。

readLine という標準入力から読み込み、String? で返す関数を使って、標準入力を [String] に整えます。

func main() {
    guard let line0 = readLine() else { fatalError("no input")}
    guard let line1 = readLine() else { fatalError("no input")}
    guard let line2 = readLine() else { fatalError("no input")}
    let lines = [line0, line1, line2]
    let results = Func(lines)
    for result in results {
        print(result)
    }
}

上記は、3行の入力があるケースで、3行渡されることを決めうちしてコードを書いています。

# 実際のアプリでは、エラー処理必須だと思いますが、AtCoder では 前提条件を満たさない入力を考慮する必要はありません。

可変行数を読み込むことが必要なケースでは、それ以前に読み込むべき行数が与えられるはずなので、その情報を使って読み込み、[String] を作成します。

guard let line0 = readLine() else { fatalError("no input")}
let lineNum = line0.components(separatedBy: " ").map{Int($0)!}.dropFirst().first!var lines = [line0]
for _ in 0..

問題によっては、1行に複数の情報を与えられることもあります。例えば上記は、1行目の 2つ目の数値がそれ以降に与えられる情報の行数である場合です。(問題によって変わりますので、適切な String 関連の関数を使って、読み取る必要があります。)

main.swift 中の Func

Func は、AtCoder で要求されるロジックを記述する関数です。

# 特に名前に制約はありませんが、毎回考えるのが手間&コピペミス等を防げるため、関数名を固定してしまっています。

func Func(_ lines: [String]) -> [String] {
    let ones = ["1", "l"]
    let zeros = ["0", "o"]
    for pair in zip(lines[1], lines[2]) {
        guard (pair.0 == pair.1) ||
                (ones.contains(String(pair.0)) && ones.contains(String(pair.1))) ||
                (zeros.contains(String(pair.0)) && zeros.contains(String(pair.1))) else { return ["No"] }
    }
    return ["Yes"]
}
MEMO

Swift では、String に対して 配列的にアクセスできず、String.Index や Range<String.Index> を使用しなければいけないことが 少し手間に感じますが、諦めるしかありません。

main.swift へのライブラリコピー

自前のライブラリ等を使っている場合は、main.swift へコピーしておきます。

main.swift をまるっと 提出できる状態にしておくと、提出作業が

「main.swift を選択し、⌘-a(全選択) -> ⌘-c(コピー) -> (AtCoder のサイトへ移動) -> ⌘-v(ペースト) 」

という固定作業にできるので、間違いを少なくできます。

UnitTest 中のテスト

UnitTest は、先に例を挙げましたが、以下のようになります。

//
//  UnitTest.swift
//
//  Created by : Tomoaki Yagishita on 2023/05/31
//  © 2023  SmallDeskSoftware
//

import XCTest

final class UnitTest: XCTestCase {
    func testExample() throws {
        let input1 = """
                     3
                     l0w
                     1ow
                     """.split(separator: "\n").map{ String($0)}
        XCTAssertEqual(Func(input1), ["Yes"])
    }

}

上記は、以下の3行が入力として与えられ、

3
l0w
1ow

次の行が出力されることをテストするものです。

Yes

Func の入力を [String]と決め打ちしているので、入力を [String] で作成して、Func を呼び出します。
Func の出力も [String] と決め打ちしているので、その結果が正しいかどうかを [String] の比較でテストします。

注意

Func の出力は、[String] ですが、課題によっては、実数計算が必要になることがあり、出力も Double を String に変換したものが必要になることがあります。そのような場合は、内部の計算誤差により、String も例題の回答と少し異なることがあります。
実際の計算結果を確認して誤差と考えられるのであれば、テストを調整する必要があります。

AtCoder Beginner Contest303-B をやってみる

実際に、使用シーンを想定して使ってみます。
お題は、こちら

UnitTest

せっかく UnitTest しやすい環境を作っているので、まずは、UnitTest を書きます。

AtCoder の問題は、例題がついているので、例題をテストとして書きます。
その他、自分の思いついたロジックで、コーナーケースを思いつくならそのテストも書いたほうが良いかもしれません。

この問題では、3つの例題とその回答が与えられていますので、そのまま使っています。

final class UnitTest: XCTestCase {
    func testExample() throws {
        let input1 = """
                     4 2
                     1 2 3 4
                     4 3 1 2
                     """.split(separator: "\n").map{ String($0)}
        XCTAssertEqual(Func(input1), ["2"])
        let input2 = """
                     3 3
                     1 2 3
                     3 1 2
                     1 2 3
                     """.split(separator: "\n").map{ String($0)}
        XCTAssertEqual(Func(input2), ["0"])

        let input3 = """
                     10 10
                     4 10 7 2 8 3 9 1 6 5
                     3 6 2 9 1 8 10 7 4 5
                     9 3 4 5 7 10 1 8 2 6
                     7 3 1 8 4 9 5 6 2 10
                     5 2 1 4 10 7 9 8 3 6
                     5 8 1 6 9 3 2 4 7 10
                     8 10 3 4 5 7 2 9 6 1
                     3 10 2 7 8 5 1 4 9 6
                     10 6 1 5 4 2 3 8 9 7
                     4 5 9 1 8 2 7 6 3 10
                     """.split(separator: "\n").map{ String($0)}
        XCTAssertEqual(Func(input3), ["6"])
    }

}

なお、Swift で複数行の String 定義を行うときは、””” を使うと便利です。
[Swift] String の定義方法

Func からは [String] で返されることに気をつけて XCTAssertEqual を使います。

# この問題では、Int を返しても良いですが、都度変更していると書き換えが手間なので、返り値も [String] で固定しています。

まだ コンパイルすら通りませんが、テストができたので、実装していきます。

main の修正

問題文を読むとわかりますが、可変行が渡されます。
1行目に、各行にいくつの情報が含まれているか と 何行あるか が渡されます。
その情報を使って、以降の行を読むことが必要です。

func main() {
    guard let line0 = readLine() else { fatalError("no input")}
    let lineNum = line0.components(separatedBy: " ").map{Int($0)!}.dropFirst().first!
    var lines = [line0]
    for _ in 0..<lineNum {
        guard let line = readLine() else { fatalError("no input")}
        lines.append(line)
    }
    let results = Func(lines)
    for result in results {
        print(result)
    }
}

1行目の2つ目の数字が、以降の行数です。その数だけ読んで、Func を呼び出します。

入力には、パターンがあるので、main 関数は、複数のテンプレートを用意しておくと、選ぶだけにすることも可能です。

Func の修正

# この記事執筆時点では、swift-collections や swift-algorithms は使えませんでした。
# 配列中の前後要素を使った処理のために、自前の PairIterator を使用しています。

方向性としては、以下です。
・各行を調べて友達のペアを確認する
・確認したペア情報を Set に入れる
・最後に Set の要素数を確認することで、友達のペア数を確認する

各行を調べて友達のペアを確認する

PairIterator は、[1,2,3,4,5] という配列に対して、(1,2), (2,3), (3,4), (4,5), (5,nil), nil というペアを返してくる Iterator なので、取得できるペアを Set に保存しつつ、最後のペア(5,nil)を破棄することで、友達のペアすべてを確認できることになります。

確認したペア情報を Set に入れる

課題には、「人 x と人 y からなる二人組と人 y と人 x からなる二人組は区別しません」 という条件があります。二人組をあらわす FriendPair を生成する時に、一人目を二人目より小さな数字になるように調整することで、同一視できるようにし、さらに、Set に保存することで重複が無いようにします。
こうすることで、(x,y) と (y,x) を同一視できるようになります。

なお、Set の要素は、Hashable でなければいけません。

各行を確認した後、最終的に Set に保存されている要素数が 友達と考えられるペアの数です。

N 人の人がいる時に 考えられる最大の友達ペアの数は N * (N-1) / 2 ですから、不仲(?)と考えられるペアの数は N * (N-1) / 2 から Set の要素数を引くと得られます。

ということで、Func は、以下のようになります。最終的に [String] として返すようにしています。

struct FriendPair: Hashable {
    let one: Int
    let two: Int

    init(_ one: Int,_ two: Int) { // always one  two
        self.one = min(one, two)
        self.two = max(one, two)
    }
}

func Func(_ lines: [String]) -> [String] {
    let numOfPerson = Int(lines[0].components(separatedBy: " ").first!)!
    var friendSet: Set = Set()

    for line in lines.dropFirst() {
        let pairs = line.components(separatedBy: " ").map({Int($0)!})
        var pairIte = pairs.makePairIterator()
        while let (current, next) = pairIte.next() {
            guard let next = next else { break }
            friendSet.insert(FriendPair(current, next))
        }
    }

    return [String(numOfPerson * (numOfPerson - 1) / 2 - friendSet.count)]
}


// copy from another library
public struct PairIterator{
    let collection: C
    var currentIterator: C.Iterator
    var nextIterator: C.Iterator

    public init(_ collection: C) {
        self.collection = collection
        currentIterator = collection.makeIterator()
        nextIterator = collection.makeIterator()
        _ = nextIterator.next()
    }

    public mutating func next() -> (C.Element, C.Element?)? {
        guard let current = currentIterator.next() else { return nil }
        let next = nextIterator.next()
        return (current, next)
    }
}

extension Array {
    public func makePairIterator() -> PairIterator {
        return PairIterator(self)
    }
}

あとは、Target に UnitTest を選択してから、コンパイル -> テスト -> 修正 を繰り返していくだけです。
Target に UnitTest を選択して、⌘-U を押下すると (コンパイル対象に設定しているので) main.swift もコンパイルされますので、ターゲットに Command Line Tool を選択することは不要となります。

まとめ

AtCoder 向けのテスト環境構築を改めて書きました。

AtCoder 向けのテスト環境構築
  • Command Line Tool プロジェクトを使用する
  • Unit Testing Bundle は、後から追加する必要がある
    その時、Target to be tested は、None 指定
  • テストしたい関数を含む main.swift を UnitTest ターゲットにも追加する
  • 標準入力読み込みは、readLine が便利

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です