いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-343 (UTF-8の各文字が何バイトであるのかを判定する)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。

特にGo限定の話でもなんでもないのですが、サンプルをGoで作ったのでここに。

UTF-8の文字は、1バイトから4バイトまでの可変長でエンコードされます。

Goでは、utf8.RuneLen() とかを使うとサイズが分かりますが、C言語とかだと自力でやる必要があります。

UTF-8では、先頭バイトを見ることで、その文字が何バイトでエンコードされているかが分かります。

ソースとコメントでご覧になったほうが分かりやすいと思いますので、以下に画像で貼り付け。

上記の理屈は、どの言語でも同じ。

ちなみに、utf8.RuneLen() の実装の方は、先頭バイトのビット状態ではなくUnicode コードポイントの上限の値を判定条件にして何バイトか判定していますね。

https://cs.opensource.google/go/go/+/refs/tags/go1.21.1:src/unicode/utf8/utf8.go;l=321

サンプル

package main

import (
    "flag"
    "fmt"
    "unicode/utf8"

    "github.com/devlights/gomy/output"
)

func main() {
    var (
        u = flag.Bool("u", false, "use rune")
    )

    flag.Parse()

    if err := run(*u); err != nil {
        panic(err)
    }
}

func run(runeMode bool) error {
    var (
        strs = []string{
            // 全角かな (3bytes)
            "こんにちは",
            // 全角カタカナ (3bytes)
            "コンニチハ",
            // 半角カタカナ (3bytes)
            "コンニチハ",
            // 英数字記号 (1byte)
            "golang->60l4n6",
            // ©¼½¾ (2bytes)
            "\U000000A9\U000000BC\U000000BD\U000000BE",
            // 🍺🍻🍷🍜 (4bytes)
            "\U0001F37A\U0001F37B\U0001F377\U0001F35C",
        }
        fn = manual
    )

    if runeMode {
        fn = useRune
    }

    for _, v := range strs {
        output.Stdoutf("", "[%s]", v)
        output.StdoutHr()

        if err := fn(v); err != nil {
            return err
        }
    }

    return nil
}

func manual(s string) error {

    for i := 0; i < len(s); {
        var (
            b = s[i]
            l = 0
        )

        //
        // UTF-8の先頭バイトを判定し、バイトサイズ算出
        //
        // UTF-8エンコーディングでは、各文字は1バイトから4バイトまでの可変長でエンコードされる。
        // 先頭バイト(最初のバイト)を見ることで、その文字が何バイトでエンコードされているかを判定できる。
        //
        // - 0xxxxxxx: 1バイト(ASCIIと互換性あり)
        // - 110xxxxx: 続く1バイトと合わせて2バイト
        // - 1110xxxx: 続く2バイトと合わせて3バイト
        // - 11110xxx: 続く3バイトと合わせて4バイト
        //
        // 以下の case は上記を判定している.
        //
        // - (b & 0x80) == 0   : 最上位1ビットが0    であるなら、この文字は1バイト
        // - (b & 0xE0) == 0xC0: 最上位2ビットが110  であるなら、この文字は2バイト
        // - (b & 0xF0) == 0xE0: 最上位3ビットが1110 であるなら、この文字は3バイト
        // - (b & 0xF8) == 0xF0: 最上位4ビットが11110であるなら、この文字は4バイト
        //
        // REFERENCES:
        //   - https://ja.wikipedia.org/wiki/UTF-8
        //
        switch {
        case (b & 0x80) == 0:
            l = 1
        case (b & 0xE0) == 0xC0:
            l = 2
        case (b & 0xF0) == 0xE0:
            l = 3
        case (b & 0xF8) == 0xF0:
            l = 4
        default:
            return fmt.Errorf("invalid utf-8 char (%b)", b)
        }

        output.Stdoutf("[byte-count]", "%s (%d)\n", s[i:i+l], l)

        i += l
    }

    return nil
}

func useRune(s string) error {

    for _, r := range s {
        l := utf8.RuneLen(r)
        if l == -1 {
            return fmt.Errorf("invalid utf-8 char (%c)", r)
        }

        output.Stdoutf("[byte-count]", "%c (%d)\n", r, l)
    }

    return nil
}

タスクファイルは以下。

# https://taskfile.dev

version: '3'

tasks:
  default:
    cmds:
      - task: run-manual
  run-manual:
    cmds:
      - go run main.go
  run-userune:
    cmds:
      - go run main.go -u
  diff:
    cmds:
      - go run main.go > manual.txt
      - go run main.go -u > userune.txt
      - diff manual.txt userune.txt
    ignore_error: true
  clean:
    cmds:
      - rm -f ./*.txt

実行すると以下のようになります。

$ task
task: [run-manual] go run main.go
[こんにちは]-------------------------------------------------- 
[byte-count](3)
[byte-count](3)
[byte-count](3)
[byte-count](3)
[byte-count](3)
[コンニチハ]-------------------------------------------------- 
[byte-count](3)
[byte-count](3)
[byte-count](3)
[byte-count](3)
[byte-count](3)
[コンニチハ]-------------------------------------------------- 
[byte-count](3)
[byte-count](3)
[byte-count](3)
[byte-count](3)
[byte-count](3)
[golang->60l4n6]-------------------------------------------------- 
[byte-count]         g (1)
[byte-count]         o (1)
[byte-count]         l (1)
[byte-count]         a (1)
[byte-count]         n (1)
[byte-count]         g (1)
[byte-count]         - (1)
[byte-count]         > (1)
[byte-count]         6 (1)
[byte-count]         0 (1)
[byte-count]         l (1)
[byte-count]         4 (1)
[byte-count]         n (1)
[byte-count]         6 (1)
[©¼½¾]-------------------------------------------------- 
[byte-count]         © (2)
[byte-count]         ¼ (2)
[byte-count]         ½ (2)
[byte-count]         ¾ (2)
[🍺🍻🍷🍜]-------------------------------------------------- 
[byte-count]         🍺 (4)
[byte-count]         🍻 (4)
[byte-count]         🍷 (4)
[byte-count]         🍜 (4)


$ task diff
task: [diff] go run main.go > manual.txt
task: [diff] go run main.go -u > userune.txt
task: [diff] diff manual.txt userune.txt

#
# 何も出力されない。つまり utf8.RuneLen を使った場合は同じ結果が出力出来ている
#

リポジトリ

github.com

参考情報

ja.wikipedia.org

Goのおすすめ書籍

Go言語による並行処理

Go言語による並行処理

Amazon


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。