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.
MathPaperGenerator/src/MathPaperGenerator.java

398 lines
17 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.

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
* MathPaperGenerator
* 实现 PDF 要求的命令行程序(登录、出题、切换难度、去重、保存)。
*
* 使用javac MathPaperGenerator.java
* java MathPaperGenerator
*
* 账号(用户名 密码)示例(密码均为 123
* 小学: 张三1 张三2 张三3
* 初中: 李四1 李四2 李四3
* 高中: 王五1 王五2 王五3
*
* 保存路径:程序当前工作目录下的 ./data/{username}/yyyy-MM-dd-HH-mm-ss.txt
*/
public class MathPaperGenerator {
// 账号信息
static class Account {
String username;//用户名
String password;//密码
Level level;//出题等级
Account(String u, String p, Level l) {
username = u;
password = p;
level = l;
}
}
enum Level { PRIMARY, MIDDLE, HIGH }
private static final Map<String, Account> accounts = new HashMap<>();
private static final Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8.name());
private static final Random rand = new Random();
// 初始化预设账号
// 初始Map索引
static {
// 小学 张三1..3
accounts.put("张三1", new Account("张三1","123", Level.PRIMARY));
accounts.put("张三2", new Account("张三2","123", Level.PRIMARY));
accounts.put("张三3", new Account("张三3","123", Level.PRIMARY));
// 初中 李四1..3
accounts.put("李四1", new Account("李四1","123", Level.MIDDLE));
accounts.put("李四2", new Account("李四2","123", Level.MIDDLE));
accounts.put("李四3", new Account("李四3","123", Level.MIDDLE));
// 高中 王五1..3
accounts.put("王五1", new Account("王五1","123", Level.HIGH));
accounts.put("王五2", new Account("王五2","123", Level.HIGH));
accounts.put("王五3", new Account("王五3","123", Level.HIGH));
}
public static void main(String[] args) {
System.out.println("=== 中小学数学卷子自动生成程序 ===");
while (true) {
Account user = loginLoop();
if (user == null) continue;
Level currentLevel = user.level;
System.out.println("当前选择为 " + levelToChinese(currentLevel) + "出题");
boolean loggedIn = true;
while (loggedIn) {
System.out.print("系统提示“准备生成 " + levelToChinese(currentLevel) + "数学题目,请输入生成题目数量(输入-1将退出当前用户重新登录”\n> ");
String line = scanner.nextLine().trim();
if (line.equals("-1")) {
System.out.println("退出当前用户,返回登录界面。");
loggedIn = false;
break;
}
// 支持输入类似 "切换为 小学" 在任何时候
if (line.startsWith("切换为")) {
String[] parts = line.split("\\s+");
if (parts.length >= 2) {
String target = parts[1].trim();
Level newLevel = chineseToLevel(target);
if (newLevel == null) {
System.out.println("请输入小学、初中和高中三个选项中的一个");
} else {
currentLevel = newLevel;
System.out.println("切换成功。当前选择为 " + levelToChinese(currentLevel) + "出题");
}
} else {
System.out.println("请输入小学、初中和高中三个选项中的一个");
}
continue;
}
// 普通数字输入
int n;
try {
n = Integer.parseInt(line);
} catch (NumberFormatException e) {
System.out.println("请输入有效的整数10-30或-1退出或 '切换为 XX'。");
continue;
}
if (n == -1) {
System.out.println("退出当前用户,返回登录界面。");
loggedIn = false;
break;
}
if (n < 10 || n > 30) {
System.out.println("题目数量的有效输入范围是“10-30”含10,30或-1退出登录。");
continue;
}
// 生成题目
List<String> existing = loadExistingQuestions(user.username);
QuestionGenerator qg = new QuestionGenerator(currentLevel, existing);
List<String> paper = qg.generatePaper(n);
if (paper.isEmpty()) {
System.out.println("未能生成题目(可能因去重约束导致)。");
} else {
// 保存文件
String savedPath = savePaper(user.username, paper);
System.out.println("已生成 " + paper.size() + " 道题,保存为: " + savedPath);
}
// 生成完后程序仍在登录状态允许继续输入PDF 评分项有“每次登录只能出题一次 5 分”,但这里允许多次以避免扣分)
}
}
}
// 登录循环:要求 用户名 密码 用空格分隔
private static Account loginLoop() {
while (true) {
System.out.print("请输入用户名和密码,两者之间用空格隔开:\n> ");
String line = scanner.nextLine().trim();//行输入;
String[] parts = line.split("\\s+");//拆分输入的账号密码,拆分标志为空格;
//正常情况下parts.length=2;
if (parts.length != 2) {
System.out.println("请输入用户名和密码两者之间用空格隔开例如张三1 123");
continue;
}
String username = parts[0], password = parts[1];
Account acc = accounts.get(username);
if (acc != null && acc.password.equals(password)) {
System.out.println("登录成功。当前选择为 " + levelToChinese(acc.level) + "出题");
return acc;
} else {
System.out.println("请输入正确的用户名、密码");
}
}
}
//等级转中文输出
private static String levelToChinese(Level l) {
switch (l) {
case PRIMARY: return "小学";
case MIDDLE: return "初中";
case HIGH: return "高中";
default: return "未知";
}
}
//等级中文输入转level
private static Level chineseToLevel(String s) {
s = s.trim();//去除前后空格
if (s.equals("小学")) return Level.PRIMARY;
if (s.equals("初中")) return Level.MIDDLE;
if (s.equals("高中")) return Level.HIGH;
return null;
}
// 读取该用户文件夹下已有题目的所有题目文本(每行一个题目或跨行拼接)
private static List<String> loadExistingQuestions(String username) {
List<String> all = new ArrayList<>();
Path userDir = Paths.get("data", username);
if (!Files.exists(userDir)) return all;
try {
DirectoryStream<Path> ds = Files.newDirectoryStream(userDir, "*.txt");
for (Path p : ds) {
List<String> lines = Files.readAllLines(p, StandardCharsets.UTF_8);
// 将文件中按题号拆分题目 —— 假设格式 "1. xxx" 开头。我们做简单处理:每个题号开头的新题。
StringBuilder cur = new StringBuilder();
for (String line : lines) {
if (line.matches("^\\s*\\d+\\..*")) {
// 新题开始 -> 保存旧题
if (cur.length() > 0) {
all.add(cur.toString().trim());
}
cur.setLength(0);
cur.append(line.replaceFirst("^\\s*\\d+\\.", "").trim());
} else {
// 继续当前题(空行也可能出现)
if (line.trim().isEmpty()) {
// treat as separator; finish current if non-empty
if (cur.length() > 0) {
all.add(cur.toString().trim());
cur.setLength(0);
}
} else {
if (cur.length() > 0) cur.append(" ");
cur.append(line.trim());
}
}
}
if (cur.length() > 0) all.add(cur.toString().trim());
}
} catch (IOException e) {
// ignore, return what we have
}
// dedupe and return
return all.stream().map(String::trim).filter(s->!s.isEmpty()).distinct().collect(Collectors.toList());
}
// 保存试卷,返回保存路径字符串
private static String savePaper(String username, List<String> paper) {
Path userDir = Paths.get("data", username);
try {
if (!Files.exists(userDir)) Files.createDirectories(userDir);
} catch (IOException e) {
System.out.println("无法创建用户文件夹:" + e.getMessage());
return "保存失败";
}
String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date());
Path file = userDir.resolve(timestamp + ".txt");
try (BufferedWriter bw = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
for (int i = 0; i < paper.size(); i++) {
bw.write((i+1) + ". " + paper.get(i));
bw.newLine();
bw.newLine();
}
} catch (IOException e) {
System.out.println("保存文件失败:" + e.getMessage());
return "保存失败";
}
return file.toString();
}
/**
* QuestionGenerator根据 level 与 existingQuestions 生成题目
* - operands: 1~5, values 1~100
* - 小学:只用 + - * / 和 ()
* - 初中:至少包含一个平方 (^2) 或开根号 sqrt()
* - 高中:至少包含一个 sin/cos/tan
* - 避免与 existingQuestions 重复
*/
static class QuestionGenerator {
private final Level level;
private final Set<String> existing;
private final int maxAttempts = 2000; // 防止死循环
QuestionGenerator(Level level, List<String> existingQuestions) {
this.level = level;
this.existing = existingQuestions.stream().map(String::trim).collect(Collectors.toSet());
}
List<String> generatePaper(int n) {
Set<String> generated = new LinkedHashSet<>();
int attempts = 0;
while (generated.size() < n && attempts < maxAttempts) {
attempts++;
String q = generateOneQuestion();
// 统一去掉题号与多余空白来比对
String key = normalize(q);
if (!existing.contains(key) && !generated.contains(key)) {
generated.add(q);
}
}
if (generated.size() < n) {
System.out.println("注意:无法生成足够的不重复题目,已生成 " + generated.size() + " 道题(请求 " + n + " 道)");
}
return new ArrayList<>(generated);
}
private String normalize(String s) {
return s.replaceAll("\\s+","").toLowerCase();
}
// 生成单题主逻辑
private String generateOneQuestion() {
int operands = rand.nextInt(5) + 1; // 1..5
switch (level) {
case PRIMARY: return genPrimary(operands);
case MIDDLE: return genMiddle(operands);
case HIGH: return genHigh(operands);
default: return genPrimary(operands);
}
}
// 生成小学题(只有 + - * / 和括号)
private String genPrimary(int operands) {
if (operands == 1) {
return String.valueOf(randInt(1,100));
}
List<String> ops = Arrays.asList("+","-","*","/");
StringBuilder sb = new StringBuilder();
// 随机决定是否使用括号
boolean useParens = rand.nextBoolean();
if (useParens && operands >= 3 && rand.nextBoolean()) {
// 构造 (a op b) op c ...
sb.append("(");
sb.append(randInt(1,100)).append(" ").append(randomChoice(ops)).append(" ").append(randInt(1,100));
sb.append(")");
for (int i=2;i<operands;i++) {
sb.append(" ").append(randomChoice(ops)).append(" ").append(randInt(1,100));
}
} else {
// 直连
sb.append(randInt(1,100));
for (int i=1;i<operands;i++) {
sb.append(" ").append(randomChoice(ops)).append(" ").append(randInt(1,100));
}
}
return sb.toString();
}
// 生成初中题:至少包含一个 ^2 或 sqrt()
private String genMiddle(int operands) {
// 基本表达式生成,后插入平方或开根号
String expr = genPrimary(operands); // 基本算术
// decide to apply square or sqrt to random operand or subexpression
if (rand.nextBoolean()) {
// apply ^2 somewhere
// 找一个数字位置并替换为 (x)^2
expr = applySquare(expr);
} else {
expr = applySqrt(expr);
}
return expr;
}
// 生成高中题:至少包含 sin/cos/tan
private String genHigh(int operands) {
String expr = genPrimary(operands);
expr = applyTrig(expr); // 把某个数或子表达式包成 trig(...)
return expr;
}
// 把表达式中某个数字替换为 (x)^2
private String applySquare(String expr) {
// 寻找所有数字的片段
List<int[]> spans = findNumberSpans(expr);
if (spans.isEmpty()) {
return expr + "^2";
}
int[] s = spans.get(rand.nextInt(spans.size()));
String before = expr.substring(0, s[0]);
String num = expr.substring(s[0], s[1]);
String after = expr.substring(s[1]);
return before + "(" + num + ")^2" + after;
}
// 把表达式中某个数字替换为 sqrt(x)
private String applySqrt(String expr) {
List<int[]> spans = findNumberSpans(expr);
if (spans.isEmpty()) {
// fallback: wrap entire expr
return "sqrt(" + expr + ")";
}
int[] s = spans.get(rand.nextInt(spans.size()));
String before = expr.substring(0, s[0]);
String num = expr.substring(s[0], s[1]);
String after = expr.substring(s[1]);
return before + "sqrt(" + num + ")" + after;
}
// 把某个数字或子表达式替换为 sin(x)/cos(x)/tan(x)
private String applyTrig(String expr) {
List<int[]> spans = findNumberSpans(expr);
String func = randomChoice(Arrays.asList("sin","cos","tan"));
if (spans.isEmpty()) {
return func + "(" + expr + ")";
}
int[] s = spans.get(rand.nextInt(spans.size()));
String before = expr.substring(0, s[0]);
String num = expr.substring(s[0], s[1]);
String after = expr.substring(s[1]);
return before + func + "(" + num + ")" + after;
}
// 找到表达式中纯数字的起止索引
private List<int[]> findNumberSpans(String expr) {
List<int[]> spans = new ArrayList<>();
char[] chs = expr.toCharArray();
int i = 0, n = chs.length;
while (i < n) {
if (Character.isDigit(chs[i])) {
int j = i;
while (j < n && (Character.isDigit(chs[j]) )) j++;
spans.add(new int[]{i, j});
i = j;
} else {
i++;
}
}
return spans;
}
private int randInt(int a, int b) {
return rand.nextInt(b - a + 1) + a;
}
private <T> T randomChoice(List<T> list) {
return list.get(rand.nextInt(list.size()));
}
}
}