とあるブログ記事でVBA(というかコンピュータ一般)で小数演算が正確にできないことについて書かれているのを読んだ。
表したい数値が無理数だと話は別だが、分数ならば有理数をあらわす型を作って数として表現し、正確な計算ができるように演算を定義する、というのはよくある。そこでVBAでも有理数を扱うサンプル的なプログラムを作ってみた。あえてデータ構造をクラス化せず、以下のような単純な配列で分子と分母を保持して、関数主体で実装した。
分子 → 配列の最初の要素
分母 → 配列の2番目の要素
符号 → 分子に付ける
分母と分子は互いに素なLong値(make_ratio関数で作成した場合)
(LongLongにしてもいいけど・・・)
'有理数の生成
Function make_ratio(ByRef a As Variant, ByRef b As Variant) As Variant
Dim gcd As Long: gcd = getGcd(a, b)
make_ratio = VBA.Array(Sgn(a * b) * (Abs(a) \ gcd), Abs(b) \ gcd)
End Function
'最大公約数
Public Function getGcd(ByVal a As Long, ByVal b As Long) As Long
If a = 0 Then
getGcd = 1
ElseIf b = 0 Then
getGcd = Abs(a)
Else
getGcd = getGcd(b, Abs(a) Mod Abs(b))
End If
End Function
make_ratio
を含め、以下の関数を作った。1
関数 | 内容 | |
---|---|---|
make_ratio | 有理数の生成 | |
ratio2double | Doubleに変換 | |
ratio2str | 文字列に変換 | |
ratio_plus | 有理数の加算 | |
ratio_negate | 有理数の符号変更 | |
ratio_minus | 有理数の減算 | |
ratio_mult | 有理数の乗算 | |
ratio_pow | 有理数のベキ乗 | |
ratio_divide | 有理数の除算 | |
ratio_sgn | 有理数の符号 | |
ratio_abs | 有理数の絶対値 | |
ratio_equal | 有理数の比較 (a = b) | |
ratio_not_equal | 有理数の比較 (a <> b) | |
ratio_less | 有理数の比較 (a < b) | |
ratio_less_equal | 有理数の比較 (a <= b) | |
ratio_greater | 有理数の比較 (a > b) | |
ratio_greater_equal | 有理数の比較 (a >= b) |
元の記事では、1から0.001を千回引いても0にならないことから話が始まっていたので、それがどうなるか確認してみる。
r1=make_ratio(1,1) ' 1/1
r2=make_ratio(1, 1000) ' 1/1000
' 1から1/1000を1000回引いた結果
printM repeat_while(r1, p_true, p_ratio_minus(,r2), 1000)
0 1000 ' 0/1000
' 1から1/1000を1003回引いた過程を配列に入れる
m = generate_while(r1, p_true, p_ratio_minus(,r2), 1003)
' その各要素をDoubleに変換する
d = mapF(p_ratio2double, m)
' 先頭6要素を確認
printM d,6
1 0.999 0.998 0.997 0.996 0.995
' 末尾6要素を確認
printM d,-6
0.002 0.001 0 -0.001 -0.002 -0.003
問題はなさそうだ。
比較演算があるのでソートもできる。2
' 分子の列(-20~20 のランダム整数10個)
nums = mapF(p_getCLng,mapF(p_rnd(,20), repeat(-20, 10)))
' 分母の列(1~20 のランダム整数10個)
dens = mapF(p_getCLng,mapF(p_rnd(,20), repeat(1, 10)))
' zipWithに分子の列、分母の列を与えて有理数列を作れる
m = zipWith(p_make_ratio, nums, dens)
' 表示してみる(ratio2str関数で文字列化)
printM mapF(p_ratio2str, m)
4/3 -5/7 11/18 -5/17 -8/1 17/11 5/18 5/9 -3/14 -16/11
' 比較演算として ratio_less を使ってソート
m2 = subV(m, sortIndex_pred(m, p_ratio_less))
' ソート後の有理数列を表示してみる
printM mapF(p_ratio2str, m2)
-8/1 -16/11 -5/7 -5/17 -3/14 5/18 5/9 11/18 4/3 17/11
正確性はいいのだが、以下の点で不満だ。
・遅い(計算のたびに通分計算が入る)
・オーバーフローが心配(ベキ乗とかで問題になりそう。LongLongにしても同じことだ。)
・Haskell関係ない()
VBAHaskellの紹介 その19(ParamArrayとswapVariant)
VBAHaskellの紹介 その1(最初はmapF)
ソース https://github.com/mYmd/VBA