UWSCで分数を扱う

問題

DIM a = 1 / 3
MSGBOX((1 / 3 * 3 = a * 3) + "")

MSGBOXにはなんと出る?



答え

「False」です。
そう、計算中はそれなりの精度っぽいですが、一旦変数に格納すると誤差が出る。
誤差、嫌いですよね?よね?


そんなあなたに、分数用のFraモジュール。

DIM a = Fra.Div(1, 3)
MSGBOX((1 / 3 * 3 = Fra.Mul(a, 3)) + "")

これなら「True」です。シアワセ。

スクリプト

Fraction.uws

OPTION EXPLICIT

IFB GET_UWSC_NAME = "Fraction.uws" THEN
	DIM m = 252, n = 105
	PRINT m + "と" + n + "の最大公約数は" + Fra.GetGcd(m, n)
	DIM fra = "1/97"
	PRINT fra + "=" + Fra.ToRd(fra)	// 1/97の逆変換は勘弁して
	fra = "1212123/9999999"
	PRINT fra + "=" + Fra.ToRd(fra) + "=" + Fra.FromRd(Fra.ToRd(fra))
	fra = "338420370199/3333333000"
	PRINT fra + "=" + Fra.ToRd(fra) + "=" + Fra.FromRd(Fra.ToRd(fra))
	fra = "1/0"
	PRINT fra + "=" + Fra.ToRd(fra) + "=" + Fra.FromRd(Fra.ToRd(fra))
	fra = "1/2"
	DIM fra2 = "1/3"
	PRINT fra + " + " + fra2 + " = " + Fra.Add(fra, fra2)
	fra = "2/3"
	PRINT fra + " - " + fra2 + " = " + Fra.Sub(fra, fra2)
	fra2 = "3"
	PRINT fra + " * " + fra2 + " = " + Fra.Mul(fra, fra2)
	PRINT fra + " / " + fra2 + " = " + Fra.Div(fra, fra2)
	FOR m = 2 TO 1000
		fra = "1/" + m
		fra2 = Fra.ToRd(fra)
		IF LENGTH(fra2) > m + 2 THEN PRINT fra + "=" + fra2
	NEXT
ENDIF


MODULE Fra

	// 汎用
	FUNCTION GetGcd(m, n)
		// Euclidの互除法
		IF m < n THEN m = Shift(m, n)
		WHILE n <> 0
			m = Shift(m MOD n, n)
		WEND
		RESULT = m
	FEND
	FUNCTION Shift(m, var n)
		RESULT = n
		n = m
	FEND


	// 分数(fraction)関連
	//  フォーマットは、"numerator/denominator"の文字列
	//  分母省略は、当然1
	//  帯分数(mixed number)はただの乗算として表現する想定
	FUNCTION ToNumber(fraction, var numerator, var denominator)
		DIM i = POS("/", fraction)
		IFB i = 0 THEN
			numerator = VAL(fraction)
			denominator = 1
		ELSE
			numerator = VAL(COPY(fraction, 1, i - 1))
			denominator = VAL(COPY(fraction, i + 1))
		ENDIF
		RESULT = (numerator <> ERR_VALUE) AND (denominator <> ERR_VALUE)
	FEND
	FUNCTION FromNum(numerator, denominator)
		RESULT = numerator + "/" + denominator
		IF denominator = 1 THEN RESULT = numerator
	FEND
	// 四則演算
	CONST CALC_ADD = 1
	CONST CALC_SUB = 2
	CONST CALC_MUL = 3
	CONST CALC_DIV = 4
	FUNCTION Calc(c, fra1, fra2)
		RESULT = EMPTY
		DIM n1, d1, n2, d2
		IFB ToNumber(fra1, n1, d1) AND ToNumber(fra2, n2, d2) THEN
			SELECT c
			CASE CALC_ADD
				n1 = n1 * d2 + n2 * d1
				d1 = d1 * d2
			CASE CALC_SUB
				n1 = n1 * d2 - n2 * d1
				d1 = d1 * d2
			CASE CALC_MUL
				n1 = n1 * n2
				d1 = d1 * d2
			CASE CALC_DIV
				n1 = n1 * d2
				d1 = d1 * n2
			DEFAULT
				EXIT
			SELEND
			n2 = GetGcd(n1, d1)
			RESULT = FromNum(n1 / n2, d1 / n2)
		ENDIF
	FEND
	FUNCTION Add(fra1, fra2)
		RESULT = Calc(CALC_ADD, fra1, fra2)
	FEND
	FUNCTION Sub(fra1, fra2)
		RESULT = Calc(CALC_SUB, fra1, fra2)
	FEND
	FUNCTION Mul(fra1, fra2)
		RESULT = Calc(CALC_MUL, fra1, fra2)
	FEND
	FUNCTION Div(fra1, fra2)
		RESULT = Calc(CALC_DIV, fra1, fra2)
	FEND



	// 循環小数(recurring/repeating decimal)関連
	//  フォーマットは、循環部を{}表記
	// 桁数取得
	DIM _regex = NULL
	CONST NOT_DECIMAL = 0
	CONST INFINITE_DECIMAL = 1
	CONST FINITE_DECIMAL = 2
	CONST RECURRING_DECIMAL = 3
	FUNCTION GetType(fraction, var numerator, var denominator)
		RESULT = NOT_DECIMAL
		DIM n, d
		IFB ToNumber(fraction, n, d) THEN
			numerator = n
			denominator = d
			IFB d > 0 THEN
				// 約分
				n = GetGcd(d, n)
				d = d / n
				// 分母が25で割り切れなかったら、純循環小数
				// (元の分母と比べれば、混循環小数と区別がつく)
				// 最後まで割り切れたら、有限小数
				RESULT = RECURRING_DECIMAL
				n = d - 1
				WHILE d <> n
					n = d
					IF d MOD 2 = 0 THEN d = d / 2
					IF d MOD 5 = 0 THEN d = d / 5
				WEND
				IF d = 1 THEN RESULT = FINITE_DECIMAL
			ELSE
				RESULT = INFINITE_DECIMAL
			ENDIF
		ENDIF
	FEND
	FUNCTION GetKeta(n)
		IFB n = 0 THEN
			RESULT = 1
		ELSE
			RESULT = INT(LOGN(10, ABS(n))) + 1
		ENDIF
	FEND
	FUNCTION RdToNumber(rd, var ld, var rep)
		RESULT = FALSE
		IFB _regex = NULL THEN
			_regex = CREATEOLEOBJ("VBScript.RegExp")
			_regex.Pattern = "^(\d*\.?\d*){?(\d*)}?$"
		ENDIF
		DIM matches = _regex.Execute(rd)
		IFB matches.Count = 1 THEN
			IFB matches.Item(0).SubMatches.Count = 2 THEN
				ld = VAL(matches.Item(0).SubMatches.Item(0))
				rep = VAL(matches.Item(0).SubMatches.Item(1))
				IF rep = ERR_VALUE THEN rep = 0
				RESULT = (ld <> ERR_VALUE)
			ENDIF
		ENDIF
	FEND
	FUNCTION ToRd(fraction)
		RESULT = EMPTY
		DIM nu, de
		SELECT GetType(fraction, nu, de)
			CASE FINITE_DECIMAL
				RESULT = nu / de
			CASE RECURRING_DECIMAL
				// 既約分数にする
				DIM res = GetGcd(nu, de), count = 0
				nu = nu / res
				de = de / res
				res = ""
				// 筆算方式が現実的みたい
				DIM bketa = GetKeta(INT(nu / de))
				HASHTBL surp
				REPEAT
					surp[nu] = count
					res = res + INT(nu / de)
					nu = (nu MOD de) * 10
					count = count + 1
				UNTIL surp[nu, HASH_EXISTS]
				// 小数点と括弧をつけて返す
				RESULT = COPY(res, 1, bketa) + "." + COPY(res, bketa + 1, surp[nu] - 1) + "{" + COPY(res, bketa + surp[nu]) + "}"
			CASE INFINITE_DECIMAL
				RESULT = "00"
		SELEND
	FEND
	FUNCTION FromRd(rd)
		RESULT = EMPTY
		// 本当は等比数列の和から算出すべきだけどカンベン
		DIM ld = 0, rep = 0
		IFB RdToNumber(rd, ld, rep) THEN
			DIM base = POWER(10, GetKeta(rep))
			DIM ko = "" + ld
			IF POS(".", ko) = 0 THEN ko = ko + "."
			ko = VAL(ko + rep) * base - ld
			ld = POS(".", ko)
			IF ld THEN ld = LENGTH(ko) - ld
			ko = ko * POWER(10, ld)
			base = (base - 1) * POWER(10, ld)
			ld = GetGcd(ko, base)
			RESULT = FromNum(ko / ld, base / ld)
		ENDIF
	FEND

ENDMODULE


興味が出たので、循環小数変換する関数もおまけでついてます。
が、分数しか興味ないなら、ばっさり削除可能です。


循環小数関連は、桁数が一定を超えるとエラーだたり、小数の桁数処理がおかしかったり、あくまでおまけ。


でも、このおまけがないと、注目するほどのモジュールではないですね、、、。

使い方

加減乗除は、Fra.Add/Sub/Mul/Divのそれぞれの関数で行います。
最終的な結果は、Fra.GetTypeすると、その数のタイプと分子・分母が得られます。
Fra.NOT_DECIMALの場合、数値ではありません。
Fra.INFINITE_DECIMALの場合、無限です。
Fra.FINITE_DECIMALの場合、割り切れる数です。分子・分母から(精度内で)値が求まります。
Fra.RECURRING_DECIMALの場合、循環小数です。分子・分母からは近似値が求まります。


循環小数関連に興味がなくて、Fra.GetTypeを削除している場合は、Fra.ToNumberで分子・分母が得られます。


分数は文字列として保持するので、遅いです。
また、分子・分母の精度はUWSCに依存するため、すごく大きな数等が扱えるわけではないです。
一応、Fra.ToRd関数で、分数表現からの変換ができます。(不完全かも)
Fra.FromRd関数で、分数表現に戻せるはずですが、、、不完全です。