big.Rat.Scan() の代替手段:Go言語での効率的な有理数パース

2025-06-01

基本的な動作

big.Rat.Scan(r io.Reader) (n int, err error)

  • 戻り値 err error: 読み取りまたは解析中にエラーが発生した場合、そのエラーを返します。成功した場合は nil を返します。
  • 戻り値 n int: 読み取られたバイト数を返します。
  • r io.Reader: この引数は、読み取り元の io.Reader インターフェースを満たすオブジェクトです。例えば、os.Stdin(標準入力)、bytes.Bufferstrings.Reader などが考えられます。

解析されるテキストの形式

big.Rat.Scan() が解析できるテキストの形式は、以下のいずれかです。

  1. 整数
    例えば、"123""-45" など。この場合、分母は暗黙的に 1 となります。
  2. 分数形式
    "<numerator>/<denominator>" の形式です。例えば、"3/4""-5/2" など。分子と分母は整数として解析されます。分母が 0 の場合はエラーとなります。
package main

import (
	"fmt"
	"math/big"
	"strings"
)

func main() {
	// 文字列から Rat 型の値を読み取る例
	reader1 := strings.NewReader("123")
	rat1 := new(big.Rat)
	n1, err1 := rat1.Scan(reader1)
	if err1 != nil {
		fmt.Println("Error scanning rat1:", err1)
	} else {
		fmt.Printf("rat1: %s, bytes read: %d\n", rat1.String(), n1) // 出力: rat1: 123/1, bytes read: 3
	}

	reader2 := strings.NewReader("-5/3")
	rat2 := new(big.Rat)
	n2, err2 := rat2.Scan(reader2)
	if err2 != nil {
		fmt.Println("Error scanning rat2:", err2)
	} else {
		fmt.Printf("rat2: %s, bytes read: %d\n", rat2.String(), n2) // 出力: rat2: -5/3, bytes read: 4
	}

	reader3 := strings.NewReader("invalid")
	rat3 := new(big.Rat)
	n3, err3 := rat3.Scan(reader3)
	if err3 != nil {
		fmt.Println("Error scanning rat3:", err3) // 出力: Error scanning rat3: malformed rational
	} else {
		fmt.Printf("rat3: %s, bytes read: %d\n", rat3.String(), n3)
	}

	reader4 := strings.NewReader("10/0")
	rat4 := new(big.Rat)
	n4, err4 := rat4.Scan(reader4)
	if err4 != nil {
		fmt.Println("Error scanning rat4:", err4) // 出力: Error scanning rat4: invalid denominator
	} else {
		fmt.Printf("rat4: %s, bytes read: %d\n", rat4.String(), n4)
	}
}


一般的なエラー

    • 原因
      io.Reader から読み込んだテキストが、big.Rat.Scan() が期待する整数または分数 (<numerator>/<denominator>) の形式になっていない場合に発生します。例えば、数字と / 以外の文字が含まれていたり、/ が複数含まれていたりする場合などです。
    • トラブルシューティング
      • 入力となるテキストの形式を注意深く確認してください。不要な空白文字や記号が含まれていないかを確認します。
      • 入力元がユーザー入力の場合、入力時に正しい形式で入力するように促すメッセージを表示することが有効です。
      • ファイルから読み込んでいる場合は、ファイルの内容が正しい形式になっているかを確認します。
  1. invalid denominator (無効な分母)

    • 原因
      分数形式 (<numerator>/<denominator>) で入力されたテキストの分母 (<denominator>) が 0 であった場合に発生します。有理数において、分母が 0 であることは数学的に定義されていません。
    • トラブルシューティング
      • 入力テキストの分母部分が 0 になっていないか確認してください。
      • 入力元がユーザー入力の場合、分母に 0 を入力しないように指示する必要があります。
      • ファイルから読み込んでいる場合は、ファイル内の分数が正しい分母を持っているかを確認します。
  2. strconv.ParseInt 関連のエラー

    • 原因
      分子または分母として解析しようとした文字列が、整数として正しくパースできなかった場合に発生します。例えば、非常に大きな数値で int64 の範囲を超えていたり、数字以外の文字が含まれていたりする場合などです。
    • トラブルシューティング
      • 入力テキストの分子と分母が整数として有効な形式になっているか確認します。
      • 扱う数値の範囲が big.Int で扱える範囲内であるか(通常は問題ありませんが、極端に長い数字の場合に注意が必要です)。
      • 予期しない文字が数値部分に含まれていないかを確認します。
  3. io.EOF (End of File)

    • 原因
      io.Reader から読み込もうとした際に、すでにファイルの終端に達しているなど、読み込むデータが存在しない場合に発生します。これは big.Rat.Scan() 特有のエラーではありませんが、入力ストリームが途中で終わってしまった場合に起こりえます。
    • トラブルシューティング
      • 入力元の io.Reader が意図したデータを提供しているか確認します。
      • ループなどで複数回 Scan() を呼び出している場合、読み込みが完了した後の処理を適切に行っているか確認します。

トラブルシューティングの一般的なヒント

  • strings.Reader を利用したテスト
    io.Reader の挙動が不明な場合は、strings.NewReader を使って既知の文字列を入力として与え、big.Rat.Scan() の動作を確認するのも有効な方法です。
  • エラー処理を適切に行う
    big.Rat.Scan() はエラーを返す可能性があるため、戻り値の err を必ずチェックし、適切に処理するようにしてください。エラーが発生した場合の処理(例えば、エラーメッセージの表示やプログラムの終了など)を実装しておくことが重要です。
  • 小さなテストケースを作成する
    問題が発生する入力を特定したら、その入力だけを使った小さなプログラムを作成して動作を確認することで、問題の切り分けがしやすくなります。
  • 入力データを検証する
    big.Rat.Scan() に渡す前の入力データ(文字列や io.Reader の内容)をログ出力するなどして確認し、期待される形式になっているかをチェックします。
  • エラーメッセージをよく読む
    エラーメッセージは、問題の原因を特定するための重要な情報源です。表示されたエラーメッセージを正確に理解するように努めてください。


例1: 標準入力から有理数を読み込む

この例では、ユーザーが標準入力から有理数を入力し、それを big.Rat 型として読み込んで表示します。

package main

import (
	"bufio"
	"fmt"
	"math/big"
	"os"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	fmt.Println("有理数を入力してください (例: 3/4 または 123):")

	rat := new(big.Rat)
	_, err := rat.Scan(reader)

	if err != nil {
		fmt.Println("エラー:", err)
		return
	}

	fmt.Println("読み込んだ有理数:", rat.String())
}

解説

  1. bufio.NewReader(os.Stdin) で標準入力からの読み取りを行う bufio.Reader を作成します。
  2. ユーザーに有理数の入力形式を促すメッセージを表示します。
  3. rat := new(big.Rat) で新しい big.Rat 型の変数を生成します。
  4. rat.Scan(reader) を呼び出して、標準入力から読み取ったテキストを解析し、rat に値を設定します。
  5. Scan() は読み取ったバイト数とエラーを返します。エラーが発生した場合はエラーメッセージを表示してプログラムを終了します。
  6. 成功した場合、rat.String() メソッドを使って big.Rat 型の値を文字列形式(例: "3/4"、"123/1")で表示します。

実行方法

このコードをコンパイルして実行すると、ターミナルで入力を待ち受けます。例えば、3/4-10/35 などの有理数を入力して Enter キーを押すと、読み込まれた有理数が表示されます。不正な形式で入力するとエラーメッセージが表示されます。

例2: 文字列から有理数を読み込む

この例では、あらかじめ定義された文字列から big.Rat.Scan() を使って有理数を読み込みます。strings.Reader を使うことで、文字列を io.Reader インターフェースを満たすオブジェクトとして扱えます。

package main

import (
	"fmt"
	"math/big"
	"strings"
)

func main() {
	input := "7/2"
	reader := strings.NewReader(input)
	rat := new(big.Rat)

	_, err := rat.Scan(reader)
	if err != nil {
		fmt.Println("エラー:", err)
		return
	}
	fmt.Printf("文字列 '%s' から読み込んだ有理数: %s\n", input, rat.String())

	invalidInput := "12a/3"
	invalidReader := strings.NewReader(invalidInput)
	invalidRat := new(big.Rat)
	_, err = invalidRat.Scan(invalidReader)
	if err != nil {
		fmt.Printf("文字列 '%s' の解析エラー: %s\n", invalidInput, err)
	}
}

解説

  1. 読み込みたい有理数を表す文字列 input を定義します。
  2. strings.NewReader(input) で、この文字列を読み取り可能な io.Reader を作成します。
  3. big.Rat 型の変数 rat を生成し、rat.Scan(reader) で文字列の内容を解析して rat に設定します。
  4. 成功した場合、元の文字列と読み込まれた big.Rat の値を表示します。
  5. 不正な形式の文字列 invalidInput を用いた場合のエラー処理も示しています。この場合、Scan()malformed rational エラーを返します。

例3: 複数の有理数を連続して読み込む

この例では、スペース区切りで複数の有理数が含まれる文字列から、bufio.Scanner を使って一つずつ有理数を読み込みます。

package main

import (
	"bufio"
	"fmt"
	"math/big"
	"strings"
)

func main() {
	input := "1/2 -3/4 5"
	scanner := bufio.NewScanner(strings.NewReader(input))
	scanner.Split(bufio.ScanWords) // スペースでトークンを分割

	for scanner.Scan() {
		token := scanner.Text()
		rat := new(big.Rat)
		_, err := rat.Scan(strings.NewReader(token)) // 各トークンを個別に Scan
		if err != nil {
			fmt.Printf("トークン '%s' の解析エラー: %s\n", token, err)
			continue
		}
		fmt.Printf("読み込んだ有理数: %s\n", rat.String())
	}

	if err := scanner.Err(); err != nil {
		fmt.Println("スキャナーのエラー:", err)
	}
}
  1. 複数の有理数がスペースで区切られた文字列 input を定義します。
  2. bufio.NewScanner で文字列をスキャンするためのスキャナーを作成します。
  3. scanner.Split(bufio.ScanWords) で、スペースを区切り文字としてトークンを分割するように設定します。
  4. scanner.Scan() をループで呼び出し、次のトークンが存在するかどうかを確認します。
  5. scanner.Text() で現在のトークン(文字列)を取得し、strings.NewReader(token) でそのトークンに対する io.Reader を作成します。
  6. rat.Scan() を使って各トークンを big.Rat 型の値に変換します。エラーが発生した場合はエラーメッセージを表示し、次のトークンに進みます。
  7. ループ終了後、scanner.Err() でスキャナーのエラーを確認します。


  1. big.Rat.SetString() メソッド

    • big.Rat 型には、文字列を直接解析して値を設定する SetString(s string) (*big.Rat, bool) メソッドがあります。
    • このメソッドは、Scan() と同様に、整数形式 ("123""-45") または分数形式 ("3/4""-5/2") の文字列を解析できます。
    • 成功した場合はレシーバー (*big.Rat) と true を、失敗した場合は nil レシーバーと false を返します。エラーの詳細な情報は返されませんが、解析が成功したか否かを簡単に確認できます。
    package main
    
    import (
    	"fmt"
    	"math/big"
    )
    
    func main() {
    	rat1 := new(big.Rat)
    	_, ok1 := rat1.SetString("123")
    	if ok1 {
    		fmt.Println("SetString(\"123\"): ", rat1.String()) // 出力: SetString("123"):  123/1
    	} else {
    		fmt.Println("SetString(\"123\") failed")
    	}
    
    	rat2 := new(big.Rat)
    	_, ok2 := rat2.SetString("-5/3")
    	if ok2 {
    		fmt.Println("SetString(\"-5/3\"): ", rat2.String()) // 出力: SetString("-5/3"):  -5/3
    	} else {
    		fmt.Println("SetString(\"-5/3\") failed")
    	}
    
    	rat3 := new(big.Rat)
    	_, ok3 := rat3.SetString("invalid")
    	if ok3 {
    		fmt.Println("SetString(\"invalid\"): ", rat3.String())
    	} else {
    		fmt.Println("SetString(\"invalid\") failed") // 出力: SetString("invalid") failed
    	}
    
    	rat4 := new(big.Rat)
    	_, ok4 := rat4.SetString("10/0")
    	if ok4 {
    		fmt.Println("SetString(\"10/0\"): ", rat4.String())
    	} else {
    		fmt.Println("SetString(\"10/0\") failed") // 出力: SetString("10/0") failed
    	}
    }
    
  2. strings.Split() と big.Int.SetString() を組み合わせる方法

    • 分数形式の文字列を自分でパースし、分子と分母を別々に big.Int 型として読み込んだ後、big.NewRat() 関数を使って big.Rat を作成する方法です。
    • この方法は、より細かいエラー処理や、入力形式が厳密に定義されている場合に有効です。
    package main
    
    import (
    	"fmt"
    	"math/big"
    	"strings"
    )
    
    func parseRational(s string) (*big.Rat, error) {
    	parts := strings.Split(s, "/")
    	if len(parts) > 2 {
    		return nil, fmt.Errorf("不正な形式: スラッシュが多すぎます")
    	}
    
    	numStr := parts[0]
    	denStr := "1" // デフォルトの分母
    
    	if len(parts) == 2 {
    		denStr = parts[1]
    	}
    
    	num := new(big.Int)
    	_, ok := num.SetString(numStr, 10)
    	if !ok {
    		return nil, fmt.Errorf("不正な分子: %s", numStr)
    	}
    
    	den := new(big.Int)
    	_, ok = den.SetString(denStr, 10)
    	if !ok {
    		return nil, fmt.Errorf("不正な分母: %s", denStr)
    	}
    
    	if den.Sign() == 0 {
    		return nil, fmt.Errorf("分母がゼロです")
    	}
    
    	return big.NewRat(0, 1).SetFrac(num, den), nil
    }
    
    func main() {
    	rat1, err1 := parseRational("3/4")
    	if err1 != nil {
    		fmt.Println("エラー:", err1)
    	} else {
    		fmt.Println("parseRational(\"3/4\"): ", rat1.String()) // 出力: parseRational("3/4"):  3/4
    	}
    
    	rat2, err2 := parseRational("-10")
    	if err2 != nil {
    		fmt.Println("エラー:", err2)
    	} else {
    		fmt.Println("parseRational(\"-10\"): ", rat2.String()) // 出力: parseRational("-10"):  -10/1
    	}
    
    	rat3, err3 := parseRational("1/0")
    	if err3 != nil {
    		fmt.Println("エラー:", err3) // 出力: エラー: 分母がゼロです
    	} else {
    		fmt.Println("parseRational(\"1/0\"): ", rat3.String())
    	}
    
    	rat4, err4 := parseRational("invalid")
    	if err4 != nil {
    		fmt.Println("エラー:", err4) // 出力: エラー: 不正な分子: invalid
    	} else {
    		fmt.Println("parseRational(\"invalid\"): ", rat4.String())
    	}
    }
    
  3. 正規表現 (regexp) を利用する方法

    • より複雑な形式の有理数を解析する必要がある場合、正規表現を使って文字列から分子と分母の部分を抽出し、それを big.Int.SetString() で解析する方法も考えられます。
    • この方法は、入力形式が多様である可能性がある場合に有効ですが、実装がやや複雑になる可能性があります。
    package main
    
    import (
    	"fmt"
    	"math/big"
    	"regexp"
    )
    
    func parseRationalWithRegexp(s string) (*big.Rat, error) {
    	re := regexp.MustCompile(`^([-+]?\d+)(?:/([-+]?\d+))?$`)
    	match := re.FindStringSubmatch(s)
    	if len(match) == 0 {
    		return nil, fmt.Errorf("不正な形式: %s", s)
    	}
    
    	numStr := match[1]
    	denStr := "1"
    
    	if len(match) > 2 && match[2] != "" {
    		denStr = match[2]
    	}
    
    	num := new(big.Int)
    	_, ok := num.SetString(numStr, 10)
    	if !ok {
    		return nil, fmt.Errorf("不正な分子: %s", numStr)
    	}
    
    	den := new(big.Int)
    	_, ok = den.SetString(denStr, 10)
    	if !ok {
    		return nil, fmt.Errorf("不正な分母: %s", denStr)
    	}
    
    	if den.Sign() == 0 {
    		return nil, fmt.Errorf("分母がゼロです")
    	}
    
    	return big.NewRat(0, 1).SetFrac(num, den), nil
    }
    
    func main() {
    	rat1, err1 := parseRationalWithRegexp("123")
    	if err1 != nil {
    		fmt.Println("エラー:", err1)
    	} else {
    		fmt.Println("parseRationalWithRegexp(\"123\"): ", rat1.String()) // 出力: parseRationalWithRegexp("123"):  123/1
    	}
    
    	rat2, err2 := parseRationalWithRegexp("-7/8")
    	if err2 != nil {
    		fmt.Println("エラー:", err2)
    	} else {
    		fmt.Println("parseRationalWithRegexp(\"-7/8\"): ", rat2.String()) // 出力: parseRationalWithRegexp("-7/8"):  -7/8
    	}
    
    	rat3, err3 := parseRationalWithRegexp("invalid")
    	if err3 != nil {
    		fmt.Println("エラー:", err3) // 出力: エラー: 不正な形式: invalid
    	} else {
    		fmt.Println("parseRationalWithRegexp(\"invalid\"): ", rat3.String())
    	}
    }
    
  • 正規表現
    より複雑な入力形式に対応する必要がある場合に利用できますが、実装はやや複雑になります。
  • strings.Split() と big.Int.SetString() の組み合わせ
    分数形式を明示的にパースし、より詳細なエラー処理を行いたい場合に有効です。
  • big.Rat.SetString()
    最も簡潔で、一般的な整数または分数形式の文字列を big.Rat に変換するのに適しています。成功/失敗の簡単なステータスが返されます。