Goプログラミング big.Ratで浮動小数点数の誤差を回避する

2025-06-01

big.Rat は、Go の標準パッケージである math/big パッケージで提供されている型の一つです。これは、任意の精度を持つ有理数を表現するために使用されます。

「任意の精度を持つ」 という点が重要です。通常の浮動小数点数型(float32float64)は、表現できる数値の範囲や精度に限界があります。そのため、繰り返しの計算や非常に大きな数、非常に小さな数を扱う際に、わずかな誤差が累積してしまい、最終的な結果が不正確になることがあります。

一方、big.Rat は、分子と分母をそれぞれ big.Int 型(これも任意の精度を持つ整数型)で保持することで、このような精度の問題を回避します。つまり、big.Rat は、分数 qp(ただし、p は分子、q は分母で、q=0)を厳密に表現し、計算を行うことができるのです。

big.Rat の主な特徴と用途

  • 記号演算
    より複雑な数値計算の基礎として利用されることもあります。
  • 金融計算や科学技術計算
    わずかな誤差も許容できないような、厳密な計算が求められる分野で活用されます。
  • 正確な分数表現
    小数で表現すると無限小数になるような分数(例: 31​)も、分子と分母の整数として正確に保持できます。
  • 高精度な計算
    浮動小数点数の誤差を気にすることなく、正確な有理数演算(加算、減算、乗算、除算など)を行うことができます。

big.Rat の基本的な使い方

big.Rat の変数を宣言し、値を設定するには、通常以下のようないくつかの方法があります。

  1. New(num, den int64) 関数を使う
    package main
    
    import (
        "fmt"
        "math/big"
    

)

func main() {
    // 3/4 を表す big.Rat を作成
    r := big.NewRat(3, 4)
    fmt.Println(r.String()) // 出力: 3/4
}
```
ここで、最初の引数が分子(numerator)、2番目の引数が分母(denominator)です。
  1. SetString(s string) メソッドを使う
    文字列から big.Rat の値を設定できます。文字列は "分子/分母" の形式である必要があります。

    package main
    
    import (
        "fmt"
        "math/big"
    )
    
    func main() {
        r := new(big.Rat)
        _, ok := r.SetString("5/7")
        if !ok {
            fmt.Println("invalid rat string")
            return
        }
        fmt.Println(r.String()) // 出力: 5/7
    }
    
  2. 既存の big.Int から設定する

    package main
    
    import (
        "fmt"
        "math/big"
    )
    
    func main() {
        num := big.NewInt(10)
        den := big.NewInt(3)
        r := new(big.Rat).SetFrac(num, den)
        fmt.Println(r.String()) // 出力: 10/3
    }
    

主なメソッド

big.Rat 型は、様々な操作を行うためのメソッドを提供しています。

  • Denom() *Int
    分母を返します。
  • Num() *Int
    分子を返します。
  • String() string
    big.Rat を "分子/分母" の形式の文字列で返します。
  • Float64() (f float64, exact bool)
    big.Ratfloat64 に変換します。exact は正確に変換できたかどうかを示します。
  • Cmp(y *Rat) int
    ry を比較します。r > y なら +1, r == y なら 0, r < y なら -1 を返します。
  • Neg(z, a *Rat) *Rat
    z = -a
  • Abs(z, a *Rat) *Rat
    z = |a| (絶対値)
  • Inv(z, a *Rat) *Rat
    z = 1 / a
  • Quo(z, a, b *Rat) *Rat
    z = a / b
  • Mul(z, a, b *Rat) *Rat
    z = a * b
  • Sub(z, a, b *Rat) *Rat
    z = a - b
  • Add(z, a, b *Rat) *Rat
    z = a + b


よくあるエラーとトラブルシューティング

    • 原因
      big.Rat の分母にゼロを設定しようとした場合や、ゼロで除算しようとした場合に発生します。big.Rat は有理数 qpを表現するため、q (分母) はゼロであってはいけません。
    • トラブルシューティング
      • big.NewRat() 関数で 0 を分母に指定していないか確認してください。
      • 除算を行う際に、除数がゼロでないことを事前に確認してください。Cmp(new(big.Rat)) を用いてゼロと比較できます。
      • SetString() で文字列から big.Rat を生成する場合、分母が 0 になっていないか確認してください。
  1. SetString() の解析エラー (invalid syntax)

    • 原因
      SetString() メソッドに渡す文字列の形式が正しくない場合に発生します。big.Rat は通常 "分子/分母" の形式の文字列を期待します。
    • トラブルシューティング
      • SetString() に渡す文字列が "整数/整数" の形式になっているか確認してください。
      • 余計な空白文字や記号が含まれていないか確認してください。
      • 分子または分母が整数として解析できる文字列であることを確認してください。
  2. Float64() の精度損失

    • 原因
      big.Rat が表現できる値を float64 型に変換しようとした際に、float64 の精度限界を超える場合、精度が失われる可能性があります。Float64() メソッドは、変換が正確に行われたかどうかを示す exact bool 型の戻り値を返します。
    • トラブルシューティング
      • Float64() の戻り値である exact を確認し、false の場合は精度が失われている可能性があることを認識してください。
      • 高精度な計算結果が必要な場合は、float64 への変換を避け、big.Rat のまま演算を行うか、文字列 (String()) として出力することを検討してください。
  3. nil レシーバでのメソッド呼び出し (Panic: runtime error: invalid memory address or nil pointer dereference)

    • 原因
      big.Rat 型のポインタ変数を宣言しただけで、new(big.Rat) などで初期化せずにメソッドを呼び出そうとすると発生します。
    • トラブルシューティング
      • big.Rat 型の変数を宣言したら、必ず new(big.Rat) で初期化するか、既存の big.Rat 変数を代入してからメソッドを呼び出してください。
      var r *big.Rat // これは nil
      // r.SetString("1/2") // これはパニックを引き起こす可能性あり
      
      r = new(big.Rat) // これで初期化される
      r.SetString("1/2")
      
  4. 予期しない計算結果

    • 原因
      big.Rat は有理数を正確に扱いますが、演算の順序や意図しない型変換によって、期待と異なる結果になることがあります。
    • トラブルシューティング
      • 演算の順序を括弧 () で明示的に指定してください。
      • 異なる型(例えば intbig.Rat)の間で演算を行う場合は、明示的に big.NewRat() などを用いて big.Rat 型に変換してから演算を行ってください。
      a := big.NewRat(1, 2)
      b := int64(3)
      c := new(big.Rat).Mul(a, big.NewRat(b, 1)) // 正しい
      // d := new(big.Rat).Mul(a, b) // これは型エラーになる
      fmt.Println(c.String()) // 出力: 3/2
      
  5. 大きな分子・分母によるパフォーマンスへの影響

    • 原因
      big.Rat は任意の精度を扱えるため、非常に大きな分子や分母を持つ有理数を扱うことができますが、その分計算に時間がかかることがあります。
    • トラブルシューティング
      • 本当に高精度な計算が必要かどうか検討してください。場合によっては、浮動小数点数型でもある程度の精度で済むかもしれません。
      • 計算の途中で共通の約数があれば、Rat.Num().Div(Rat.Num(), gcd)Rat.Denom().Div(Rat.Denom(), gcd) などを用いて簡約化することを検討してください(ただし、big.Rat のメソッドは自動的に簡約化を行う場合があります)。

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

  • Go のドキュメントを参照する
    math/big パッケージの公式ドキュメントには、各型やメソッドの詳細な説明が記載されています。
  • ログ出力を活用する
    計算の途中経過や変数の値などをログ出力することで、問題の箇所を特定しやすくなります。
  • 簡単なコードで再現させる
    問題が発生する複雑なコードの一部を抜き出し、最小限のコードでエラーを再現させて原因を特定します。
  • エラーメッセージをよく読む
    Go のコンパイラや実行時のエラーメッセージは、問題の原因を特定するための重要な情報を含んでいます。


基本的な演算

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 2/3 を作成
	a := big.NewRat(2, 3)
	fmt.Println("a =", a.String()) // 出力: a = 2/3

	// 1/4 を作成
	b := big.NewRat(1, 4)
	fmt.Println("b =", b.String()) // 出力: b = 1/4

	// 加算: a + b
	sum := new(big.Rat).Add(a, b)
	fmt.Println("a + b =", sum.String()) // 出力: a + b = 11/12

	// 減算: a - b
	diff := new(big.Rat).Sub(a, b)
	fmt.Println("a - b =", diff.String()) // 出力: a - b = 5/12

	// 乗算: a * b
	prod := new(big.Rat).Mul(a, b)
	fmt.Println("a * b =", prod.String()) // 出力: a * b = 1/6

	// 除算: a / b
	quo := new(big.Rat).Quo(a, b)
	fmt.Println("a / b =", quo.String()) // 出力: a / b = 8/3
}

この例では、big.NewRat() 関数を使って二つの有理数 a (2/3) と b (1/4) を作成し、それぞれの和、差、積、商を計算して表示しています。new(big.Rat) で新しい big.Rat 型の変数を初期化し、Add(), Sub(), Mul(), Quo() メソッドを使って演算を行います。結果は String() メソッドで文字列として表示しています。

文字列からの変換と浮動小数点数への変換

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 文字列から big.Rat を作成
	r1 := new(big.Rat)
	_, ok := r1.SetString("5/8")
	if !ok {
		fmt.Println("invalid rat string")
		return
	}
	fmt.Println("r1 from string =", r1.String()) // 出力: r1 from string = 5/8

	r2 := new(big.Rat)
	_, ok = r2.SetString("10/4") // 自動的に簡約化される
	if !ok {
		fmt.Println("invalid rat string")
		return
	}
	fmt.Println("r2 from string =", r2.String()) // 出力: r2 from string = 5/2

	// big.Rat から float64 へ変換
	f, exact := r1.Float64()
	if exact {
		fmt.Println("r1 as float64 =", f) // 出力: r1 as float64 = 0.625
	} else {
		fmt.Println("r1 as float64 (inexact) =", f)
	}

	r3 := big.NewRat(1, 3)
	f, exact = r3.Float64()
	if exact {
		fmt.Println("r3 as float64 =", f)
	} else {
		fmt.Println("r3 as float64 (inexact) =", f) // 出力: r3 as float64 (inexact) = 0.3333333333333333
	}
}

この例では、SetString() メソッドを使って文字列から big.Rat を作成する方法を示しています。また、Float64() メソッドを使って big.Ratfloat64 型に変換する方法と、変換が正確に行われたかどうかを確認する方法を示しています。31のように float64 で正確に表現できない場合は、exactfalse になります。

比較

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewRat(3, 5)
	b := big.NewRat(6, 10)
	c := big.NewRat(1, 2)

	// a と b を比較 (3/5 と 6/10 は等しい)
	if a.Cmp(b) == 0 {
		fmt.Println(a.String(), "==", b.String()) // 出力: 3/5 == 3/5
	}

	// a と c を比較 (3/5 は 1/2 より大きい)
	if a.Cmp(c) > 0 {
		fmt.Println(a.String(), ">", c.String()) // 出力: 3/5 > 1/2
	}

	// c と b を比較 (1/2 は 6/10 より小さい)
	if c.Cmp(b) < 0 {
		fmt.Println(c.String(), "<", b.String())
	}
}

この例では、Cmp() メソッドを使って二つの big.Rat を比較する方法を示しています。Cmp() メソッドは、レシーバ (a) が引数 (b) より大きい場合は +1、等しい場合は 0、小さい場合は -1 を返します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	r := big.NewRat(7, 9)
	fmt.Println("r =", r.String()) // 出力: r = 7/9

	// 絶対値
	absR := new(big.Rat).Abs(big.NewRat(-7, 9))
	fmt.Println("|r| =", absR.String()) // 出力: |r| = 7/9

	// 逆数
	invR := new(big.Rat).Inv(r)
	fmt.Println("1/r =", invR.String()) // 出力: 1/r = 9/7

	// 符号反転
	negR := new(big.Rat).Neg(r)
	fmt.Println("-r =", negR.String()) // 出力: -r = -7/9

	// 分子と分母を取得
	num := r.Num()
	den := r.Denom()
	fmt.Println("numerator of r =", num.String())   // 出力: numerator of r = 7
	fmt.Println("denominator of r =", den.String()) // 出力: denominator of r = 9
}


既存の big.Int から big.Rat を作成する

big.Rat は内部的に分子と分母を big.Int 型で保持しています。もし既に big.Int 型の変数として分子と分母を持っている場合、それらを使って big.Rat を作成できます。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	num := big.NewInt(15)
	den := big.NewInt(6)

	// SetFrac メソッドを使って big.Int から big.Rat を作成
	r := new(big.Rat).SetFrac(num, den)
	fmt.Println(r.String()) // 出力: 5/2 (自動的に簡約化される)
}

SetFrac() メソッドは、レシーバの big.Rat に、引数で与えられた分子 (num) と分母 (den) を設定します。この際、自動的に最大公約数で割って簡約化されます。

複合代入演算子の利用

big.Rat 同士の演算結果を直接レシーバに代入する複合代入演算子はありませんが、メソッドの戻り値をそのまま同じ変数に代入することで、同様の操作が可能です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	r := big.NewRat(1, 2)
	fmt.Println("r =", r.String()) // 出力: r = 1/2

	// r に 1/3 を加える
	r.Add(r, big.NewRat(1, 3))
	fmt.Println("r + 1/3 =", r.String()) // 出力: r + 1/3 = 5/6

	// r から 1/6 を引く
	r.Sub(r, big.NewRat(1, 6))
	fmt.Println("r - 1/6 =", r.String()) // 出力: r - 1/6 = 2/3
}

このように、メソッドの第一引数にレシーバ自身を指定することで、演算結果を元の変数に上書きできます。

Rat.Set() メソッドによるコピー

既存の big.Rat の値を別の big.Rat 変数にコピーしたい場合、直接代入 (=) ではなく、Set() メソッドを使用します。これは、big.Rat が内部的にポインタを持っている可能性があるため、意図しない共有を避けるためです。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	r1 := big.NewRat(3, 4)
	r2 := new(big.Rat).Set(r1) // r1 の値を r2 にコピー

	fmt.Println("r1 =", r1.String()) // 出力: r1 = 3/4
	fmt.Println("r2 =", r2.String()) // 出力: r2 = 3/4

	// r1 を変更しても r2 は影響を受けない
	r1.Mul(r1, big.NewRat(2, 1))
	fmt.Println("r1 * 2 =", r1.String()) // 出力: r1 * 2 = 3/2
	fmt.Println("r2 =", r2.String())     // 出力: r2 = 3/4
}

もし r2 = r1 のように直接代入した場合、内部のポインタが共有される可能性があり、一方を変更すると他方も影響を受けることがあります。Set() メソッドは安全なコピーを提供します。

Rat.SetInt() メソッドによる整数からの設定

整数値を big.Rat として扱いたい場合、SetInt() メソッドを使用すると、分母が 1 の有理数として設定できます。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	n := big.NewInt(10)
	r := new(big.Rat).SetInt(n)
	fmt.Println(r.String()) // 出力: 10/1
}

これは、整数と有理数の間で演算を行う際に便利です。例えば、big.Rat に整数値を掛けたい場合などに、整数を big.Rat に変換してから演算を行います。

Rat.SetFloat64() メソッドによる浮動小数点数からの設定 (注意点あり)

float64 型の値を big.Rat に変換することも可能ですが、浮動小数点数は内部的に近似値で表現されるため、完全に正確な有理数への変換が保証されるわけではありません。SetFloat64() メソッドのドキュメントをよく理解した上で使用する必要があります。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	f := 0.75
	r := new(big.Rat)
	_, ok := r.SetFloat64(f)
	if ok {
		fmt.Println(r.String()) // 出力: 3/4
	} else {
		fmt.Println("float64 to big.Rat conversion was inexact")
	}

	f2 := 0.1 // これは正確な有理数として表現できない
	r2 := new(big.Rat)
	_, ok = r2.SetFloat64(f2)
	if ok {
		fmt.Println(r2.String())
	} else {
		fmt.Println("float64 to big.Rat conversion of 0.1 was inexact:", r2.String()) // 出力例: float64 to big.Rat conversion of 0.1 was inexact: 3602879701896397/36028797018963968
	}
}

浮動小数点数の性質上、変換が不正確になる場合があることに注意してください。