Stringモジュールで遊んでみたシリーズ① -String.at ~ String.chunk の紹介

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

# 目次
1.Stringモジュールで遊んでみたシリーズ① -String.at ~ String.chunk の紹介(本記事)
2.[Stringモジュールで遊んでみたシリーズ② -String.codepoints ~ String.ends_with? の紹介](https://qiita.com/Alicesky2127/items/5db34c69993d35b8f3a7)
3.[Stringモジュールで遊んでみたシリーズ③ -String.equivalent? ~ String.last の紹介](https://qiita.com/Alicesky2127/items/37aa8e62e8e1d5fef3b6)
4.[Stringモジュールで遊んでみたシリーズ④ -String.length ~ String.next_grapheme の紹介](https://qiita.com/Alicesky2127/items/e3fa2092a67cb9f6ec9c)
5.[Stringモジュールで遊んでみたシリーズ⑤ -String.next_grapheme_size ~ String.printable? の紹介](https://qiita.com/Alicesky2127/items/a315148e966c9f103a7c)
6.[Stringモジュールで遊んでみたシリーズ⑥ -String.replace ~ String.replace_suffix の紹介](https://qiita.com/Alicesky2127/items/94a77afe270587ed21a9)
7.[Stringモジュールで遊んでみたシリーズ⑦ -String.replace_trailing ~ String.split の紹介](https://qiita.com/Alicesky2127/items/1a8e9c92fa758d0b1337)
8.[Stringモジュールで遊んでみたシリーズ⑧ -String.split_at ~ String.to_charlist の紹介](https://qiita.com/Alicesky2127/items/40e74cd622653796df5c)
9.[Stringモジュールで遊んでみたシリーズ⑨ -String.to_existing_atom ~ String.trim の紹介](https://qiita.com/Alicesky2127/items/9bf2b1effc3b68819ac2)
10.[Stringモジュールで遊んでみたシリーズ10 -String.trim_leading ~ String.valid? の紹介](https://qiita.com/Alicesky2127/items/b8310a3f5fc5b040ca78)


# 目的 
Stringモジュールに含まれる関数を触って機能を理解したい

https://hexdocs.pm/elixir/String.html

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

# String.atとは
`String.at(string, position)`は0ベースインデックスで、`string`から`position`番目の文字を取り出します

## 例
```diff_elixir:iex
String.at("elixir", 0)
```
```
0
```

`string`より`position`の値が大きい場合はnilを返します
```diff_elixir:iex
String.at("elixir", 10)
```
```
nil
```

`position`が負の値の場合は最後尾から数えます
```diff_elixir:iex
String.at("elixir", -1)
```
```
"r"
```


# String.bag_distanceとは
`String.bag_distance(string1, string2)`は2つの文字列間の類似度を計算します。
`string1` と `string2`の類似度を表す0から1までの浮動小数点値(`bag distance`と呼ばれる値)を返します。
この関数の目的は2つの文字列をマッチングし、大幅に異なる文字列をすばやく除外することです。


## 例
`string1` と `string2`に含まれる文字数が全く一致しない場合
```diff_elixir:iex
String.bag_distance("abc", "")
```
```
0.0
```

`string1` と `string2`に含まれる文字数が1文字だけ一致する場合
```diff_elixir:iex
String.bag_distance("abcd", "a")
```
```
0.25
```

`string1` と `string2`に含まれる文字数が2文字だけ一致する場合
```diff_elixir:iex
String.bag_distance("abcd", "ab")
```
```
0.5
```

`string1` と `string2`に含まれる文字数が完全一致する場合
```diff_elixir:iex
String.bag_distance("abcd", "abcd")
```
```
1.0
```

`string1` と `string2`に含まれる文字数が完全一致する場合、かつ並び順だけ異なる場合。
文字列内における順番は考慮しないことが分かります
```diff_elixir:iex
String.bag_distance("abcd", "dbca")
```
```
1.0
```

`string1` と `string2`に含まれる文字数が完全一致する場合、かつ、文字列長が異なる場合。
文字列長も考慮することが分かります
```diff_elixir:iex
String.bag_distance("abcd", "daccadcb")
```
```
0.5
```

# String.byte_sliceとは
`String.byte_slice(string, start_bytes, size_bytes)`は`start_bytes`(0ベースインデックス)から抽出を開始して、`size_bytes`のバイト数ぶんの部分文字列を返します。
この関数の目的は、文字列の特定の部分をバイト単位で取り出すことです。

## 例
### 前提
文字列`héllo`は下記6バイトからなるバイナリデータである。
なお`é`は<<195, 169>>の2バイトで1文字を表す結合文字列である。
ここで先頭の2バイトを取り出し、かつ書記素として有効な文字列だけ出力したいとする。[^1]
つまり、期待する出力は`"h"`のみである。
```diff_elixir:iex
inspect("héllo", binaries: :as_binaries)
```
```
"<<104, 195, 169, 108, 108, 111>>"
```

String.byte_sliceを使用すると期待値通りの出力が得られました。
```diff_elixir:iex
String.byte_slice("héllo", 0, 2)
```
```
"h"
```

## 類似の関数との比較
### binary_slice
`binary_slice(binary, start, size)`を使用すると書記素を意識せずに先頭から指定されたバイト数の文字（今回は2バイト）を取り出します。
`é`は<<195, 169>>の2バイトで1文字を表す結合文字列です。
したがって、今回の場合にbinary_sliceを使用して取り出すと書記素として無効なバイナリが出力されてしまいます。
```diff_elixir:iex
binary_slice("héllo", 0, 2)
```
```
<<104, 195>>
```

### String.slice
`String.slice(string, start, length)`を使用すると書記素ベースで先頭から指定された文字数（今回は2文字）を取り出します。
`hé`は"<<104, 195, 169>>であり3バイトです。
したがって、今回の場合に`String.slice`を使用して取り出すと先頭の2バイトだけを取り出せません。
```diff_elixir:iex
String.slice("héllo", 0, 2)
```
```
"hé"
```


# String.capitalizeとは
`String.capitalize(string, mode \\ :default)`は`string`の先頭の文字を大文字に変換し、残りの文字を`mode`に従って小文字に変換します。
## 例
```diff_elixir:iex
String.capitalize("abcd")
```
```
"Abcd"
```

1文字目がリガチャ(合字)あり文字列の場合、分解されます
```diff_elixir:iex
String.capitalize("ﬁnﬁ")
```
```
"Finﬁ"
```


# String.chunkとは
`String.chunk(string, trait)`は`string`を文字列を共通の特性(trait)を持つかたまり(chunk)に分割します。
traitは `:valid` または `:printable`のいずれかを取ります。
この関数の目的は、（この記事を執筆した段階では）バイナリ型の第一引数の内部をフィルタすることだと理解しています。

## 例
### `trait = :valid` の場合
有効な文字列(=UTF-8でエンコードされたバイナリ)か否かで分割します

#### 無効な文字列を含まない場合
```diff_elixir:iex
String.chunk(<<?a, ?b, ?c, 0>>, :valid)
```
```
[<<97, 98, 99, 0>>]
```

#### 無効な文字列を含む場合
`<<0xFFFF::utf16>>`は、0xFFFFをUTF-16でエンコードしたもので、Elixirでの有効な文字列(=UTF-8でエンコードされたバイナリ)の範囲外です

```diff_elixir:iex
String.chunk(<<?a, ?b, ?c, 0, 0xFFFF::utf16>>, :valid)
```
```
[<<97, 98, 99, 0>>, <<255, 255>>]
```


### `trait = :printable` の場合
印刷可能か否かで分割します。
つまり、制御文字などの非表示文字をフィルタします。

```diff_elixir:iex
String.chunk(<<?a, ?b, ?c, 0, 0x0FFFF::utf8>>, :printable)
```
```
["abc", <<0, 239, 191, 191>>]
```

#### 余談
iex内で `i`関数を実行すると印刷可能な文字列か分かる

#### 印刷可能な文字列の場合
Descriptionの説明にprintable(印刷可能)と記載されます
```diff_elixir:iex
i <<?a>>
```
```
Term
  "a"
Data type
  BitString
Byte size
  1
Description
  This is a string: a UTF-8 encoded binary. It's printed surrounded by
  "double quotes" because all UTF-8 encoded code points in it are printable.
Raw representation
  <<97>>
Reference modules
  String, :binary
Implemented protocols
  Collectable, IEx.Info, Inspect, List.Chars, String.Chars
```

#### 印刷不能な文字列の場合
Descriptionの説明にnon-printable(印刷不能)と記載されます
```diff_elixir:iex
i <<0x0FFFF::utf8>>
```
```
Term
  <<239, 191, 191>>
Data type
  BitString
Byte size
  3
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
  `<<239, 191, 191>>`).
Reference modules
  String, :binary
Implemented protocols
  Collectable, IEx.Info, Inspect, List.Chars, String.Chars
```

# ～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]:ユーザーが文字として認識する単位のことを書記素(grapheme)と呼びます。

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


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