|
|
|
|
@ -9,21 +9,45 @@ import java.util.List;
|
|
|
|
|
import java.util.Random;
|
|
|
|
|
import java.util.Set;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 通用选择题生成器,将基础题目生成器的结果转换为带选项的选择题。
|
|
|
|
|
* 确保生成的题目列表中不包含重复的题干。
|
|
|
|
|
* 特别处理:1. 初中题避免负数开根号;2. 小学题选项避免负数。
|
|
|
|
|
*
|
|
|
|
|
* @author 你的名字
|
|
|
|
|
* <p>该类确保生成的题目列表中不包含重复的题干。
|
|
|
|
|
* 特殊处理规则:
|
|
|
|
|
* <ul>
|
|
|
|
|
* <li>初中题:在计算过程中避免负数开根号(由 {@link AdvancedCaculate} 抛出异常,此处理捕获并跳过)。</li>
|
|
|
|
|
* <li>小学题:生成的选项避免出现负数。</li>
|
|
|
|
|
* </ul>
|
|
|
|
|
*
|
|
|
|
|
* @author 杨博文
|
|
|
|
|
* @since 2025
|
|
|
|
|
*/
|
|
|
|
|
public class MultipleChoiceGenerator {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 用于格式化数字的格式化器,保留两位小数。
|
|
|
|
|
*/
|
|
|
|
|
private static final DecimalFormat df = new DecimalFormat("#0.00");
|
|
|
|
|
/**
|
|
|
|
|
* 基础题目生成器。
|
|
|
|
|
*/
|
|
|
|
|
private final QuestionGenerator baseGenerator;
|
|
|
|
|
/**
|
|
|
|
|
* 随机数生成器。
|
|
|
|
|
*/
|
|
|
|
|
private final Random random = new Random();
|
|
|
|
|
private static final DecimalFormat df = new DecimalFormat("#0.00");
|
|
|
|
|
private final String level; // 记录当前题目类型,用于特殊处理
|
|
|
|
|
/**
|
|
|
|
|
* 当前题目类型,用于特殊处理逻辑。
|
|
|
|
|
*/
|
|
|
|
|
private final String level;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构造一个新的选择题生成器。
|
|
|
|
|
*
|
|
|
|
|
* @param baseGenerator 基础题目生成器
|
|
|
|
|
* @param level 题目所属的级别 ("小学", "初中", "高中")
|
|
|
|
|
*/
|
|
|
|
|
public MultipleChoiceGenerator(QuestionGenerator baseGenerator, String level) {
|
|
|
|
|
this.baseGenerator = baseGenerator;
|
|
|
|
|
this.level = level;
|
|
|
|
|
@ -32,8 +56,8 @@ public class MultipleChoiceGenerator {
|
|
|
|
|
/**
|
|
|
|
|
* 生成指定数量的选择题,确保题干不重复。
|
|
|
|
|
*
|
|
|
|
|
* @param count 题目数量
|
|
|
|
|
* @return 选择题列表 (去重后)
|
|
|
|
|
* @param count 要生成的题目数量
|
|
|
|
|
* @return 生成的选择题列表(已去重)
|
|
|
|
|
*/
|
|
|
|
|
public List<QuestionWithOptions> generateMultipleChoiceQuestions(int count) {
|
|
|
|
|
List<QuestionWithOptions> mcQuestions = new ArrayList<>();
|
|
|
|
|
@ -42,9 +66,8 @@ public class MultipleChoiceGenerator {
|
|
|
|
|
while (mcQuestions.size() < count) {
|
|
|
|
|
String baseQuestion = generateUniqueBaseQuestion(seenQuestionTexts);
|
|
|
|
|
if (baseQuestion == null) {
|
|
|
|
|
// 如果无法生成不重复的基础题目,可能需要退出或处理
|
|
|
|
|
// 例如,如果基础生成器的可能组合用尽了
|
|
|
|
|
break; // 或者抛出异常
|
|
|
|
|
// 如果无法生成不重复的基础题目,可能基础生成器的可能组合已用尽
|
|
|
|
|
break; // 退出循环
|
|
|
|
|
}
|
|
|
|
|
QuestionWithOptions mcq = generateSingleMCQ(baseQuestion);
|
|
|
|
|
if (mcq != null) {
|
|
|
|
|
@ -58,6 +81,9 @@ public class MultipleChoiceGenerator {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 生成一个唯一的、未处理过的基础题目。
|
|
|
|
|
*
|
|
|
|
|
* @param seenQuestionTexts 已生成题干的集合
|
|
|
|
|
* @return 一个唯一的题干字符串,如果在限定尝试次数内无法找到则返回 null
|
|
|
|
|
*/
|
|
|
|
|
private String generateUniqueBaseQuestion(Set<String> seenQuestionTexts) {
|
|
|
|
|
int attempts = 0;
|
|
|
|
|
@ -75,25 +101,34 @@ public class MultipleChoiceGenerator {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从单个基础题目生成选择题对象。
|
|
|
|
|
*
|
|
|
|
|
* @param baseQuestion 基础题干字符串,例如 "3 + 5 = ?"
|
|
|
|
|
* @return 生成的选择题对象,如果计算或生成选项失败则返回 null
|
|
|
|
|
*/
|
|
|
|
|
private QuestionWithOptions generateSingleMCQ(String baseQuestion) {
|
|
|
|
|
try {
|
|
|
|
|
// 从基础题干中提取表达式部分,例如 "3 + 5 = ?" -> "3 + 5"
|
|
|
|
|
String expression = baseQuestion.substring(0, baseQuestion.lastIndexOf(" =")).trim();
|
|
|
|
|
List<String> tokens = tokenizeExpression(expression);
|
|
|
|
|
// 计算正确答案
|
|
|
|
|
double correctAnswer = AdvancedCaculate.calculate(tokens);
|
|
|
|
|
|
|
|
|
|
// 生成选项列表
|
|
|
|
|
List<String> options = generateOptions(correctAnswer);
|
|
|
|
|
if (options == null) {
|
|
|
|
|
// 无法生成足够的选项
|
|
|
|
|
// 无法生成足够的有效选项
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 随机打乱选项顺序
|
|
|
|
|
Collections.shuffle(options);
|
|
|
|
|
// 找到正确答案在打乱后列表中的索引
|
|
|
|
|
int correctIndex = options.indexOf(df.format(correctAnswer));
|
|
|
|
|
|
|
|
|
|
return new QuestionWithOptions(baseQuestion, options, correctIndex);
|
|
|
|
|
|
|
|
|
|
} catch (ArithmeticException | IllegalArgumentException e) {
|
|
|
|
|
// 计算或表达式格式错误,跳过此题
|
|
|
|
|
// System.out.println("计算或表达式错误,跳过题目: " + baseQuestion + ", Error: " + e.getMessage());
|
|
|
|
|
return null; // 返回 null 表示生成失败
|
|
|
|
|
}
|
|
|
|
|
@ -101,6 +136,11 @@ public class MultipleChoiceGenerator {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 生成选项列表 (正确答案 + 错误答案)。
|
|
|
|
|
*
|
|
|
|
|
* <p>对于小学级别,错误答案不会包含负数。
|
|
|
|
|
*
|
|
|
|
|
* @param correctAnswer 正确答案
|
|
|
|
|
* @return 包含正确答案和错误答案的字符串列表,如果无法生成足够选项则返回 null
|
|
|
|
|
*/
|
|
|
|
|
private List<String> generateOptions(double correctAnswer) {
|
|
|
|
|
Set<Double> wrongAnswers = new HashSet<>();
|
|
|
|
|
@ -109,12 +149,13 @@ public class MultipleChoiceGenerator {
|
|
|
|
|
int numWrongOptions = 3; // 假设总共4个选项,需要3个错误答案
|
|
|
|
|
|
|
|
|
|
while (wrongAnswers.size() < numWrongOptions && attempts < maxAttempts) {
|
|
|
|
|
int offset = random.nextInt(20) + 1;
|
|
|
|
|
int offset = random.nextInt(20) + 1; // 生成 1-20 的偏移量
|
|
|
|
|
if (random.nextBoolean()) {
|
|
|
|
|
offset = -offset;
|
|
|
|
|
offset = -offset; // 随机正负
|
|
|
|
|
}
|
|
|
|
|
double wrongAnswer = correctAnswer + offset;
|
|
|
|
|
|
|
|
|
|
// 确保错误答案与正确答案不同,并且对于小学题不为负数
|
|
|
|
|
if (Math.abs(df.format(wrongAnswer).compareTo(df.format(correctAnswer))) != 0) {
|
|
|
|
|
if (!level.equals("小学") || wrongAnswer >= 0) {
|
|
|
|
|
wrongAnswers.add(wrongAnswer);
|
|
|
|
|
@ -128,10 +169,12 @@ public class MultipleChoiceGenerator {
|
|
|
|
|
return null; // 无法生成足够选项
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将正确答案和错误答案合并
|
|
|
|
|
List<Double> allAnswers = new ArrayList<>();
|
|
|
|
|
allAnswers.add(correctAnswer);
|
|
|
|
|
allAnswers.addAll(wrongAnswers);
|
|
|
|
|
|
|
|
|
|
// 格式化所有答案为字符串
|
|
|
|
|
List<String> options = new ArrayList<>();
|
|
|
|
|
for (Double ans : allAnswers) {
|
|
|
|
|
options.add(df.format(ans));
|
|
|
|
|
@ -143,6 +186,13 @@ public class MultipleChoiceGenerator {
|
|
|
|
|
|
|
|
|
|
// --- 表达式分词逻辑 ---
|
|
|
|
|
// 将 "3 + 开根号 ( 4 ) 平方 - sin 30" 分割成 ["3", "+", "开根号", "(", "4", ")", "平方", "-", "sin", "30"]
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 将表达式字符串分割成标记列表。
|
|
|
|
|
*
|
|
|
|
|
* @param expression 表达式字符串
|
|
|
|
|
* @return 标记列表
|
|
|
|
|
*/
|
|
|
|
|
private List<String> tokenizeExpression(String expression) {
|
|
|
|
|
List<String> tokens = new ArrayList<>();
|
|
|
|
|
int i = 0;
|
|
|
|
|
@ -165,8 +215,9 @@ public class MultipleChoiceGenerator {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查找从指定位置开始的下一个 token。
|
|
|
|
|
*
|
|
|
|
|
* @param expression 表达式字符串
|
|
|
|
|
* @param startPos 起始查找位置
|
|
|
|
|
* @param startPos 起始查找位置
|
|
|
|
|
* @return 找到的 token,如果未找到则返回 null
|
|
|
|
|
*/
|
|
|
|
|
private String _findNextToken(String expression, int startPos) {
|
|
|
|
|
@ -182,7 +233,8 @@ public class MultipleChoiceGenerator {
|
|
|
|
|
if (Character.isDigit(c) || c == '.') {
|
|
|
|
|
// 查找连续的数字或小数点
|
|
|
|
|
int j = startPos;
|
|
|
|
|
while (j < expression.length() && (Character.isDigit(expression.charAt(j)) || expression.charAt(j) == '.')) {
|
|
|
|
|
while (j < expression.length() && (Character.isDigit(expression.charAt(j))
|
|
|
|
|
|| expression.charAt(j) == '.')) {
|
|
|
|
|
j++;
|
|
|
|
|
}
|
|
|
|
|
return expression.substring(startPos, j);
|
|
|
|
|
|