LivebookでAtCoderの正解チェックを行ってみた

# はじめに

LivebookにAtCoderの回答を入力すると、問題にある入力例を入力として実行して、出力例と一致するかをチェックするLivebookを作ってみました。

次のURLをLivebookで開くことで使えす。
https://github.com/masahiro-999/easy_atcoder/blob/main/easy_atcoder.livemd

# 使い方

コンテスト、問題名を入力します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532235/567636e1-3220-95d7-7655-25a993ee05a5.png)

Mainのモジュールに回答プログラムを書きます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532235/9c393ab3-34d8-f165-dd39-58935ede10ec.png)

最後に、入力例を使って実行して、結果が出力例と一致するか表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532235/211e6cba-e822-6895-2dfb-590118aaf0fa.png)


# 実際のlivemd

# Easy AtCoder

```elixir
Mix.install(
  [
    {:httpoison, "~> 2.2.1"},
    {:floki, "~> 0.35.2"},
    {:kino, "~> 0.12.0"}
  ],
  config: [my_app: [use_myio: true]]
)
```

## コンテスト、問題名を入力

```elixir
contest_name = Kino.Input.text("コンテスト名")
```

```elixir
problem_name = Kino.Input.text("問題名")
```

```elixir
url =
  "https://atcoder.jp/contests/#{Kino.Input.read(contest_name)}/tasks/#{Kino.Input.read(contest_name)}_#{Kino.Input.read(problem_name)}"

doc = HTTPoison.get!(url)
{:ok, html} = Floki.parse_document(doc.body)
section_list = Floki.find(html, "section")

pre_in =
  section_list
  |> Enum.filter(fn {_, _, [hd | _]} -> String.starts_with?(Floki.text(hd), "入力例") end)
  |> Enum.map(fn {_, _, [_hd | tail]} -> Floki.text(hd(tail)) end)
  |> Enum.map(fn x -> String.replace(x, "\r\n", "\n") end)

pre_out =
  section_list
  |> Enum.filter(fn {_, _, [hd | _]} -> String.starts_with?(Floki.text(hd), "出力例") end)
  |> Enum.map(fn {_, _, [_hd | tail]} -> Floki.text(hd(tail)) end)
  |> Enum.map(fn x -> String.replace(x, "\r\n", "\n") end)

{pre_in, pre_out}
```

```elixir
# Livebookで実行する時だけMyIOに置き換える事ができないので、MyIOはMainと同じファイルに記述する
# defmodule MyIO do
#   def read(_device \\ nil, option) do
#     {file_in, _} = Application.get_env(:my_app, :file, {:stdio, :stdio})
#     IO.read(file_in, option)
#   end

#   def puts(_device \\ nil, item) do
#     {_, file_out} = Application.get_env(:my_app, :file, {:stdio, :stdio})
#     IO.puts(file_out, item)
#   end

#   def getn(_device \\ nil, a, b) do
#     {_, file_out} = Application.get_env(:my_app, :file, {:stdio, :stdio})
#     IO.getn(file_out, a, b)
#   end
# end
```

## 回答を作成

```elixir
defmodule Main do
  # Livebookで実行する時だけMyIOに置き換える(条件文の中だとaliasが働かないので保留)
  # if Application.compile_env(:my_app, :use_myio, :false) do
  #   alias MyIO, as: IO
  # end
  alias MyIO, as: IO

  def next_token(acc \\ "") do
    case IO.getn(:stdio, "", 1) do
      " " -> acc
      "\n" -> acc
      x -> next_token(acc <> x)
    end
  end

  def input(), do: IO.read(:line) |> String.trim()
  def ii(), do: next_token() |> String.to_integer()
  def li(), do: input() |> String.split(" ") |> Enum.map(&String.to_integer/1)

  def next_day(y, m, d, end_of_day, _end_of_month) when d < end_of_day, do: {y, m, d + 1}

  def next_day(y, m, d, end_of_day, end_of_month) when d == end_of_day,
    do: next_month(y, m, end_of_month)

  def next_month(y, m, end_of_month) when m < end_of_month, do: {y, m + 1, 1}
  def next_month(y, m, end_of_month) when m == end_of_month, do: {y + 1, 1, 1}

  def main() do
    end_of_month = ii()
    end_of_day = ii()
    y = ii()
    m = ii()
    d = ii()
    {y, m, d} = next_day(y, m, d, end_of_day, end_of_month)
    IO.puts("#{y} #{m} #{d}")
  end
end

defmodule MyIO do
  def read(_device \\ nil, option) do
    {file_in, _} = Application.get_env(:my_app, :file, {:stdio, :stdio})
    IO.read(file_in, option)
  end

  def puts(_device \\ nil, item) do
    {_, file_out} = Application.get_env(:my_app, :file, {:stdio, :stdio})
    IO.puts(file_out, item)
  end

  def getn(_device \\ nil, a, b) do
    {_, file_out} = Application.get_env(:my_app, :file, {:stdio, :stdio})
    IO.getn(file_out, a, b)
  end
end
```

```elixir
defmodule Judge do
  def judge(string_in, string_out) do
    {:ok, file} = StringIO.open(string_in)
    Application.put_env(:my_app, :file, {file, file})

    Main.main()
    {:ok, {_, result}} = StringIO.close(file)

    if string_out == result do
      IO.puts("OK")
      true
    else
      IO.inspect(string_in, label: "Input")
      IO.inspect(string_out, label: "Expected")
      IO.inspect(result, label: "Received")
      false
    end
  end
end
```

```elixir
n =
  for {{sample_in, sample_out}, n} <- Enum.zip([pre_in, pre_out]) |> Enum.with_index() do
    IO.puts("-- TEST #{n} --")
    Judge.judge(sample_in, sample_out)
  end
  |> Enum.count()

"Passed #{n} of #{length(pre_in)}"
```