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: # 整数の場合は Fraction の分母が 1 なら整数として表示 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 # 定数部分 T の式(文字列)とその値 T_value を生成 text_const, tex_const, T_value = create_integer_arithmetic_problem_with_result( min_val, max_val, max_terms ) # 非零の係数 k と解 x₀ をランダムに選ぶ 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) # 右辺は T_value + k*x₀ R_value = T_value + k * x0 # x の項の配置位置をランダムに決定 mode = random.choice(["begin", "middle", "end"]) if mode == "begin": # x の項を先頭に配置 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": # x の項を末尾に配置(従来の形式) 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: # mode == "middle" # 定数 T_value を 2 つの定数 A と B に分割し、その間に x の項を配置 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 # 数値の場合、TeX 表記も同じ 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}" # 右辺の数値表示(整数なら整数、Fraction なら float 表示) 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以上の整数である必要があります。") # 指定した精度に応じたスケール(例: precision=3 なら scale=1000) scale = int(10**precision) valid_candidates = [] # (整数部分, 許容されるfraction部の整数値リスト) のタプルを格納 # 整数部分の候補は、min_valの整数部分からmax_valの整数部分まで for k in range(math.floor(min_val), math.floor(max_val) + 1): # kを共通の整数部分とする場合の、小数部分がとれる範囲は # r = 数値 - k が [r_lower, r_upper) に収まる必要がある r_lower = max(0, min_val - k) # 下限 r_upper = min(1, max_val - k) # 上限(1未満にする) # 小数部分は、指定精度で離散化された値 i/scale (i = 0, 1, ..., scale-1) として扱う # i/scale が [r_lower, r_upper) に入る i を抽出します allowed_i = [ i for i in range(scale) if (i / scale) >= r_lower and (i / scale) < r_upper ] if allowed_i: # 少なくとも1つの値がとれるなら候補に追加 valid_candidates.append((k, allowed_i)) if not valid_candidates: raise ValueError("指定された範囲内で共通の整数部分を持つ数値が生成できません。") # 有効な候補からランダムに1つ選びます k, allowed_i = random.choice(valid_candidates) if len(allowed_i) < 2: # もし候補が1種類しかなければ、異なる2つの数値を生成できません raise ValueError("指定された範囲と精度では、異なる2つの数値を生成できません。") # allowed_i から重複しない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)