こんにちは!
プログラミング未経験文系出身、Elixirの国に迷い込んだ?!見習いアルケミストのaliceと申します。
今回はString.splitについて学んだことをまとめます。
目的
String.splitの使いどころを理解する。
実行環境
Windows 11 + WSL2 + Ubuntu 22.04
Elixir v1.17.0
Erlang v27.0
前提
Elixirの文字列はUTF-8エンコードされたバイナリです。
string = "hello"
is_binary(string)
true
byte_size(string)
5
String.split/1
String.split(binary)
は、
-
binary
の先頭と末尾の空白を無視した上で、残りの文字をUnicodeの空白文字を区切りとして部分文字列に変換します - 空白文字がいくつ連続しても1つの区切り文字として扱われます
例1
String.split("foo bar")
["foo", "bar"]
例2
String.split("foo" <> <<194, 133>> <> "bar")
["foo", "bar"]
例2の補足
<<194, 133>>は印刷不可能な文字列
"foo" <> <<194, 133>> <> "bar"
は印刷不可能な文字列を含む結合文字列です。
<<194, 133>>
がバイナリのまま表示されていることに違和感がある1のですが、Elixirの文字列はUTF-8でエンコードされたバイナリであることを念頭に置けば混乱せずに済みます。
i <<194, 133>>
Term
<<194, 133>>
Data type
BitString
Byte size
2
Description
This is a string: a UTF-8 encoded binary. It's printed with the `<<>>`
syntax (as opposed to double quotes) because it contains non-printable
UTF-8 encoded code points (the first non-printable code point being
`<<194, 133>>`).
Reference modules
String, :binary
Implemented protocols
Collectable, IEx.Info, Inspect, List.Chars, String.Chars
iex(2)>
上記を要約すると「<194, 133>>は文字列ではあるが、印刷不可能な文字が含まれている」とのことです。
印刷不可能な文字列はバイナリとしてそのまま表示されます。
ゆえに"foo" <> <<194, 133>> <> "bar"
は印刷不可能な文字列を含む結合文字列だといえます。
<<194, 133>>はUnicodeの空白文字
実行結果より、<<194, 133>>が区切り文字として扱われていることが分かります。
String.split/1
の定義より、区切り文字はUnicodeの空白文字です。
ゆえに<<194, 133>>
はUnicodeの空白であるといえます。
例3
fooとbarの間には半角スペースと全角スペースがそれぞれ1つずつ入っていますが、空白文字がいくつ連続しても1つの区切り文字として扱われます。
String.split(" foo bar ")
["foo", "bar"]
例4
String.split("no\u00a0break")
["no break"]
例4の補足
\u00a0は、Unicodeの「U+00A0」を示すもので、これはno-break space(=ノンブレークスペース)という制御文字。
これもUnicodeの空白文字に含まれているため、String.split/1
の区切り文字として機能します。
String.split/3
String.split(string, pattern, options \\ [])
は、
-
string
をpattern
に沿って区切り、部分文字列に変換します -
option
にはRegex.split/3のオプション全てが使用できます
例1
option
無し。カンマを区切り文字として部分文字列に分割
String.split("a,b,c", ",")
["a", "b", "c"]
String.split("a,bc", ",")
["a", "bc"]
例2
option
あり。正の整数で部分文字列の要素数を指定。デフォルトは:infinity
String.split("a,b,c", ",", parts: 2)
["a", "b,c"]
String.split("a,b,c,d,e", ",", parts: 3)
["a", "b", "c,d,e"]
例3
option
無し。半角スペースおよびカンマを区切り文字として部分文字列に分割
String.split("1,2 3,4", [" ", ","])
["1", "2", "3", "4"]
String.split/3にRegex.split/3のoptionを組み合わせる
Regex.split/3のオプションを使ってみます
例1
正規表現でカンマを区切り文字として部分文字列に分割
String.split("a,b,c", ~r{,})
["a", "b", "c"]
例2
正規表現でカンマを区切り文字として部分文字列に分割、かつoption
の正の整数で部分文字列の要素数を指定
String.split("a,b,c", ~r{,}, parts: 2)
["a", "b,c"]
例3
trim
オプションはtrue
のとき、出力結果のうち前後の空白文字列をトリミングする。デフォルトはfalse
正規表現の\s
は1文字の区切り文字を指すメタ文字2
String.split(" a b c ", ~r{\s}, trim: true)
["a", "b", "c"]
例4
正規表現で"b"を区切り文字として部分文字列に分割
String.split("abc", ~r{b})
["a", "c"]
例5
正規表現で"b"を区切り文字として部分文字列に分割
include_captures
オプションはtrue
のとき正規表現でマッチした部分(今回は"b")も返す。デフォルトはfalse
String.split("abc", ~r{b}, include_captures: true)
["a", "b", "c"]
例6
正規表現で"b"を区切り文字として部分文字列に分割
include_captures
オプションはtrue
のとき正規表現でマッチした部分(今回は"b")も返す。デフォルトはfalse
include_captures
オプションとparts
オプションを併用した場合、マッチした部分(今回は"b")については最大要素数にカウントされない。なのでこの出力結果について目検では要素数が3つあるように見える
String.split("abc", ~r{b}, include_captures: true, parts: 2)
["a", "b", "c"]
例7
正規表現で"b"および"d"を区切り文字として部分文字列に分割
String.split("abcde", ~r{[b,d]})
["a", "c", "e"]
例8
正規表現で"b"および"d"を区切り文字として部分文字列に分割
include_captures
オプションはtrue
のとき正規表現でマッチした部分(今回は"b"と"d)も返す。デフォルトはfalse
String.split("abcde", ~r{[b,d]}, include_captures: true)
["a", "b", "c", "d", "e"]
例9
正規表現で"b"および"d"を区切り文字として部分文字列に分割
include_captures
オプションはtrue
のとき正規表現でマッチした部分(今回は"b"と"d)も返す。デフォルトはfalse
include_capturesオプションとpartsオプションを併用した場合、マッチした部分(今回は"b")については最大要素数にカウントされない。なのでこの出力結果について目検では要素数が3つあるように見える
String.split("abcde", ~r{[b,d]}, include_captures: true, parts: 2)
["a", "b", "cde"]
例10
正規表現で"b"および"d"を区切り文字として部分文字列に分割
include_captures
オプションはtrue
のとき正規表現でマッチした部分(今回は"b"と"d)も返す。デフォルトはfalse
include_capturesオプションとpartsオプションを併用した場合、マッチした部分(今回は"b"と"d)は最大要素数にカウントされない。なのでこの出力結果について目検では要素数5つあるように見える
String.split("abcde", ~r{[b,d]}, include_captures: true, parts: 3)
["a", "b", "c", "d", "e"]
例11
区切り文字を変数化
pattern = :binary.compile_pattern([" ", ","])
String.split("1,2 3,4", pattern)
["1", "2", "3", "4"]
例12
区切り文字に空文字を指定した場合書記素ごとに分解される
String.split("abc", "")
["", "a", "b", "c", ""]
String.splitと書記素の関係
String.split
は書記素の境界を越えることがあるので注意が必要です
結合文字列ありの場合
string1は U+0065 と U+0301 の結合文字列。
したがって"e"を区切り文字として部分文字列に分割される。
また、この時書記素の境界を越える
string1 = "é"
String.split(String.normalize(string1, :nfd), "e")
["", "́"]
結合文字列なしの場合
string2はU+00E9であり結合文字列ではない。
したがって"e"を区切り文字として部分文字列に分割されない。
string2 = "é"
String.split(String.normalize(string2, :nfc), "e")
["é"]
備考
Elixirにおける書記素については過去記事をご参照ください
~Elixirの国のご案内~
↓Elixirって何ぞや?と思ったらこちらもどぞ。Elixirは先端のアレコレをだいたい全部できちゃいます
↓ゼロからElixirを始めるなら「エリクサーチ」がおすすめ!私もエンジニア未経験から学習中です。
↓We Are The Alchemists, my friends!3
Elixirコミュニティは本当に優しくて温かい人たちばかり!
私が挫折せずにいられるのもこの恵まれた環境のおかげです。
まずは気軽にコミュニティを訪れてみてください。4
-
初見で
"foo" <> <<194, 133>> <> "bar"
について「文字列とバイナリが結合してる?何これ?」と混乱したため補足としてまとめました ↩ -
@torifukukaiouさんのAwesomeな名言をお借りしました。Elixirコミュニティを一言で表すと、これに尽きます。 ↩