5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

ハイライト

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

0. インストールとプロジェクトの作成

  • まずはElixirをインストールしましょう
  • インストールができましたら以下のコマンドでプロジェクトを作ります
$ mix new camel_to_snake
$ cd camel_to_snake

ソースコードを書く 〜その1〜

  • 文字を1個ずつ確認して大文字であれば小文字に、必要に応じて_を挿入したりします
  • 注目している文字の1個前と2個前が大文字であったか、小文字であったかを気にすることにします
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を消す場合はテストのほうもあわせて消すか、気にしないことにするかしてください

実行

$ iex -S mix

iex> CamelToSnake.convert("RememberMe")
"remember_me"
  • うん、動いています :tada::tada::tada:
  • テストケースに挙げたパターンは正しい値が返っていますが、いろいろ対応できていないケースが多いです:sweat:
iex> CamelToSnake.convert "ActiveModel::Errors"
"active_model_::_errors"
  • 正しくは、"active_model/errors"
    • 後述するActiveSupportのunderscoreメソッドの結果を正としています

ソースコードを書く 〜その2〜

  • そういえばRuby on Railsで、この用途にぴったりのメソッドがあったことをおもいだしました
  • underscoreというメソッド名で実装はここにありました
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で書いてみます
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の対象となるように設定をします
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!
5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?