mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Add interactive mathematics for elementary review and four secondary-school years.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
524269edd7
commit
0e24e8c649
38 changed files with 9507 additions and 11 deletions
|
|
@ -198,6 +198,31 @@ final class AppModel: ObservableObject {
|
|||
refresh()
|
||||
}
|
||||
|
||||
func markMat0(_ id: Int) {
|
||||
progress.markMat0(id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
func markMat(_ id: Int) {
|
||||
progress.markMat(id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
func markMat2(_ id: Int) {
|
||||
progress.markMat2(id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
func markMat3(_ id: Int) {
|
||||
progress.markMat3(id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
func markMat4(_ id: Int) {
|
||||
progress.markMat4(id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
func markMluv(_ n: Int) {
|
||||
progress.markMluv(n)
|
||||
refresh()
|
||||
|
|
|
|||
|
|
@ -58,3 +58,658 @@ func hasUmlaut(_ s: String?) -> Bool {
|
|||
guard let s else { return false }
|
||||
return s.contains(where: { "äöüÄÖÜß".contains($0) })
|
||||
}
|
||||
|
||||
// MARK: - Mathematics
|
||||
// Same rules as src/math_check.c. Do not route numbers through normalizeAnswer.
|
||||
|
||||
private struct MFrac { var n: Int64 = 0; var d: Int64 = 1 }
|
||||
private struct MTerm {
|
||||
var ok = true
|
||||
var a = MFrac()
|
||||
var b = MFrac()
|
||||
var rad: Int64 = 0
|
||||
var p = MFrac()
|
||||
}
|
||||
|
||||
private func mGcd(_ a0: Int64, _ b0: Int64) -> Int64 {
|
||||
var a = a0 < 0 ? -a0 : a0
|
||||
var b = b0 < 0 ? -b0 : b0
|
||||
while b != 0 {
|
||||
let t = a % b
|
||||
a = b
|
||||
b = t
|
||||
}
|
||||
return a == 0 ? 1 : a
|
||||
}
|
||||
|
||||
private func mMul(_ a: Int64, _ b: Int64) -> Int64? {
|
||||
let r = a.multipliedReportingOverflow(by: b)
|
||||
return r.overflow ? nil : r.partialValue
|
||||
}
|
||||
|
||||
private func mSet(_ f: inout MFrac, _ n: Int64, _ d: Int64) -> Bool {
|
||||
if d == 0 { return false }
|
||||
var n = n, d = d
|
||||
if d < 0 { n = -n; d = -d }
|
||||
let g = mGcd(n, d)
|
||||
f.n = n / g
|
||||
f.d = d / g
|
||||
return true
|
||||
}
|
||||
|
||||
private func mAddF(_ x: MFrac, _ y: MFrac) -> MFrac? {
|
||||
guard let xn = mMul(x.n, y.d), let yn = mMul(y.n, x.d),
|
||||
let d = mMul(x.d, y.d) else { return nil }
|
||||
let s = xn.addingReportingOverflow(yn)
|
||||
if s.overflow { return nil }
|
||||
var o = MFrac()
|
||||
return mSet(&o, s.partialValue, d) ? o : nil
|
||||
}
|
||||
|
||||
private func mMulF(_ x: MFrac, _ y: MFrac) -> MFrac? {
|
||||
guard let n = mMul(x.n, y.n), let d = mMul(x.d, y.d) else { return nil }
|
||||
var o = MFrac()
|
||||
return mSet(&o, n, d) ? o : nil
|
||||
}
|
||||
|
||||
private func mDivF(_ x: MFrac, _ y: MFrac) -> MFrac? {
|
||||
if y.n == 0 { return nil }
|
||||
return mMulF(x, MFrac(n: y.d, d: y.n))
|
||||
}
|
||||
|
||||
private func mSameF(_ x: MFrac, _ y: MFrac) -> Bool { x.n == y.n && x.d == y.d }
|
||||
|
||||
private func mSqrtReduce(_ n0: Int64) -> (out: Int64, rad: Int64)? {
|
||||
if n0 < 0 { return nil }
|
||||
if n0 > 1_000_000 { return nil }
|
||||
var n = n0
|
||||
var out: Int64 = 1
|
||||
var f: Int64 = 2
|
||||
while f * f <= n {
|
||||
var c = 0
|
||||
while n % f == 0 { n /= f; c += 1 }
|
||||
if c >= 2 { out *= mPow(f, c / 2) }
|
||||
if c % 2 == 1 { n *= f }
|
||||
if f == 2 { f = 3 } else { f += 2 }
|
||||
if f > 10000 { break }
|
||||
}
|
||||
return (out, n)
|
||||
}
|
||||
|
||||
private func mPow(_ b: Int64, _ e: Int) -> Int64 {
|
||||
var r: Int64 = 1
|
||||
var i = 0
|
||||
while i < e { r *= b; i += 1 }
|
||||
return r
|
||||
}
|
||||
|
||||
private func mNorm(_ t: inout MTerm) {
|
||||
if !t.ok { return }
|
||||
if t.rad > 1, let s = mSqrtReduce(t.rad) {
|
||||
guard let nb = mMulF(t.b, MFrac(n: s.out, d: 1)) else { t.ok = false; return }
|
||||
t.b = nb
|
||||
t.rad = s.rad
|
||||
}
|
||||
if t.rad <= 1 {
|
||||
guard let na = mAddF(t.a, t.b) else { t.ok = false; return }
|
||||
t.a = na
|
||||
t.b = MFrac()
|
||||
t.rad = 0
|
||||
}
|
||||
if t.b.n == 0 { t.rad = 0 }
|
||||
}
|
||||
|
||||
private func mAdd(_ x: MTerm, _ y: MTerm) -> MTerm {
|
||||
if !x.ok || !y.ok { return MTerm(ok: false) }
|
||||
var o = MTerm()
|
||||
guard let a = mAddF(x.a, y.a), let p = mAddF(x.p, y.p) else { return MTerm(ok: false) }
|
||||
o.a = a
|
||||
o.p = p
|
||||
if x.rad == 0 && y.rad == 0 {
|
||||
o.b = MFrac(); o.rad = 0
|
||||
} else if x.rad == 0 {
|
||||
o.b = y.b; o.rad = y.rad
|
||||
} else if y.rad == 0 || x.rad == y.rad {
|
||||
guard let b = mAddF(x.b, y.b) else { return MTerm(ok: false) }
|
||||
o.b = b; o.rad = x.rad
|
||||
} else { return MTerm(ok: false) }
|
||||
mNorm(&o)
|
||||
return o
|
||||
}
|
||||
|
||||
private func mScale(_ x: MTerm, _ f: MFrac) -> MTerm {
|
||||
if !x.ok { return MTerm(ok: false) }
|
||||
var o = MTerm()
|
||||
guard let a = mMulF(x.a, f), let b = mMulF(x.b, f), let p = mMulF(x.p, f) else {
|
||||
return MTerm(ok: false)
|
||||
}
|
||||
o.a = a; o.b = b; o.rad = x.rad; o.p = p
|
||||
mNorm(&o)
|
||||
return o
|
||||
}
|
||||
|
||||
private func mMulT(_ x: MTerm, _ y: MTerm) -> MTerm {
|
||||
if !x.ok || !y.ok { return MTerm(ok: false) }
|
||||
if (x.p.n != 0 && (y.p.n != 0 || y.rad != 0)) ||
|
||||
(y.p.n != 0 && (x.p.n != 0 || x.rad != 0)) { return MTerm(ok: false) }
|
||||
if x.rad != 0 && y.rad != 0 && x.rad != y.rad { return MTerm(ok: false) }
|
||||
var o = MTerm()
|
||||
guard let aa = mMulF(x.a, y.a) else { return MTerm(ok: false) }
|
||||
o.a = aa
|
||||
if x.rad == 0 && y.rad == 0 {
|
||||
o.b = MFrac(); o.rad = 0
|
||||
} else if x.rad == 0 {
|
||||
guard let b = mMulF(x.a, y.b) else { return MTerm(ok: false) }
|
||||
o.b = b; o.rad = y.rad
|
||||
} else if y.rad == 0 {
|
||||
guard let b = mMulF(y.a, x.b) else { return MTerm(ok: false) }
|
||||
o.b = b; o.rad = x.rad
|
||||
} else {
|
||||
guard let ab = mMulF(x.a, y.b), let ba = mMulF(x.b, y.a),
|
||||
let cross = mAddF(ab, ba),
|
||||
let sq = mMul(x.rad, y.rad) else { return MTerm(ok: false) }
|
||||
guard let bb = mMulF(x.b, y.b), let extra = mMulF(bb, MFrac(n: sq, d: 1)),
|
||||
let na = mAddF(o.a, extra) else { return MTerm(ok: false) }
|
||||
o.a = na; o.b = cross; o.rad = x.rad
|
||||
}
|
||||
if x.p.n != 0 { o.p = x.p } else { o.p = y.p }
|
||||
if y.a.n != 1 || y.a.d != 1 || y.b.n != 0 || y.rad != 0 || y.p.n != 0 {
|
||||
if x.p.n != 0 {
|
||||
guard let p = mMulF(x.p, y.a) else { return MTerm(ok: false) }
|
||||
o.p = p
|
||||
}
|
||||
}
|
||||
if x.a.n != 1 || x.a.d != 1 || x.b.n != 0 || x.rad != 0 || x.p.n != 0 {
|
||||
if y.p.n != 0 && x.p.n == 0 {
|
||||
guard let p = mMulF(y.p, x.a) else { return MTerm(ok: false) }
|
||||
o.p = p
|
||||
}
|
||||
}
|
||||
mNorm(&o)
|
||||
return o
|
||||
}
|
||||
|
||||
private func mDivT(_ x: MTerm, _ y: MTerm) -> MTerm {
|
||||
if !x.ok || !y.ok || y.p.n != 0 { return MTerm(ok: false) }
|
||||
if y.rad == 0 {
|
||||
guard let f = mDivF(MFrac(n: 1, d: 1), y.a) else { return MTerm(ok: false) }
|
||||
return mScale(x, f)
|
||||
}
|
||||
if x.p.n != 0 { return MTerm(ok: false) }
|
||||
guard let yb = mMulF(y.b, MFrac(n: y.rad, d: 1)),
|
||||
let bb = mMulF(y.b, yb),
|
||||
let aa = mMulF(y.a, y.a),
|
||||
let den = mAddF(aa, MFrac(n: -bb.n, d: bb.d)) else { return MTerm(ok: false) }
|
||||
var num = MTerm()
|
||||
num.a = y.a
|
||||
num.b = MFrac(n: -y.b.n, d: y.b.d)
|
||||
num.rad = y.rad
|
||||
guard let inv = mDivF(MFrac(n: 1, d: 1), den) else { return MTerm(ok: false) }
|
||||
return mMulT(x, mScale(num, inv))
|
||||
}
|
||||
|
||||
private enum MTok {
|
||||
case num(Int64, Int64)
|
||||
case sqrt, pi, add, sub, mul, div, lp, rp
|
||||
}
|
||||
|
||||
private func mLex(_ s: String) -> [MTok]? {
|
||||
var toks: [MTok] = []
|
||||
var i = s.startIndex
|
||||
func peek(_ k: Int) -> Character? {
|
||||
var j = i
|
||||
var n = 0
|
||||
while n < k && j < s.endIndex { j = s.index(after: j); n += 1 }
|
||||
return j < s.endIndex ? s[j] : nil
|
||||
}
|
||||
while i < s.endIndex {
|
||||
while i < s.endIndex && s[i].isWhitespace { i = s.index(after: i) }
|
||||
if i >= s.endIndex { break }
|
||||
let ch = s[i]
|
||||
if ch.isNumber || ((ch == "." || ch == ",") && (peek(1)?.isNumber == true)) {
|
||||
var whole: Int64 = 0
|
||||
var saw = false
|
||||
while i < s.endIndex && s[i].isNumber {
|
||||
saw = true
|
||||
whole = whole * 10 + Int64(s[i].wholeNumberValue ?? 0)
|
||||
i = s.index(after: i)
|
||||
}
|
||||
if i < s.endIndex && (s[i] == "." || s[i] == ",") && (s.index(after: i) < s.endIndex && s[s.index(after: i)].isNumber) {
|
||||
i = s.index(after: i)
|
||||
var frac: Int64 = 0
|
||||
var div: Int64 = 1
|
||||
while i < s.endIndex && s[i].isNumber {
|
||||
frac = frac * 10 + Int64(s[i].wholeNumberValue ?? 0)
|
||||
div *= 10
|
||||
i = s.index(after: i)
|
||||
}
|
||||
let n = whole * div + frac
|
||||
toks.append(.num(n, div))
|
||||
} else if saw {
|
||||
toks.append(.num(whole, 1))
|
||||
} else { return nil }
|
||||
continue
|
||||
}
|
||||
if ch == "+" { toks.append(.add); i = s.index(after: i); continue }
|
||||
if ch == "-" || ch == "−" || ch == "–" { toks.append(.sub); i = s.index(after: i); continue }
|
||||
if ch == "*" || ch == "×" || ch == "·" { toks.append(.mul); i = s.index(after: i); continue }
|
||||
if ch == "/" || ch == ":" { toks.append(.div); i = s.index(after: i); continue }
|
||||
if ch == "(" { toks.append(.lp); i = s.index(after: i); continue }
|
||||
if ch == ")" { toks.append(.rp); i = s.index(after: i); continue }
|
||||
if ch == "π" { toks.append(.pi); i = s.index(after: i); continue }
|
||||
if ch == "√" { toks.append(.sqrt); i = s.index(after: i); continue }
|
||||
if ch.isLetter {
|
||||
var j = i
|
||||
while j < s.endIndex && s[j].isLetter { j = s.index(after: j) }
|
||||
let w = s[i..<j].lowercased()
|
||||
if w == "pi" { toks.append(.pi) }
|
||||
else if w == "sqrt" { toks.append(.sqrt) }
|
||||
else { return nil }
|
||||
i = j
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return toks
|
||||
}
|
||||
|
||||
private struct MParse {
|
||||
let t: [MTok]
|
||||
var i = 0
|
||||
func peek() -> MTok? { i < t.count ? t[i] : nil }
|
||||
mutating func take() -> MTok? {
|
||||
if i >= t.count { return nil }
|
||||
defer { i += 1 }
|
||||
return t[i]
|
||||
}
|
||||
mutating func expr() -> MTerm? {
|
||||
guard var left = term() else { return nil }
|
||||
while let p = peek() {
|
||||
if case .add = p { _ = take() }
|
||||
else if case .sub = p { _ = take() }
|
||||
else { break }
|
||||
guard let r = term() else { return nil }
|
||||
if case .sub = p { left = mAdd(left, mScale(r, MFrac(n: -1, d: 1))) }
|
||||
else { left = mAdd(left, r) }
|
||||
if !left.ok { return nil }
|
||||
}
|
||||
return left
|
||||
}
|
||||
mutating func term() -> MTerm? {
|
||||
guard var left = unary() else { return nil }
|
||||
while let p = peek() {
|
||||
if case .mul = p { _ = take() }
|
||||
else if case .div = p { _ = take() }
|
||||
else { break }
|
||||
guard let r = unary() else { return nil }
|
||||
if case .div = p { left = mDivT(left, r) }
|
||||
else { left = mMulT(left, r) }
|
||||
if !left.ok { return nil }
|
||||
}
|
||||
return left
|
||||
}
|
||||
mutating func unary() -> MTerm? {
|
||||
if case .sub = peek() {
|
||||
_ = take()
|
||||
guard let v = unary() else { return nil }
|
||||
return mScale(v, MFrac(n: -1, d: 1))
|
||||
}
|
||||
if case .add = peek() {
|
||||
_ = take()
|
||||
return unary()
|
||||
}
|
||||
return factor()
|
||||
}
|
||||
mutating func factor() -> MTerm? {
|
||||
guard var left = primary() else { return nil }
|
||||
while true {
|
||||
guard let p = peek() else { break }
|
||||
switch p {
|
||||
case .sqrt, .pi, .lp:
|
||||
guard let r = primary() else { return nil }
|
||||
left = mMulT(left, r)
|
||||
if !left.ok { return nil }
|
||||
case .num:
|
||||
return nil
|
||||
default:
|
||||
return left
|
||||
}
|
||||
}
|
||||
return left
|
||||
}
|
||||
mutating func primary() -> MTerm? {
|
||||
guard let p = take() else { return nil }
|
||||
switch p {
|
||||
case .num(let n, let d):
|
||||
if case .div = peek(), case .num = t[safe: i + 1] {
|
||||
_ = take()
|
||||
if case .num(let n2, let d2) = take() {
|
||||
if n2 <= 0 || d2 != 1 || d != 1 { return nil }
|
||||
var o = MTerm()
|
||||
if !mSet(&o.a, n, n2) { return nil }
|
||||
return o
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if d == 1, case .num(let n2, let d2) = peek(), n >= 0 {
|
||||
_ = take()
|
||||
if case .div = peek(), case .num(let n3, let d3) = t[safe: i + 1], d2 == 1, d3 == 1, n2 >= 0, n3 > 0 {
|
||||
_ = take()
|
||||
_ = take()
|
||||
var frac = MFrac()
|
||||
if !mSet(&frac, n2, n3) { return nil }
|
||||
var whole = MTerm()
|
||||
whole.a = MFrac(n: n, d: 1)
|
||||
var part = MTerm()
|
||||
part.a = frac
|
||||
return mAdd(whole, part)
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
var o = MTerm()
|
||||
if !mSet(&o.a, n, d) { return nil }
|
||||
return o
|
||||
case .pi:
|
||||
var o = MTerm()
|
||||
o.p = MFrac(n: 1, d: 1)
|
||||
return o
|
||||
case .sqrt:
|
||||
if case .lp = peek() {
|
||||
_ = take()
|
||||
guard let inner = expr(), case .rp = take() else { return nil }
|
||||
return mSqrtTerm(inner)
|
||||
}
|
||||
if case .num(let n, let d) = peek(), d == 1, n >= 0 {
|
||||
_ = take()
|
||||
return mSqrtTerm(MTerm(a: MFrac(n: n, d: 1)))
|
||||
}
|
||||
return nil
|
||||
case .lp:
|
||||
guard let inner = expr(), case .rp = take() else { return nil }
|
||||
return inner
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array {
|
||||
subscript(safe i: Int) -> Element? { indices.contains(i) ? self[i] : nil }
|
||||
}
|
||||
|
||||
private func mSqrtTerm(_ inner: MTerm) -> MTerm? {
|
||||
if !inner.ok || inner.p.n != 0 || inner.rad != 0 || inner.b.n != 0 { return nil }
|
||||
if inner.a.n < 0 || inner.a.d != 1 { return nil }
|
||||
guard let s = mSqrtReduce(inner.a.n) else { return nil }
|
||||
var o = MTerm()
|
||||
o.b = MFrac(n: s.out, d: 1)
|
||||
o.rad = s.rad
|
||||
mNorm(&o)
|
||||
return o.ok ? o : nil
|
||||
}
|
||||
|
||||
private func mPrep(_ raw: String) -> String {
|
||||
var s = raw.trimmingCharacters(in: .whitespaces)
|
||||
var again = true
|
||||
while again {
|
||||
again = false
|
||||
if s.count >= 2 {
|
||||
let pairs: [(Character, Character)] = [("(", ")"), ("[", "]"), ("{", "}")]
|
||||
if let last = s.last, pairs.contains(where: { $0.0 == s.first && $0.1 == last }) {
|
||||
s = String(s.dropFirst().dropLast()).trimmingCharacters(in: .whitespaces)
|
||||
again = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
s = mStripUnits(s).trimmingCharacters(in: .whitespaces)
|
||||
return s
|
||||
}
|
||||
|
||||
private func mParseTerm(_ s: String) -> MTerm? {
|
||||
let s = mPrep(s)
|
||||
if s.isEmpty { return nil }
|
||||
guard let toks = mLex(s) else { return nil }
|
||||
if toks.isEmpty { return nil }
|
||||
var p = MParse(t: toks)
|
||||
guard let v = p.expr(), p.i == toks.count, v.ok else { return nil }
|
||||
return v
|
||||
}
|
||||
|
||||
private func mExact(_ x: MTerm, _ y: MTerm) -> Bool {
|
||||
x.ok && y.ok && mSameF(x.a, y.a) && mSameF(x.b, y.b) && x.rad == y.rad && mSameF(x.p, y.p)
|
||||
}
|
||||
|
||||
private func mDouble(_ t: MTerm) -> Double {
|
||||
var v = Double(t.a.n) / Double(t.a.d) + Double(t.p.n) / Double(t.p.d) * Double.pi
|
||||
if t.b.n != 0 && t.rad > 0 {
|
||||
v += Double(t.b.n) / Double(t.b.d) * (Double(t.rad)).squareRoot()
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
private func mClose(_ x: MTerm, _ y: MTerm) -> Bool {
|
||||
if !x.ok || !y.ok { return false }
|
||||
if mExact(x, y) { return true }
|
||||
if x.p.n != 0 || y.p.n != 0 { return false }
|
||||
let diff = abs(mDouble(x) - mDouble(y))
|
||||
if x.b.n != 0 || y.b.n != 0 { return diff <= 0.001 }
|
||||
return diff <= 0.0005
|
||||
}
|
||||
|
||||
private func mFold(_ s: String) -> String {
|
||||
var out = ""
|
||||
for ch in s.lowercased() {
|
||||
switch ch {
|
||||
case "á", "à", "ä", "â": out.append("a")
|
||||
case "č", "ć": out.append("c")
|
||||
case "ď": out.append("d")
|
||||
case "é", "ě", "ë", "ê": out.append("e")
|
||||
case "í", "ï": out.append("i")
|
||||
case "ň": out.append("n")
|
||||
case "ó", "ö", "ô": out.append("o")
|
||||
case "ř": out.append("r")
|
||||
case "š", "ś": out.append("s")
|
||||
case "ť": out.append("t")
|
||||
case "ú", "ů", "ü": out.append("u")
|
||||
case "ý": out.append("y")
|
||||
case "ž", "ź": out.append("z")
|
||||
default:
|
||||
if ch.isLetter || ch.isNumber { out.append(ch) }
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private func mStripUnits(_ raw: String) -> String {
|
||||
var s = raw.trimmingCharacters(in: .whitespaces)
|
||||
let units = ["procenta", "procent", "cm", "mm", "dm", "km", "m"]
|
||||
var cut = true
|
||||
while cut {
|
||||
cut = false
|
||||
while s.last?.isWhitespace == true { s.removeLast() }
|
||||
if s.hasSuffix("°") || s.hasSuffix("%") || s.hasSuffix(".") {
|
||||
s.removeLast()
|
||||
cut = true
|
||||
continue
|
||||
}
|
||||
for u in units {
|
||||
guard s.count > u.count else { continue }
|
||||
if s.lowercased().hasSuffix(u) {
|
||||
let keep = s.dropLast(u.count)
|
||||
if keep.last?.isLetter == true { continue }
|
||||
s = String(keep)
|
||||
cut = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return s.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
private func mSplit(_ s: String) -> [String]? {
|
||||
let s = s.trimmingCharacters(in: .whitespaces)
|
||||
if s.isEmpty { return nil }
|
||||
let semi = s.contains(";")
|
||||
var parts: [String] = []
|
||||
var rest = Substring(s)
|
||||
while !rest.isEmpty {
|
||||
let end: Substring.Index
|
||||
let step: Int
|
||||
if semi {
|
||||
if let i = rest.firstIndex(of: ";") { end = i; step = 1 }
|
||||
else { end = rest.endIndex; step = 0 }
|
||||
} else if let r = rest.range(of: ", ") {
|
||||
end = r.lowerBound
|
||||
step = 2
|
||||
} else {
|
||||
end = rest.endIndex
|
||||
step = 0
|
||||
}
|
||||
let piece = rest[..<end].trimmingCharacters(in: .whitespaces)
|
||||
if piece.isEmpty { return nil }
|
||||
parts.append(piece)
|
||||
if step == 0 { break }
|
||||
rest = rest[rest.index(end, offsetBy: step)...]
|
||||
}
|
||||
return parts.isEmpty ? nil : parts
|
||||
}
|
||||
|
||||
func mathAnswerOk(_ spec: String?, _ user: String?) -> Bool {
|
||||
guard var spec, let user else { return false }
|
||||
spec = spec.trimmingCharacters(in: .whitespaces)
|
||||
if spec.hasPrefix("word:") {
|
||||
let opts = spec.dropFirst(5).split(separator: "|").map { mFold(String($0)) }
|
||||
return opts.contains(mFold(user))
|
||||
}
|
||||
if spec.hasPrefix("set:") || spec.hasPrefix("pair:") {
|
||||
let ordered = spec.hasPrefix("pair:")
|
||||
let body = String(spec.dropFirst(ordered ? 5 : 4))
|
||||
guard let want = mSplit(body), let got = mSplit(user) else { return false }
|
||||
if want.count != got.count || want.isEmpty { return false }
|
||||
var wt: [MTerm] = []
|
||||
var gt: [MTerm] = []
|
||||
for w in want {
|
||||
guard let t = mParseTerm(w) else { return false }
|
||||
wt.append(t)
|
||||
}
|
||||
for g in got {
|
||||
guard let t = mParseTerm(g) else { return false }
|
||||
gt.append(t)
|
||||
}
|
||||
if ordered {
|
||||
for i in wt.indices where !mClose(wt[i], gt[i]) { return false }
|
||||
return true
|
||||
}
|
||||
var used = Array(repeating: false, count: gt.count)
|
||||
for w in wt {
|
||||
var hit = gt.indices.first { !used[$0] && mExact(w, gt[$0]) }
|
||||
if hit == nil { hit = gt.indices.first { !used[$0] && mClose(w, gt[$0]) } }
|
||||
guard let hit else { return false }
|
||||
used[hit] = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
let body = spec.hasPrefix("num:") ? String(spec.dropFirst(4)) : spec
|
||||
guard let tw = mParseTerm(body), let tg = mParseTerm(user) else { return false }
|
||||
return mClose(tw, tg)
|
||||
}
|
||||
|
||||
func mathIsDraw(_ spec: String?) -> Bool {
|
||||
guard let spec else { return false }
|
||||
return spec.hasPrefix("square:") || spec.hasPrefix("mid:") || spec.hasPrefix("right:")
|
||||
|| spec.hasPrefix("para:") || spec.hasPrefix("points:")
|
||||
}
|
||||
|
||||
func mathDrawNeed(_ spec: String?) -> Int {
|
||||
guard let spec else { return 0 }
|
||||
if spec.hasPrefix("square:") { return 2 }
|
||||
if spec.hasPrefix("mid:") || spec.hasPrefix("right:") || spec.hasPrefix("para:") { return 1 }
|
||||
if spec.hasPrefix("points:") {
|
||||
let body = spec.dropFirst(7)
|
||||
return body.split(separator: ";").filter { !$0.hasPrefix("fix:") }.count
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private func mParseXY(_ s: Substring) -> (Int, Int)? {
|
||||
let t = s.trimmingCharacters(in: .whitespaces)
|
||||
let bits = t.split(separator: ",")
|
||||
guard bits.count == 2, let x = Int(bits[0].trimmingCharacters(in: .whitespaces)),
|
||||
let y = Int(bits[1].trimmingCharacters(in: .whitespaces)) else { return nil }
|
||||
return (x, y)
|
||||
}
|
||||
|
||||
func mathDrawGiven(_ spec: String?) -> [(Int, Int)] {
|
||||
guard let spec else { return [] }
|
||||
if spec.hasPrefix("points:") {
|
||||
return spec.dropFirst(7).split(separator: ";").compactMap { part -> (Int, Int)? in
|
||||
guard part.hasPrefix("fix:") else { return nil }
|
||||
return mParseXY(part.dropFirst(4))
|
||||
}
|
||||
}
|
||||
let body: Substring
|
||||
let n: Int
|
||||
if spec.hasPrefix("para:") { body = spec.dropFirst(5); n = 3 }
|
||||
else if spec.hasPrefix("square:") { body = spec.dropFirst(7); n = 2 }
|
||||
else if spec.hasPrefix("right:") { body = spec.dropFirst(6); n = 2 }
|
||||
else if spec.hasPrefix("mid:") { body = spec.dropFirst(4); n = 2 }
|
||||
else { return [] }
|
||||
return body.split(separator: ";").prefix(n).compactMap { mParseXY($0) }
|
||||
}
|
||||
|
||||
private func mOnBoard(_ p: (Int, Int)) -> Bool {
|
||||
p.0 >= 0 && p.0 <= 10 && p.1 >= 0 && p.1 <= 8
|
||||
}
|
||||
|
||||
func mathDrawOk(_ spec: String?, _ pts: [(Int, Int)]) -> Bool {
|
||||
guard let spec, mathDrawNeed(spec) == pts.count else { return false }
|
||||
if pts.contains(where: { !mOnBoard($0) }) { return false }
|
||||
if Set(pts.map { "\($0.0),\($0.1)" }).count != pts.count { return false }
|
||||
let g = mathDrawGiven(spec)
|
||||
if spec.hasPrefix("mid:") {
|
||||
guard g.count == 2, (g[0].0 + g[1].0) % 2 == 0, (g[0].1 + g[1].1) % 2 == 0 else { return false }
|
||||
return pts[0] == ((g[0].0 + g[1].0) / 2, (g[0].1 + g[1].1) / 2)
|
||||
}
|
||||
if spec.hasPrefix("para:") {
|
||||
guard g.count == 3 else { return false }
|
||||
return pts[0] == (g[1].0 + g[2].0 - g[0].0, g[1].1 + g[2].1 - g[0].1)
|
||||
}
|
||||
if spec.hasPrefix("right:") {
|
||||
guard g.count == 2 else { return false }
|
||||
let c = pts[0]
|
||||
if c == g[0] || c == g[1] { return false }
|
||||
let ux = g[0].0 - c.0, uy = g[0].1 - c.1
|
||||
let vx = g[1].0 - c.0, vy = g[1].1 - c.1
|
||||
return ux * vx + uy * vy == 0 && (ux != 0 || uy != 0) && (vx != 0 || vy != 0)
|
||||
}
|
||||
if spec.hasPrefix("square:") {
|
||||
guard g.count == 2, pts.count == 2 else { return false }
|
||||
let a = g[0], b = g[1]
|
||||
let dx = b.0 - a.0, dy = b.1 - a.1
|
||||
if dx == 0 && dy == 0 { return false }
|
||||
let c1 = (b.0 - dy, b.1 + dx), d1 = (a.0 - dy, a.1 + dx)
|
||||
let c2 = (b.0 + dy, b.1 - dx), d2 = (a.0 + dy, a.1 - dx)
|
||||
func same(_ p: (Int, Int), _ q: (Int, Int), _ r: (Int, Int), _ s: (Int, Int)) -> Bool {
|
||||
(p == r && q == s) || (p == s && q == r)
|
||||
}
|
||||
if !mOnBoard(c1) || !mOnBoard(d1) {
|
||||
if !mOnBoard(c2) || !mOnBoard(d2) { return false }
|
||||
}
|
||||
return same(pts[0], pts[1], c1, d1) || same(pts[0], pts[1], c2, d2)
|
||||
}
|
||||
if spec.hasPrefix("points:") {
|
||||
let want = spec.dropFirst(7).split(separator: ";").compactMap { part -> (Int, Int)? in
|
||||
if part.hasPrefix("fix:") { return nil }
|
||||
return mParseXY(part)
|
||||
}
|
||||
if want.count != pts.count { return false }
|
||||
var used = Array(repeating: false, count: pts.count)
|
||||
for w in want {
|
||||
guard let j = pts.indices.first(where: { !used[$0] && pts[$0] == w }) else { return false }
|
||||
used[j] = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@ final class Content {
|
|||
let fyz2: [J]
|
||||
let fyz3: [J]
|
||||
let fyz4: [J]
|
||||
let mat0: [J]
|
||||
let mat: [J]
|
||||
let mat2: [J]
|
||||
let mat3: [J]
|
||||
let mat4: [J]
|
||||
let mluvnice: [J]
|
||||
let books: [J]
|
||||
let changelog: [J]
|
||||
|
|
@ -90,6 +95,11 @@ final class Content {
|
|||
fyz2 = raw.arr("fyz2")
|
||||
fyz3 = raw.arr("fyz3")
|
||||
fyz4 = raw.arr("fyz4")
|
||||
mat0 = raw.arr("mat0")
|
||||
mat = raw.arr("mat")
|
||||
mat2 = raw.arr("mat2")
|
||||
mat3 = raw.arr("mat3")
|
||||
mat4 = raw.arr("mat4")
|
||||
mluvnice = raw.arr("mluvnice")
|
||||
books = raw.arr("books")
|
||||
changelog = raw.arr("changelog")
|
||||
|
|
@ -112,6 +122,11 @@ final class Content {
|
|||
func fyz2Lesson(_ id: Int) -> J? { fyz2.first { $0.int("id") == id } }
|
||||
func fyz3Lesson(_ id: Int) -> J? { fyz3.first { $0.int("id") == id } }
|
||||
func fyz4Lesson(_ id: Int) -> J? { fyz4.first { $0.int("id") == id } }
|
||||
func mat0Lesson(_ id: Int) -> J? { mat0.first { $0.int("id") == id } }
|
||||
func matLesson(_ id: Int) -> J? { mat.first { $0.int("id") == id } }
|
||||
func mat2Lesson(_ id: Int) -> J? { mat2.first { $0.int("id") == id } }
|
||||
func mat3Lesson(_ id: Int) -> J? { mat3.first { $0.int("id") == id } }
|
||||
func mat4Lesson(_ id: Int) -> J? { mat4.first { $0.int("id") == id } }
|
||||
func mluv(_ n: Int) -> J? { mluvnice.first { $0.int("id") == n } }
|
||||
func book(_ id: String) -> J? { books.first { $0.str("id") == id } }
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,16 @@ final class ProgressStore {
|
|||
func markFyz3(_ id: Int) { defaults.set(true, forKey: "fyz3.\(id)") }
|
||||
func fyz4Done(_ id: Int) -> Bool { defaults.bool(forKey: "fyz4.\(id)") }
|
||||
func markFyz4(_ id: Int) { defaults.set(true, forKey: "fyz4.\(id)") }
|
||||
func mat0Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat0.\(id)") }
|
||||
func markMat0(_ id: Int) { defaults.set(true, forKey: "mat0.\(id)") }
|
||||
func matDone(_ id: Int) -> Bool { defaults.bool(forKey: "mat.\(id)") }
|
||||
func markMat(_ id: Int) { defaults.set(true, forKey: "mat.\(id)") }
|
||||
func mat2Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat2.\(id)") }
|
||||
func markMat2(_ id: Int) { defaults.set(true, forKey: "mat2.\(id)") }
|
||||
func mat3Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat3.\(id)") }
|
||||
func markMat3(_ id: Int) { defaults.set(true, forKey: "mat3.\(id)") }
|
||||
func mat4Done(_ id: Int) -> Bool { defaults.bool(forKey: "mat4.\(id)") }
|
||||
func markMat4(_ id: Int) { defaults.set(true, forKey: "mat4.\(id)") }
|
||||
|
||||
func mluvDone(_ n: Int) -> Bool { defaults.bool(forKey: "mluv.\(n)") }
|
||||
func markMluv(_ n: Int) { defaults.set(true, forKey: "mluv.\(n)") }
|
||||
|
|
@ -223,6 +233,46 @@ func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
|
|||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
for l in content.mat0 {
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if p.mat0Done(l.int("id")) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
for l in content.mat {
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if p.matDone(l.int("id")) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
for l in content.mat2 {
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if p.mat2Done(l.int("id")) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
for l in content.mat3 {
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if p.mat3Done(l.int("id")) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
for l in content.mat4 {
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if p.mat4Done(l.int("id")) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
sum.totalEx += content.mluvnice.count
|
||||
sum.openUnits += 1
|
||||
let md = content.mluvnice.filter { p.mluvDone($0.int("id")) }.count
|
||||
|
|
|
|||
|
|
@ -56,6 +56,22 @@ enum Route: Hashable {
|
|||
case bioMap
|
||||
case bioLesson(Int)
|
||||
case bioEx(Int)
|
||||
case matYears
|
||||
case mat0Map
|
||||
case mat0Lesson(Int)
|
||||
case mat0Ex(Int)
|
||||
case matMap
|
||||
case matLesson(Int)
|
||||
case matEx(Int)
|
||||
case mat2Map
|
||||
case mat2Lesson(Int)
|
||||
case mat2Ex(Int)
|
||||
case mat3Map
|
||||
case mat3Lesson(Int)
|
||||
case mat3Ex(Int)
|
||||
case mat4Map
|
||||
case mat4Lesson(Int)
|
||||
case mat4Ex(Int)
|
||||
case fyzYears
|
||||
case fyzMap
|
||||
case fyzLesson(Int)
|
||||
|
|
@ -127,6 +143,22 @@ enum Route: Hashable {
|
|||
case .bioMap: return "biomap"
|
||||
case .bioLesson(let id): return "biounit\(id)"
|
||||
case .bioEx(let id): return "bioex\(id)"
|
||||
case .matYears: return "matyears"
|
||||
case .mat0Map: return "mat0map"
|
||||
case .mat0Lesson(let id): return "mat0unit\(id)"
|
||||
case .mat0Ex(let id): return "mat0ex\(id)"
|
||||
case .matMap: return "matmap"
|
||||
case .matLesson(let id): return "matunit\(id)"
|
||||
case .matEx(let id): return "matex\(id)"
|
||||
case .mat2Map: return "mat2map"
|
||||
case .mat2Lesson(let id): return "mat2unit\(id)"
|
||||
case .mat2Ex(let id): return "mat2ex\(id)"
|
||||
case .mat3Map: return "mat3map"
|
||||
case .mat3Lesson(let id): return "mat3unit\(id)"
|
||||
case .mat3Ex(let id): return "mat3ex\(id)"
|
||||
case .mat4Map: return "mat4map"
|
||||
case .mat4Lesson(let id): return "mat4unit\(id)"
|
||||
case .mat4Ex(let id): return "mat4ex\(id)"
|
||||
case .fyzYears: return "fyzyears"
|
||||
case .fyzMap: return "fyzmap"
|
||||
case .fyzLesson(let id): return "fyzunit\(id)"
|
||||
|
|
@ -254,6 +286,38 @@ enum Route: Hashable {
|
|||
return [.sciMap, .bioMap, .bioLesson(id)]
|
||||
case .bioEx(let id):
|
||||
return [.sciMap, .bioMap, .bioLesson(id), .bioEx(id)]
|
||||
case .matYears:
|
||||
return [.matYears]
|
||||
case .mat0Map:
|
||||
return [.matYears, .mat0Map]
|
||||
case .mat0Lesson(let id):
|
||||
return [.matYears, .mat0Map, .mat0Lesson(id)]
|
||||
case .mat0Ex(let id):
|
||||
return [.matYears, .mat0Map, .mat0Lesson(id), .mat0Ex(id)]
|
||||
case .matMap:
|
||||
return [.matYears, .matMap]
|
||||
case .matLesson(let id):
|
||||
return [.matYears, .matMap, .matLesson(id)]
|
||||
case .matEx(let id):
|
||||
return [.matYears, .matMap, .matLesson(id), .matEx(id)]
|
||||
case .mat2Map:
|
||||
return [.matYears, .mat2Map]
|
||||
case .mat2Lesson(let id):
|
||||
return [.matYears, .mat2Map, .mat2Lesson(id)]
|
||||
case .mat2Ex(let id):
|
||||
return [.matYears, .mat2Map, .mat2Lesson(id), .mat2Ex(id)]
|
||||
case .mat3Map:
|
||||
return [.matYears, .mat3Map]
|
||||
case .mat3Lesson(let id):
|
||||
return [.matYears, .mat3Map, .mat3Lesson(id)]
|
||||
case .mat3Ex(let id):
|
||||
return [.matYears, .mat3Map, .mat3Lesson(id), .mat3Ex(id)]
|
||||
case .mat4Map:
|
||||
return [.matYears, .mat4Map]
|
||||
case .mat4Lesson(let id):
|
||||
return [.matYears, .mat4Map, .mat4Lesson(id)]
|
||||
case .mat4Ex(let id):
|
||||
return [.matYears, .mat4Map, .mat4Lesson(id), .mat4Ex(id)]
|
||||
case .fyzYears:
|
||||
return [.fyzYears]
|
||||
case .fyzMap:
|
||||
|
|
@ -321,6 +385,12 @@ func routeFromPage(_ page: String) -> Route? {
|
|||
case "scimap": return .sciMap
|
||||
case "chemmap": return .chemMap
|
||||
case "biomap": return .bioMap
|
||||
case "matyears": return .matYears
|
||||
case "mat0map": return .mat0Map
|
||||
case "matmap": return .matMap
|
||||
case "mat2map": return .mat2Map
|
||||
case "mat3map": return .mat3Map
|
||||
case "mat4map": return .mat4Map
|
||||
case "fyzyears": return .fyzYears
|
||||
case "fyzmap": return .fyzMap
|
||||
case "fyz2map": return .fyz2Map
|
||||
|
|
@ -381,6 +451,36 @@ func routeFromPage(_ page: String) -> Route? {
|
|||
if let m = page.wholeMatch(of: /bioex(\d+)/), let n = Int(m.1) {
|
||||
return .bioEx(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /mat0unit(\d+)/), let n = Int(m.1) {
|
||||
return .mat0Lesson(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /mat0ex(\d+)/), let n = Int(m.1) {
|
||||
return .mat0Ex(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /mat4unit(\d+)/), let n = Int(m.1) {
|
||||
return .mat4Lesson(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /mat4ex(\d+)/), let n = Int(m.1) {
|
||||
return .mat4Ex(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /mat3unit(\d+)/), let n = Int(m.1) {
|
||||
return .mat3Lesson(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /mat3ex(\d+)/), let n = Int(m.1) {
|
||||
return .mat3Ex(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /mat2unit(\d+)/), let n = Int(m.1) {
|
||||
return .mat2Lesson(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /mat2ex(\d+)/), let n = Int(m.1) {
|
||||
return .mat2Ex(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /matunit(\d+)/), let n = Int(m.1) {
|
||||
return .matLesson(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /matex(\d+)/), let n = Int(m.1) {
|
||||
return .matEx(n)
|
||||
}
|
||||
if let m = page.wholeMatch(of: /fyz4unit(\d+)/), let n = Int(m.1) {
|
||||
return .fyz4Lesson(n)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -609,6 +609,7 @@ func subjectSymbol(_ icon: String) -> String {
|
|||
case "people": return "person.2.fill"
|
||||
case "flask": return "flask.fill"
|
||||
case "bolt": return "bolt.fill"
|
||||
case "function": return "function"
|
||||
default: return "lock.fill"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -646,6 +646,255 @@ struct SciQuizScreen: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func mathLesson(_ vm: AppModel, year: Int, id: Int) -> J? {
|
||||
switch year {
|
||||
case 0: return vm.content.mat0Lesson(id)
|
||||
case 2: return vm.content.mat2Lesson(id)
|
||||
case 3: return vm.content.mat3Lesson(id)
|
||||
case 4: return vm.content.mat4Lesson(id)
|
||||
default: return vm.content.matLesson(id)
|
||||
}
|
||||
}
|
||||
|
||||
private func markMathLesson(_ vm: AppModel, year: Int, id: Int) {
|
||||
switch year {
|
||||
case 0: vm.markMat0(id)
|
||||
case 2: vm.markMat2(id)
|
||||
case 3: vm.markMat3(id)
|
||||
case 4: vm.markMat4(id)
|
||||
default: vm.markMat(id)
|
||||
}
|
||||
}
|
||||
|
||||
struct MathPracticeScreen: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
let year: Int
|
||||
let id: Int
|
||||
@State private var text: [Int: String] = [:]
|
||||
@State private var points: [Int: [(Int, Int)]] = [:]
|
||||
@State private var marks: [Int: Bool] = [:]
|
||||
@State private var revealed = false
|
||||
@State private var feedback = ""
|
||||
@State private var kind = ""
|
||||
|
||||
var body: some View {
|
||||
if let lesson = mathLesson(vm, year: year, id: id) {
|
||||
let problems = lesson.arr("problems")
|
||||
ExerciseScaffold(
|
||||
vm: vm,
|
||||
title: vm.tr(lesson.str("exTitleKey")),
|
||||
sub: vm.tr(lesson.str("quizHeadKey")),
|
||||
action: vm.tr("check"),
|
||||
onAction: { check(problems) },
|
||||
feedback: feedback,
|
||||
kind: kind
|
||||
) {
|
||||
Text(vm.tr("math_quiz_intro"))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 4)
|
||||
ForEach(problems.indices, id: \.self) { i in
|
||||
mathCard(problems[i], index: i)
|
||||
}
|
||||
}
|
||||
.onChange(of: year) { _, _ in reset() }
|
||||
.onChange(of: id) { _, _ in reset() }
|
||||
}
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
text = [:]
|
||||
points = [:]
|
||||
marks = [:]
|
||||
revealed = false
|
||||
feedback = ""
|
||||
kind = ""
|
||||
}
|
||||
|
||||
private func check(_ problems: [J]) {
|
||||
var all = !problems.isEmpty
|
||||
var next: [Int: Bool] = [:]
|
||||
for i in problems.indices {
|
||||
let spec = problems[i].str("answer")
|
||||
let good = mathIsDraw(spec)
|
||||
? mathDrawOk(spec, points[i] ?? [])
|
||||
: mathAnswerOk(spec, text[i] ?? "")
|
||||
next[i] = good
|
||||
if !good { all = false }
|
||||
}
|
||||
marks = next
|
||||
revealed = true
|
||||
if all {
|
||||
feedback = vm.tr("feedback_ok")
|
||||
kind = "ok"
|
||||
markMathLesson(vm, year: year, id: id)
|
||||
} else {
|
||||
feedback = vm.tr("feedback_retry_short")
|
||||
kind = "bad"
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func mathCard(_ problem: J, index i: Int) -> some View {
|
||||
let spec = problem.str("answer")
|
||||
let p = vm.palette
|
||||
let border: Color = {
|
||||
guard let mark = marks[i] else { return p.surface2 }
|
||||
return mark ? p.success : p.error
|
||||
}()
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(problem.str("prompt"))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
if mathIsDraw(spec) {
|
||||
Text(vm.tr("math_draw_hint"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
MathGrid(
|
||||
given: mathDrawGiven(spec),
|
||||
user: points[i] ?? [],
|
||||
need: mathDrawNeed(spec),
|
||||
mark: marks[i],
|
||||
palette: p
|
||||
) { next in
|
||||
points[i] = next
|
||||
}
|
||||
HStack {
|
||||
Button(vm.tr("math_undo")) {
|
||||
var pts = points[i] ?? []
|
||||
if !pts.isEmpty {
|
||||
pts.removeLast()
|
||||
points[i] = pts
|
||||
}
|
||||
}
|
||||
Button(vm.tr("math_clear")) { points[i] = [] }
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
} else {
|
||||
TextField(vm.tr("math_answer_ph"), text: Binding(
|
||||
get: { text[i] ?? "" },
|
||||
set: { text[i] = $0 }
|
||||
))
|
||||
.textFieldStyle(.roundedBorder)
|
||||
#if os(iOS)
|
||||
.textInputAutocapitalization(.never)
|
||||
.keyboardType(.numbersAndPunctuation)
|
||||
#endif
|
||||
.autocorrectionDisabled()
|
||||
}
|
||||
if revealed {
|
||||
Text(problem.str("hint"))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.background(p.surface0, in: RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).stroke(border, lineWidth: 1.5))
|
||||
}
|
||||
}
|
||||
|
||||
private struct MathGrid: View {
|
||||
let given: [(Int, Int)]
|
||||
let user: [(Int, Int)]
|
||||
let need: Int
|
||||
let mark: Bool?
|
||||
let palette: Palette
|
||||
var onChange: ([(Int, Int)]) -> Void
|
||||
|
||||
private let cell: CGFloat = 28
|
||||
private let pad: CGFloat = 18
|
||||
private let gx = 10
|
||||
private let gy = 8
|
||||
private var width: CGFloat { pad * 2 + CGFloat(gx) * cell }
|
||||
private var height: CGFloat { pad * 2 + CGFloat(gy) * cell }
|
||||
|
||||
private func screen(_ x: Int, _ y: Int) -> CGPoint {
|
||||
CGPoint(x: pad + CGFloat(x) * cell, y: height - pad - CGFloat(y) * cell)
|
||||
}
|
||||
|
||||
private func snap(_ loc: CGPoint) -> (Int, Int)? {
|
||||
let cx = (loc.x - pad) / cell
|
||||
let cy = (height - pad - loc.y) / cell
|
||||
let ix = Int(cx.rounded())
|
||||
let iy = Int(cy.rounded())
|
||||
if ix < 0 || ix > gx || iy < 0 || iy > gy { return nil }
|
||||
if abs(cx - CGFloat(ix)) > 0.45 || abs(cy - CGFloat(iy)) > 0.45 { return nil }
|
||||
return (ix, iy)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Canvas { ctx, _ in
|
||||
let rect = CGRect(x: 0, y: 0, width: width, height: height)
|
||||
ctx.fill(Path(rect), with: .color(palette.surface1))
|
||||
var grid = Path()
|
||||
for i in 0...gx {
|
||||
let x = pad + CGFloat(i) * cell
|
||||
grid.move(to: CGPoint(x: x, y: pad))
|
||||
grid.addLine(to: CGPoint(x: x, y: height - pad))
|
||||
}
|
||||
for i in 0...gy {
|
||||
let y = pad + CGFloat(i) * cell
|
||||
grid.move(to: CGPoint(x: pad, y: y))
|
||||
grid.addLine(to: CGPoint(x: width - pad, y: y))
|
||||
}
|
||||
ctx.stroke(grid, with: .color(palette.subtext.opacity(0.55)), lineWidth: 1)
|
||||
var axes = Path()
|
||||
axes.move(to: CGPoint(x: pad, y: height - pad))
|
||||
axes.addLine(to: CGPoint(x: width - pad, y: height - pad))
|
||||
axes.move(to: CGPoint(x: pad, y: height - pad))
|
||||
axes.addLine(to: CGPoint(x: pad, y: pad))
|
||||
ctx.stroke(axes, with: .color(palette.text), lineWidth: 1.6)
|
||||
if given.count >= 2 {
|
||||
var seg = Path()
|
||||
seg.move(to: screen(given[0].0, given[0].1))
|
||||
seg.addLine(to: screen(given[1].0, given[1].1))
|
||||
if given.count >= 3 {
|
||||
seg.move(to: screen(given[0].0, given[0].1))
|
||||
seg.addLine(to: screen(given[2].0, given[2].1))
|
||||
}
|
||||
ctx.stroke(seg, with: .color(palette.accent), lineWidth: 2)
|
||||
}
|
||||
let names = ["A", "B", "C", "D"]
|
||||
for (i, g) in given.enumerated() where i < names.count {
|
||||
let pt = screen(g.0, g.1)
|
||||
ctx.fill(Path(ellipseIn: CGRect(x: pt.x - 5.5, y: pt.y - 5.5, width: 11, height: 11)), with: .color(palette.accent))
|
||||
ctx.draw(
|
||||
Text(names[i]).font(.system(size: 13, weight: .semibold)).foregroundStyle(palette.text),
|
||||
at: CGPoint(x: pt.x + 8, y: pt.y - 8),
|
||||
anchor: .bottomLeading
|
||||
)
|
||||
}
|
||||
let dot = mark == false ? palette.error : palette.success
|
||||
for (i, u) in user.enumerated() {
|
||||
let pt = screen(u.0, u.1)
|
||||
ctx.fill(Path(ellipseIn: CGRect(x: pt.x - 5.5, y: pt.y - 5.5, width: 11, height: 11)), with: .color(dot))
|
||||
ctx.draw(
|
||||
Text("\(i + 1)").font(.system(size: 12, weight: .bold)).foregroundStyle(palette.text),
|
||||
at: CGPoint(x: pt.x + 8, y: pt.y - 8),
|
||||
anchor: .bottomLeading
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(width: width, height: height)
|
||||
.contentShape(Rectangle())
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0).onEnded { value in
|
||||
guard let hit = snap(value.location) else { return }
|
||||
if given.contains(where: { $0.0 == hit.0 && $0.1 == hit.1 }) { return }
|
||||
var next = user
|
||||
if let idx = next.firstIndex(where: { $0.0 == hit.0 && $0.1 == hit.1 }) {
|
||||
next.remove(at: idx)
|
||||
} else if next.count < need {
|
||||
next.append(hit)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
onChange(next)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct FyzQuizScreen: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
let year: Int
|
||||
|
|
|
|||
|
|
@ -699,6 +699,13 @@ private func subjectSum(_ vm: AppModel, _ s: J) -> ProgressSum {
|
|||
sum.totalEx = vm.content.chem.count + vm.content.bio.count
|
||||
sum.doneEx = vm.content.chem.filter { vm.progress.chemDone($0.int("id")) }.count
|
||||
+ vm.content.bio.filter { vm.progress.bioDone($0.int("id")) }.count
|
||||
case "matyears", "mat0map", "matmap", "mat2map", "mat3map", "mat4map":
|
||||
sum.totalEx = vm.content.mat0.count + vm.content.mat.count + vm.content.mat2.count + vm.content.mat3.count + vm.content.mat4.count
|
||||
sum.doneEx = vm.content.mat0.filter { vm.progress.mat0Done($0.int("id")) }.count
|
||||
+ vm.content.mat.filter { vm.progress.matDone($0.int("id")) }.count
|
||||
+ vm.content.mat2.filter { vm.progress.mat2Done($0.int("id")) }.count
|
||||
+ vm.content.mat3.filter { vm.progress.mat3Done($0.int("id")) }.count
|
||||
+ vm.content.mat4.filter { vm.progress.mat4Done($0.int("id")) }.count
|
||||
case "fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map":
|
||||
sum.totalEx = vm.content.fyz.count + vm.content.fyz2.count + vm.content.fyz3.count + vm.content.fyz4.count
|
||||
sum.doneEx = vm.content.fyz.filter { vm.progress.fyzDone($0.int("id")) }.count
|
||||
|
|
@ -935,6 +942,32 @@ private func buildSearch(_ vm: AppModel) -> [Hit] {
|
|||
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "lit4unit\(l.int("id"))", extra: "literatura")
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit4ex\(l.int("id"))")
|
||||
}
|
||||
add(vm.tr("Matematika"), vm.tr("search_lesson"), "matyears", extra: "matematika")
|
||||
add(vm.tr("mat0_year"), vm.tr("search_lesson"), "mat0map", extra: "zakladka")
|
||||
add(vm.tr("mat_year1"), vm.tr("search_lesson"), "matmap", extra: "rocnik")
|
||||
add(vm.tr("mat_year2"), vm.tr("search_lesson"), "mat2map", extra: "rocnik")
|
||||
add(vm.tr("mat_year3"), vm.tr("search_lesson"), "mat3map", extra: "rocnik")
|
||||
add(vm.tr("mat_year4"), vm.tr("search_lesson"), "mat4map", extra: "rocnik")
|
||||
for l in vm.content.mat0 {
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat0unit\(l.int("id"))", extra: "matematika")
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat0ex\(l.int("id"))")
|
||||
}
|
||||
for l in vm.content.mat {
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "matunit\(l.int("id"))", extra: "matematika")
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "matex\(l.int("id"))")
|
||||
}
|
||||
for l in vm.content.mat2 {
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat2unit\(l.int("id"))", extra: "matematika")
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat2ex\(l.int("id"))")
|
||||
}
|
||||
for l in vm.content.mat3 {
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat3unit\(l.int("id"))", extra: "matematika")
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat3ex\(l.int("id"))")
|
||||
}
|
||||
for l in vm.content.mat4 {
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat4unit\(l.int("id"))", extra: "matematika")
|
||||
add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat4ex\(l.int("id"))")
|
||||
}
|
||||
add(vm.tr("Fyzika"), vm.tr("search_lesson"), "fyzyears", extra: "fyzika")
|
||||
add(vm.tr("fyz_year1"), vm.tr("search_lesson"), "fyzmap", extra: "rocnik")
|
||||
add(vm.tr("fyz_year2"), vm.tr("search_lesson"), "fyz2map", extra: "rocnik")
|
||||
|
|
@ -1512,6 +1545,51 @@ struct LitYearsScreen: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct MatYearsScreen: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
let years: [(String, String, Route, String)] = [
|
||||
("mat0_year", "mat0_sub", .mat0Map, "ZŠ"),
|
||||
("mat_year1", "mat_sub", .matMap, "1."),
|
||||
("mat_year2", "mat2_sub", .mat2Map, "2."),
|
||||
("mat_year3", "mat3_sub", .mat3Map, "3."),
|
||||
("mat_year4", "mat4_sub", .mat4Map, "4."),
|
||||
]
|
||||
MacFillGrid(count: years.count, minWidth: 260, minHeight: 180) { i in
|
||||
NavigationLink(value: years[i].2) {
|
||||
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: years[i].3)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.navigationTitle(vm.tr("Matematika"))
|
||||
.navigationSubtitle(vm.tr("mat_years_sub"))
|
||||
#else
|
||||
List {
|
||||
NavigationLink(value: Route.mat0Map) {
|
||||
ListRowLabel(title: vm.tr("mat0_year"), subtitle: vm.tr("mat0_sub"))
|
||||
}
|
||||
NavigationLink(value: Route.matMap) {
|
||||
ListRowLabel(title: vm.tr("mat_year1"), subtitle: vm.tr("mat_sub"))
|
||||
}
|
||||
NavigationLink(value: Route.mat2Map) {
|
||||
ListRowLabel(title: vm.tr("mat_year2"), subtitle: vm.tr("mat2_sub"))
|
||||
}
|
||||
NavigationLink(value: Route.mat3Map) {
|
||||
ListRowLabel(title: vm.tr("mat_year3"), subtitle: vm.tr("mat3_sub"))
|
||||
}
|
||||
NavigationLink(value: Route.mat4Map) {
|
||||
ListRowLabel(title: vm.tr("mat_year4"), subtitle: vm.tr("mat4_sub"))
|
||||
}
|
||||
}
|
||||
.navigationTitle(vm.tr("Matematika"))
|
||||
.navigationSubtitle(vm.tr("mat_years_sub"))
|
||||
.appListStyle()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
struct FyzYearsScreen: View {
|
||||
@ObservedObject var vm: AppModel
|
||||
|
||||
|
|
@ -2321,6 +2399,88 @@ struct RouteDestination: View {
|
|||
}
|
||||
case .bioEx(let id):
|
||||
SciQuizScreen(vm: vm, area: "bio", id: id)
|
||||
case .matYears:
|
||||
MatYearsScreen(vm: vm)
|
||||
case .mat0Map:
|
||||
LessonListScreen(
|
||||
vm: vm,
|
||||
title: vm.tr("mat0_year"),
|
||||
subtitle: vm.tr("mat0_sub"),
|
||||
rows: vm.content.mat0.map { l in
|
||||
let id = l.int("id")
|
||||
return LessonRow(id: "m0\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat0Done(id), destination: .mat0Lesson(id))
|
||||
}
|
||||
)
|
||||
case .mat0Lesson(let id):
|
||||
if let l = vm.content.mat0Lesson(id) {
|
||||
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat0Ex(id))
|
||||
}
|
||||
case .mat0Ex(let id):
|
||||
MathPracticeScreen(vm: vm, year: 0, id: id)
|
||||
case .matMap:
|
||||
LessonListScreen(
|
||||
vm: vm,
|
||||
title: vm.tr("mat_year1"),
|
||||
subtitle: vm.tr("mat_sub"),
|
||||
rows: vm.content.mat.map { l in
|
||||
let id = l.int("id")
|
||||
return LessonRow(id: "m1\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.matDone(id), destination: .matLesson(id))
|
||||
}
|
||||
)
|
||||
case .matLesson(let id):
|
||||
if let l = vm.content.matLesson(id) {
|
||||
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .matEx(id))
|
||||
}
|
||||
case .matEx(let id):
|
||||
MathPracticeScreen(vm: vm, year: 1, id: id)
|
||||
case .mat2Map:
|
||||
LessonListScreen(
|
||||
vm: vm,
|
||||
title: vm.tr("mat_year2"),
|
||||
subtitle: vm.tr("mat2_sub"),
|
||||
rows: vm.content.mat2.map { l in
|
||||
let id = l.int("id")
|
||||
return LessonRow(id: "m2\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat2Done(id), destination: .mat2Lesson(id))
|
||||
}
|
||||
)
|
||||
case .mat2Lesson(let id):
|
||||
if let l = vm.content.mat2Lesson(id) {
|
||||
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat2Ex(id))
|
||||
}
|
||||
case .mat2Ex(let id):
|
||||
MathPracticeScreen(vm: vm, year: 2, id: id)
|
||||
case .mat3Map:
|
||||
LessonListScreen(
|
||||
vm: vm,
|
||||
title: vm.tr("mat_year3"),
|
||||
subtitle: vm.tr("mat3_sub"),
|
||||
rows: vm.content.mat3.map { l in
|
||||
let id = l.int("id")
|
||||
return LessonRow(id: "m3\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat3Done(id), destination: .mat3Lesson(id))
|
||||
}
|
||||
)
|
||||
case .mat3Lesson(let id):
|
||||
if let l = vm.content.mat3Lesson(id) {
|
||||
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat3Ex(id))
|
||||
}
|
||||
case .mat3Ex(let id):
|
||||
MathPracticeScreen(vm: vm, year: 3, id: id)
|
||||
case .mat4Map:
|
||||
LessonListScreen(
|
||||
vm: vm,
|
||||
title: vm.tr("mat_year4"),
|
||||
subtitle: vm.tr("mat4_sub"),
|
||||
rows: vm.content.mat4.map { l in
|
||||
let id = l.int("id")
|
||||
return LessonRow(id: "m4\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat4Done(id), destination: .mat4Lesson(id))
|
||||
}
|
||||
)
|
||||
case .mat4Lesson(let id):
|
||||
if let l = vm.content.mat4Lesson(id) {
|
||||
SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat4Ex(id))
|
||||
}
|
||||
case .mat4Ex(let id):
|
||||
MathPracticeScreen(vm: vm, year: 4, id: id)
|
||||
case .fyzYears:
|
||||
FyzYearsScreen(vm: vm)
|
||||
case .fyzMap:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue