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 という実装で以下のことがわかりました。
- byte文字列と ASCII コードはセットで計算できる
- byte文字列と Unicode コードポイントはセットで計算できる。
文字列の変換、奥が深い...。