emahiro/b.log

日々の勉強の記録とか育児の記録とか。

byte 文字列を Int に変換する

Overview

ちょっと凝ったユースケースですが、byte 文字列を Int に変換したいユースケースがあったときに使えるテクニックについてまとめます。

以下のようなユースケースを考えます。

func main() {
    b := []byte("123456")

    for idx := range b {
        fmt.Println(b[idx])
    }

}

// OUTPUT:
// 
// 49 <- 1 
// 50 <- 2
// 51 <- 3
// 52 <- 4
// 53 <- 5
// 54 <- 6

ここで byte 配列に格納された書く byte 文字列を自然数の配列に変換したいとします。
byte 文字列で 49(ASCIIコード上) は 1 なのでこの与えられた byte 配列を 1,2,3,... という Int の配列に変換することを目指します。

ASCII コードの 0 を使う

要は各 byte 文字列から 48 を引いた値が Int に変換したときの数字に一致するので 48 を引いてあげると良さそうです。

文字としての 0 は ASCII コード表 の上では 48 番目で対応する ASCII コードは 0x30 です。 なのでこれを与えられた数から引きます。

func main() {
    b := []byte("123456")

    for idx := range b {
        fmt.Println(b[idx] - 0x30)
    }

}

これで目的とする byte 文字列を Int に変換することができます。

Atoi の実装を参考にする。

文字列から int に変換するときによく使う strconv.Atoi の実装で似たようなことをしています。
ref: https://golang.org/src/strconv/atoi.go?s=5658:5690#L241

// Atoi is equivalent to ParseInt(s, 10, 0), converted to type int.
func Atoi(s string) (int, error) {
    const fnAtoi = "Atoi"

    sLen := len(s)
    if intSize == 32 && (0 < sLen && sLen < 10) ||
        intSize == 64 && (0 < sLen && sLen < 19) {
        // Fast path for small integers that fit int type.
        s0 := s
        if s[0] == '-' || s[0] == '+' {
            s = s[1:]
            if len(s) < 1 {
                return 0, &NumError{fnAtoi, s0, ErrSyntax}
            }
        }

        n := 0
        for _, ch := range []byte(s) {
            ch -= '0'
            if ch > 9 {
                return 0, &NumError{fnAtoi, s0, ErrSyntax}
            }
            n = n*10 + int(ch)
        }
        if s0[0] == '-' {
            n = -n
        }
        return n, nil
    }

    // Slow path for invalid, big, or underscored integers.
    i64, err := ParseInt(s, 10, 0)
    if nerr, ok := err.(*NumError); ok {
        nerr.Func = fnAtoi
    }
    return int(i64), err
}

ch -= '0' に注目します。一つ前では文字列 0 を表す ASCII コードを目的とする byte 文字列から引いて目的とする Int を取り出してましたが、 Atoi の実装ではUnicodeコードポイントを持つ rune で 0 を表現してそれを byte から引くことで実現してます。

'0' の中身を出力してみるとわかります。

func main() {
    fmt.Println('0')
}

// OUTPUT:
//
// 48

つまり '0' を使うと byte 文字列からそのまま目的とする int に変換することができます。

func main() {
    b := []byte("123456")
    for idx := range b {
        fmt.Println(b[idx] - '0')
    }

}

これでも同様に動作させることができます。

まとめ

byte -> int という実装で以下のことがわかりました。

  1. byte文字列と ASCII コードはセットで計算できる
  2. byte文字列と Unicode コードポイントはセットで計算できる。

文字列の変換、奥が深い...。