Rubyで文字コードを扱うコードを書くときに便利なメソッド集

## はじめに

[Ruby Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ruby) の11日目の記事です。

昨日は、@universato さんの [Ruby競プロTips\(基本・罠・高速化108 2\.7x2\.7\)](https://zenn.dev/universato/articles/20201210-z-ruby) でした。

最近文字コードに依存したコードを書く機会があり、  `String` の便利なメソッドをいろいろと知ったので紹介します。

## 動作確認環境

- Ruby 2.7.2
- UTF-8

## 前提情報

- Ruby は他の多くの言語と異なり、 `String` オブジェクト自体がエンコーディング情報を持っています。
- 現在のスクリプトエンコーディングは疑似変数 `__ENCODING__` で確認できます。
- Unicodeでは人間が認識する自然な１文字と、Unicodeのデータ上の１文字が異なることがあります。人間が認識する自然な１文字は「書記素クラスタ（grapheme_cluster）」という単位でカウントされます。
- 特に断りがない場合、 `String` のメソッドです。
- 特に断りがない場合、UTF-8を前提に記述しています。

## 文字コードを知りたい

`ord` , `each_codepoint` , `unpack("U*")` 

### ord

文字列の最初の文字の文字コードを10進数の値（ `Integer` ）で返します。
値を16進数にするには `to_s(16)` を使います。

```ruby
p "a".ord
# => 97

p "a".ord.to_s(16)
# => "61"
```

16進数の数値をU+nnnnの形式で検索すると、文字コードから文字が分かります。

例えば `a` は16進数で `61` という値でした。 `U+0061` で検索すると、 `a` という文字であることが分かります。 

[Unicode Utilities: Character Properties](https://util.unicode.org/UnicodeJsps/character.jsp?a=0061) 

### each_codepoint

文字列の各文字の文字コードを知りたい場合は `each_codepoint` を使うと便利です。

```ruby
"abcあいう漢字".each_codepoint { |s| p s.to_s(16) }

# "61" # a
# "62" # b
# "63" # c
# "3042" # あ
# "3044" # い
# "3046" # う
# "6f22" # 漢
# "5b57" # 字
```

### unpack("U*")

`unpack("U*")` でも文字コードを調べることが出来ます。返り値は `Array` です。

引数の詳細は Ruby リファレンスマニュアルを参照してください。[String\#unpack \(Ruby 2\.7\.0 リファレンスマニュアル\)](https://docs.ruby-lang.org/ja/latest/method/String/i/unpack.html)

```ruby
p "a".unpack("U*")
# => [97]

"abc".unpack("U*").each { |s| p s.to_s(16) }
# "61"
# "62"
# "63"
# => [97, 98, 99]
```

返り値のエンコーディング情報は UTF-8 とは限りません。必要に応じ後述のメソッドでエンコーディング情報を変換してください。

```ruby
a = "a".unpack("U*")
# => [97]
a.each { |s| p s.to_s.encoding }
# #<Encoding:US-ASCII>

i = "い".unpack("U*")
# => [12356]
i.each { |s| p s.to_s.encoding }
# #<Encoding:US-ASCII>

p "👨‍👩‍👦‍👦".unpack("H*")
# => ["f09f91a8e2808df09f91a9e2808df09f91a6e2808df09f91a6"]

p ["f09f91a8e2808df09f91a9e2808df09f91a6e2808df09f91a6"].pack("H*")
#  => "\xF0\x9F\x91\xA8\xE2\x80\x8D\xF0\x9F\x91\xA9\xE2\x80\x8D\xF0\x9F\x91\xA6\xE2\x80\x8D\xF0\x9F\x91\xA6"

p ["f09f91a8e2808df09f91a9e2808df09f91a6e2808df09f91a6"].pack("H*").encoding
# => #<Encoding:ASCII-8BIT>
```

## 文字コードから文字にしたい

`\unnnn`,  `\u{nnnnnn}` , `Array#pack`, `Integer#chr`

### \unnnn, \u{nnnnnn}

文字コードが4桁までの場合は \unnnn（nは16進数の数値）を使えます。5桁〜6桁の場合は `{}` でくくることで入力できます。また、 `{}` の中には連続して文字コードを入れることができます。

```ruby
"\u0061"
# => "a"
"\u{1F97A}"
# => "🥺"
"\u{1F31F 1F3B6}"
# => "🌟🎶"
```

### Array#pack

配列の内容を引数で指定された形でパックし、文字列を返します。16進数の文字列を指定するには `0x` を前につけます。

```ruby
ary = "abc".unpack("U*")
# => [97, 98, 99]
p ary.pack("U*")
# => "abc"

p [0x3042].pack("U*")
# => "あ"
```

### Integer#chr
`self` を文字コードと見たときに対応する文字を返します。引数にエンコーディングを渡すことができます。
引数を渡さなかった場合はエンコーディングを `US-ASCII`, `ASCII-8BIT`, `Encoding.default_internal` の順で解釈します。

```ruby
p 33440.chr(Encoding::SJIS)
# => "\x{82A0}"
p 0x3042.chr(Encoding::UTF_8)
# => "あ"
```

## バイト列から文字にしたい
`pack("H*")` を使ってバイナリにしたあと、`encode` します。 
`pack("H*")` の返り値の encoding は変換したい文字コードとは限らないため、 `encode` の第2引数に変換したい文字コードを指定します。

この方法を取る場合、 `encode` で変換する文字コードを決め打ちするか、なんらかの手段でバイト列から文字コードを推測する必要があります。

```ruby
ary = 'あいう'.bytes
# => [227, 129, 130, 227, 129, 132, 227, 129, 134]
[ ary.map { _1.to_s(16) }.join ].pack("H*").encode('UTF-8', 'UTF-8')
# => "あいう"
```

## 文字数を知りたい

`size` , `length` や、それに `grapheme_clusters` を組み合わせて使うことができます。

### size, length

```ruby
p "a".size
# => 1
p "ああ".size
# => 2
p "ああ".length
# => 2
```

ただし、 `size` や `length` では結合された文字に対して想定と異なる結果を返すことがあります。

これは主に結合文字で起こる事象です。見た目には1文字に見えますが、内部的には複数の文字が組み合わさっています。

```ruby
p "👨‍👩‍👧‍👦".length # U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466
# => 7
p "0︀".length # U+0030 U+FE00
# => 2
p "が".length # U+304B U+3099 「か」と濁点を組み合わせた結合文字の「が」
# => 2
p "が".length # U+304C 普段入力する「が」
# => 1
```

### grapheme_clusters

このような結合文字を1文字としてカウントしたいときには、 `grapheme_clusters` を使います。これはUnicodeの書記素クラスタ単位でカウントしてくれます。

```ruby
p "👨‍👩‍👧‍👦".grapheme_clusters.length
# => 1
p "0︀".grapheme_clusters.length
# => 1
p "が".grapheme_clusters.length
# => 1
```

## 文字のバイト長を知りたい

### bytesize

文字列が何バイトあるか分かります。

```ruby
p "a".bytesize
# => 1
p "あ".bytesize
# => 3
p "aあ".bytesize
# => 4
p "👨‍👩‍👧‍👦".bytesize
# => 25
```

## 文字列の内容が壊れてないか知りたい

### valid_encoding?

受け取った文字列が現在のエンコーディングで妥当であれば `true` を返します。いわゆる文字化けするような文字でないか確かめられます。

```ruby
p __ENCODING__
# => #<Encoding:UTF-8>
p "\x61".valid_encoding? # a
# => true
p "\x61\xff".valid_encoding? # a に ff を追加して存在しない文字をつくった
# => false
```

## エンコーディングを変更したい

### encode

`encode` でエンコーディングを変更・変換できます。エンコーディングの一覧は [class Encoding \(Ruby 2\.7\.0 リファレンスマニュアル\)](https://docs.ruby-lang.org/ja/latest/class/Encoding.html) で確認できます。self のエンコーディングは変更されません。

細かいエンコーディングの操作には [class Encoding::Converter \(Ruby 2\.7\.0 リファレンスマニュアル\)](https://docs.ruby-lang.org/ja/latest/class/Encoding=3a=3aConverter.html) もあります。

```ruby
s = "あいう".encode("Windows-31J")
# => "\x{82A0}\x{82A2}\x{82A4}"
p s.encoding
# => #<Encoding:Windows-31J>

hoge = "ほげ"
p hoge.encode("WINDOWS-31J")
# => "\x{82D9}\x{82B0}" # 返り値はエンコーディングが変換されている
p hoge.encoding # hogeのエンコーディングは変更されない
# => #<Encoding:UTF-8>
```

### force_encoding

`self` のエンコーディング情報を破壊的に変更します。このとき、実際のエンコーディングは変換されません。また、 `valid_encoding?` のようなチェックも行われません。 

```ruby
piyo = "piyo"
p piyo.encoding
# => #<Encoding:UTF-8>

piyo.force_encoding("EUC-JP")
# => "piyo"
piyo.encoding # piyo のエンコーディングが変更されたことを確認
# => #<Encoding:EUC-JP>
```

## エンコーディング情報を知りたい

### encoding

`encoding` で self のエンコーディング情報が分かります。

```ruby
e = "言語".encode("EUC-JP")
# => "\x{B8C0}\x{B8EC}"
u = "言語".encode("UTF-8")
# => "言語"
p e.encoding
# => #<Encoding:EUC-JP>
p u.encoding
# => #<Encoding:UTF-8>
```

## 見た目は同じだが文字コードが異なる文字を同じ文字として扱いたい

### unicode_normalize

Unicode正規化を行うことで、異なる文字を同じ文字に変換することができます。Unicode正規化についてはここでは割愛します。

```ruby
ga1 = "が"
ga1.each_codepoint { |s| p s.to_s(16) }
# "304b"
# "3099"

ga2 = "が"
ga2.each_codepoint { |s| p s.to_s(16) }
# "304c"

p(ga1 == ga2)
# => false

p(ga1.unicode_normalize == ga2.unicode_normalize)
# => true
```

## Rubyで扱えるエンコーディングを知りたい

`Encoding` クラスの定数に格納されています。

[class Encoding \(Ruby 3\.0\.0 リファレンスマニュアル\)](https://docs.ruby-lang.org/ja/latest/class/Encoding.html)

```ruby
pp Encoding.constants
# => 
# [:CompatibilityError,
#  :BINARY,
#  :ASCII_8BIT,
#  :UTF_8,
#  :US_ASCII,
# 以下略
```

## 参考資料

- [class String \(Ruby 2\.7\.0 リファレンスマニュアル\)](https://docs.ruby-lang.org/ja/latest/class/String.html)
- [多言語化 \(Ruby 2\.7\.0 リファレンスマニュアル\)](https://docs.ruby-lang.org/ja/latest/doc/spec=2fm17n.html)

Ruby の多言語化対応について知りたい場合はこちらの記事に詳しく書いてあります。

- [Ruby M17N の設計と実装](https://magazine.rubyist.net/articles/0025/0025-Ruby19_m17n.html)