Elixirでキャメルケースをスネークケースに変換する

# はじめに
- @nejinejio さんの[Go言語でキャメルケースをスネークケースに変換する](https://qiita.com/nejinejio/items/36d735e50b2d58a3d3da)を拝見しまして、私はぜひ[Elixir](https://elixir-lang.org/)でやってみようとおもいました
- [Elixir](https://elixir-lang.org/)は`Elixir 1.10.3 (compiled with Erlang/OTP 23)`を使いました

## ハイライト

```elixir:lib/awesome.ex
defmodule Awesome do
  def underscore(camel_cased_word) do
    case String.match?(camel_cased_word, ~r/[A-Z-]|::/) do
      false -> camel_cased_word
      _ -> do_underscore(camel_cased_word)
    end
  end

  defp do_underscore(camel_cased_word) do
    String.replace(camel_cased_word, "::", "/")
    |> (fn word ->
          Regex.replace(~r/(?:(?<=([A-Za-z\d]))|\b)((?-mix:(?=a)b))(?=\b|[^a-z])/, word, fn _,
                                                                                            g1,
                                                                                            g2 ->
            "#{g1 && "_"}#{String.downcase(g2)}"
          end)
        end).()
    |> String.replace(~r/([A-Z\d]+)([A-Z][a-z])/, "\\g{1}_\\g{2}")
    |> String.replace(~r/([a-z\d])([A-Z])/, "\\g{1}_\\g{2}")
    |> String.replace("-", "_")
    |> String.downcase()
  end
end
```
- [String.match?/2](https://hexdocs.pm/elixir/String.html#match?/2)
- [String.replace/4](https://hexdocs.pm/elixir/String.html#replace/4)
- [String.downcase/2](https://hexdocs.pm/elixir/String.html#downcase/2)
- [Regex.replace/4](https://hexdocs.pm/elixir/Regex.html#replace/4)



# 0. インストールとプロジェクトの作成
- まずは[Elixir](https://elixir-lang.org/)をインストールしましょう
    - 手前味噌ですが、[インストール](https://qiita.com/torifukukaiou/items/d04d0273749c41eb50af#0-%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB) などを参考にしてください
- インストールができましたら以下のコマンドでプロジェクトを作ります

```console
$ mix new camel_to_snake
$ cd camel_to_snake
```

# ソースコードを書く 〜その1〜
- 文字を1個ずつ確認して大文字であれば小文字に、必要に応じて`_`を挿入したりします
- 注目している文字の1個前と2個前が大文字であったか、小文字であったかを気にすることにします

```elixir:lib/camel_to_snake.ex
defmodule CamelToSnake do
  @doc """
  convert camel to snake.

  ## Examples

      iex> CamelToSnake.convert("RememberMe")
      "remember_me"
      iex> CamelToSnake.convert("requestID")
      "request_id"
      iex> CamelToSnake.convert("HTTPRequest")
      "http_request"
      iex> CamelToSnake.convert("HTML5Script")
      "html5_script"

  """
  def convert(s) do
    s
    |> String.to_charlist()
    |> Enum.reduce({[], nil, nil}, &do_convert/2)
    |> elem(0)
    |> List.to_string()
  end

  defp do_convert(c, {list, :upcase, :upcase}) when ?a <= c and c <= ?z do
    {Enum.slice(list, 0..-2) ++ [?_, Enum.at(list, -1), c], :downcase, :upcase}
  end

  defp do_convert(c, {list, before_case, _}) when ?a <= c and c <= ?z do
    {list ++ [c], :downcase, before_case}
  end

  defp do_convert(c, {list, :downcase, _}) do
    {list ++ [?_, shift(c)], :upcase, :downcase}
  end

  defp do_convert(c, {list, before_case, _}) do
    {list ++ [shift(c)], :upcase, before_case}
  end

  defp shift(c) when ?A <= c and c <= ?Z, do: c + ?a - ?A

  defp shift(c), do: c
end
```

## テスト

```
$ mix test
```

- すべてパスしていることでしょう
    - `mix new`したときにできている`CamelToSnake.hello/1`は👆のコード例では消しています
    - `test/camel_to_snake_test.exs`には、`CamelToSnake.hello/1`のテストが含まれていますので、`CamelToSnake.hello/1`を消す場合はテストのほうもあわせて消すか、気にしないことにするかしてください

## 実行

```elixir
$ iex -S mix

iex> CamelToSnake.convert("RememberMe")
"remember_me"
```

- うん、動いています :tada::tada::tada:
- テストケースに挙げたパターンは正しい値が返っていますが、いろいろ対応できていないケースが多いです:sweat:

```elixir
iex> CamelToSnake.convert "ActiveModel::Errors"
"active_model_::_errors"
```
- 正しくは、`"active_model/errors"`
    - 後述するActiveSupportの`underscore`メソッドの結果を正としています

# ソースコードを書く 〜その2〜
- そういえば[Ruby on Rails](https://rubyonrails.org/)で、この用途にぴったりのメソッドがあったことをおもいだしました
- `underscore`というメソッド名で実装は[ここ](https://github.com/rails/rails/blob/v6.0.3.2/activesupport/lib/active_support/inflector/methods.rb#L80-L100)にありました

```ruby:activesupport/lib/active_support/inflector/methods.rb
    # Makes an underscored, lowercase form from the expression in the string.
    #
    # Changes '::' to '/' to convert namespaces to paths.
    #
    #   underscore('ActiveModel')         # => "active_model"
    #   underscore('ActiveModel::Errors') # => "active_model/errors"
    #
    # As a rule of thumb you can think of +underscore+ as the inverse of
    # #camelize, though there are cases where that does not hold:
    #
    #   camelize(underscore('SSLError'))  # => "SslError"
    def underscore(camel_cased_word)
      return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
      word = camel_cased_word.to_s.gsub("::", "/")
      word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" }
      word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
      word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
      word.tr!("-", "_")
      word.downcase!
      word
    end
```

- こちらを参考に[Elixir](https://elixir-lang.org/)で書いてみます

```lib/awesome.ex
defmodule Awesome do
  @doc """
  underscore

  ## Examples

      iex> Awesome.underscore("RememberMe")
      "remember_me"
      iex> Awesome.underscore("requestID")
      "request_id"
      iex> Awesome.underscore("HTTPRequest")
      "http_request"
      iex> Awesome.underscore("HTML5Script")
      "html5_script"
      iex> Awesome.underscore("ActiveModel::Errors")
      "active_model/errors"
      iex> Awesome.underscore("Product")
      "product"
      iex> Awesome.underscore("SpecialGuest")
      "special_guest"
      iex> Awesome.underscore("ApplicationController")
      "application_controller"
      iex> Awesome.underscore("Area51Controller")
      "area51_controller"
      iex> Awesome.underscore("HTMLTidy")
      "html_tidy"
      iex> Awesome.underscore("HTMLTidyGenerator")
      "html_tidy_generator"
      iex> Awesome.underscore("FreeBSD")
      "free_bsd"
      iex> Awesome.underscore("HTML")
      "html"
      iex> Awesome.underscore("ForceXMLController")
      "force_xml_controller"
      iex> Awesome.underscore("Admin::Product")
      "admin/product"
      iex> Awesome.underscore("Users::Commission::Department")
      "users/commission/department"
      iex> Awesome.underscore("UsersSection::CommissionDepartment")
      "users_section/commission_department"

  """
  def underscore(camel_cased_word) do
    case String.match?(camel_cased_word, ~r/[A-Z-]|::/) do
      false -> camel_cased_word
      _ -> do_underscore(camel_cased_word)
    end
  end

  defp do_underscore(camel_cased_word) do
    String.replace(camel_cased_word, "::", "/")
    |> (fn word ->
          Regex.replace(~r/(?:(?<=([A-Za-z\d]))|\b)((?-mix:(?=a)b))(?=\b|[^a-z])/, word, fn _,
                                                                                            g1,
                                                                                            g2 ->
            "#{g1 && "_"}#{String.downcase(g2)}"
          end)
        end).()
    |> String.replace(~r/([A-Z\d]+)([A-Z][a-z])/, "\\g{1}_\\g{2}")
    |> String.replace(~r/([a-z\d])([A-Z])/, "\\g{1}_\\g{2}")
    |> String.replace("-", "_")
    |> String.downcase()
  end
end
```

- `Awesome.underscore/1`を作ってみました
- `Awesome`モジュールが、doctestの対象となるように設定をします

```elixir:test/camel_to_snake_test.exs
defmodule CamelToSnakeTest do
  use ExUnit.Case
  doctest CamelToSnake
  doctest Awesome # add
```

## テスト

```
$ mix test
```

- すべてパスしていることでしょう :rocket::rocket::rocket:
    - 正直に申しますと私は、`(~r/(?:(?<=([A-Za-z\d]))|\b)((?-mix:(?=a)b))(?=\b|[^a-z])/`の部分はよくわかっていないです:sweat_smile:


# Wrapping Up
- お好きな言語でスネークケースをお楽しみください
- **Enjoy!**