You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
math-paper-generator/src/main/java/com/coproject/service/UnifiedQuestionEngine.java

548 lines
24 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package com.coproject.service;
import java.text.DecimalFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 统一题目引擎(单文件可移植版)
* - 封装小学/初中/高中三段题目生成逻辑
* - 保留与原项目一致的表达式与答案生成策略
* - 额外提供选择题与试卷生成功能(不依赖数据库)
*/
public class UnifiedQuestionEngine {
private final Random random = new Random();
private final DecimalFormat df = new DecimalFormat("#.##");
/** 根据难度生成一道题(与原项目接口保持一致) */
public MathProblem generateProblem(String difficulty) {
switch (difficulty) {
case "小学":
return new Elementary(random, df).generate();
case "初中":
return new Middle(random, df).generate();
case "高中":
return new High(random, df).generate();
default:
throw new IllegalArgumentException("不支持的难度级别: " + difficulty);
}
}
/** 根据难度生成一道选择题(四个选项,含正确答案) */
public ChoiceQuestion generateChoiceQuestion(String difficulty) {
MathProblem p = generateProblem(difficulty);
// 高中:题干保持三角函数形式,纯三角函数题生成符号化(根号)选项
if ("高中".equals(p.getDifficulty())) {
if (isSimpleTrigExpression(p.getExpression())) {
return toTrigChoice(p);
} else if (containsTrig(p.getExpression())) {
// 混合常数 + 三角项,构造符号化选项
return toMixedTrigSumChoice(p);
}
}
return toChoice(p);
}
/** 生成试卷:确保题干唯一(同一张卷子不重复),返回选择题列表 */
public List<ChoiceQuestion> generatePaper(String difficulty, int count) {
if (count <= 0) throw new IllegalArgumentException("题目数量必须为正数");
Set<String> seen = new HashSet<>();
List<ChoiceQuestion> paper = new ArrayList<>(count);
int attempts = 0, maxAttempts = count * 10;
while (paper.size() < count && attempts < maxAttempts) {
attempts++;
ChoiceQuestion q = generateChoiceQuestion(difficulty);
// 初中:不允许出现只有一个根号项的题干,如 "√121 = ?"
if ("初中".equals(difficulty)) {
String stem = q.getStem();
// 修复Java 字符串中 ? 需要使用 \\? 转义,避免非法转义
if (stem != null && stem.matches("^√\\d+\\s*=\\s*\\?$")) {
continue; // 跳过,重新生成
}
}
if (seen.add(q.getStem())) {
paper.add(q);
}
}
if (paper.size() < count) {
throw new RuntimeException("在限定尝试次数内未能生成足够的不重复题目");
}
return paper;
}
// ---------- 选择题构造 ----------
private ChoiceQuestion toChoice(MathProblem p) {
String correct = p.getAnswer();
double correctVal = safeParseDouble(correct);
// 高中:纯三角函数题直接生成符号化选项,题干保持三角函数形式
if ("高中".equals(p.getDifficulty())) {
if (isSimpleTrigExpression(p.getExpression())) {
return toTrigChoice(p);
}
}
Set<String> optionsSet = new LinkedHashSet<>();
optionsSet.add(correct);
// 生成三个干扰项:围绕正确值的合理偏差与格式一致
int guard = 0;
if (isEffectivelyInteger(correctVal) && !"高中".equals(p.getDifficulty())) {
// 若答案为整数(且非高中三角函数情形),优先生成整数干扰项,避免不合理小数
int base = (int) Math.round(correctVal);
while (optionsSet.size() < 4 && guard++ < 100) {
int delta = pickIntegerDelta(base);
int candidate = base + delta;
String formatted = String.valueOf(candidate);
if (!formatted.equals(correct)) optionsSet.add(formatted);
}
} else {
while (optionsSet.size() < 4 && guard++ < 100) {
double delta = pickDelta(correctVal);
double candidate = correctVal + delta;
String formatted = df.format(candidate);
if (!formatted.equals(correct)) optionsSet.add(formatted);
}
}
// 若仍不足,则退化为随机整数干扰项
while (optionsSet.size() < 4) {
optionsSet.add(String.valueOf(random.nextInt(200) + 1));
}
List<String> options = new ArrayList<>(optionsSet);
Collections.shuffle(options, random);
int correctIndex = options.indexOf(correct);
return new ChoiceQuestion(p.getExpression() + " = ?", options, correctIndex);
}
private ChoiceQuestion toTrigChoice(MathProblem p) {
String correctSymbolic = p.getAnswer(); // 已经是符号化形式,如 "√3/2"
Set<String> optionsSet = new LinkedHashSet<>();
optionsSet.add(correctSymbolic);
// 从常见特殊角值构造候选池
String[] commonSymbolic = {"0", "1/2", "√2/2", "√3/2", "1", "1/√3", "√3"};
for (String sym : commonSymbolic) {
if (!sym.equals(correctSymbolic)) {
optionsSet.add(sym);
}
if (optionsSet.size() >= 4) break;
}
// 若仍不足,添加一些变形
if (optionsSet.size() < 4) {
String[] extras = {"2", "-1/2", "2√3", "√6/2"};
for (String extra : extras) {
if (!optionsSet.contains(extra)) {
optionsSet.add(extra);
}
if (optionsSet.size() >= 4) break;
}
}
List<String> options = new ArrayList<>(optionsSet);
Collections.shuffle(options, random);
int correctIndex = options.indexOf(correctSymbolic);
return new ChoiceQuestion(p.getExpression() + " = ?", options, correctIndex);
}
private boolean isSimpleTrigExpression(String expr) {
return expr.matches("(sin|cos|tan)\\d+");
}
private boolean containsTrig(String expr) {
return expr.matches(".*(sin|cos|tan)\\d+.*");
}
private boolean isEffectivelyInteger(double val) {
return Math.abs(val - Math.round(val)) < 1e-9;
}
private int pickIntegerDelta(int base) {
int[] deltas = {-10, -5, -3, -2, -1, 1, 2, 3, 5, 10};
return deltas[random.nextInt(deltas.length)];
}
private double pickDelta(double base) {
double[] factors = {-0.3, -0.2, -0.1, -0.05, 0.05, 0.1, 0.2, 0.3};
double factor = factors[random.nextInt(factors.length)];
return base * factor;
}
private double safeParseDouble(String s) {
try {
return Double.parseDouble(s);
} catch (NumberFormatException e) {
return 0.0; // 符号字符串无法解析为数值,返回默认值
}
}
// 供外层方法(如 toMixedTrigSumChoice使用的简单运算符优先级计算
private static double evalWithPrecedence(double[] operands, String[] operators) {
if (operands.length == 1) return operands[0];
List<Double> vals = new ArrayList<>();
for (double v : operands) vals.add(v);
List<String> ops = new ArrayList<>(Arrays.asList(operators));
for (int i = 0; i < ops.size(); i++) {
if ("*".equals(ops.get(i)) || "/".equals(ops.get(i))) {
double a = vals.get(i), b = vals.get(i + 1);
double r = "/".equals(ops.get(i)) ? (b != 0 ? a / b : a) : a * b;
vals.set(i, r); vals.remove(i + 1); ops.remove(i); i--;
}
}
for (int i = 0; i < ops.size(); i++) {
double a = vals.get(i), b = vals.get(i + 1);
String op = ops.get(i);
double r = "+".equals(op) ? a + b : a - b;
vals.set(i, r); vals.remove(i + 1); ops.remove(i); i--;
}
return vals.get(0);
}
// ---------- 三角函数符号化 ----------
private static String getSymbolicTrig(String function, int angle) {
String key = function + angle;
switch (key) {
case "sin0": case "cos90": case "tan0": return "0";
case "sin30": case "cos60": return "1/2";
case "sin45": case "cos45": return "√2/2";
case "sin60": case "cos30": return "√3/2";
case "sin90": case "cos0": return "1";
case "tan30": return "1/√3";
case "tan45": return "1";
case "tan60": return "√3";
default: return "0"; // 兜底
}
}
// ---------- 数据类 ----------
public static class MathProblem {
private final String expression, answer, difficulty;
public MathProblem(String expression, String answer, String difficulty) {
this.expression = expression; this.answer = answer; this.difficulty = difficulty;
}
public String getExpression() { return expression; }
public String getAnswer() { return answer; }
public String getDifficulty() { return difficulty; }
}
public static class ChoiceQuestion {
private final String stem;
private final List<String> options;
private final int correctIndex;
public ChoiceQuestion(String stem, List<String> options, int correctIndex) {
this.stem = stem; this.options = options; this.correctIndex = correctIndex;
}
public String getStem() { return stem; }
public List<String> getOptions() { return options; }
public int getCorrectIndex() { return correctIndex; }
}
// ---------- 题目生成器基类 ----------
abstract static class Base {
protected final Random random;
protected final DecimalFormat df;
public Base(Random random, DecimalFormat df) { this.random = random; this.df = df; }
public abstract MathProblem generate();
protected double applyOperatorDouble(double a, double b, String op) {
switch (op) {
case "+": return a + b;
case "-": return a - b;
case "*": return a * b;
case "/": return b != 0 ? a / b : a;
default: return a;
}
}
protected double evaluateWithPrecedenceDouble(double[] operands, String[] operators) {
if (operands.length == 1) return operands[0];
List<Double> vals = new ArrayList<>(); for (double v : operands) vals.add(v);
List<String> ops = new ArrayList<>(Arrays.asList(operators));
for (int i = 0; i < ops.size(); i++) {
if ("*".equals(ops.get(i)) || "/".equals(ops.get(i))) {
double result = applyOperatorDouble(vals.get(i), vals.get(i + 1), ops.get(i));
vals.set(i, result); vals.remove(i + 1); ops.remove(i); i--;
}
}
for (int i = 0; i < ops.size(); i++) {
double result = applyOperatorDouble(vals.get(i), vals.get(i + 1), ops.get(i));
vals.set(i, result); vals.remove(i + 1); ops.remove(i); i--;
}
return vals.get(0);
}
protected int findBracketStart(double[] operands, String[] operators, double probability) {
if (random.nextDouble() > probability || operands.length < 3) return -1;
return random.nextInt(operands.length - 1);
}
}
// ---------- 小学题目生成器 ----------
static class Elementary extends Base {
public Elementary(Random random, DecimalFormat df) { super(random, df); }
@Override public MathProblem generate() {
int operandCount = random.nextInt(3) + 3; // 3-5
int[] operands = new int[operandCount];
String[] operators = new String[operandCount - 1];
for (int i = 0; i < operandCount; i++) operands[i] = random.nextInt(100) + 1;
String[] ops = {"+", "-", "*"};
for (int i = 0; i < operandCount - 1; i++) operators[i] = ops[random.nextInt(ops.length)];
StringBuilder expr = new StringBuilder();
for (int i = 0; i < operandCount; i++) {
expr.append(operands[i]);
if (i < operandCount - 1) expr.append(" ").append(operators[i]).append(" ");
}
double[] vals = new double[operandCount]; for (int i = 0; i < operandCount; i++) vals[i] = operands[i];
double result = evaluateWithPrecedenceDouble(vals, operators);
return new MathProblem(expr.toString(), df.format(result), "小学");
}
}
// ---------- 初中题目生成器 ----------
static class Middle extends Base {
public Middle(Random random, DecimalFormat df) { super(random, df); }
@Override public MathProblem generate() {
// 始终生成 3-5 个操作数;当包含根号时,根号为其中一个或两个项
int operandCount = random.nextInt(3) + 3; // 3-5
boolean includeSqrt = random.nextDouble() < 0.5; // 约半数题含根号
int sqrtCount = includeSqrt ? (1 + random.nextInt(2)) : 0; // 1-2 个根号项
if (sqrtCount > operandCount) sqrtCount = operandCount; // 不超过操作数数量
double[] operands = new double[operandCount];
String[] operators = new String[operandCount - 1];
String[] termTexts = new String[operandCount];
// 随机选择根号项位置
Set<Integer> sqrtPos = new HashSet<>();
while (sqrtPos.size() < sqrtCount) {
sqrtPos.add(random.nextInt(operandCount));
}
// 构造各项
for (int i = 0; i < operandCount; i++) {
if (sqrtPos.contains(i)) {
// 使用完全平方数,确保计算结果为整数,便于初中题目
int base = random.nextInt(9) + 2; // 2-10确保 √n 的 n ≤ 100
int square = base * base;
operands[i] = Math.sqrt(square); // = base
termTexts[i] = "√" + square;
} else {
int val = random.nextInt(100) + 1; // 1-100 的常数
operands[i] = val;
termTexts[i] = String.valueOf(val);
}
}
// 构造运算符(保持简单四则)
String[] ops = {"+", "-", "*", "/"};
for (int i = 0; i < operandCount - 1; i++) {
operators[i] = ops[random.nextInt(ops.length)];
}
// 拼接表达式文本
StringBuilder expr = new StringBuilder();
for (int i = 0; i < operandCount; i++) {
expr.append(termTexts[i]);
if (i < operandCount - 1) expr.append(" ").append(operators[i]).append(" ");
}
double result = evaluateWithPrecedenceDouble(operands, operators);
return new MathProblem(expr.toString(), df.format(result), "初中");
}
}
// ---------- 高中题目生成器 ----------
static class High extends Base {
public High(Random random, DecimalFormat df) { super(random, df); }
@Override public MathProblem generate() {
// 生成包含三角函数与常数混合运算的表达式3-5个操作数至少包含一个三角函数
int operandCount = random.nextInt(3) + 3; // 3-5
int trigIndex = random.nextInt(operandCount);
double[] operands = new double[operandCount];
String[] operators = new String[operandCount - 1];
String[] termTexts = new String[operandCount];
// 构造操作数
for (int i = 0; i < operandCount; i++) {
if (i == trigIndex) {
String[] functions = {"sin", "cos", "tan"};
String func = functions[random.nextInt(functions.length)];
int[] angles = {0, 30, 45, 60, 90};
int angle = angles[random.nextInt(angles.length)];
if ("tan".equals(func) && angle == 90) angle = 60; // 避免未定义
operands[i] = calculateTrigFunction(func, angle);
termTexts[i] = func + angle;
} else {
int val = random.nextInt(100) + 1; // 1-100 的常数
operands[i] = val;
termTexts[i] = String.valueOf(val);
}
}
// 构造运算符(+ - * /
String[] ops = {"+", "-", "*", "/"};
for (int i = 0; i < operandCount - 1; i++) {
operators[i] = ops[random.nextInt(ops.length)];
}
// 为了让选项可符号化展示,约束三角项相邻运算为加/减
String[] addOps = {"+", "-"};
if (trigIndex > 0) {
operators[trigIndex - 1] = addOps[random.nextInt(addOps.length)];
}
if (trigIndex < operandCount - 1) {
operators[trigIndex] = addOps[random.nextInt(addOps.length)];
}
// 拼接表达式文本
StringBuilder expr = new StringBuilder();
for (int i = 0; i < operandCount; i++) {
expr.append(termTexts[i]);
if (i < operandCount - 1) expr.append(" ").append(operators[i]).append(" ");
}
// 计算结果(运算符优先级)
double result = evaluateWithPrecedenceDouble(operands, operators);
return new MathProblem(expr.toString(), df.format(result), "高中");
}
}
// 混合三角表达式(包含常数与一个三角项)选项符号化
private ChoiceQuestion toMixedTrigSumChoice(MathProblem p) {
String expr = p.getExpression();
String[] tokens = expr.split(" ");
// 解析操作数与运算符
List<String> ops = new ArrayList<>();
List<Double> vals = new ArrayList<>();
int trigIdx = -1; String trigFunc = null; int trigAngle = 0;
for (int i = 0; i < tokens.length; i++) {
if (i % 2 == 0) { // 操作数
String tk = tokens[i];
if (tk.matches("(sin|cos|tan)\\d+")) {
trigIdx = vals.size();
trigFunc = tk.substring(0, 3);
trigAngle = Integer.parseInt(tk.substring(3));
vals.add(calculateTrigFunction(trigFunc, trigAngle));
} else {
vals.add(Double.parseDouble(tk));
}
} else { // 运算符
ops.add(tokens[i]);
}
}
// 计算仅常数部分将三角项替换为0
double[] consts = new double[vals.size()];
for (int i = 0; i < vals.size(); i++) consts[i] = (i == trigIdx) ? 0.0 : vals.get(i);
String[] opArr = ops.toArray(new String[0]);
double K = evalWithPrecedence(consts, opArr);
// 确定与三角项相连的加减符号(左侧优先)
String signOp;
if (trigIdx > 0) signOp = ops.get(trigIdx - 1);
else if (!ops.isEmpty()) signOp = ops.get(0);
else signOp = "+";
String signChar = "+".equals(signOp) ? "+" : "-".equals(signOp) ? "-" : "+";
// 三角项符号化与数值
String trigSym = getSymbolicTrig(trigFunc, trigAngle);
double trigVal = calculateTrigFunction(trigFunc, trigAngle);
String kText = isEffectivelyInteger(K) ? String.valueOf((int)Math.round(K)) : df.format(K);
// 决策:若三角值为可有理(不含“√”),则合并为数值;若为无理数(含“√”),保持符号形式
boolean trigIsIrrational = trigSym.contains("√");
// 构造正确答案
String correct;
if (trigIsIrrational) {
correct = kText + " " + signChar + " " + trigSym;
} else {
double combined = "+".equals(signChar) ? (K + trigVal) : (K - trigVal);
correct = isEffectivelyInteger(combined) ? String.valueOf((int)Math.round(combined)) : df.format(combined);
}
// 干扰项:变化 K、变化三角值、变化符号
Set<String> set = new LinkedHashSet<>();
set.add(correct);
// 1) 变化K
int guard = 0;
while (set.size() < 3 && guard++ < 50) {
int delta = pickIntegerDelta((int)Math.round(K));
double kCand = isEffectivelyInteger(K) ? (int)Math.round(K) + delta : K + delta;
String cand;
if (trigIsIrrational) {
String kCandText = isEffectivelyInteger(kCand) ? String.valueOf((int)Math.round(kCand)) : df.format(kCand);
cand = kCandText + " " + signChar + " " + trigSym;
} else {
double combined = "+".equals(signChar) ? (kCand + trigVal) : (kCand - trigVal);
cand = isEffectivelyInteger(combined) ? String.valueOf((int)Math.round(combined)) : df.format(combined);
}
set.add(cand);
}
// 2) 变化三角值
String[] symPool = {"0", "1/2", "√2/2", "√3/2", "1", "1/√3", "√3"};
for (String s : symPool) {
if (set.size() >= 4) break;
if (s.equals(trigSym)) continue;
boolean sIrrational = s.contains("√");
String cand;
if (trigIsIrrational || sIrrational) {
// 若当前或候选三角值是无理数,保持符号形式
cand = kText + " " + signChar + " " + s;
} else {
// 有理数:合并为数值
double sVal = parseSymbolicToDouble(s);
double combined = "+".equals(signChar) ? (K + sVal) : (K - sVal);
cand = isEffectivelyInteger(combined) ? String.valueOf((int)Math.round(combined)) : df.format(combined);
}
set.add(cand);
}
// 3) 变化符号
if (set.size() < 4) {
String otherSign = "+".equals(signChar) ? "-" : "+";
String cand;
if (trigIsIrrational) {
cand = kText + " " + otherSign + " " + trigSym;
} else {
double combined = "+".equals(otherSign) ? (K + trigVal) : (K - trigVal);
cand = isEffectivelyInteger(combined) ? String.valueOf((int)Math.round(combined)) : df.format(combined);
}
set.add(cand);
}
List<String> options = new ArrayList<>(set);
Collections.shuffle(options, random);
int correctIndex = options.indexOf(correct);
return new ChoiceQuestion(p.getExpression() + " = ?", options, correctIndex);
}
// 解析常见符号三角值为数值(仅用于没有根号的情况)
private double parseSymbolicToDouble(String s) {
switch (s) {
case "0": return 0.0;
case "1/2": return 0.5;
case "1": return 1.0;
default:
// 兜底含根号的情况不会走到这里其它未知返回0
return 0.0;
}
}
// 供外层与静态内部类调用的三角函数计算方法(与 Base 内实现保持一致)
private static double calculateTrigFunction(String function, int angle) {
double radians = Math.toRadians(angle);
switch (function) {
case "sin": return Math.sin(radians);
case "cos": return Math.cos(radians);
case "tan": return Math.tan(radians);
default: return 0.0;
}
}
}