はじめに
- @nejinejio さんのGo言語でキャメルケースをスネークケースに変換するを拝見しまして、私はぜひElixirでやってみようとおもいました
-
Elixirは
Elixir 1.10.3 (compiled with Erlang/OTP 23)
を使いました
ハイライト
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. インストールとプロジェクトの作成
$ 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"
- うん、動いています
- テストケースに挙げたパターンは正しい値が返っていますが、いろいろ対応できていないケースが多いです
iex> CamelToSnake.convert "ActiveModel::Errors"
"active_model_::_errors"
- 正しくは、
"active_model/errors"
- 後述するActiveSupportの
underscore
メソッドの結果を正としています
- 後述するActiveSupportの
ソースコードを書く 〜その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
- すべてパスしていることでしょう
- 正直に申しますと私は、
(~r/(?:(?<=([A-Za-z\d]))|\b)((?-mix:(?=a)b))(?=\b|[^a-z])/
の部分はよくわかっていないです
- 正直に申しますと私は、
Wrapping Up
- お好きな言語でスネークケースをお楽しみください
- Enjoy!