数字分类支持 1-999 数字学习,按规则运行时生成词条
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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))。")
|
||||
}
|
||||
}
|
||||
@@ -62,10 +62,10 @@ enum QuizGenerator {
|
||||
|
||||
private static func blankWord(_ word: String) -> String {
|
||||
let chars = Array(word)
|
||||
let count = chars.count
|
||||
guard count > 2 else { return String(repeating: "_", count: count) }
|
||||
let blanks = max(1, Int(Double(count) * 0.4))
|
||||
let indices = Set(Array(0..<count).shuffled().prefix(blanks))
|
||||
let letterPositions = chars.indices.filter { chars[$0].isLetter }
|
||||
guard letterPositions.count > 2 else { return String(repeating: "_", count: chars.count) }
|
||||
let blanks = max(1, Int(Double(letterPositions.count) * 0.4))
|
||||
let indices = Set(letterPositions.shuffled().prefix(blanks))
|
||||
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": "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": "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 {
|
||||
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 {
|
||||
loadError = WordStoreError.decodingFailed(error).localizedDescription
|
||||
}
|
||||
|
||||
@@ -162,9 +162,14 @@ final class QuizSetupViewController: UIViewController {
|
||||
}
|
||||
|
||||
private func setupCountControl() {
|
||||
countControl.insertSegment(withTitle: "全部 \(request.words.count) 题", at: 0, animated: false)
|
||||
if request.words.count > 10 {
|
||||
countControl.insertSegment(withTitle: "随机 10 题", at: 1, animated: false)
|
||||
if request.words.count > 50 {
|
||||
countControl.insertSegment(withTitle: "随机 10 题", at: 0, 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
|
||||
}
|
||||
@@ -198,9 +203,15 @@ final class QuizSetupViewController: UIViewController {
|
||||
private func startQuiz() {
|
||||
guard let selectedIndex else { return }
|
||||
Haptics.selection()
|
||||
let words = countControl.selectedSegmentIndex == 0
|
||||
? request.words
|
||||
: Array(request.words.shuffled().prefix(10))
|
||||
let words: [Word]
|
||||
if request.words.count > 50 {
|
||||
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)
|
||||
navigationController?.pushViewController(QuizViewController(config: config), animated: true)
|
||||
}
|
||||
|
||||
@@ -126,8 +126,19 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
|
||||
|
||||
private func rebuildBoxes() {
|
||||
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
|
||||
let box = makeBox()
|
||||
let box = makeBox(fontSize: fontSize)
|
||||
box.tag = index
|
||||
if ch != "_", let label = box.viewWithTag(100) as? UILabel {
|
||||
label.text = ch.uppercased()
|
||||
@@ -136,7 +147,7 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
|
||||
box.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(boxTapped(_:))))
|
||||
stack.addArrangedSubview(box)
|
||||
NSLayoutConstraint.activate([
|
||||
box.widthAnchor.constraint(equalToConstant: 36),
|
||||
box.widthAnchor.constraint(equalToConstant: boxWidth),
|
||||
box.heightAnchor.constraint(equalToConstant: 44)
|
||||
])
|
||||
return box
|
||||
@@ -158,7 +169,7 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
|
||||
activate()
|
||||
}
|
||||
|
||||
private func makeBox() -> UIView {
|
||||
private func makeBox(fontSize: CGFloat) -> UIView {
|
||||
let box = UIView()
|
||||
box.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.15)
|
||||
box.layer.cornerRadius = 8
|
||||
@@ -166,8 +177,7 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
|
||||
box.layer.borderColor = UIColor.separator.cgColor
|
||||
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .title2)
|
||||
label.font = .systemFont(ofSize: label.font.pointSize, weight: .bold)
|
||||
label.font = .systemFont(ofSize: fontSize, weight: .bold)
|
||||
label.isHidden = true
|
||||
label.tag = 100
|
||||
box.addSubview(label)
|
||||
|
||||
Reference in New Issue
Block a user