できない.dev

整数を2進数や16進数の文字列にするには

255 を 11111111 や ff のように、2 進数・8 進数・16 進数の文字列にする方法と、その文字列を整数へ戻す方法を各言語で示す。
桁数のそろえ方、負の数の出力、0x などの接頭辞の扱いまでを扱う。

公開:

選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。

Python

n = 255
print(bin(n), oct(n), hex(n))                          # 0b11111111 0o377 0xff
print(format(n, "b"), format(n, "o"), format(n, "x"))  # 11111111 377 ff
print(f"{n:X}")                                        # FF
print(f"{5:08b}")                                      # 00000101(幅 8 を 0 で埋める)
print(f"{n:#012b}")                                    # 0b0011111111(接頭辞も幅 12 に数える)
print(format(-5, "b"), bin(-5))                        # -101 -0b101(2 の補数にならない)
 
print(int("ff", 16), int("11111111", 2))               # 255 255
print(int("0xff", 16), int("0xff", 0))                 # 255 255
 
try:
    print("0x" + 255)
except TypeError as e:
    print(e)                                           # can only concatenate str (not "int") to str
print("0x" + format(255, "x"))                         # 0xff
 
try:
    int("0xff")
except ValueError as e:
    print(e)                                           # invalid literal for int() with base 10: '0xff'

format(n, "b") や f-string の {n:08b} で整数を 2 進数・8 進数・16 進数の文字列にできる。
bin と hex は 0b 0x の接頭辞付きで返り、文字列に戻すときは int(text, 16) のように基数を渡す。
"0x" + 255 のように数値を + でつなぐと TypeError になるため、format で文字列にしてからつなぐ。

できないときPython で「TypeError: can only concatenate str (not "int") to str」が直らない

JavaScript

const n = 255;
console.log(n.toString(2), n.toString(8), n.toString(16)); // 11111111 377 ff
console.log(n.toString(16).toUpperCase());                 // FF
console.log((5).toString(2).padStart(8, "0"));             // 00000101
console.log("0x" + n.toString(16));                        // 0xff
console.log((-5).toString(2));                             // -101(2 の補数にならない)
console.log((-5 >>> 0).toString(2));                       // 11111111111111111111111111111011(32 ビットの補数)
 
console.log(parseInt("ff", 16), parseInt("11111111", 2));  // 255 255
console.log(parseInt("0xff", 16), parseInt("0xff"));       // 255 255
console.log(Number("0xff"), Number("0b101"));              // 255 5
console.log(parseInt("0b101", 2));                         // 0(b で読み取りが止まる)
console.log(parseInt("102", 2));                           // 2(2 進数に無い桁の手前までを読む)
console.log(parseInt("12", 2), Number("12abc"));           // 1 NaN

Number.prototype.toString に基数を渡すと 2 進数から 36 進数までの文字列になり、戻すときは parseInt(text, radix) を使う。
parseInt は基数に無い文字の手前で読み取りを止めて例外にしないため、parseInt("102", 2) は 2 を返す。
外部の入力は事前に検証する。

TypeScript

function toRadix(value: number, radix: number, width = 0): string {
  if (!Number.isInteger(value)) {
    throw new RangeError(`整数ではない: ${value}`);
  }
  return value.toString(radix).padStart(width, "0");
}
 
function fromRadix(text: string, radix: number): number {
  const digits = "0123456789abcdefghijklmnopqrstuvwxyz".slice(0, radix);
  if (!new RegExp(`^[${digits}]+$`, "i").test(text)) {
    throw new RangeError(`${radix} 進数として読めない: ${text}`);
  }
  return parseInt(text, radix);
}
 
console.log(toRadix(255, 2));      // 11111111
console.log(toRadix(255, 16));     // ff
console.log(toRadix(5, 2, 8));     // 00000101
console.log(fromRadix("ff", 16));  // 255
console.log(fromRadix("1010", 2)); // 10
 
try {
  fromRadix("102", 2);
} catch (e) {
  console.log((e as Error).message); // 2 進数として読めない: 102
}
console.log(parseInt("102", 2));     // 2(標準の parseInt は黙って途中までを読む)

toString(radix) と padStart の組み合わせを、整数だけを受け付ける関数にまとめた。
標準の parseInt は不正な桁を黙って読み捨てるので、正規表現で桁を先に検証してから戻している。

Go

package main
 
import (
	"fmt"
	"strconv"
)
 
func main() {
	n := 255
	fmt.Println(strconv.FormatInt(int64(n), 2), strconv.FormatInt(int64(n), 8), strconv.FormatInt(int64(n), 16)) // 11111111 377 ff
	fmt.Printf("%b %o %x %X\n", n, n, n, n)                                                                    // 11111111 377 ff FF
	fmt.Printf("%08b\n", 5)                                                                                    // 00000101
	fmt.Printf("%#x %#b\n", n, n)                                                                              // 0xff 0b11111111
	fmt.Println(strconv.FormatInt(-5, 2))                                                                      // -101(2 の補数にならない)
	m := int32(-5)
	fmt.Printf("%b\n", uint32(m)) // 11111111111111111111111111111011(uint32 にすると 2 の補数)
 
	a, _ := strconv.ParseInt("ff", 16, 64)
	b, _ := strconv.ParseInt("11111111", 2, 64)
	c, _ := strconv.ParseInt("0xff", 0, 64)
	fmt.Println(a, b, c) // 255 255 255
 
	_, err := strconv.ParseInt("0xff", 16, 64)
	fmt.Println(err) // strconv.ParseInt: parsing "0xff": invalid syntax
	_, err = strconv.ParseInt("1ff", 16, 8)
	fmt.Println(err) // strconv.ParseInt: parsing "1ff": value out of range
}

strconv.FormatInt と fmt の書式指定 %b %o %x で文字列にし、strconv.ParseInt(text, base, bitSize) で戻す。
ParseInt は 0x 付きの文字列を基数 16 では読めずエラーにするので、接頭辞ごと読むなら基数に 0 を渡す。
桁数が bitSize に収まらないときもエラーになる。

Rust

fn main() {
    let n: i32 = 255;
    println!("{:b} {:o} {:x} {:X}", n, n, n, n); // 11111111 377 ff FF
    println!("{:08b}", 5);                      // 00000101
    println!("{:#x} {:#b}", n, n);              // 0xff 0b11111111
    println!("{:#012b}", n);                    // 0b0011111111(接頭辞も幅 12 に数える)
    println!("{:b}", -5i32);                    // 11111111111111111111111111111011(2 の補数)
    println!("{:x}", -255i32);                  // ffffff01
 
    println!("{:?}", i64::from_str_radix("ff", 16));      // Ok(255)
    println!("{:?}", i64::from_str_radix("11111111", 2)); // Ok(255)
    println!("{:?}", i64::from_str_radix("0xff", 16));    // Err(ParseIntError { kind: InvalidDigit })
    println!("{:?}", i64::from_str_radix("0xff".trim_start_matches("0x"), 16)); // Ok(255)
    println!("{:?}", i8::from_str_radix("1ff", 16));      // Err(ParseIntError { kind: PosOverflow })
    match u8::from_str_radix("zz", 16) {
        Ok(v) => println!("{}", v),
        Err(e) => println!("{}", e), // invalid digit found in string
    }
}

書式指定の {:b} {:o} {:x} で文字列にし、from_str_radix(text, radix) で戻す。
接頭辞付きにするには {:#x} と書く。
負の数は 2 の補数のビット列になるほか、from_str_radix は 0x を読めず InvalidDigit を返すため、先に trim_start_matches で取り除く。

Java

public class Main {
    public static void main(String[] args) {
        int n = 255;
        System.out.println(Integer.toBinaryString(n) + " " + Integer.toOctalString(n) + " " + Integer.toHexString(n)); // 11111111 377 ff
        System.out.println(Integer.toString(n, 16));              // ff
        System.out.println(Integer.toHexString(n).toUpperCase()); // FF
        System.out.println(String.format("%x %X %o", n, n, n));   // ff FF 377
        System.out.println(String.format("%8s", Integer.toBinaryString(5)).replace(' ', '0')); // 00000101
        System.out.println(Integer.toBinaryString(-5));           // 11111111111111111111111111111011(2 の補数)
        System.out.println(Integer.toString(-5, 2));              // -101
 
        System.out.println(Integer.parseInt("ff", 16) + " " + Integer.parseInt("11111111", 2)); // 255 255
        System.out.println(Integer.decode("0xff"));               // 255
        try {
            Integer.parseInt("0xff", 16);
        } catch (NumberFormatException e) {
            System.out.println(e.getMessage()); // For input string: "0xff" under radix 16
        }
        try {
            Integer.parseInt("ffffffff", 16);
        } catch (NumberFormatException e) {
            System.out.println(e.getMessage()); // For input string: "ffffffff" under radix 16
        }
        System.out.println(Integer.parseUnsignedInt("ffffffff", 16)); // -1
    }
}

Integer.toBinaryString toOctalString toHexString で文字列にし、Integer.parseInt(text, radix) で戻す。
toString(n, radix) は負の数を -101 のように符号付きで返すが、toBinaryString は 32 ビットの 2 の補数を返す。
parseInt は 0x を読めないので、接頭辞付きなら Integer.decode を使う。

C#

using System;
using System.Globalization;
 
int n = 255;
Console.WriteLine(Convert.ToString(n, 2) + " " + Convert.ToString(n, 8) + " " + Convert.ToString(n, 16)); // 11111111 377 ff
Console.WriteLine(n.ToString("x") + " " + n.ToString("X")); // ff FF
Console.WriteLine(n.ToString("X4"));                        // 00FF
Console.WriteLine(Convert.ToString(5, 2).PadLeft(8, '0'));  // 00000101
Console.WriteLine(Convert.ToString(-5, 2));                 // 11111111111111111111111111111011(32 ビットの補数)
Console.WriteLine((-5).ToString("X"));                      // FFFFFFFB(符号なしの 16 進数に見える)
 
Console.WriteLine(Convert.ToInt32("ff", 16) + " " + Convert.ToInt32("11111111", 2)); // 255 255
Console.WriteLine(Convert.ToInt32("0xff", 16));             // 255
Console.WriteLine(int.Parse("ff", NumberStyles.HexNumber)); // 255
try { Convert.ToInt32("zz", 16); }
catch (FormatException e) { Console.WriteLine(e.Message); } // Could not find any recognizable digits.
try { int.Parse("0xff", NumberStyles.HexNumber); }
catch (FormatException e) { Console.WriteLine(e.Message); } // The input string '0xff' was not in a correct format.

Convert.ToString(n, 2) で 2・8・10・16 進数の文字列にし、Convert.ToInt32(text, 16) で戻す。
n.ToString("X4") のような書式指定なら 4 桁にそろえた大文字の 16 進数になる。
基数に使えるのは 2・8・10・16 だけで、負の数は 32 ビットの 2 の補数で出力される。

つまずき

整数の 2 進数・16 進数表記は、基数を指定して整数を文字列にする関数(Python の format、JavaScript の toString(radix)、Go の strconv.FormatInt、Java の Integer.toString(n, radix)、C# の Convert.ToString)か、書式指定(%b %x {:b} {:x})で作る。
戻すときは基数を渡して読む関数(int(text, 16)、parseInt(text, 16)、strconv.ParseInt、from_str_radix、Integer.parseInt、Convert.ToInt32)を使う。
255 は 2 進数で 11111111、8 進数で 377、16 進数で ff になり、7 言語とも実行して確認した。

負の数は言語で出力が変わる

-5 を 2 進数にすると、言語によって 2 通りの結果になった。

  • -101(符号と絶対値):Python の format(-5, "b")、JavaScript の (-5).toString(2)、Go の strconv.FormatInt(-5, 2)、Java の Integer.toString(-5, 2)
  • 11111111111111111111111111111011(32 ビットの 2 の補数):Rust の {:b}、Java の Integer.toBinaryString、C# の Convert.ToString(-5, 2)。Go でも uint32 に変換してから %b で出すと同じ並びになる

C# の (-5).ToString("X") は FFFFFFFB を返し、符号なしの値のように見える。
ビット列を見たいのか、数値としての表記を見たいのかを先に決め、負の数が入りうるなら出力を実行して確かめる。
JavaScript で 2 の補数を得たいときは (-5 >>> 0).toString(2) と書く。

接頭辞と桁数のそろえ方

0x や 0b の接頭辞は、文字列にする側と読む側で扱いが違う。

  • 文字列にする側:Python の bin と hex は接頭辞付きで返す。Go・Rust・Python の書式指定は # を付けたとき(%#x、{:#x}、{n:#b})だけ接頭辞が付く。それ以外の関数は付けない
  • 読む側:Python の int("0xff", 16)、JavaScript の parseInt("0xff", 16)、C# の Convert.ToInt32("0xff", 16) は接頭辞を許して 255 を返す。Go の ParseInt("0xff", 16, 64) は invalid syntax、Rust の from_str_radix は InvalidDigit、Java の Integer.parseInt は NumberFormatException、C# の int.Parse(…, NumberStyles.HexNumber) は FormatException になる

Go と Python は基数に 0 を渡し、Java は Integer.decode を使うと接頭辞から基数を判断して読める。
桁数をそろえるときは、Python・Go・Rust の 08b、JavaScript の padStart(8, "0")、C# の PadLeft か X4 を使う。
Java の String.format は 2 進数の文字列に 0 を詰めるフラグが無いため、%8s で空白埋めしてから replace(' ', '0') で置き換えた。

不正な入力と範囲外の値

基数に無い文字を含む文字列を読ませたときの挙動も、言語で分かれた。

  • 例外やエラーにする:Go の ParseInt、Rust の from_str_radix、Java の parseInt、C# の Convert.ToInt32(zz を渡すと Could not find any recognizable digits.)
  • 黙って途中までを読む:JavaScript の parseInt("102", 2) は 2、parseInt("0b101", 2) は 0 を返す。TypeScript の例のように、正規表現で桁を先に検証する

値が型に収まらないときも違いがあり、Go の ParseInt("1ff", 16, 8) は value out of range、Rust の i8::from_str_radix("1ff", 16) は PosOverflow を返した。
Java で ffffffff を parseInt で読むと NumberFormatException になるが、Integer.parseUnsignedInt("ffffffff", 16) なら -1 として読める。
ビットパターンを扱うときは符号なしの読み取りを使う。

このコードで動いた?