File size: 16,470 Bytes
5bba7a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
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)