Elixir String.splitの紹介

こんにちは！
プログラミング未経験文系出身、Elixirの国に迷い込んだ？！見習いアルケミストのaliceと申します。
今回はString.splitについて学んだことをまとめます。

# 目的
String.splitの使いどころを理解する。

## 実行環境
Windows 11 ＋ WSL2 ＋ Ubuntu 22.04
Elixir v1.17.0
Erlang v27.0

# 前提
Elixirの文字列はUTF-8エンコードされたバイナリです。
```diff_elixir:iex
string = "hello"
is_binary(string)
```
```
true
```

```diff_elixir:iex
byte_size(string)
```
```
5
```

# String.split/1
`String.split(binary)`は、
- `binary`の先頭と末尾の空白を無視した上で、残りの文字をUnicodeの空白文字を区切りとして部分文字列に変換します
- 空白文字がいくつ連続しても1つの区切り文字として扱われます

## 例1
```diff_elixir:iex
String.split("foo bar")
```
```
["foo", "bar"]
```

## 例2
```diff_elixir:iex
String.split("foo" <> <<194, 133>> <> "bar")
```
```
["foo", "bar"]
```

### 例2の補足
#### <<194, 133>>は印刷不可能な文字列
`"foo" <> <<194, 133>> <> "bar"`は印刷不可能な文字列を含む結合文字列です。
`<<194, 133>>`がバイナリのまま表示されていることに違和感がある[^1]のですが、Elixirの文字列はUTF-8でエンコードされたバイナリであることを念頭に置けば混乱せずに済みます。
```diff_elixir:iex
 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)>
```
上記を要約すると<font color="red">「<194, 133>>は文字列ではあるが、印刷不可能な文字が含まれている」</font>とのことです。
印刷不可能な文字列はバイナリとしてそのまま表示されます。
ゆえに`"foo" <> <<194, 133>> <> "bar"`は印刷不可能な文字列を含む結合文字列だといえます。


#### <<194, 133>>はUnicodeの空白文字
実行結果より、<<194, 133>>が区切り文字として扱われていることが分かります。
`String.split/1`の定義より、区切り文字はUnicodeの空白文字です。
ゆえに`<<194, 133>>`はUnicodeの空白であるといえます。

## 例3
fooとbarの間には半角スペースと全角スペースがそれぞれ1つずつ入っていますが、空白文字がいくつ連続しても1つの区切り文字として扱われます。
```diff_elixir:iex
String.split(" foo 　bar ")
```
```
["foo", "bar"]
```

## 例4
```diff_elixir:iex
String.split("no\u00a0break")
```
```
["no break"]
```

### 例4の補足
\u00a0は、Unicodeの「U+00A0」を示すもので、これはno-break space(=ノンブレークスペース)という制御文字。
これもUnicodeの空白文字に含まれているため、`String.split/1`の区切り文字として機能します。

https://ja.wikipedia.org/wiki/%E7%A9%BA%E7%99%BD%E6%96%87%E5%AD%97



# String.split/3
`String.split(string, pattern, options \\ [])`は、
- `string`を`pattern`に沿って区切り、部分文字列に変換します
- `option`には[Regex.split/3](https://hexdocs.pm/elixir/Regex.html#split/3-options)のオプション全てが使用できます


## 例1
`option`無し。カンマを区切り文字として部分文字列に分割
```diff_elixir:iex
String.split("a,b,c", ",")
```
```
["a", "b", "c"]
```
```diff_elixir:iex
String.split("a,bc", ",")
```
```
["a", "bc"]
```

## 例2
`option`あり。正の整数で部分文字列の要素数を指定。デフォルトは`:infinity`
```diff_elixir:iex
String.split("a,b,c", ",", parts: 2)
```
```
["a", "b,c"]
```

```diff_elixir:iex
String.split("a,b,c,d,e", ",", parts: 3)
```
```
["a", "b", "c,d,e"]
```


## 例3
`option`無し。半角スペースおよびカンマを区切り文字として部分文字列に分割
```diff_elixir:iex
String.split("1,2 3,4", [" ", ","])
```
```
["1", "2", "3", "4"]
```

# String.split/3にRegex.split/3のoptionを組み合わせる
Regex.split/3のオプションを使ってみます

## 例1
正規表現でカンマを区切り文字として部分文字列に分割
```diff_elixir:iex
String.split("a,b,c", ~r{,}) 
```
```
["a", "b", "c"]
```

## 例2
正規表現でカンマを区切り文字として部分文字列に分割、かつ`option`の正の整数で部分文字列の要素数を指定
```diff_elixir:iex
String.split("a,b,c", ~r{,}, parts: 2)
```
```
["a", "b,c"]
```

## 例3
`trim`オプションは`true`のとき、出力結果のうち前後の空白文字列をトリミングする。デフォルトは`false`
正規表現の`\s`は1文字の区切り文字を指すメタ文字[^2]

```diff_elixir:iex
String.split(" a b c ", ~r{\s}, trim: true)
```
```
["a", "b", "c"]
```

## 例4
正規表現で"b"を区切り文字として部分文字列に分割
```diff_elixir:iex
String.split("abc", ~r{b})
```
```
["a", "c"]
```

## 例5
正規表現で"b"を区切り文字として部分文字列に分割
`include_captures`オプションは`true`のとき正規表現でマッチした部分（今回は"b"）も返す。デフォルトは`false`

```diff_elixir:iex
String.split("abc", ~r{b}, include_captures: true)
```
```
["a", "b", "c"]
```

## 例6
正規表現で"b"を区切り文字として部分文字列に分割
`include_captures`オプションは`true`のとき正規表現でマッチした部分（今回は"b"）も返す。デフォルトは`false`
`include_captures`オプションと`parts`オプションを併用した場合、マッチした部分（今回は"b"）については最大要素数にカウントされない。なのでこの出力結果について目検では要素数が3つあるように見える
```diff_elixir:iex
String.split("abc", ~r{b}, include_captures: true, parts: 2)
```
```
["a", "b", "c"]
```

## 例7
正規表現で"b"および"d"を区切り文字として部分文字列に分割
```diff_elixir:iex
String.split("abcde", ~r{[b,d]})
```
```
["a", "c", "e"]
```

## 例8
正規表現で"b"および"d"を区切り文字として部分文字列に分割
`include_captures`オプションは`true`のとき正規表現でマッチした部分（今回は"b"と"d）も返す。デフォルトは`false`
```diff_elixir:iex
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つあるように見える

```diff_elixir:iex
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つあるように見える

```diff_elixir:iex
String.split("abcde", ~r{[b,d]}, include_captures: true, parts: 3)
```
```
["a", "b", "c", "d", "e"]
```


# 例11
区切り文字を変数化
```diff_elixir:iex
pattern = :binary.compile_pattern([" ", ","])
String.split("1,2 3,4", pattern)
```
```
["1", "2", "3", "4"]
```

# 例12
区切り文字に空文字を指定した場合書記素ごとに分解される
```diff_elixir:iex
String.split("abc", "")
```
```
["", "a", "b", "c", ""]
```



# String.splitと書記素の関係
`String.split`は書記素の境界を越えることがあるので注意が必要です

## 結合文字列ありの場合
string1は U+0065 と U+0301 の結合文字列。
したがって"e"を区切り文字として部分文字列に分割される。
また、この時書記素の境界を越える

```diff_elixir:iex
string1 = "é"
String.split(String.normalize(string1, :nfd), "e")
```
```
["", "́"]
```

## 結合文字列なしの場合
string2はU+00E9であり結合文字列ではない。
したがって"e"を区切り文字として部分文字列に分割されない。
```diff_elixir:iex
string2 = "é"
String.split(String.normalize(string2, :nfc), "e")
```
```
["é"]
```

## 備考
Elixirにおける書記素については過去記事をご参照ください

https://qiita.com/Alicesky2127/items/8dcb4612dd3f6ba6f59b


# ～Elixirの国のご案内～
↓Elixirって何ぞや？と思ったらこちらもどぞ。Elixirは先端のアレコレをだいたい全部できちゃいます:laughing::sparkles::sparkles:

https://qiita.com/piacerex/items/09876caa1e17169ec5e1

↓ゼロからElixirを始めるなら「エリクサーチ」がおすすめ！私もエンジニア未経験から学習中です。

https://elixir-lang.info/topics/entry_column


↓[We Are The Alchemists, my friends!:bouquet:](https://qiita.com/torifukukaiou/items/6bcd5bbb80df9e594e9e "Awesome-san's Awesome article")[^10]
Elixirコミュニティは本当に優しくて温かい人たちばかり！
私が挫折せずにいられるのもこの恵まれた環境のおかげです。
まずは気軽にコミュニティを訪れてみてください。[^11]


https://speakerdeck.com/elijo/elixirkomiyunitei-falsebu-kifang-guo-nei-onrainbian


[^1]: 初見で`"foo" <> <<194, 133>> <> "bar"`について「文字列とバイナリが結合してる？何これ？」と混乱したため補足としてまとめました

[^2]:参考：正規表現 https://www.tohoho-web.com/js/regexp.htm

[^10]: @torifukukaiouさんのAwesomeな名言をお借りしました。Elixirコミュニティを一言で表すと、これに尽きます。


[^11]: @kn339264さんの素敵なスライドをお借りしました。Elixirコミュニティはいろんな形で活動中！