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 // 分母が2か5で割り切れなかったら、純循環小数 // (元の分母と比べれば、混循環小数と区別がつく) // 最後まで割り切れたら、有限小数 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関数で、分数表現に戻せるはずですが、、、不完全です。