|
import random |
|
import math |
|
from typing import Callable |
|
from fractions import Fraction |
|
|
|
|
|
|
|
|
|
def is_terminating(frac: Fraction) -> bool: |
|
""" |
|
Fraction frac が有限小数(=分母の素因数が2と5のみ)かどうかを判定する |
|
""" |
|
d = frac.denominator |
|
while d % 2 == 0: |
|
d //= 2 |
|
while d % 5 == 0: |
|
d //= 5 |
|
return d == 1 |
|
|
|
|
|
def precedence(op: str) -> int: |
|
"""演算子の優先順位を返す(+,-:1、*,/:2)""" |
|
if op in ["+", "-"]: |
|
return 1 |
|
elif op in ["*", "/"]: |
|
return 2 |
|
return 0 |
|
|
|
|
|
def fraction_to_decimal_string(f: Fraction, decimal_places: int) -> str: |
|
""" |
|
Fraction f を decimal_places 桁の小数文字列に変換する。 |
|
※ f は既に有限小数(循環しない)と仮定。 |
|
""" |
|
return format(float(f), f".{decimal_places}f") |
|
|
|
|
|
|
|
|
|
|
|
def generate_expr_tree(n: int, leaf_generator: Callable) -> dict: |
|
""" |
|
n 個の項(葉)を持つ式ツリーを再帰的に生成する。 |
|
leaf_generator() は葉(数値)を生成する関数。 |
|
演算子は '+', '-', '*', '/' の中からランダムに選ぶ。 |
|
""" |
|
if n == 1: |
|
return {"type": "num", "value": leaf_generator()} |
|
else: |
|
k = random.randint(1, n - 1) |
|
left_tree = generate_expr_tree(k, leaf_generator) |
|
right_tree = generate_expr_tree(n - k, leaf_generator) |
|
op = random.choice(["+", "-", "*", "/"]) |
|
return {"type": "op", "op": op, "left": left_tree, "right": right_tree} |
|
|
|
|
|
def evaluate(tree: dict) -> Fraction: |
|
""" |
|
式ツリー tree を Fraction を用いて再帰的に評価する。 |
|
ゼロ除算が起こった場合は ZeroDivisionError を発生する。 |
|
""" |
|
if tree["type"] == "num": |
|
return tree["value"] |
|
else: |
|
op = tree["op"] |
|
left_val = evaluate(tree["left"]) |
|
right_val = evaluate(tree["right"]) |
|
if op == "+": |
|
return left_val + right_val |
|
elif op == "-": |
|
return left_val - right_val |
|
elif op == "*": |
|
return left_val * right_val |
|
elif op == "/": |
|
if right_val == 0: |
|
raise ZeroDivisionError |
|
return left_val / right_val |
|
|
|
|
|
def tree_to_text( |
|
tree: dict, |
|
parent_precedence: int = 0, |
|
force_paren: bool = False, |
|
decimal_places: int = None, |
|
) -> str: |
|
""" |
|
式ツリー tree を普通のテキスト形式の文字列に変換する。 |
|
・葉("num")の場合、decimal_places が指定されていれば小数形式で表示する。 |
|
・内部ノードの場合、標準の演算子優先順位に従い必要に応じて括弧を追加するほか、確率的に追加する。 |
|
""" |
|
if tree["type"] == "num": |
|
if decimal_places is not None: |
|
return fraction_to_decimal_string(tree["value"], decimal_places) |
|
else: |
|
|
|
if tree["value"].denominator == 1: |
|
return str(tree["value"].numerator) |
|
else: |
|
return str(tree["value"]) |
|
else: |
|
op = tree["op"] |
|
current_precedence = precedence(op) |
|
left_str = tree_to_text( |
|
tree["left"], current_precedence, decimal_places=decimal_places |
|
) |
|
right_str = tree_to_text( |
|
tree["right"], current_precedence, decimal_places=decimal_places |
|
) |
|
expr_str = f"{left_str} {op} {right_str}" |
|
|
|
if ( |
|
current_precedence < parent_precedence |
|
or force_paren |
|
or random.random() < 0.3 |
|
): |
|
return f"({expr_str})" |
|
else: |
|
return expr_str |
|
|
|
|
|
def tree_to_tex( |
|
tree: dict, |
|
parent_precedence: int = 0, |
|
force_paren: bool = False, |
|
decimal_places: int = None, |
|
) -> str: |
|
""" |
|
式ツリー tree を TeX 形式の文字列に変換する。 |
|
・掛け算は "\\times"、割り算は分数形式 "\\frac{...}{...}" で表現する。 |
|
・必要に応じて \left( ... \right) で括弧を追加する。 |
|
""" |
|
if tree["type"] == "num": |
|
if decimal_places is not None: |
|
return fraction_to_decimal_string(tree["value"], decimal_places) |
|
else: |
|
if tree["value"].denominator == 1: |
|
return str(tree["value"].numerator) |
|
else: |
|
return str(tree["value"]) |
|
else: |
|
op = tree["op"] |
|
current_precedence = precedence(op) |
|
if op == "/": |
|
|
|
left_tex = tree_to_tex(tree["left"], 0, decimal_places=decimal_places) |
|
right_tex = tree_to_tex(tree["right"], 0, decimal_places=decimal_places) |
|
expr_tex = f"\\frac{{{left_tex}}}{{{right_tex}}}" |
|
else: |
|
left_tex = tree_to_tex( |
|
tree["left"], current_precedence, decimal_places=decimal_places |
|
) |
|
right_tex = tree_to_tex( |
|
tree["right"], current_precedence, decimal_places=decimal_places |
|
) |
|
op_tex = op |
|
if op == "*": |
|
op_tex = "\\times" |
|
expr_tex = f"{left_tex} {op_tex} {right_tex}" |
|
if ( |
|
current_precedence < parent_precedence |
|
or force_paren |
|
or random.random() < 0.3 |
|
): |
|
return f"\\left({expr_tex}\\right)" |
|
else: |
|
return expr_tex |
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_integer_arithmetic_problem( |
|
min_val: int = -10, max_val: int = 10, max_terms: int = 5 |
|
) -> tuple[str, str, str]: |
|
""" |
|
指定範囲の整数(負の値も可)と、'+', '-', '*', '/'、括弧を組み合わせた |
|
四則演算の計算問題(項数は 2 ~ max_terms のランダムな個数)を作成する。 |
|
※ただし、計算結果が循環小数(有限小数でない)になったり、ゼロ除算となるものは除外。 |
|
戻り値は (テキスト形式の式, TeX 形式の式) のタプル。 |
|
""" |
|
while True: |
|
num_operands = random.randint(2, max_terms) |
|
tree = generate_expr_tree( |
|
num_operands, lambda: Fraction(random.randint(min_val, max_val)) |
|
) |
|
try: |
|
result = evaluate(tree) |
|
except ZeroDivisionError: |
|
continue |
|
if not is_terminating(result): |
|
continue |
|
text_expr = tree_to_text(tree) |
|
tex_expr = tree_to_tex(tree) |
|
return text_expr, tex_expr, eval(str(result)) |
|
|
|
|
|
|
|
def generate_decimal_leaf(min_val: int, max_val: int, decimal_places: int) -> Fraction: |
|
""" |
|
指定範囲の整数部分に、指定桁数の小数部分を持つ数値を生成する。 |
|
内部的には Fraction(n, 10**decimal_places) として正確に扱う。 |
|
""" |
|
factor = 10**decimal_places |
|
n = random.randint(min_val * factor, max_val * factor) |
|
return Fraction(n, factor) |
|
|
|
|
|
def create_decimal_arithmetic_problem( |
|
min_val: int = -10, max_val: int = 10, max_terms: int = 5, decimal_places: int = 1 |
|
) -> tuple[str, str, str]: |
|
""" |
|
小数(有限小数となるように生成)を用いた四則演算の計算問題を作成する関数。 |
|
戻り値は (テキスト形式の式, TeX 形式の式) のタプル。 |
|
""" |
|
while True: |
|
num_operands = random.randint(2, max_terms) |
|
tree = generate_expr_tree( |
|
num_operands, |
|
lambda: generate_decimal_leaf(min_val, max_val, decimal_places), |
|
) |
|
try: |
|
result = evaluate(tree) |
|
except ZeroDivisionError: |
|
continue |
|
if not is_terminating(result): |
|
continue |
|
text_expr = tree_to_text(tree, decimal_places=decimal_places) |
|
tex_expr = tree_to_tex(tree, decimal_places=decimal_places) |
|
return text_expr, tex_expr, eval(str(result)) |
|
|
|
|
|
|
|
def format_coefficient(k: int) -> str: |
|
""" |
|
係数 k による x 項の表示を整形する。 |
|
例えば、1 → "x"、それ以外は "kx" とする。 |
|
""" |
|
if k == 1: |
|
return "x" |
|
else: |
|
return f"{k}x" |
|
|
|
|
|
def create_linear_equation_problem( |
|
min_val: int = -10, max_val: int = 10, max_terms: int = 3 |
|
) -> tuple[str, str, str]: |
|
""" |
|
一変数 (x) を含む一次方程式の問題を作成する関数です。 |
|
まず、定数部分 T を(整数の四則演算の問題として)作成し、 |
|
ランダムな非零係数 k と解 x₀ を決定して、 |
|
|
|
T + k*x = T の値 + k*x₀ |
|
|
|
という形の方程式とします。 |
|
|
|
ここで、左辺の表示において x の項の位置を |
|
"begin"(先頭) / "middle"(定数部分を 2 分割して中央) / "end"(末尾) |
|
のいずれかにランダムで配置するように変更しています。 |
|
|
|
戻り値は (テキスト形式の方程式, TeX 形式の方程式, 解 x₀) のタプルです。 |
|
""" |
|
|
|
def create_integer_arithmetic_problem_with_result( |
|
min_val: int, max_val: int, max_terms: int |
|
) -> tuple[str, str, Fraction]: |
|
while True: |
|
num_operands = random.randint(2, max_terms) |
|
tree = generate_expr_tree( |
|
num_operands, lambda: Fraction(random.randint(min_val, max_val)) |
|
) |
|
try: |
|
result = evaluate(tree) |
|
except ZeroDivisionError: |
|
continue |
|
if not is_terminating(result): |
|
continue |
|
text_expr = tree_to_text(tree) |
|
tex_expr = tree_to_tex(tree) |
|
return text_expr, tex_expr, result |
|
|
|
|
|
text_const, tex_const, T_value = create_integer_arithmetic_problem_with_result( |
|
min_val, max_val, max_terms |
|
) |
|
|
|
possible = [i for i in range(min_val, max_val + 1) if i != 0] |
|
k = random.choice(possible) if possible else 1 |
|
x0 = random.randint(min_val, max_val) |
|
|
|
R_value = T_value + k * x0 |
|
|
|
|
|
mode = random.choice(["begin", "middle", "end"]) |
|
|
|
if mode == "begin": |
|
|
|
if k >= 0: |
|
lhs_text = f"{format_coefficient(k)} + {text_const}" |
|
lhs_tex = f"{format_coefficient(k)} + {tex_const}" |
|
else: |
|
lhs_text = f"- {format_coefficient(abs(k))} + {text_const}" |
|
lhs_tex = f"- {format_coefficient(abs(k))} + {tex_const}" |
|
elif mode == "end": |
|
|
|
if k >= 0: |
|
lhs_text = f"{text_const} + {format_coefficient(k)}" |
|
lhs_tex = f"{tex_const} + {format_coefficient(k)}" |
|
else: |
|
lhs_text = f"{text_const} - {format_coefficient(abs(k))}" |
|
lhs_tex = f"{tex_const} - {format_coefficient(abs(k))}" |
|
else: |
|
|
|
def format_fraction(frac: Fraction) -> str: |
|
if frac.denominator == 1: |
|
return str(frac.numerator) |
|
else: |
|
return str(float(frac)) |
|
|
|
A = Fraction(random.randint(min_val, max_val)) |
|
B = T_value - A |
|
A_text = format_fraction(A) |
|
B_text = format_fraction(B) |
|
A_tex = A_text |
|
B_tex = B_text |
|
if k >= 0: |
|
lhs_text = f"{A_text} + {format_coefficient(k)} + {B_text}" |
|
lhs_tex = f"{A_tex} + {format_coefficient(k)} + {B_tex}" |
|
else: |
|
lhs_text = f"{A_text} - {format_coefficient(abs(k))} + {B_text}" |
|
lhs_tex = f"{A_tex} - {format_coefficient(abs(k))} + {B_tex}" |
|
|
|
|
|
if R_value.denominator == 1: |
|
rhs_text = str(R_value.numerator) |
|
rhs_tex = str(R_value.numerator) |
|
else: |
|
rhs_text = str(float(R_value)) |
|
rhs_tex = str(float(R_value)) |
|
|
|
eq_text = lhs_text + " = " + rhs_text |
|
eq_tex = lhs_tex + " = " + rhs_tex |
|
return eq_text, eq_tex, eval(str(x0)) |
|
|
|
|
|
def create_two_decimals( |
|
min_val: float, max_val: float, precision: float |
|
) -> tuple[float, float, float]: |
|
""" |
|
指定された範囲 [min_val, max_val] 内で、 |
|
整数部分が共通かつ、小数点以下の桁数(precision)を指定して、 |
|
その精度以下の値をランダムに選んだ2つの数値を生成する関数です。 |
|
|
|
Parameters: |
|
min_val (float): 生成する数値の最小値 |
|
max_val (float): 生成する数値の最大値 |
|
precision (int): 小数点以下の桁数(0以上の整数) |
|
|
|
Returns: |
|
tuple: 生成された2つの数値 (num1, num2) |
|
""" |
|
if min_val >= max_val: |
|
raise ValueError("min_valはmax_valより小さくなければなりません。") |
|
if precision < 0: |
|
raise ValueError("precisionは0以上の整数である必要があります。") |
|
|
|
|
|
scale = int(10**precision) |
|
|
|
valid_candidates = [] |
|
|
|
|
|
for k in range(math.floor(min_val), math.floor(max_val) + 1): |
|
|
|
|
|
r_lower = max(0, min_val - k) |
|
r_upper = min(1, max_val - k) |
|
|
|
|
|
|
|
allowed_i = [ |
|
i for i in range(scale) if (i / scale) >= r_lower and (i / scale) < r_upper |
|
] |
|
|
|
if allowed_i: |
|
valid_candidates.append((k, allowed_i)) |
|
|
|
if not valid_candidates: |
|
raise ValueError("指定された範囲内で共通の整数部分を持つ数値が生成できません。") |
|
|
|
|
|
k, allowed_i = random.choice(valid_candidates) |
|
|
|
if len(allowed_i) < 2: |
|
|
|
raise ValueError("指定された範囲と精度では、異なる2つの数値を生成できません。") |
|
|
|
|
|
i1, i2 = random.sample(allowed_i, 2) |
|
|
|
num1 = k + i1 / scale |
|
num2 = k + i2 / scale |
|
|
|
greater = max(num1, num2) |
|
|
|
return num1, num2, greater |
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
print("【整数の四則演算の問題】") |
|
text_expr, tex_expr, result = create_integer_arithmetic_problem( |
|
-100, |
|
100, |
|
max_terms=8, |
|
) |
|
print("テキスト形式 :", text_expr) |
|
print("TeX 形式 :", tex_expr) |
|
print("答え :", result) |
|
|
|
print("\n【小数を含む四則演算の問題】") |
|
text_expr, tex_expr, result = create_decimal_arithmetic_problem( |
|
-100, |
|
100, |
|
max_terms=8, |
|
decimal_places=1, |
|
) |
|
print("テキスト形式 :", text_expr) |
|
print("TeX 形式 :", tex_expr) |
|
print("答え :", result) |
|
|
|
print("\n【一次方程式の問題】") |
|
eq_text, eq_tex, x0 = create_linear_equation_problem( |
|
-100, |
|
100, |
|
max_terms=8, |
|
) |
|
print("テキスト形式 :", eq_text) |
|
print("TeX 形式 :", eq_tex) |
|
print("解 x0 :", x0) |
|
|
|
print("\n【2つの小数の比較問題】") |
|
num1, num2, greater = create_two_decimals(-100.0, 100.0, 3) |
|
print("小数1 :", num1) |
|
print("小数2 :", num2) |
|
print("大きい方 :", greater) |
|
|