数字分类支持 1-999 数字学习,按规则运行时生成词条

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 23:10:08 +08:00
parent cb37b809f0
commit 75b967893b
6 changed files with 167 additions and 17 deletions
@@ -0,0 +1,115 @@
import Foundation
enum NumberWords {
static let range = 1...999
private static let atomNumbers: [String: Int] = [
"one": 1, "two": 2, "three": 3, "four": 4, "five": 5,
"six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10,
"eleven": 11, "twelve": 12, "thirteen": 13, "fourteen": 14, "fifteen": 15,
"sixteen": 16, "seventeen": 17, "eighteen": 18, "nineteen": 19, "twenty": 20,
"thirty": 30, "forty": 40, "fifty": 50, "sixty": 60,
"seventy": 70, "eighty": 80, "ninety": 90, "hundred": 100
]
private static let ones = [
"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
"eleven", "twelve", "thirteen", "fourteen", "fifteen",
"sixteen", "seventeen", "eighteen", "nineteen"
]
private static let onesPhonetics = [
"wʌn", "tuː", "θriː", "fɔːr", "faɪv", "sɪks", "ˈsevn", "eɪt", "naɪn", "ten",
"ɪˈlevn", "twelv", "ˌθɜːrˈtiːn", "ˌfɔːrˈtiːn", "ˌfɪfˈtiːn",
"ˌsɪksˈtiːn", "ˌsevnˈtiːn", "ˌeɪˈtiːn", "ˌnaɪnˈtiːn"
]
private static let tens: [Int: String] = [
20: "twenty", 30: "thirty", 40: "forty", 50: "fifty",
60: "sixty", 70: "seventy", 80: "eighty", 90: "ninety"
]
private static let tensPhonetics: [Int: String] = [
20: "ˈtwenti", 30: "ˈθɜːrti", 40: "ˈfɔːrti", 50: "ˈfɪfti",
60: "ˈsɪksti", 70: "ˈsevnti", 80: "ˈeɪti", 90: "ˈnaɪnti"
]
static func makeWords(atoms: [Word]) -> [Word] {
var byNumber: [Int: Word] = [:]
for atom in atoms {
if let number = atomNumbers[atom.word] {
byNumber[number] = atom
}
}
return range.map { byNumber[$0] ?? makeWord($0) }
}
static func english(_ n: Int) -> String {
switch n {
case 1...19:
return ones[n - 1]
case 20...99:
let ten = n / 10 * 10
let one = n % 10
return one == 0 ? tens[ten]! : "\(tens[ten]!)-\(ones[one - 1])"
case 100...999:
let head = "\(ones[n / 100 - 1]) hundred"
let rest = n % 100
return rest == 0 ? head : "\(head) \(english(rest))"
default:
return "\(n)"
}
}
static func phonetic(_ n: Int) -> String {
switch n {
case 1...19:
return onesPhonetics[n - 1]
case 20...99:
let ten = n / 10 * 10
let one = n % 10
return one == 0 ? tensPhonetics[ten]! : "\(tensPhonetics[ten]!) \(onesPhonetics[one - 1])"
case 100...999:
let head = "\(onesPhonetics[n / 100 - 1]) ˈhʌndrəd"
let rest = n % 100
return rest == 0 ? head : "\(head) \(phonetic(rest))"
default:
return ""
}
}
static func chinese(_ n: Int) -> String {
let digits = ["", "", "", "", "", "", "", "", "", ""]
switch n {
case 0...9:
return digits[n]
case 10...19:
return "" + (n % 10 == 0 ? "" : digits[n % 10])
case 20...99:
return digits[n / 10] + "" + (n % 10 == 0 ? "" : digits[n % 10])
case 100...999:
let head = digits[n / 100] + ""
let rest = n % 100
switch rest {
case 0:
return head
case 1...9:
return head + "" + digits[rest]
case 10...19:
return head + "一十" + (rest % 10 == 0 ? "" : digits[rest % 10])
default:
return head + digits[rest / 10] + "" + (rest % 10 == 0 ? "" : digits[rest % 10])
}
default:
return "\(n)"
}
}
private static func makeWord(_ n: Int) -> Word {
Word(word: english(n),
phonetic: "/\(phonetic(n))/",
meaning: chinese(n),
example: "I can count to \(english(n)).",
exampleMeaning: "我能数到\(chinese(n))")
}
}
+4 -4
View File
@@ -62,10 +62,10 @@ enum QuizGenerator {
private static func blankWord(_ word: String) -> String { private static func blankWord(_ word: String) -> String {
let chars = Array(word) let chars = Array(word)
let count = chars.count let letterPositions = chars.indices.filter { chars[$0].isLetter }
guard count > 2 else { return String(repeating: "_", count: count) } guard letterPositions.count > 2 else { return String(repeating: "_", count: chars.count) }
let blanks = max(1, Int(Double(count) * 0.4)) let blanks = max(1, Int(Double(letterPositions.count) * 0.4))
let indices = Set(Array(0..<count).shuffled().prefix(blanks)) let indices = Set(letterPositions.shuffled().prefix(blanks))
return chars.enumerated().map { indices.contains($0.offset) ? "_" : String($0.element) }.joined() return chars.enumerated().map { indices.contains($0.offset) ? "_" : String($0.element) }.joined()
} }
} }
@@ -166,7 +166,15 @@
{ "word": "seventeen", "phonetic": "/ˌsevnˈtiːn/", "meaning": "十七", "example": "He got seventeen points.", "exampleMeaning": "他得了十七分。" }, { "word": "seventeen", "phonetic": "/ˌsevnˈtiːn/", "meaning": "十七", "example": "He got seventeen points.", "exampleMeaning": "他得了十七分。" },
{ "word": "eighteen", "phonetic": "/ˌeɪˈtiːn/", "meaning": "十八", "example": "She is eighteen years old.", "exampleMeaning": "她十八岁。" }, { "word": "eighteen", "phonetic": "/ˌeɪˈtiːn/", "meaning": "十八", "example": "She is eighteen years old.", "exampleMeaning": "她十八岁。" },
{ "word": "nineteen", "phonetic": "/ˌnaɪnˈtiːn/", "meaning": "十九", "example": "Nineteen people came to the party.", "exampleMeaning": "十九个人来参加了聚会。" }, { "word": "nineteen", "phonetic": "/ˌnaɪnˈtiːn/", "meaning": "十九", "example": "Nineteen people came to the party.", "exampleMeaning": "十九个人来参加了聚会。" },
{ "word": "twenty", "phonetic": "/ˈtwenti/", "meaning": "二十", "example": "I have twenty yuan.", "exampleMeaning": "我有二十元。" } { "word": "twenty", "phonetic": "/ˈtwenti/", "meaning": "二十", "example": "I have twenty yuan.", "exampleMeaning": "我有二十元。" },
{ "word": "thirty", "phonetic": "/ˈθɜːrti/", "meaning": "三十", "example": "She has thirty stamps.", "exampleMeaning": "她有三十张邮票。" },
{ "word": "forty", "phonetic": "/ˈfɔːrti/", "meaning": "四十", "example": "There are forty students.", "exampleMeaning": "有四十个学生。" },
{ "word": "fifty", "phonetic": "/ˈfɪfti/", "meaning": "五十", "example": "My grandfather is fifty.", "exampleMeaning": "我爷爷五十岁。" },
{ "word": "sixty", "phonetic": "/ˈsɪksti/", "meaning": "六十", "example": "An hour has sixty minutes.", "exampleMeaning": "一小时有六十分钟。" },
{ "word": "seventy", "phonetic": "/ˈsevnti/", "meaning": "七十", "example": "My grandmother is seventy.", "exampleMeaning": "我奶奶七十岁。" },
{ "word": "eighty", "phonetic": "/ˈeɪti/", "meaning": "八十", "example": "He can count to eighty.", "exampleMeaning": "他能数到八十。" },
{ "word": "ninety", "phonetic": "/ˈnaɪnti/", "meaning": "九十", "example": "The book has ninety pages.", "exampleMeaning": "这本书有九十页。" },
{ "word": "hundred", "phonetic": "/ˈhʌndrəd/", "meaning": "百", "example": "One hundred people came.", "exampleMeaning": "来了一百个人。" }
] ]
}, },
{ {
@@ -26,7 +26,13 @@ final class WordStore {
} }
do { do {
let data = try Data(contentsOf: url) let data = try Data(contentsOf: url)
bank = try JSONDecoder().decode(WordBank.self, from: data) let decoded = try JSONDecoder().decode(WordBank.self, from: data)
let categories = decoded.categories.map { category -> WordCategory in
guard category.id == "numbers" else { return category }
return WordCategory(id: category.id, name: category.name, icon: category.icon,
words: NumberWords.makeWords(atoms: category.words))
}
bank = WordBank(version: decoded.version, categories: categories)
} catch { } catch {
loadError = WordStoreError.decodingFailed(error).localizedDescription loadError = WordStoreError.decodingFailed(error).localizedDescription
} }
@@ -162,9 +162,14 @@ final class QuizSetupViewController: UIViewController {
} }
private func setupCountControl() { private func setupCountControl() {
countControl.insertSegment(withTitle: "全部 \(request.words.count)", at: 0, animated: false) if request.words.count > 50 {
if request.words.count > 10 { countControl.insertSegment(withTitle: "随机 10 题", at: 0, animated: false)
countControl.insertSegment(withTitle: "随机 10 题", at: 1, animated: false) countControl.insertSegment(withTitle: "随机 20 题", at: 1, animated: false)
} else {
countControl.insertSegment(withTitle: "全部 \(request.words.count)", at: 0, animated: false)
if request.words.count > 10 {
countControl.insertSegment(withTitle: "随机 10 题", at: 1, animated: false)
}
} }
countControl.selectedSegmentIndex = 0 countControl.selectedSegmentIndex = 0
} }
@@ -198,9 +203,15 @@ final class QuizSetupViewController: UIViewController {
private func startQuiz() { private func startQuiz() {
guard let selectedIndex else { return } guard let selectedIndex else { return }
Haptics.selection() Haptics.selection()
let words = countControl.selectedSegmentIndex == 0 let words: [Word]
? request.words if request.words.count > 50 {
: Array(request.words.shuffled().prefix(10)) let count = countControl.selectedSegmentIndex == 0 ? 10 : 20
words = Array(request.words.shuffled().prefix(count))
} else if countControl.selectedSegmentIndex == 0 {
words = request.words
} else {
words = Array(request.words.shuffled().prefix(10))
}
let config = QuizConfig(mode: items[selectedIndex].mode, words: words) let config = QuizConfig(mode: items[selectedIndex].mode, words: words)
navigationController?.pushViewController(QuizViewController(config: config), animated: true) navigationController?.pushViewController(QuizViewController(config: config), animated: true)
} }
@@ -126,8 +126,19 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
private func rebuildBoxes() { private func rebuildBoxes() {
stack.arrangedSubviews.forEach { $0.removeFromSuperview() } stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
let count = CGFloat(max(chars.count, 1))
let available = (window?.screen.bounds.width ?? 390) - 32
var spacing: CGFloat = 6
var boxWidth = (available - spacing * (count - 1)) / count
if boxWidth < 16 {
spacing = 3
boxWidth = (available - spacing * (count - 1)) / count
}
boxWidth = min(36, boxWidth)
stack.spacing = spacing
let fontSize = min(22, max(12, boxWidth * 0.62))
boxViews = chars.enumerated().map { index, ch in boxViews = chars.enumerated().map { index, ch in
let box = makeBox() let box = makeBox(fontSize: fontSize)
box.tag = index box.tag = index
if ch != "_", let label = box.viewWithTag(100) as? UILabel { if ch != "_", let label = box.viewWithTag(100) as? UILabel {
label.text = ch.uppercased() label.text = ch.uppercased()
@@ -136,7 +147,7 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
box.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(boxTapped(_:)))) box.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(boxTapped(_:))))
stack.addArrangedSubview(box) stack.addArrangedSubview(box)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
box.widthAnchor.constraint(equalToConstant: 36), box.widthAnchor.constraint(equalToConstant: boxWidth),
box.heightAnchor.constraint(equalToConstant: 44) box.heightAnchor.constraint(equalToConstant: 44)
]) ])
return box return box
@@ -158,7 +169,7 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
activate() activate()
} }
private func makeBox() -> UIView { private func makeBox(fontSize: CGFloat) -> UIView {
let box = UIView() let box = UIView()
box.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.15) box.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.15)
box.layer.cornerRadius = 8 box.layer.cornerRadius = 8
@@ -166,8 +177,7 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
box.layer.borderColor = UIColor.separator.cgColor box.layer.borderColor = UIColor.separator.cgColor
let label = UILabel() let label = UILabel()
label.font = .preferredFont(forTextStyle: .title2) label.font = .systemFont(ofSize: fontSize, weight: .bold)
label.font = .systemFont(ofSize: label.font.pointSize, weight: .bold)
label.isHidden = true label.isHidden = true
label.tag = 100 label.tag = 100
box.addSubview(label) box.addSubview(label)