数字分类支持 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 {
|
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,10 +162,15 @@ final class QuizSetupViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setupCountControl() {
|
private func setupCountControl() {
|
||||||
|
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)
|
countControl.insertSegment(withTitle: "全部 \(request.words.count) 题", at: 0, animated: false)
|
||||||
if request.words.count > 10 {
|
if request.words.count > 10 {
|
||||||
countControl.insertSegment(withTitle: "随机 10 题", at: 1, animated: false)
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user