Excel VBAでBigIntを作ってみた
175862 ワード
環境
- Windows11 Home 21H2(OSビルド 22000.613)
- Microsoft Office Home and Business Premium
- Excel バージョン2208(ビルド 15601.20088 クイック実行)
経緯
Excel VBAで大きな整数を扱う処理をしたかったが、大きな整数を扱える型が無いようなので自作した。
機能・特徴
- String型とLongPtr型を組み合わせて処理するので数値型の上限以上の桁数の整数を扱える。
- 四則演算、剰余算、冪乗算、冪根算、進数変換ができる。
- 小数は切り捨て。
- 扱える最大桁数は理論上String型の上限(約20億桁)まで。(確認はしていない)
- Decimal型で計算可能な場合はDecimal型で計算することでパフォーマンスを優先しつつ、オーバーフローする場合は独自のアルゴリズム(筆算類似のもの等)で計算している。
使い方
- BICalculatorのインスタンス
calc
(名前は自由)を生成 -
calc
のメソッドを呼び出し、引数にString型の数値を渡す(Int、Long、Double等の数値型またはBigInt型も可) - 計算結果は、第3引数にTrueを指定するとBigInt型、Falseまたは省略するとString型で返す
- その他各メソッドの使い方等はコメント参照
Use Case
Sub Test()
Dim calc As New BICalculator
Dim num1 As String: num1 = "123,456,789,012,345,678,901,234,567,890,123"
Dim num2 As String: num2 = "9,876,543,210,987"
Dim BI As BigInt
Dim QR As BIDivResult
'足し算
Set BI = calc.Add(num1, num2, True)
Debug.Print BI.ValueFmt
'123,456,789,012,345,678,911,111,111,101,110
'引き算
Set BI = calc.Subtract(num1, num2, True)
Debug.Print BI.ValueFmt
'123,456,789,012,345,678,891,358,024,679,136
'掛け算
Set BI = calc.Multiply(num1, num2, True)
Debug.Print BI.ValueFmt
'1,219,326,311,370,137,171,880,013,717,187,996,860,222,381,401
'割り算
Set BI = calc.Divide(num1, num2, True)
Debug.Print BI.ValueFmt
'12,499,999,886,094,578,126
'剰余算
Set BI = calc.Modulo(num1, num2, True)
Debug.Print BI.ValueFmt
'5,499,394,819,761
'商と剰余
Set QR = calc.DivAndMod(num1, num2)
Debug.Print QR.Quotient.ValueFmt '商
'12,499,999,886,094,578,126
Debug.Print QR.Remainder.ValueFmt '剰余
'5,499,394,819,761
'検算
Set BI = calc.Multiply(QR.Quotient, num2, True)
Set BI = calc.Add(BI, QR.Remainder, True)
Debug.Print BI.ValueFmt
'123,456,789,012,345,678,901,234,567,890,123
'冪乗
Set BI = calc.Power(256, 256, True)
Debug.Print BI.ValueFmt
'32,317,006,071,311,007,300,714,876,688,669,951,960,444,102,669,715,484,032,130,345,427,524,655,138,867,890,893,197,201,411,522,913,463,688,717,960,921,898,019,494,119,559,150,490,921,095,088,152,386,448,283,120,630,877,367,300,996,091,750,197,750,389,652,106,796,057,638,384,067,568,276,792,218,642,619,756,161,838,094,338,476,170,470,581,645,852,036,305,042,887,575,891,541,065,808,607,552,399,123,930,385,521,914,333,389,668,342,420,684,974,786,564,569,494,856,176,035,326,322,058,077,805,659,331,026,192,708,460,314,150,258,592,864,177,116,725,943,603,718,461,857,357,598,351,152,301,645,904,403,697,613,233,287,231,227,125,684,710,820,209,725,157,101,726,931,323,469,678,542,580,656,697,935,045,997,268,352,998,638,215,525,166,389,437,335,543,602,135,433,229,604,645,318,478,604,952,148,193,555,853,611,059,596,230,656
'冪根
Set BI = calc.Root(num1, 2, True)
Debug.Print BI.ValueFmt
'11,111,111,061,111,110
'10進数から他の進数へ変換
Set BI = calc.ToBase(256, 2, True)
Debug.Print BI.ValueFmt(4, " ", True)
'0001 0000 0000
'==============================================================
'注1
'DivAndMod以外の各メソッドの第3引数(True)を省略すると、String型
'の値を返すので変数で受けなくてもそのまま使える。
'例)
''引き算
'Debug.Print calc.Subtract(num1, num2)
'
'注2
'BI.Value => 桁区切り無しの符号付きの値
'BI.ValueAbs => 桁区切り無しの絶対値
'BI.ValueFmt => 桁区切り有りの符号付きの値
'==============================================================
End Sub
コード
クラスモジュールに以下の各クラスを作成
- BigInt: 数値を格納し数値単体への処理等を行うクラス
- BICalculator: 各種計算メソッドを格納しているクラス
(標準モジュールに記述すると使用時にインスタンスの生成は不要になる) - BIDivResult: 商と剰余の計算結果をまとめて格納するクラス
(アクセス修飾子について Friend はBICalculatorからの呼び出しでのみ利用されることを想定し、Public はそれ以外からも呼び出されることを想定して使い分けをしている。ただし、Friend でも同一プロジェクト内からは呼び出し可能であり実用上の違いはない。)
BigInt
Option Explicit
'==========================
'===== Private Fields =====
'==========================
'数値の絶対値を格納するフィールド
Private value_ As String
'数値の符号がマイナスか否かを格納するフィールド
Private negative_ As Boolean
'iterationをする際に数値を分割して格納する配列のフィールド
Private digits_() As LongPtr
'iterationのカウンター変数
Private counter_ As LongPtr
'iterationのカウントの完了値を格納する定数
Private Const COUNT_FINISHED As LongPtr = -1
'LongPtr型の変数に格納可能な桁数の列挙型
Private Enum DigitLength
#If VBA7 And Win64 Then
'Trueならば64bit処理が可能なのでLongptr⇒LongLong型
HALF = 9 '2乗しても格納可能な最大桁数
FULL = 18 '全ての桁に9を格納可能な最大桁数
#Else
'Falseならば32bit処理になるのでLongptr⇒Long型
HALF = 4 '2乗しても格納可能な最大桁数
FULL = 9 '全ての桁に9を格納可能な最大桁数
#End If
End Enum
'digits_の各要素に格納可能な最大桁数を格納するフィールド
Private digitLength_ As Byte
'=======================
'===== Constructor =====
'=======================
Private Sub Class_Initialize()
value_ = "0"
negative_ = False
counter_ = COUNT_FINISHED
End Sub
'======================
'===== Properties =====
'======================
'数値の桁数を取得するプロパティ
Public Property Get Length() As LongPtr
If value_ = "0" Then
Length = 0
Exit Property
End If
Length = Len(value_)
End Property
'符号付きの数値を取得するプロパティ
Public Property Get Value() As String
Value = PutSign(value_)
End Property
'数値の絶対値を取得するプロパティ
Public Property Get ValueAbs() As String
ValueAbs = value_
End Property
'体裁を調えた数値を取得するプロパティ
'[digits] 区切る桁数を指定する。省略した場合は3桁区切り
'[delimiter] 区切り文字を指定する。省略した場合はカンマ
'[digits] 引数[digits]で指定した桁数に数値が足りない場合0で埋めるか否かを指定する。省略した場合はfalse
Public Property Get ValueFmt( _
Optional ByVal digits As Integer = 3, _
Optional ByVal delimiter As String = ",", _
Optional ByVal fill0s As Boolean = False _
) As String
ValueFmt = ""
Me.PrepareIteration (digits)
ValueFmt = CStr(Me.DigitLeft())
If fill0s Then ValueFmt = Me.To1DigitLength(CLngPtr(ValueFmt))
Do Until Me.Finished
ValueFmt = ValueFmt & delimiter & Me.To1DigitLength(Me.DigitLeft())
Loop
ValueFmt = PutSign(ValueFmt)
End Property
'1Digit当たりの格納可能最大桁数を取得するプロパティ
Friend Property Get DLFull() As Long
DLFull = DigitLength.FULL
End Property
'DigitLeftまたはDigitRightメソッドのiterationが終了したか否かを返すプロパティ
Friend Property Get Finished() As Boolean
Finished = (counter_ = COUNT_FINISHED)
End Property
'==========================
'===== Friend Methods =====
'==========================
'数値をインスタンスに格納するメソッド(一度設定した値は変更不能)
'[strNum] 数値をString型で指定する
Public Sub Init(ByVal strNum As String)
Static hasValueBeenSet As Boolean
If hasValueBeenSet Then
Call Err.Raise(vbObjectError + 387, , _
"[BigInt.Init]" & vbCrLf & _
"The value of this instance has been set." & vbCrLf & _
"This method can only be called once in same instance.")
End If
If strNum = "" Then Exit Sub
strNum = Replace(strNum, ",", "") '桁区切りのカンマを除去する
If InStr(1, strNum, ".") <> 0 Then '小数の場合小数点以下を除去する
strNum = Split(strNum, ".")(0)
End If
If Left(strNum, 1) = "+" Then '+符号が付いている場合除去する
strNum = Replace(strNum, "+", "")
End If
If strNum <> "0" And Left(strNum, 1) = "0" Then '0始まりの数値の場合先頭の0を除去する
strNum = RemoveLeading0s(strNum)
End If
If strNum = "-0" Then strNum = "0"
If Not IsInteger(strNum) Then '整数として識別可能か判別する
Call Err.Raise(vbObjectError + 5, , _
"[BigInt.Init]" & vbCrLf & _
"Value(" & strNum & ") could not be evaluated as an integer.")
Exit Sub
End If
If Left(strNum, 1) = "-" Then '負数の場合-記号を除去し、negative_ = Trueとする
negative_ = True
strNum = Replace(strNum, "-", "")
End If
value_ = LTrim(RTrim(strNum)) '絶対値化した数値をvalue_に格納する
hasValueBeenSet = True '今後数値の変更を不可とする
End Sub
'インスタンスのクローンを作成し返すメソッド
Friend Function Clone() As BigInt
Set Clone = New BigInt
Call Clone.Init(Me.Value)
If Me.IsNegative Then Clone.ToNegative
End Function
'DigitLeftまたはDigitRightメソッドのiterationの準備メソッド
'[Full_Half] digits_の各要素に格納する数値の桁数を指定する
Friend Sub PrepareIteration(ByVal Full_Half As String)
Dim strNum As String: strNum = value_
Dim start As Long: start = Len(strNum) + 1 'Mid()の引数Startの値を格納
Dim lenUnits As Long, i As Long
Select Case Full_Half
Case "Full": digitLength_ = DigitLength.FULL
Case "Half": digitLength_ = DigitLength.HALF
Case Else
If IsInteger(Full_Half) Then digitLength_ = CLngPtr(Full_Half)
End Select
lenUnits = CLng(Int(Len(strNum) / digitLength_))
If Len(strNum) Mod digitLength_ = 0 Then lenUnits = lenUnits - 1
ReDim digits_(lenUnits)
'数値の右端から順にdigitLength_毎に区切った数値をdigits_に移す
For i = lenUnits To 0 Step -1
start = start - digitLength_
If start < 1 Then start = 1
digits_(i) = CLngPtr(Mid(strNum, start))
Mid(strNum, start, digitLength_) = String(digitLength_, " ")
strNum = RTrim(strNum)
Next i
GoTo Last:
Last:
InitializeCounter
End Sub
'digits_の各要素を左側(全体の位が大きい側)から返すメソッド
'[alternative] カウント終了後に呼ばれた場合に返す値を指定する
Friend Function DigitLeft(Optional alternative As Variant = Null) As LongPtr
'返すdigits_がなくなった⇒以降alternativeを返す
If counter_ = COUNT_FINISHED Then
DigitLeft = alternative
Exit Function
End If
'digits_から返す要素を選択・代入
DigitLeft = digits_(UBound(digits_) - counter_)
'カウンター変数のデクリメント
counter_ = counter_ - 1
End Function
'digits_の各要素右側(全体の位が小さい側)から返すメソッド
'[alternative] カウント終了後に呼ばれた場合に返す値を指定する
Friend Function DigitRight(Optional alternative As Variant = Null) As LongPtr
'返すdigits_がなくなった⇒以降alternativeを返す
If counter_ = COUNT_FINISHED Then
DigitRight = alternative
Exit Function
End If
'digits_から返す要素を選択・代入
DigitRight = digits_(counter_)
'カウンター変数のデクリメント
counter_ = counter_ - 1
End Function
'digits_のカウンター変数counter_を初期化するメソッド
Friend Sub InitializeCounter()
counter_ = UBound(digits_)
End Sub
'1Digitのサイズに0埋めで桁数を揃えるメソッド
'[lptNum] 数値をLongPtr型で指定する
Friend Function To1DigitLength(ByVal lptNum As LongPtr) As String
To1DigitLength = Format(CStr(lptNum), String(digitLength_, "0"))
End Function
'1Digit相当の10進数の値を返すメソッド
Friend Function DecOf1Digit() As Variant
DecOf1Digit = 10 ^ digitLength_
End Function
'iteration中の現在のdigits_の桁数に相当する10進数に調えるメソッド
'[strNum] 数値をString型で指定する
Friend Function AdjustMagnitude(ByVal strNum As String) As String
Dim len0s As Long: len0s = (UBound(digits_) - (counter_ + 1)) * digitLength_
AdjustMagnitude = strNum & String(len0s, "0")
End Function
'除算に使用する概算の除数を生成するメソッド
Friend Function GenApproxDivisor() As BigInt
Set GenApproxDivisor = New BigInt
If Me.IsLessThan1DLen() Then
Call GenApproxDivisor.Init(value_)
Exit Function
End If
Dim lptDvsrApr As LongPtr, lptLetfDLFull As LongPtr
lptLetfDLFull = CLngPtr(Left(value_, DigitLength.FULL))
lptDvsrApr = WorksheetFunction.Ceiling(lptLetfDLFull / 10, 1)
If lptDvsrApr = CLngPtr(Left(value_, DigitLength.FULL - 1)) Then
lptDvsrApr = lptDvsrApr + 1
End If
Dim len0s As LongPtr: len0s = Me.Length - Len(CStr(lptDvsrApr))
Dim strDvsrApr As String: strDvsrApr = CStr(lptDvsrApr) & String(len0s, "0")
Call GenApproxDivisor.Init(strDvsrApr)
End Function
'1Digit相当の桁数よりもインスタンスの数値の桁数が短い否かを判別するメソッド
Friend Function IsLessThan1DLen() As Boolean
IsLessThan1DLen = Me.Length < DigitLength.FULL
End Function
'数値が正の数であるか否かを返すメソッド
Friend Function IsPositive() As Boolean
IsPositive = Not negative_
End Function
'数値が負の数であるか否かを返すメソッド
Friend Function IsNegative() As Boolean
IsNegative = negative_
End Function
'数値の符号をプラスに変更するメソッド
Friend Sub ToPositive()
negative_ = False
End Sub
'数値の符号をマイナスに変更するメソッド
Friend Sub ToNegative()
negative_ = True
End Sub
'数値の符号を反転するメソッド
Friend Sub InvertSign()
negative_ = Not negative_
End Sub
'数値が奇数であるか否かを返すメソッド
Friend Function IsOdd() As Boolean
IsOdd = CInt(Right(value_, 1)) Mod 2 = 1
End Function
'本インスタンスの数値と引数otherの数値の大小を比較するメソッド
'本インスタンスの数値 > 引数otherの数値 ⇒ 1を返す
'本インスタンスの数値 = 引数otherの数値 ⇒ 0を返す
'本インスタンスの数値 < 引数otherの数値 ⇒ -1を返す
'[other] 比較対象のBigiInt型の値を指定する
Friend Function CompareAbs(ByVal other As BigInt) As Long
'絶対値の桁数で大小比較する
Select Case Me.Length
Case Is > other.Length: CompareAbs = 1: Exit Function
Case Is < other.Length: CompareAbs = -1: Exit Function
Case Is = other.Length
'絶対値の桁数が等しいので先頭から順に大小比較する
Dim cMe As BigInt: Set cMe = Me.Clone
Dim cOt As BigInt: Set cOt = other.Clone
Call cMe.PrepareIteration("Full")
Call cOt.PrepareIteration("Full")
Dim valMe As LongPtr, valOt As LongPtr
Do
valMe = cMe.DigitLeft(-1)
valOt = cOt.DigitLeft(-1)
Select Case valMe
Case Is > valOt: CompareAbs = 1: Exit Function
Case Is < valOt: CompareAbs = -1: Exit Function
End Select
Loop Until cMe.Finished And cOt.Finished
'大小比較の結果も等しいので同値
CompareAbs = 0
End Select
End Function
'===========================
'===== Support Methods =====
'===========================
'引数で指定した文字列型の値が整数として認識可能かを判別するメソッド
'[strValue] 文字列型の値を指定する
Private Function IsInteger(ByVal strValue As String) As Boolean
Dim regex As New RegExp
'RegExp: メニュー⇒参照設定⇒Microsoft VBScript Regular Expressions 5.5 にチェック
regex.Pattern = "^(0|\-?[1-9][0-9]*)$"
regex.Global = True
IsInteger = regex.Test(strValue)
End Function
'先頭が0始まりの数値の場合先頭の0を除去する(複数個ある場合はすべて除去)
'[strNum] 数値をString型で指定する
Private Function RemoveLeading0s(ByVal strNum As String) As String
Dim i As Long
'先頭から順に検索し0以外の数値以降の数値返す
For i = 1 To Len(strNum)
If Mid(strNum, i, 1) <> "0" Then
RemoveLeading0s = Mid(strNum, i)
Exit Function
End If
Next i
'全て0の場合は"0"を返す
RemoveLeading0s = "0"
End Function
'文字列型の数値に符号を付けて返すメソッド
'[strNum] 数値をString型で指定する
Private Function PutSign(ByVal strNum As String) As String
If negative_ Then
PutSign = "-" & strNum
Exit Function
End If
PutSign = strNum
End Function
BICalculator
Option Explicit
'=====================================
'===== Public Arithmetic Methods =====
'=====================================
'足し算の和を返すメソッド
'[num1] 数値1をString型等で指定する
'[num2] 数値2をString型等で指定する
'[returnBigInt] 戻り値をBigInt型で返すか否かを指定する
' falseを指定した場合はString型で返す
Public Function Add( _
ByVal num1 As Variant, _
ByVal num2 As Variant, _
Optional returnBigInt As Boolean = False _
) As Variant
Dim bi1 As BigInt: Set bi1 = ToBigInt(num1)
Dim bi2 As BigInt: Set bi2 = ToBigInt(num2)
Dim ans As BigInt
Select Case True
Case bi1.Value = "0": Set ans = bi2 '0 + num2 = num2
Case bi2.Value = "0": Set ans = bi1 'num1 + 0 = num1
Case bi1.IsPositive And bi2.IsPositive
Set ans = CalcSum(bi1, bi2)
Case bi1.IsPositive And bi2.IsNegative
Select Case bi1.CompareAbs(bi2)
Case 1: Set ans = CalcDifference(bi1, bi2)
Case -1: Set ans = CalcDifference(bi2, bi1): Call ans.ToNegative
Case 0: Set ans = New BigInt
End Select
Case bi1.IsNegative And bi2.IsPositive
Select Case bi1.CompareAbs(bi2)
Case 1: Set ans = CalcDifference(bi2, bi1)
Case -1: Set ans = CalcDifference(bi1, bi2): Call ans.ToNegative
Case 0: Set ans = New BigInt
End Select
Case bi1.IsNegative And bi2.IsNegative
Set ans = CalcSum(bi1, bi2): Call ans.ToNegative
End Select
If returnBigInt Then
Set Add = ans
Exit Function
End If
Add = ans.Value
End Function
'引き算の差を返すメソッド
'[num1] 数値1をString型等で指定する
'[num2] 数値2をString型等で指定する
'[returnBigInt] 戻り値をBigInt型で返すか否かを指定する。
' falseを指定した場合はString型で返す
Public Function Subtract( _
ByVal num1 As Variant, _
ByVal num2 As Variant, _
Optional returnBigInt As Boolean = False _
) As Variant
Dim bi1 As BigInt: Set bi1 = ToBigInt(num1)
Dim bi2 As BigInt: Set bi2 = ToBigInt(num2)
Dim ans As BigInt
Select Case True
Case bi1.Value = "0": Set ans = bi2: Call ans.InvertSign
Case bi2.Value = "0": Set ans = bi1
Case bi1.IsPositive And bi2.IsPositive
Select Case bi1.CompareAbs(bi2)
Case 1: Set ans = CalcDifference(bi1, bi2)
Case -1: Set ans = CalcDifference(bi2, bi1): Call ans.ToNegative
Case 0: Set ans = New BigInt
End Select
Case bi1.IsPositive And bi2.IsNegative
Set ans = CalcSum(bi1, bi2)
Case bi1.IsNegative And bi2.IsPositive
Set ans = CalcSum(bi1, bi2): Call ans.ToNegative
Case bi1.IsNegative And bi2.IsNegative
Select Case bi1.CompareAbs(bi2)
Case 1: Set ans = CalcDifference(bi1, bi2): Call ans.ToNegative
Case -1: Set ans = CalcDifference(bi2, bi1)
Case 0: Set ans = New BigInt
End Select
End Select
If returnBigInt Then
Set Subtract = ans
Exit Function
End If
Subtract = ans.Value
End Function
'掛け算の積を返すメソッド
'[num1] 数値1をString型等で指定する
'[num2] 数値2をString型等で指定する
'[returnBigInt] 戻り値をBigInt型で返すか否かを指定する
' falseを指定した場合はString型で返す
Public Function Multiply( _
ByVal num1 As Variant, _
ByVal num2 As Variant, _
Optional returnBigInt As Boolean = False _
) As Variant
Dim bi1 As BigInt: Set bi1 = ToBigInt(num1)
Dim bi2 As BigInt: Set bi2 = ToBigInt(num2)
Dim ans As BigInt
If bi1.Value = "0" Or bi2.Value = "0" Then
Set ans = New BigInt
GoTo Last:
End If
Dim isMinus As Boolean: isMinus = bi1.IsPositive Eqv bi2.IsNegative
Set ans = CalcProduct(bi1, bi2)
If isMinus Then Call ans.ToNegative
Last:
If returnBigInt Then
Set Multiply = ans
Exit Function
End If
Multiply = ans.Value
End Function
'割り算の商を返すメソッド
'[dividend] 被除数をString型等で指定する
'[divisor] 除数をString型等で指定する
'[returnBigInt] 戻り値をBigInt型で返すか否かを指定する
' falseを指定した場合はString型で返す
Public Function Divide( _
ByVal dividend As Variant, _
ByVal divisor As Variant, _
Optional returnBigInt = False _
) As Variant
Dim ans As BIDivResult
Try: On Error GoTo Catch:
Set ans = DivAndMod(dividend, divisor)
GoTo Finally:
Catch:
If Err.number = 11 Then
Call Err.Raise(vbObjectError + 11, , _
"[BICalculator.Divide]" & vbCrLf & _
"Tried to divide by 0." & vbCrLf & _
"Divisor must be a non-zero integer.")
End If
Finally:
If returnBigInt Then
Set Divide = ans.Quotient
Exit Function
End If
Divide = ans.Quotient.Value
End Function
'割り算の剰余を返すメソッド
'[dividend] 被除数をString型等で指定する
'[divisor] 除数をString型等で指定する
'[returnBigInt] 戻り値をBigInt型で返すか否かを指定する
' falseを指定した場合はString型で返す
Public Function Modulo( _
ByVal dividend As Variant, _
ByVal divisor As Variant, _
Optional returnBigInt As Boolean = False _
) As Variant
Dim ans As BIDivResult
Try: On Error GoTo Catch:
Set ans = DivAndMod(dividend, divisor)
GoTo Finally:
Catch:
If Err.number = 11 Then
Call Err.Raise(vbObjectError + 11, , _
"[BICalculator.Modulo]" & vbCrLf & _
"Tried to divide by 0." & vbCrLf & _
"Divisor must be a non-zero integer.")
End If
Finally:
If returnBigInt Then
Set Modulo = ans.Remainder
Exit Function
End If
Modulo = ans.Remainder.Value
End Function
'割り算の商と剰余を返すメソッド
'[dividend] 被除数をString型等で指定する
'[divisor] 除数をString型等で指定する
Public Function DivAndMod( _
ByVal dividend As Variant, _
ByVal divisor As Variant _
) As BIDivResult
Dim bi1 As BigInt: Set bi1 = ToBigInt(dividend)
Dim bi2 As BigInt: Set bi2 = ToBigInt(divisor)
Dim ans As New BIDivResult
If bi1.Value = "0" Then
Call ans.SetValues("0", "0")
GoTo Last:
End If
If bi2.Value = "0" Then
Call Err.Raise(vbObjectError + 11, , _
"[BICalculator.DivAndMod]" & vbCrLf & _
"Tried to divide by 0." & vbCrLf & _
"Divisor must be a non-zero integer.")
End If
Select Case bi1.CompareAbs(bi2)
Case -1: Call ans.SetValues("0", bi1.Value)
Case 0: Call ans.SetValues("1", "0")
Case 1: Set ans = CalcQtntAndRmdr(bi1, bi2)
End Select
If bi1.IsPositive Eqv bi2.IsNegative Then ans.Quotient.ToNegative
If bi1.IsNegative Then ans.Remainder.ToNegative
Last:
Set DivAndMod = ans
End Function
'冪を返すメソッド
'[base] 基数をString型等で指定する
'[exponent] 指数をString型等で指定する(分数表記可 ”3/2”等)
'[returnBigInt] 戻り値をBigInt型で返すか否かを指定する
Public Function Power( _
ByVal base As Variant, _
ByVal exponent As Variant, _
Optional returnBigInt As Boolean = False _
) As Variant
Dim bi1 As BigInt: Set bi1 = ToBigInt(base)
Dim bi2 As BigInt
Dim ans As BigInt
Dim splited As Variant
If InStr(1, exponent, "/") <> 0 Then
splited = Split(exponent, "/")
Set bi2 = ToBigInt(splited(0))
Else
Set bi2 = ToBigInt(exponent)
End If
If bi1.Value = "1" Or bi2.Value = "0" Then
Set ans = New BigInt
Call ans.Init("1")
GoTo Last:
End If
If bi2.IsNegative Then
Call Err.Raise(vbObjectError + 5, , _
"[BICalculator.Power]" & vbCrLf & _
"Parameter ""exponent"" must be a positive integer.")
End If
Set ans = CalcPower(bi1, bi2)
If IsArray(splited) Then
Set ans = CalcRoot(ans, ToBigInt(splited(1)))
End If
If bi1.IsNegative And bi2.IsOdd Then ans.ToNegative
Last:
If returnBigInt Then
Set Power = ans
Exit Function
End If
Power = ans.Value
End Function
'冪根を返すメソッド
'[radicand] 被開数をString型等で指定する
'[index] 指数をString型等で指定する
'[returnBigInt] 戻り値をBigInt型で返すか否かを指定する
Public Function Root( _
ByVal radicand As Variant, _
ByVal index As Variant, _
Optional returnBigInt As Boolean = False _
) As Variant
Dim bi1 As BigInt: Set bi1 = ToBigInt(radicand)
Dim bi2 As BigInt: Set bi2 = ToBigInt(index)
Dim ans As BigInt
Select Case True
Case bi1.IsNegative
Call Err.Raise(vbObjectError + 5, , _
"[BICalculator.Root]" & vbCrLf & _
"Parameter ""radicand"" must be a positive integer.")
Case bi1.Value = "0"
Set ans = ToBigInt("0"): GoTo Last:
Case bi1.Value = "1"
Set ans = ToBigInt("1"): GoTo Last:
End Select
Select Case True
Case bi2.IsNegative, bi2.Value = "0"
Call Err.Raise(vbObjectError + 5, , _
"[BICalculator.Root]" & vbCrLf & _
"Parameter ""index"" must be a positive integer.")
Case bi2.Value = "1"
Set ans = bi1: GoTo Last:
End Select
Set ans = CalcRoot(bi1, bi2)
Last:
If returnBigInt Then
Set Root = ans
Exit Function
End If
Root = ans.Value
End Function
'指定した基数の値に変換するメソッド
'[num] 10進数の値をString型等で指定する
'[Base] 基数をString型等で指定する
'[returnBigInt] 戻り値をBigInt型で返すか否かを指定する
Public Function ToBase( _
ByVal number As Variant, _
ByVal base As Variant, _
Optional returnBigInt As Boolean = False _
) As Variant
Dim bi1 As BigInt: Set bi1 = ToBigInt(number)
Dim bi2 As BigInt: Set bi2 = ToBigInt(base)
Dim ans As BigInt
If bi2.Value = "0" Then
Call Err.Raise(vbObjectError + 5, , _
"[BICalculator.ToBase]" & vbCrLf & _
"The parameter ""base"" must be an integer greater than or equal to 2.")
End If
Set ans = CalcBase(bi1, bi2)
If bi1.IsNegative Then ans.ToPositive
Last:
If returnBigInt Then
Set ToBase = ans
Exit Function
End If
ToBase = ans.Value
End Function
'=======================================
'===== Private Arithmetic Methods =====
'=======================================
'絶対値の加法メソッド
'[num1] 数値1をBigiInt型で指定する
'[num2] 数値2をBigiInt型で指定する
Private Function CalcSum( _
ByVal num1 As BigInt, _
ByVal num2 As BigInt _
) As BigInt
Dim strSum As String '各Digit単位の合計値を連結した総合計を格納
Try: On Error GoTo Catch
strSum = CStr(CDec(num1.ValueAbs) + CDec(num2.ValueAbs))
GoTo Finally:
Catch:
'================================================================
'計算方針:筆算と同様のアルゴリズムで計算する。
'ただし、LongPtrに格納可能な最大桁数を1Digitのサイズとし、
'各Digit毎に計算した値をString型で連結して算出する。
'Long型 ⇒ 9桁格納可能 ⇒ 1Digit=10億進数
'LongLong型 ⇒ 18桁格納可能 ⇒ 1Digit=10^18進数
'================================================================
Dim lptSum As LongPtr: lptSum = 0 '各Digit単位の合計値を格納
Dim lptCrry As LongPtr: lptCrry = 0 '繰り上がりの数値を格納
Call num1.PrepareIteration("Full")
Call num2.PrepareIteration("Full")
Do
lptSum = num1.DigitRight(0) + num2.DigitRight(0) + lptCrry
If lptSum >= num1.DecOf1Digit() Then
lptCrry = 1
lptSum = lptSum - num1.DecOf1Digit()
Else
lptCrry = 0
End If
strSum = num1.To1DigitLength(lptSum) & strSum
Loop Until num1.Finished And num2.Finished
If lptCrry > 0 Then strSum = CStr(lptCrry) & strSum
Finally:
Set CalcSum = ToBigInt(strSum)
End Function
'絶対値の減法メソッド
'[numLrg] numSmlより大きい数値をBigiInt型で指定する
'[numSml] numLrgより小さい数値をBigiInt型で指定する
Private Function CalcDifference( _
ByVal numLrg As BigInt, _
ByVal numSml As BigInt _
) As BigInt
Dim strDiff As String
Try: On Error GoTo Catch
strDiff = CStr(CDec(numLrg.ValueAbs) - CDec(numSml.ValueAbs))
GoTo Finally:
Catch:
'================================================================
'計算方針:CalcSumと同じ
'Long型 ⇒ 9桁格納可能 ⇒ 1Digit=10億進数
'LongLong型 ⇒ 18桁格納可能 ⇒ 1Digit=10^18進数
'================================================================
Dim lptDiff As LongPtr: lptDiff = 0
Dim lptBrrw As LongPtr: lptBrrw = 0
Call numLrg.PrepareIteration("Full")
Call numSml.PrepareIteration("Full")
Do
lptDiff = numLrg.DigitRight(0) - numSml.DigitRight(0) - lptBrrw
If lptDiff < 0 Then
lptBrrw = 1
lptDiff = lptDiff + numLrg.DecOf1Digit()
Else
lptBrrw = 0
End If
strDiff = numLrg.To1DigitLength(lptDiff) & strDiff
Loop Until numLrg.Finished And numSml.Finished
Finally:
Set CalcDifference = ToBigInt(strDiff)
End Function
'絶対値の乗法メソッド
'[num1] 数値1をBigiInt型で指定する
'[num2] 数値2をBigiInt型で指定する
Private Function CalcProduct( _
ByVal num1 As BigInt, _
ByVal num2 As BigInt _
) As BigInt
Dim ans As New BigInt
Try: On Error GoTo Catch
Set ans = ToBigInt(CDec(num1.ValueAbs) * CDec(num2.ValueAbs))
GoTo Finally:
Catch:
'================================================================
'計算方針:筆算と同様のアルゴリズムで計算する。
'ただし、LongPtrに格納可能な最大桁数を1Digitのサイズとし、
'各Digit同士を掛け合わせ合算してString型で連結して算出する。
'Long型 ⇒ 4桁格納可能 ⇒ 1Digit=1万進数
'LongLong型 ⇒ 9桁格納可能 ⇒ 1Digit=10億進数
'================================================================
Dim strProd As String
Dim lptUnit2 As LongPtr
Dim lptProd As LongPtr, lptRmdr As LongPtr, lptCrry As LongPtr
Dim strSum As String
Call ans.Init("0")
Call num1.PrepareIteration("Half")
Call num2.PrepareIteration("Half")
Do
lptCrry = 0
strSum = ""
lptUnit2 = num2.DigitRight(0)
Call num1.InitializeCounter
Do
lptProd = num1.DigitRight(0) * lptUnit2 + lptCrry
lptRmdr = lptProd Mod num1.DecOf1Digit
lptCrry = (lptProd - lptRmdr) / num1.DecOf1Digit
strSum = num1.To1DigitLength(lptRmdr) & strSum
Loop Until num1.Finished
If lptCrry > 0 Then strSum = CStr(lptCrry) & strSum
strSum = num2.AdjustMagnitude(strSum)
Set ans = Add(ans, strSum, True)
Loop Until num2.Finished
Finally:
Set CalcProduct = ans
End Function
'絶対値の除法と剰余算メソッド
'[numDvdd] 被除数をBigiInt型で指定する
'[numDvsr] 除数をBigiInt型で指定する
Private Function CalcQtntAndRmdr( _
ByVal numDvdd As BigInt, _
ByVal numDvsr As BigInt _
) As BIDivResult
Dim ans As New BIDivResult
Try: On Error GoTo Catch:
Dim ansDiv As Variant
Dim ansMod As Variant
ansDiv = CDec(numDvdd.ValueAbs) \ CDec(numDvsr.ValueAbs)
ansMod = CDec(numDvdd.ValueAbs) Mod CDec(numDvsr.ValueAbs)
Call ans.SetValues(CStr(ansDiv), CStr(ansMod))
GoTo Finally:
Catch:
'====================================================================
'基本方針:除法の原理(整除法)を利用して求める。
'ただし、除数が1Digitのサイズ以上の場合は有効数字[1Digitのサイズ‐1
']桁に切り上げた除数の近似値(d)で漸近的に求め、必要であれば減法で精算
'する。
'
' N = Q * D + R (N:被除数, Q:商, D:除数, R:剰余)
'
'1-1).Dの桁数 < 1Digitのサイズ
' ⇒d = D
'1-2).Dの桁数 >= 1Digitのサイズ
' ⇒d = Dを有効数字[1Digitのサイズ - 1]桁に切り上げた値
' (例.D=123456789012 ⇒ d=123456790000 (1Digitのサイズ=9の時))
'
'2).筆算(長除法)のアルゴリズムによる計算を、Rn+1 < dを満たすまで繰
'り返す。この際、d >= D なので Qn+1 <= Q となり Rn+1 >= Rとなる。
'またループ処理の各回終了時点で、N = Qn+1 * D + Rn+1 は常に成り立つ。
'(以下、添え字付きのQとRは計算途中での商と剰余を、添え字無しは計算
'終了後の商と剰余を表す。)
' Q0 = 0, R0 = N
' Do
' Qn+1 = Qn + Rn \ d ("\":整数部のみを返す除算)
' Rn+1 = Rn - (Qn+1 * D)
' Loop Until Rn+1 < d
' Q = Qn+1, R = Rn+1
'
'3).D <= Rn+1 < d の時は2)の方法ではそれ以上近似させることは出来ないの
'で減法により精算する
' R0 = R
' Do Until Rn+1 < D
' Rn+1 = Rn - D
' q = q + 1
' Loop
' Q = Q + q, R = Rn+1
'====================================================================
'1) DArp = d
Dim DApr As BigInt: Set DApr = numDvsr.GenApproxDivisor()
'2)
Dim QnP1 As BigInt, RnP1 As BigInt, Rn As BigInt, DByQnP1 As BigInt
Set Rn = numDvdd
Do
Set QnP1 = CalcQtntApprox(Rn, DApr)
Set DByQnP1 = CalcProduct(QnP1, numDvsr)
Set RnP1 = CalcDifference(Rn, DByQnP1)
Set ans.Quotient = CalcSum(ans.Quotient, QnP1)
Set Rn = RnP1
Loop Until RnP1.Length <= DApr.Length
Set ans.Remainder = RnP1
If numDvsr.IsLessThan1DLen() Then GoTo Finally:
'3)
Call CalcQAndRBySubtraction(ans, numDvsr)
Finally:
Set CalcQtntAndRmdr = ans
End Function
'絶対値の累乗数を求めるメソッド
'[numBs] 底をBigiInt型で指定する
'[numEx] 指数をBigiInt型で指定する
Private Function CalcPower( _
ByVal numBs As BigInt, _
ByVal numEx As BigInt _
) As BigInt
Dim ans As New BigInt
Try: On Error GoTo Catch:
Set ans = ToBigInt(CDec(numBs.ValueAbs) ^ CDec(numEx.ValueAbs))
GoTo Finally:
Catch:
Dim binExp As BigInt: Set binExp = CalcBase(numEx, ToBigInt("2"))
Dim i As Long
Call ans.Init("1")
Call binExp.PrepareIteration("1")
Do
Set ans = CalcProduct(ans, ans.Clone)
If binExp.DigitLeft = 1 Then
Set ans = CalcProduct(ans, numBs)
End If
Loop Until binExp.Finished
Finally:
Set CalcPower = ans
End Function
'絶対値の累乗根を求めるメソッド
'[numRdcd] 被開平数をBigiInt型で指定する
'[numRdcd] 指数をBigiInt型で指定する
Private Function CalcRoot( _
ByVal numRdcd As BigInt, _
ByVal numIndx As BigInt _
) As BigInt
Dim XnP1 As BigInt
Try: On Error GoTo Catch:
Set XnP1 = ToBigInt(CDec(numRdcd.ValueAbs) ^ (1 / CDec(numIndx.ValueAbs)))
GoTo Finally:
Catch:
Dim Xn As BigInt, expnt As BigInt, expntM1 As BigInt
Dim cmpr As Long, cmprBfr As Long
Dim cnt As Integer
Set Xn = GetApproxX0(numRdcd, numIndx)
Set expnt = numIndx
Set expntM1 = CalcDifference(numIndx, ToBigInt("1"))
Do
Set XnP1 = NewtonsMethod(Xn, numRdcd, expnt, expntM1)
cmpr = XnP1.CompareAbs(Xn)
If cmpr = 0 Then Exit Do
cmprBfr = cmpr
Set Xn = XnP1
cnt = cnt + 1
Loop While cnt < numRdcd.Length
'大きい値から近似された場合、求める値+1の値が算出されるので1を引く。
If cmprBfr = -1 Then Set XnP1 = CalcDifference(XnP1, ToBigInt("1"))
Finally:
Set CalcRoot = XnP1
End Function
'10進数から別の進数値を求めるメソッド
'[num] 10進数の値をBigiInt型で指定する
'[numBs] 基数をBigiInt型で指定する
Private Function CalcBase( _
ByVal num As BigInt, _
numBs As BigInt _
) As BigInt
Dim strBNum As String
Try: On Error GoTo Catch:
strBNum = WorksheetFunction.base(CDbl(num.ValueAbs), CDbl(numBs.Value))
GoTo Finally:
Catch:
Dim divBase As New BIDivResult
Set divBase.Quotient = num
Do Until divBase.Quotient.Value = "0"
Set divBase = DivAndMod(divBase.Quotient, numBs)
strBNum = divBase.Remainder.ValueAbs & strBNum
Loop
Finally:
Set CalcBase = ToBigInt(strBNum)
End Function
'============================
'===== Support Methods ======
'============================
'文字列型等の値からBigInt型のインスタンスを生成するメソッド
'[num] BigInt型に変換する値を指定する
Private Function ToBigInt(ByVal num As Variant) As BigInt
If TypeName(num) = "BigInt" Then
Set ToBigInt = num
Exit Function
End If
If CStr(num) Like "*E+*" Then
Call Err.Raise(vbObjectError + 6)
End If
Set ToBigInt = New BigInt
Call ToBigInt.Init(CStr(num))
End Function
'絶対値の被除数と丸めたの除数から商を概算的に求めるメソッド
'[numDvdd] 被除数をBigiInt型で指定する
'[numDvsr] 概算の除数をBigiInt型で指定する
Private Function CalcQtntApprox( _
ByVal numDvdd As BigInt, _
ByVal numDvsr As BigInt _
) As BigInt
'================================================================
'計算方針:除数が1Digitよりも大きい場合、除数を1Digit未満に丸め
'その丸めた除数で概算の商を求める。
'================================================================
Dim lptDvdd As LongPtr, lptDvsr As LongPtr, lptQtnt As LongPtr, lptRmdr As LongPtr
Dim strDvdd As String: strDvdd = numDvdd.ValueAbs
Dim strQtnt As String, strRmdr As String
Dim str0s As String
Dim d As LongPtr
lptDvsr = CLngPtr(Mid(numDvsr.ValueAbs, 1, numDvsr.DLFull - 1))
str0s = String(Len(CStr(lptDvsr)) - 1, "0")
For d = 1 To Len(strDvdd)
lptDvdd = lptDvdd * 10 + Mid(strDvdd, d, 1)
lptRmdr = lptDvdd Mod lptDvsr
lptQtnt = lptDvdd \ lptDvsr
strQtnt = strQtnt & CStr(lptQtnt): If strQtnt = "0" Then strQtnt = ""
strRmdr = Format(CStr(lptRmdr), str0s) & Mid(strDvdd, d + 1)
If Len(strRmdr) < numDvsr.Length Then Exit For
If Len(strRmdr) = numDvsr.Length Then
If ToBigInt(strRmdr).CompareAbs(numDvsr) = -1 Then Exit For
End If
lptDvdd = lptRmdr
Next d
Set CalcQtntApprox = ToBigInt(strQtnt)
End Function
'絶対値の商と剰余を減法的に求めるメソッド
'[numQR] 概算の商と剰余をBIDivResult型で指定する
'[numDvsr] 除数をBigiInt型で指定する
Private Sub CalcQAndRBySubtraction( _
ByRef numQR As BIDivResult, _
ByVal numDvsr As BigInt _
)
'=================================================================
'計算方針:numQRの剰余から除数を引き続け、剰余が除数未満になるまで
'繰り返す。
'=================================================================
Dim cntQtnt As LongPtr: cntQtnt = 0
Do Until numQR.Remainder.CompareAbs(numDvsr) = -1
Set numQR.Remainder = CalcDifference(numQR.Remainder, numDvsr)
cntQtnt = cntQtnt + 1
Loop
If cntQtnt > 0 Then
Set numQR.Quotient = CalcSum(numQR.Quotient, ToBigInt(cntQtnt))
End If
End Sub
'ニュートン法により冪根を求めるメソッド
'[Xn] 前回値をBigiInt型で指定する
'[numRdcd] 被開平数をBigiInt型で指定する
'[expnt] 指数をBigiInt型で指定する
'[expntM1] 指数‐1の値をBigiInt型で指定する
Private Function NewtonsMethod( _
Xn As BigInt, _
numRdcd As BigInt, _
expnt As BigInt, _
expntM1 As BigInt _
) As BigInt
'================================================================
' Xn+1 = Xn - (Xn ^ expnt - numRdcd) / expnt * Xn ^ expntM1
' Calc Order: 6 1 2 5 4 3
'================================================================
Dim calc1 As BigInt: Set calc1 = CalcPower(Xn, expnt)
Dim calc2 As BigInt: Set calc2 = Subtract(calc1, numRdcd, True)
Dim calc3 As BigInt: Set calc3 = CalcPower(Xn, expntM1)
Dim calc4 As BigInt: Set calc4 = CalcProduct(expnt, calc3)
Dim calc5 As BigInt: Set calc5 = Divide(calc2, calc4, True)
Dim calc6 As BigInt: Set calc6 = Subtract(Xn, calc5, True)
Set NewtonsMethod = calc6
End Function
'NewtonsMethodの初期値(X0)を求めるメソッド
'[numRdcd] 被開平数をBigiInt型で指定する
'[numIndx] 指数をBigiInt型で指定する
Private Function GetApproxX0( _
ByVal numRdcd As BigInt, _
ByVal numIndx As BigInt _
) As BigInt
Dim strApproxX0 As String
Const lenDcmlMax As Integer = 28
'冪根の桁数を求める
Dim lenX0 As LongPtr
lenX0 = WorksheetFunction.Ceiling(numRdcd.Length / numIndx.Value, 1)
strApproxX0 = String(lenX0, "0")
If numIndx.ValueAbs < lenDcmlMax Then
'冪指数がDouble型の有効桁数未満の場合は概算の冪根を求める
Dim strAprroxRdcd As String
Dim strX0Head As String
Call numRdcd.PrepareIteration(numIndx.ValueAbs)
Do
strAprroxRdcd = strAprroxRdcd & numRdcd.To1DigitLength(numRdcd.DigitLeft(0))
If Len(strAprroxRdcd) + numIndx.ValueAbs > lenDcmlMax Then Exit Do
Loop Until numRdcd.Finished
strX0Head = CStr(Int(CDec(strAprroxRdcd) ^ (1 / numIndx.ValueAbs)))
Mid(strApproxX0, 1, Len(strX0Head)) = strX0Head
Else
'冪指数がDouble型の有効桁数以上の場合は先頭の値を5にした値を求める
'先頭を5にする理由:求める冪根の値が不明なため、桁数内で下限上限どちらからも等距離にするため
Mid(strApproxX0, 1, 1) = "5"
End If
Set GetApproxX0 = ToBigInt(strApproxX0)
End Function
BIDivResult
Option Explicit
'==========================
'===== Private Fields =====
'==========================
'商の値を格納するフィールド
Private quotient_ As BigInt
'剰余の値を格納するフィールド
Private remainder_ As BigInt
'=======================
'===== Constructor =====
'=======================
Private Sub Class_Initialize()
Set quotient_ = New BigInt
Set remainder_ = New BigInt
End Sub
'======================
'===== Properties =====
'======================
'商の値を取得するプロパティ
Public Property Get Quotient() As BigInt
Set Quotient = quotient_
End Property
'商の値を設定するプロパティ
Public Property Set Quotient(biQuotient As BigInt)
Set quotient_ = biQuotient
End Property
'剰余の値を取得するプロパティ
Public Property Get Remainder() As BigInt
Set Remainder = remainder_
End Property
'剰余の値を設定するプロパティ
Public Property Set Remainder(biRemainder As BigInt)
Set remainder_ = biRemainder
End Property
'==========================
'===== Friend Methods =====
'==========================
'文字列型の商と剰余の値を一括で設定するメソッド
'[strQuotient] 商の値を文字列型で指定する
'[strRemainder] 剰余の値を文字列型で指定する
Friend Sub SetValues(strQuotient As String, strRemainder As String)
Call quotient_.Init(strQuotient)
Call remainder_.Init(strRemainder)
End Sub
反省・課題
- Digitの単位としてLongPtr型ではなくDecimal型を使った方がより効率が良かったかもしれない。
- 自分の環境では32bitでのテストしかできないので、64bit環境でのテストができていない。
(どなたか64bit環境をお持ちの方がいらしたら、テスト結果をお知らせいただけるとうれしいです。) - あまり大規模な修正をせずに小数も扱える型に改造可能かもしれない。
Author And Source
この問題について(Excel VBAでBigIntを作ってみた), 我々は、より多くの情報をここで見つけました https://zenn.dev/kakushina/articles/94ab7229346568著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol