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
|
|
@ -191,6 +191,31 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
|
|||
refresh()
|
||||
}
|
||||
|
||||
fun markMat0(id: Int) {
|
||||
progress.markMat0(id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun markMat(id: Int) {
|
||||
progress.markMat(id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun markMat2(id: Int) {
|
||||
progress.markMat2(id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun markMat3(id: Int) {
|
||||
progress.markMat3(id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun markMat4(id: Int) {
|
||||
progress.markMat4(id)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun markFyz4(id: Int) {
|
||||
progress.markFyz4(id)
|
||||
refresh()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import org.maturita.maturita.ui.HwQuizScreen
|
|||
import org.maturita.maturita.ui.HwYearsScreen
|
||||
import org.maturita.maturita.ui.LessonListScreen
|
||||
import org.maturita.maturita.ui.FyzQuizScreen
|
||||
import org.maturita.maturita.ui.MathPracticeScreen
|
||||
import org.maturita.maturita.ui.MatYearsScreen
|
||||
import org.maturita.maturita.ui.FyzYearsScreen
|
||||
import org.maturita.maturita.ui.LitYearsScreen
|
||||
import org.maturita.maturita.ui.LessonRow
|
||||
|
|
@ -350,6 +352,77 @@ private fun MaturitaAppBody(vm: AppViewModel) {
|
|||
}
|
||||
}
|
||||
is Route.Lit4Ex -> LitQuizScreen(vm, 4, r.id)
|
||||
Route.MatYears -> MatYearsScreen(vm)
|
||||
Route.Mat0Map -> LessonListScreen(
|
||||
vm, vm.tr("mat0_year"), vm.tr("mat0_sub"),
|
||||
vm.content.mat0.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat0Done(id), Route.Mat0Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Mat0Lesson -> {
|
||||
val l = vm.content.mat0Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Mat0Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Mat0Ex -> MathPracticeScreen(vm, 0, r.id)
|
||||
Route.MatMap -> LessonListScreen(
|
||||
vm, vm.tr("mat_year1"), vm.tr("mat_sub"),
|
||||
vm.content.mat.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.matDone(id), Route.MatLesson(id))
|
||||
},
|
||||
)
|
||||
is Route.MatLesson -> {
|
||||
val l = vm.content.matLesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.MatEx(r.id))
|
||||
}
|
||||
}
|
||||
is Route.MatEx -> MathPracticeScreen(vm, 1, r.id)
|
||||
Route.Mat2Map -> LessonListScreen(
|
||||
vm, vm.tr("mat_year2"), vm.tr("mat2_sub"),
|
||||
vm.content.mat2.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat2Done(id), Route.Mat2Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Mat2Lesson -> {
|
||||
val l = vm.content.mat2Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Mat2Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Mat2Ex -> MathPracticeScreen(vm, 2, r.id)
|
||||
Route.Mat3Map -> LessonListScreen(
|
||||
vm, vm.tr("mat_year3"), vm.tr("mat3_sub"),
|
||||
vm.content.mat3.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat3Done(id), Route.Mat3Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Mat3Lesson -> {
|
||||
val l = vm.content.mat3Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Mat3Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Mat3Ex -> MathPracticeScreen(vm, 3, r.id)
|
||||
Route.Mat4Map -> LessonListScreen(
|
||||
vm, vm.tr("mat_year4"), vm.tr("mat4_sub"),
|
||||
vm.content.mat4.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat4Done(id), Route.Mat4Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Mat4Lesson -> {
|
||||
val l = vm.content.mat4Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Mat4Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Mat4Ex -> MathPracticeScreen(vm, 4, r.id)
|
||||
Route.FyzYears -> FyzYearsScreen(vm)
|
||||
Route.FyzMap -> LessonListScreen(
|
||||
vm, vm.tr("fyz_year1"), vm.tr("fyz_sub"),
|
||||
|
|
|
|||
|
|
@ -50,6 +50,22 @@ sealed class Route(val page: String) {
|
|||
data object BioMap : Route("biomap")
|
||||
data class BioLesson(val id: Int) : Route("biounit$id")
|
||||
data class BioEx(val id: Int) : Route("bioex$id")
|
||||
data object MatYears : Route("matyears")
|
||||
data object Mat0Map : Route("mat0map")
|
||||
data class Mat0Lesson(val id: Int) : Route("mat0unit$id")
|
||||
data class Mat0Ex(val id: Int) : Route("mat0ex$id")
|
||||
data object MatMap : Route("matmap")
|
||||
data class MatLesson(val id: Int) : Route("matunit$id")
|
||||
data class MatEx(val id: Int) : Route("matex$id")
|
||||
data object Mat2Map : Route("mat2map")
|
||||
data class Mat2Lesson(val id: Int) : Route("mat2unit$id")
|
||||
data class Mat2Ex(val id: Int) : Route("mat2ex$id")
|
||||
data object Mat3Map : Route("mat3map")
|
||||
data class Mat3Lesson(val id: Int) : Route("mat3unit$id")
|
||||
data class Mat3Ex(val id: Int) : Route("mat3ex$id")
|
||||
data object Mat4Map : Route("mat4map")
|
||||
data class Mat4Lesson(val id: Int) : Route("mat4unit$id")
|
||||
data class Mat4Ex(val id: Int) : Route("mat4ex$id")
|
||||
data object FyzYears : Route("fyzyears")
|
||||
data object FyzMap : Route("fyzmap")
|
||||
data class FyzLesson(val id: Int) : Route("fyzunit$id")
|
||||
|
|
@ -83,7 +99,10 @@ sealed class Route(val page: String) {
|
|||
this is Lit3Lesson || this is Lit3Ex || this is Lit4Lesson || this is Lit4Ex ||
|
||||
this is ChemLesson || this is ChemEx || this is BioLesson || this is BioEx ||
|
||||
this is FyzLesson || this is FyzEx || this is Fyz2Lesson || this is Fyz2Ex ||
|
||||
this is Fyz3Lesson || this is Fyz3Ex || this is Fyz4Lesson || this is Fyz4Ex
|
||||
this is Fyz3Lesson || this is Fyz3Ex || this is Fyz4Lesson || this is Fyz4Ex ||
|
||||
this is Mat0Lesson || this is Mat0Ex || this is MatLesson || this is MatEx ||
|
||||
this is Mat2Lesson || this is Mat2Ex || this is Mat3Lesson || this is Mat3Ex ||
|
||||
this is Mat4Lesson || this is Mat4Ex
|
||||
|
||||
/** Full stack from the practice root to this screen, so back always goes up one level. */
|
||||
fun stack(): List<Route> = when (this) {
|
||||
|
|
@ -135,6 +154,22 @@ sealed class Route(val page: String) {
|
|||
BioMap -> listOf(Subjects, SciMap, BioMap)
|
||||
is BioLesson -> listOf(Subjects, SciMap, BioMap, this)
|
||||
is BioEx -> listOf(Subjects, SciMap, BioMap, BioLesson(id), this)
|
||||
MatYears -> listOf(Subjects, MatYears)
|
||||
Mat0Map -> listOf(Subjects, MatYears, Mat0Map)
|
||||
is Mat0Lesson -> listOf(Subjects, MatYears, Mat0Map, this)
|
||||
is Mat0Ex -> listOf(Subjects, MatYears, Mat0Map, Mat0Lesson(id), this)
|
||||
MatMap -> listOf(Subjects, MatYears, MatMap)
|
||||
is MatLesson -> listOf(Subjects, MatYears, MatMap, this)
|
||||
is MatEx -> listOf(Subjects, MatYears, MatMap, MatLesson(id), this)
|
||||
Mat2Map -> listOf(Subjects, MatYears, Mat2Map)
|
||||
is Mat2Lesson -> listOf(Subjects, MatYears, Mat2Map, this)
|
||||
is Mat2Ex -> listOf(Subjects, MatYears, Mat2Map, Mat2Lesson(id), this)
|
||||
Mat3Map -> listOf(Subjects, MatYears, Mat3Map)
|
||||
is Mat3Lesson -> listOf(Subjects, MatYears, Mat3Map, this)
|
||||
is Mat3Ex -> listOf(Subjects, MatYears, Mat3Map, Mat3Lesson(id), this)
|
||||
Mat4Map -> listOf(Subjects, MatYears, Mat4Map)
|
||||
is Mat4Lesson -> listOf(Subjects, MatYears, Mat4Map, this)
|
||||
is Mat4Ex -> listOf(Subjects, MatYears, Mat4Map, Mat4Lesson(id), this)
|
||||
FyzYears -> listOf(Subjects, FyzYears)
|
||||
FyzMap -> listOf(Subjects, FyzYears, FyzMap)
|
||||
is FyzLesson -> listOf(Subjects, FyzYears, FyzMap, this)
|
||||
|
|
@ -181,6 +216,12 @@ fun routeFromPage(page: String): Route? = when (page) {
|
|||
"scimap" -> Route.SciMap
|
||||
"chemmap" -> Route.ChemMap
|
||||
"biomap" -> Route.BioMap
|
||||
"matyears" -> Route.MatYears
|
||||
"mat0map" -> Route.Mat0Map
|
||||
"matmap" -> Route.MatMap
|
||||
"mat2map" -> Route.Mat2Map
|
||||
"mat3map" -> Route.Mat3Map
|
||||
"mat4map" -> Route.Mat4Map
|
||||
"fyzyears" -> Route.FyzYears
|
||||
"fyzmap" -> Route.FyzMap
|
||||
"fyz2map" -> Route.Fyz2Map
|
||||
|
|
@ -229,6 +270,36 @@ fun routeFromPage(page: String): Route? = when (page) {
|
|||
Regex("""bioex(\d+)""").matchEntire(page)?.let {
|
||||
return Route.BioEx(it.groupValues[1].toInt())
|
||||
}
|
||||
Regex("""mat0unit(\d+)""").matchEntire(page)?.let {
|
||||
return Route.Mat0Lesson(it.groupValues[1].toInt())
|
||||
}
|
||||
Regex("""mat0ex(\d+)""").matchEntire(page)?.let {
|
||||
return Route.Mat0Ex(it.groupValues[1].toInt())
|
||||
}
|
||||
Regex("""mat4unit(\d+)""").matchEntire(page)?.let {
|
||||
return Route.Mat4Lesson(it.groupValues[1].toInt())
|
||||
}
|
||||
Regex("""mat4ex(\d+)""").matchEntire(page)?.let {
|
||||
return Route.Mat4Ex(it.groupValues[1].toInt())
|
||||
}
|
||||
Regex("""mat3unit(\d+)""").matchEntire(page)?.let {
|
||||
return Route.Mat3Lesson(it.groupValues[1].toInt())
|
||||
}
|
||||
Regex("""mat3ex(\d+)""").matchEntire(page)?.let {
|
||||
return Route.Mat3Ex(it.groupValues[1].toInt())
|
||||
}
|
||||
Regex("""mat2unit(\d+)""").matchEntire(page)?.let {
|
||||
return Route.Mat2Lesson(it.groupValues[1].toInt())
|
||||
}
|
||||
Regex("""mat2ex(\d+)""").matchEntire(page)?.let {
|
||||
return Route.Mat2Ex(it.groupValues[1].toInt())
|
||||
}
|
||||
Regex("""matunit(\d+)""").matchEntire(page)?.let {
|
||||
return Route.MatLesson(it.groupValues[1].toInt())
|
||||
}
|
||||
Regex("""matex(\d+)""").matchEntire(page)?.let {
|
||||
return Route.MatEx(it.groupValues[1].toInt())
|
||||
}
|
||||
Regex("""fyz4unit(\d+)""").matchEntire(page)?.let {
|
||||
return Route.Fyz4Lesson(it.groupValues[1].toInt())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,3 +63,656 @@ fun hasUmlaut(s: String?): Boolean {
|
|||
return s.any { it == 'ä' || it == 'ö' || it == 'ü' || it == 'ß' ||
|
||||
it == 'Ä' || it == 'Ö' || it == 'Ü' }
|
||||
}
|
||||
|
||||
// Same rules as src/math_check.c. Do not route numbers through normalizeAnswer.
|
||||
|
||||
private data class MFrac(val n: Long = 0, val d: Long = 1)
|
||||
|
||||
private class MTerm(
|
||||
var ok: Boolean = true,
|
||||
var a: MFrac = MFrac(),
|
||||
var b: MFrac = MFrac(),
|
||||
var rad: Long = 0,
|
||||
var p: MFrac = MFrac(),
|
||||
)
|
||||
|
||||
private fun mGcd(a0: Long, b0: Long): Long {
|
||||
var a = if (a0 < 0) -a0 else a0
|
||||
var b = if (b0 < 0) -b0 else b0
|
||||
while (b != 0L) {
|
||||
val t = a % b
|
||||
a = b
|
||||
b = t
|
||||
}
|
||||
return if (a == 0L) 1 else a
|
||||
}
|
||||
|
||||
private fun mMul(a: Long, b: Long): Long? = try {
|
||||
Math.multiplyExact(a, b)
|
||||
} catch (_: ArithmeticException) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun mAddL(a: Long, b: Long): Long? = try {
|
||||
Math.addExact(a, b)
|
||||
} catch (_: ArithmeticException) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun mSet(n0: Long, d0: Long): MFrac? {
|
||||
if (d0 == 0L) return null
|
||||
var n = n0
|
||||
var d = d0
|
||||
if (d < 0) {
|
||||
n = -n
|
||||
d = -d
|
||||
}
|
||||
val g = mGcd(n, d)
|
||||
return MFrac(n / g, d / g)
|
||||
}
|
||||
|
||||
private fun mAddF(x: MFrac, y: MFrac): MFrac? {
|
||||
val xn = mMul(x.n, y.d) ?: return null
|
||||
val yn = mMul(y.n, x.d) ?: return null
|
||||
val d = mMul(x.d, y.d) ?: return null
|
||||
val s = mAddL(xn, yn) ?: return null
|
||||
return mSet(s, d)
|
||||
}
|
||||
|
||||
private fun mMulF(x: MFrac, y: MFrac): MFrac? {
|
||||
val n = mMul(x.n, y.n) ?: return null
|
||||
val d = mMul(x.d, y.d) ?: return null
|
||||
return mSet(n, d)
|
||||
}
|
||||
|
||||
private fun mDivF(x: MFrac, y: MFrac): MFrac? {
|
||||
if (y.n == 0L) return null
|
||||
return mMulF(x, MFrac(y.d, y.n))
|
||||
}
|
||||
|
||||
private fun mSameF(x: MFrac, y: MFrac) = x.n == y.n && x.d == y.d
|
||||
|
||||
private fun mPow(b: Long, e: Int): Long {
|
||||
var r = 1L
|
||||
var i = 0
|
||||
while (i < e) {
|
||||
r *= b
|
||||
i++
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
private fun mSqrtReduce(n0: Long): Pair<Long, Long>? {
|
||||
if (n0 < 0 || n0 > 1_000_000) return null
|
||||
var n = n0
|
||||
var out = 1L
|
||||
var f = 2L
|
||||
while (f * f <= n) {
|
||||
var c = 0
|
||||
while (n % f == 0L) {
|
||||
n /= f
|
||||
c++
|
||||
}
|
||||
if (c >= 2) out *= mPow(f, c / 2)
|
||||
if (c % 2 == 1) n *= f
|
||||
f = if (f == 2L) 3 else f + 2
|
||||
if (f > 10000) break
|
||||
}
|
||||
return out to n
|
||||
}
|
||||
|
||||
private fun mNorm(t: MTerm) {
|
||||
if (!t.ok) return
|
||||
if (t.rad > 1) {
|
||||
val s = mSqrtReduce(t.rad) ?: run { t.ok = false; return }
|
||||
t.b = mMulF(t.b, MFrac(s.first, 1)) ?: run { t.ok = false; return }
|
||||
t.rad = s.second
|
||||
}
|
||||
if (t.rad <= 1) {
|
||||
t.a = mAddF(t.a, t.b) ?: run { t.ok = false; return }
|
||||
t.b = MFrac()
|
||||
t.rad = 0
|
||||
}
|
||||
if (t.b.n == 0L) t.rad = 0
|
||||
}
|
||||
|
||||
private fun mFail() = MTerm(ok = false)
|
||||
|
||||
private fun mAdd(x: MTerm, y: MTerm): MTerm {
|
||||
if (!x.ok || !y.ok) return mFail()
|
||||
val o = MTerm()
|
||||
o.a = mAddF(x.a, y.a) ?: return mFail()
|
||||
o.p = mAddF(x.p, y.p) ?: return mFail()
|
||||
if (x.rad == 0L && y.rad == 0L) {
|
||||
o.b = MFrac()
|
||||
o.rad = 0
|
||||
} else if (x.rad == 0L) {
|
||||
o.b = y.b
|
||||
o.rad = y.rad
|
||||
} else if (y.rad == 0L || x.rad == y.rad) {
|
||||
o.b = mAddF(x.b, y.b) ?: return mFail()
|
||||
o.rad = x.rad
|
||||
} else return mFail()
|
||||
mNorm(o)
|
||||
return o
|
||||
}
|
||||
|
||||
private fun mScale(x: MTerm, f: MFrac): MTerm {
|
||||
if (!x.ok) return mFail()
|
||||
val o = MTerm()
|
||||
o.a = mMulF(x.a, f) ?: return mFail()
|
||||
o.b = mMulF(x.b, f) ?: return mFail()
|
||||
o.p = mMulF(x.p, f) ?: return mFail()
|
||||
o.rad = x.rad
|
||||
mNorm(o)
|
||||
return o
|
||||
}
|
||||
|
||||
private fun mMulT(x: MTerm, y: MTerm): MTerm {
|
||||
if (!x.ok || !y.ok) return mFail()
|
||||
if ((x.p.n != 0L && (y.p.n != 0L || y.rad != 0L)) ||
|
||||
(y.p.n != 0L && (x.p.n != 0L || x.rad != 0L))
|
||||
) return mFail()
|
||||
if (x.rad != 0L && y.rad != 0L && x.rad != y.rad) return mFail()
|
||||
val o = MTerm()
|
||||
o.a = mMulF(x.a, y.a) ?: return mFail()
|
||||
if (x.rad == 0L && y.rad == 0L) {
|
||||
o.b = MFrac()
|
||||
o.rad = 0
|
||||
} else if (x.rad == 0L) {
|
||||
o.b = mMulF(x.a, y.b) ?: return mFail()
|
||||
o.rad = y.rad
|
||||
} else if (y.rad == 0L) {
|
||||
o.b = mMulF(y.a, x.b) ?: return mFail()
|
||||
o.rad = x.rad
|
||||
} else {
|
||||
val ab = mMulF(x.a, y.b) ?: return mFail()
|
||||
val ba = mMulF(x.b, y.a) ?: return mFail()
|
||||
val cross = mAddF(ab, ba) ?: return mFail()
|
||||
val sq = mMul(x.rad, y.rad) ?: return mFail()
|
||||
val bb = mMulF(x.b, y.b) ?: return mFail()
|
||||
val extra = mMulF(bb, MFrac(sq, 1)) ?: return mFail()
|
||||
o.a = mAddF(o.a, extra) ?: return mFail()
|
||||
o.b = cross
|
||||
o.rad = x.rad
|
||||
}
|
||||
o.p = if (x.p.n != 0L) x.p else y.p
|
||||
if ((y.a.n != 1L || y.a.d != 1L || y.b.n != 0L || y.rad != 0L || y.p.n != 0L) && x.p.n != 0L) {
|
||||
o.p = mMulF(x.p, y.a) ?: return mFail()
|
||||
}
|
||||
if ((x.a.n != 1L || x.a.d != 1L || x.b.n != 0L || x.rad != 0L || x.p.n != 0L) &&
|
||||
y.p.n != 0L && x.p.n == 0L
|
||||
) {
|
||||
o.p = mMulF(y.p, x.a) ?: return mFail()
|
||||
}
|
||||
mNorm(o)
|
||||
return o
|
||||
}
|
||||
|
||||
private fun mDivT(x: MTerm, y: MTerm): MTerm {
|
||||
if (!x.ok || !y.ok || y.p.n != 0L) return mFail()
|
||||
if (y.rad == 0L) {
|
||||
val f = mDivF(MFrac(1, 1), y.a) ?: return mFail()
|
||||
return mScale(x, f)
|
||||
}
|
||||
if (x.p.n != 0L) return mFail()
|
||||
val yb = mMulF(y.b, MFrac(y.rad, 1)) ?: return mFail()
|
||||
val bb = mMulF(y.b, yb) ?: return mFail()
|
||||
val aa = mMulF(y.a, y.a) ?: return mFail()
|
||||
val den = mAddF(aa, MFrac(-bb.n, bb.d)) ?: return mFail()
|
||||
val num = MTerm()
|
||||
num.a = y.a
|
||||
num.b = MFrac(-y.b.n, y.b.d)
|
||||
num.rad = y.rad
|
||||
val inv = mDivF(MFrac(1, 1), den) ?: return mFail()
|
||||
return mMulT(x, mScale(num, inv))
|
||||
}
|
||||
|
||||
private sealed class MTok {
|
||||
class Num(val n: Long, val d: Long) : MTok()
|
||||
object Sqrt : MTok()
|
||||
object Pi : MTok()
|
||||
object Add : MTok()
|
||||
object Sub : MTok()
|
||||
object Mul : MTok()
|
||||
object Div : MTok()
|
||||
object Lp : MTok()
|
||||
object Rp : MTok()
|
||||
}
|
||||
|
||||
private fun mLex(s: String): List<MTok>? {
|
||||
val toks = ArrayList<MTok>()
|
||||
var i = 0
|
||||
fun peek(k: Int) = if (i + k < s.length) s[i + k] else null
|
||||
while (i < s.length) {
|
||||
while (i < s.length && s[i].isWhitespace()) i++
|
||||
if (i >= s.length) break
|
||||
val ch = s[i]
|
||||
if (ch.isDigit() || ((ch == '.' || ch == ',') && peek(1)?.isDigit() == true)) {
|
||||
var whole = 0L
|
||||
var saw = false
|
||||
while (i < s.length && s[i].isDigit()) {
|
||||
saw = true
|
||||
whole = whole * 10 + (s[i] - '0')
|
||||
i++
|
||||
}
|
||||
if (i < s.length && (s[i] == '.' || s[i] == ',') && i + 1 < s.length && s[i + 1].isDigit()) {
|
||||
i++
|
||||
var frac = 0L
|
||||
var div = 1L
|
||||
while (i < s.length && s[i].isDigit()) {
|
||||
frac = frac * 10 + (s[i] - '0')
|
||||
div *= 10
|
||||
i++
|
||||
}
|
||||
val n = mAddL(mMul(whole, div) ?: return null, frac) ?: return null
|
||||
toks.add(MTok.Num(n, div))
|
||||
} else if (saw) {
|
||||
toks.add(MTok.Num(whole, 1))
|
||||
} else return null
|
||||
continue
|
||||
}
|
||||
when (ch) {
|
||||
'+' -> toks.add(MTok.Add)
|
||||
'-', '\u2212', '\u2013' -> toks.add(MTok.Sub)
|
||||
'*', '\u00D7', '\u00B7' -> toks.add(MTok.Mul)
|
||||
'/', ':' -> toks.add(MTok.Div)
|
||||
'(' -> toks.add(MTok.Lp)
|
||||
')' -> toks.add(MTok.Rp)
|
||||
'\u03C0' -> toks.add(MTok.Pi)
|
||||
'\u221A' -> toks.add(MTok.Sqrt)
|
||||
else -> {
|
||||
if (ch in 'A'..'Z' || ch in 'a'..'z') {
|
||||
val start = i
|
||||
while (i < s.length && (s[i] in 'A'..'Z' || s[i] in 'a'..'z')) i++
|
||||
when (s.substring(start, i).lowercase()) {
|
||||
"pi" -> toks.add(MTok.Pi)
|
||||
"sqrt" -> toks.add(MTok.Sqrt)
|
||||
else -> return null
|
||||
}
|
||||
continue
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
return toks
|
||||
}
|
||||
|
||||
private class MParse(val t: List<MTok>) {
|
||||
var i = 0
|
||||
fun peek() = if (i < t.size) t[i] else null
|
||||
fun take(): MTok? = if (i < t.size) t[i++] else null
|
||||
fun at(k: Int) = if (k in t.indices) t[k] else null
|
||||
|
||||
fun expr(): MTerm? {
|
||||
var left = term() ?: return null
|
||||
while (true) {
|
||||
val p = peek()
|
||||
if (p !is MTok.Add && p !is MTok.Sub) break
|
||||
take()
|
||||
val r = term() ?: return null
|
||||
left = if (p is MTok.Sub) mAdd(left, mScale(r, MFrac(-1, 1))) else mAdd(left, r)
|
||||
if (!left.ok) return null
|
||||
}
|
||||
return left
|
||||
}
|
||||
|
||||
fun term(): MTerm? {
|
||||
var left = unary() ?: return null
|
||||
while (true) {
|
||||
val p = peek()
|
||||
if (p !is MTok.Mul && p !is MTok.Div) break
|
||||
take()
|
||||
val r = unary() ?: return null
|
||||
left = if (p is MTok.Div) mDivT(left, r) else mMulT(left, r)
|
||||
if (!left.ok) return null
|
||||
}
|
||||
return left
|
||||
}
|
||||
|
||||
fun unary(): MTerm? {
|
||||
if (peek() is MTok.Sub) {
|
||||
take()
|
||||
val v = unary() ?: return null
|
||||
return mScale(v, MFrac(-1, 1))
|
||||
}
|
||||
if (peek() is MTok.Add) {
|
||||
take()
|
||||
return unary()
|
||||
}
|
||||
return factor()
|
||||
}
|
||||
|
||||
fun factor(): MTerm? {
|
||||
var left = primary() ?: return null
|
||||
while (true) {
|
||||
when (peek()) {
|
||||
is MTok.Sqrt, is MTok.Pi, is MTok.Lp -> {
|
||||
val r = primary() ?: return null
|
||||
left = mMulT(left, r)
|
||||
if (!left.ok) return null
|
||||
}
|
||||
is MTok.Num -> return null
|
||||
else -> return left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun primary(): MTerm? {
|
||||
when (val p = take()) {
|
||||
is MTok.Num -> {
|
||||
val n = p.n
|
||||
val d = p.d
|
||||
if (peek() is MTok.Div && at(i + 1) is MTok.Num) {
|
||||
take()
|
||||
val n2 = take() as? MTok.Num ?: return null
|
||||
if (n2.n <= 0 || n2.d != 1L || d != 1L) return null
|
||||
val o = MTerm()
|
||||
o.a = mSet(n, n2.n) ?: return null
|
||||
return o
|
||||
}
|
||||
if (d == 1L && n >= 0 && peek() is MTok.Num) {
|
||||
val saved = i
|
||||
val n2 = take() as MTok.Num
|
||||
val n3tok = at(i + 1)
|
||||
if (peek() is MTok.Div && n3tok is MTok.Num && n2.d == 1L && n3tok.d == 1L && n2.n >= 0 && n3tok.n > 0) {
|
||||
take()
|
||||
take()
|
||||
val frac = mSet(n2.n, n3tok.n) ?: return null
|
||||
val whole = MTerm()
|
||||
whole.a = MFrac(n, 1)
|
||||
val part = MTerm()
|
||||
part.a = frac
|
||||
return mAdd(whole, part)
|
||||
}
|
||||
i = saved
|
||||
}
|
||||
val o = MTerm()
|
||||
o.a = mSet(n, d) ?: return null
|
||||
return o
|
||||
}
|
||||
is MTok.Pi -> {
|
||||
val o = MTerm()
|
||||
o.p = MFrac(1, 1)
|
||||
return o
|
||||
}
|
||||
is MTok.Sqrt -> {
|
||||
if (peek() is MTok.Lp) {
|
||||
take()
|
||||
val inner = expr() ?: return null
|
||||
if (take() !is MTok.Rp) return null
|
||||
return mSqrtTerm(inner)
|
||||
}
|
||||
val num = peek()
|
||||
if (num is MTok.Num && num.d == 1L && num.n >= 0) {
|
||||
take()
|
||||
val inner = MTerm()
|
||||
inner.a = MFrac(num.n, 1)
|
||||
return mSqrtTerm(inner)
|
||||
}
|
||||
return null
|
||||
}
|
||||
is MTok.Lp -> {
|
||||
val inner = expr() ?: return null
|
||||
if (take() !is MTok.Rp) return null
|
||||
return inner
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mSqrtTerm(inner: MTerm): MTerm? {
|
||||
if (!inner.ok || inner.p.n != 0L || inner.rad != 0L || inner.b.n != 0L) return null
|
||||
if (inner.a.n < 0 || inner.a.d != 1L) return null
|
||||
val s = mSqrtReduce(inner.a.n) ?: return null
|
||||
val o = MTerm()
|
||||
o.b = MFrac(s.first, 1)
|
||||
o.rad = s.second
|
||||
mNorm(o)
|
||||
return if (o.ok) o else null
|
||||
}
|
||||
|
||||
private fun mStripUnits(raw: String): String {
|
||||
var s = raw.trim()
|
||||
val units = listOf("procenta", "procent", "cm", "mm", "dm", "km", "m")
|
||||
var cut = true
|
||||
while (cut) {
|
||||
cut = false
|
||||
s = s.trimEnd()
|
||||
if (s.endsWith("°") || s.endsWith("%") || s.endsWith(".")) {
|
||||
s = s.dropLast(1)
|
||||
cut = true
|
||||
continue
|
||||
}
|
||||
for (u in units) {
|
||||
if (s.length <= u.length) continue
|
||||
if (s.lowercase().endsWith(u)) {
|
||||
val keep = s.dropLast(u.length)
|
||||
if (keep.lastOrNull()?.isLetter() == true) continue
|
||||
s = keep
|
||||
cut = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return s.trim()
|
||||
}
|
||||
|
||||
private fun mPrep(raw: String): String {
|
||||
var s = raw.trim()
|
||||
val pairs = listOf('(' to ')', '[' to ']', '{' to '}')
|
||||
while (s.length >= 2 && pairs.any { it.first == s.first() && it.second == s.last() }) {
|
||||
s = s.substring(1, s.length - 1).trim()
|
||||
}
|
||||
return mStripUnits(s).trim()
|
||||
}
|
||||
|
||||
private fun mParseTerm(s: String): MTerm? {
|
||||
val text = mPrep(s)
|
||||
if (text.isEmpty()) return null
|
||||
val toks = mLex(text) ?: return null
|
||||
if (toks.isEmpty()) return null
|
||||
val p = MParse(toks)
|
||||
val v = p.expr() ?: return null
|
||||
if (p.i != toks.size || !v.ok) return null
|
||||
return v
|
||||
}
|
||||
|
||||
private fun mExact(x: MTerm, y: MTerm) =
|
||||
x.ok && y.ok && mSameF(x.a, y.a) && mSameF(x.b, y.b) && x.rad == y.rad && mSameF(x.p, y.p)
|
||||
|
||||
private fun mDouble(t: MTerm): Double {
|
||||
var v = t.a.n.toDouble() / t.a.d + t.p.n.toDouble() / t.p.d * Math.PI
|
||||
if (t.b.n != 0L && t.rad > 0) v += t.b.n.toDouble() / t.b.d * Math.sqrt(t.rad.toDouble())
|
||||
return v
|
||||
}
|
||||
|
||||
private fun mClose(x: MTerm, y: MTerm): Boolean {
|
||||
if (!x.ok || !y.ok) return false
|
||||
if (mExact(x, y)) return true
|
||||
if (x.p.n != 0L || y.p.n != 0L) return false
|
||||
val diff = Math.abs(mDouble(x) - mDouble(y))
|
||||
return if (x.b.n != 0L || y.b.n != 0L) diff <= 0.001 else diff <= 0.0005
|
||||
}
|
||||
|
||||
private fun mFold(s: String): String {
|
||||
val out = StringBuilder()
|
||||
for (ch in s.lowercase()) {
|
||||
when (ch) {
|
||||
'á', 'à', 'ä', 'â' -> out.append('a')
|
||||
'č', 'ć' -> out.append('c')
|
||||
'ď' -> out.append('d')
|
||||
'é', 'ě', 'ë', 'ê' -> out.append('e')
|
||||
'í', 'ï' -> out.append('i')
|
||||
'ň' -> out.append('n')
|
||||
'ó', 'ö', 'ô' -> out.append('o')
|
||||
'ř' -> out.append('r')
|
||||
'š', 'ś' -> out.append('s')
|
||||
'ť' -> out.append('t')
|
||||
'ú', 'ů', 'ü' -> out.append('u')
|
||||
'ý' -> out.append('y')
|
||||
'ž', 'ź' -> out.append('z')
|
||||
else -> if (ch.isLetter() || ch.isDigit()) out.append(ch)
|
||||
}
|
||||
}
|
||||
return out.toString()
|
||||
}
|
||||
|
||||
private fun mSplit(raw: String): List<String>? {
|
||||
val s = raw.trim()
|
||||
if (s.isEmpty()) return null
|
||||
val semi = s.contains(';')
|
||||
val parts = ArrayList<String>()
|
||||
var rest = s
|
||||
while (rest.isNotEmpty()) {
|
||||
val idx = if (semi) rest.indexOf(';') else rest.indexOf(", ")
|
||||
val step = if (semi) 1 else 2
|
||||
val piece: String
|
||||
val next: String
|
||||
if (idx < 0) {
|
||||
piece = rest.trim()
|
||||
next = ""
|
||||
} else {
|
||||
piece = rest.substring(0, idx).trim()
|
||||
next = rest.substring(idx + step)
|
||||
}
|
||||
if (piece.isEmpty()) return null
|
||||
parts.add(piece)
|
||||
if (idx < 0) break
|
||||
rest = next
|
||||
}
|
||||
return parts.ifEmpty { null }
|
||||
}
|
||||
|
||||
fun mathAnswerOk(spec: String?, user: String?): Boolean {
|
||||
if (spec == null || user == null) return false
|
||||
val bodySpec = spec.trim()
|
||||
if (bodySpec.startsWith("word:")) {
|
||||
val opts = bodySpec.removePrefix("word:").split('|').map { mFold(it) }
|
||||
return opts.contains(mFold(user))
|
||||
}
|
||||
if (bodySpec.startsWith("set:") || bodySpec.startsWith("pair:")) {
|
||||
val ordered = bodySpec.startsWith("pair:")
|
||||
val body = bodySpec.removePrefix(if (ordered) "pair:" else "set:")
|
||||
val want = mSplit(body) ?: return false
|
||||
val got = mSplit(user) ?: return false
|
||||
if (want.size != got.size || want.isEmpty()) return false
|
||||
val wt = want.map { mParseTerm(it) ?: return false }
|
||||
val gt = got.map { mParseTerm(it) ?: return false }
|
||||
if (ordered) return wt.indices.all { mClose(wt[it], gt[it]) }
|
||||
val used = BooleanArray(gt.size)
|
||||
for (w in wt) {
|
||||
var hit = gt.indices.firstOrNull { !used[it] && mExact(w, gt[it]) }
|
||||
if (hit == null) hit = gt.indices.firstOrNull { !used[it] && mClose(w, gt[it]) }
|
||||
if (hit == null) return false
|
||||
used[hit] = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (!bodySpec.startsWith("num:")) return false
|
||||
val tw = mParseTerm(bodySpec.removePrefix("num:")) ?: return false
|
||||
val tg = mParseTerm(user) ?: return false
|
||||
return mClose(tw, tg)
|
||||
}
|
||||
|
||||
fun mathIsDraw(spec: String?): Boolean {
|
||||
if (spec == null) return false
|
||||
return spec.startsWith("square:") || spec.startsWith("mid:") || spec.startsWith("right:") ||
|
||||
spec.startsWith("para:") || spec.startsWith("points:")
|
||||
}
|
||||
|
||||
fun mathDrawNeed(spec: String?): Int {
|
||||
if (spec == null) return 0
|
||||
if (spec.startsWith("square:")) return 2
|
||||
if (spec.startsWith("mid:") || spec.startsWith("right:") || spec.startsWith("para:")) return 1
|
||||
if (spec.startsWith("points:")) {
|
||||
return spec.removePrefix("points:").split(';').count { !it.startsWith("fix:") }
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun mParseXY(s: String): Pair<Int, Int>? {
|
||||
val bits = s.trim().split(',')
|
||||
if (bits.size != 2) return null
|
||||
val x = bits[0].trim().toIntOrNull() ?: return null
|
||||
val y = bits[1].trim().toIntOrNull() ?: return null
|
||||
return x to y
|
||||
}
|
||||
|
||||
fun mathDrawGiven(spec: String?): List<Pair<Int, Int>> {
|
||||
if (spec == null) return emptyList()
|
||||
if (spec.startsWith("points:")) {
|
||||
return spec.removePrefix("points:").split(';').mapNotNull { part ->
|
||||
if (!part.startsWith("fix:")) null else mParseXY(part.removePrefix("fix:"))
|
||||
}
|
||||
}
|
||||
val body: String
|
||||
val n: Int
|
||||
when {
|
||||
spec.startsWith("para:") -> { body = spec.removePrefix("para:"); n = 3 }
|
||||
spec.startsWith("square:") -> { body = spec.removePrefix("square:"); n = 2 }
|
||||
spec.startsWith("right:") -> { body = spec.removePrefix("right:"); n = 2 }
|
||||
spec.startsWith("mid:") -> { body = spec.removePrefix("mid:"); n = 2 }
|
||||
else -> return emptyList()
|
||||
}
|
||||
return body.split(';').take(n).mapNotNull { mParseXY(it) }
|
||||
}
|
||||
|
||||
private fun mOnBoard(p: Pair<Int, Int>) = p.first in 0..10 && p.second in 0..8
|
||||
|
||||
fun mathDrawOk(spec: String?, pts: List<Pair<Int, Int>>): Boolean {
|
||||
if (spec == null || mathDrawNeed(spec) != pts.size) return false
|
||||
if (pts.any { !mOnBoard(it) }) return false
|
||||
if (pts.map { "${it.first},${it.second}" }.toSet().size != pts.size) return false
|
||||
val g = mathDrawGiven(spec)
|
||||
if (spec.startsWith("mid:")) {
|
||||
if (g.size != 2 || (g[0].first + g[1].first) % 2 != 0 || (g[0].second + g[1].second) % 2 != 0) return false
|
||||
return pts[0] == (g[0].first + g[1].first) / 2 to (g[0].second + g[1].second) / 2
|
||||
}
|
||||
if (spec.startsWith("para:")) {
|
||||
if (g.size != 3) return false
|
||||
return pts[0] == g[1].first + g[2].first - g[0].first to g[1].second + g[2].second - g[0].second
|
||||
}
|
||||
if (spec.startsWith("right:")) {
|
||||
if (g.size != 2) return false
|
||||
val c = pts[0]
|
||||
if (c == g[0] || c == g[1]) return false
|
||||
val ux = g[0].first - c.first
|
||||
val uy = g[0].second - c.second
|
||||
val vx = g[1].first - c.first
|
||||
val vy = g[1].second - c.second
|
||||
return ux * vx + uy * vy == 0 && (ux != 0 || uy != 0) && (vx != 0 || vy != 0)
|
||||
}
|
||||
if (spec.startsWith("square:")) {
|
||||
if (g.size != 2 || pts.size != 2) return false
|
||||
val a = g[0]
|
||||
val b = g[1]
|
||||
val dx = b.first - a.first
|
||||
val dy = b.second - a.second
|
||||
if (dx == 0 && dy == 0) return false
|
||||
val c1 = b.first - dy to b.second + dx
|
||||
val d1 = a.first - dy to a.second + dx
|
||||
val c2 = b.first + dy to b.second - dx
|
||||
val d2 = a.first + dy to a.second - dx
|
||||
fun same(p: Pair<Int, Int>, q: Pair<Int, Int>, r: Pair<Int, Int>, s: Pair<Int, Int>) =
|
||||
(p == r && q == s) || (p == s && q == r)
|
||||
if ((!mOnBoard(c1) || !mOnBoard(d1)) && (!mOnBoard(c2) || !mOnBoard(d2))) return false
|
||||
return same(pts[0], pts[1], c1, d1) || same(pts[0], pts[1], c2, d2)
|
||||
}
|
||||
if (spec.startsWith("points:")) {
|
||||
val want = spec.removePrefix("points:").split(';').mapNotNull { part ->
|
||||
if (part.startsWith("fix:")) null else mParseXY(part)
|
||||
}
|
||||
if (want.size != pts.size) return false
|
||||
val used = BooleanArray(pts.size)
|
||||
for (w in want) {
|
||||
val j = pts.indices.firstOrNull { !used[it] && pts[it] == w } ?: return false
|
||||
used[j] = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ class Content(root: JSONObject) {
|
|||
val fyz2 = raw.arr("fyz2")
|
||||
val fyz3 = raw.arr("fyz3")
|
||||
val fyz4 = raw.arr("fyz4")
|
||||
val mat0 = raw.arr("mat0")
|
||||
val mat = raw.arr("mat")
|
||||
val mat2 = raw.arr("mat2")
|
||||
val mat3 = raw.arr("mat3")
|
||||
val mat4 = raw.arr("mat4")
|
||||
val mluvnice = raw.arr("mluvnice")
|
||||
val books = raw.arr("books")
|
||||
val changelog = raw.arr("changelog")
|
||||
|
|
@ -64,6 +69,11 @@ class Content(root: JSONObject) {
|
|||
fun fyz2Lesson(id: Int) = fyz2.firstOrNull { it.int("id") == id }
|
||||
fun fyz3Lesson(id: Int) = fyz3.firstOrNull { it.int("id") == id }
|
||||
fun fyz4Lesson(id: Int) = fyz4.firstOrNull { it.int("id") == id }
|
||||
fun mat0Lesson(id: Int) = mat0.firstOrNull { it.int("id") == id }
|
||||
fun matLesson(id: Int) = mat.firstOrNull { it.int("id") == id }
|
||||
fun mat2Lesson(id: Int) = mat2.firstOrNull { it.int("id") == id }
|
||||
fun mat3Lesson(id: Int) = mat3.firstOrNull { it.int("id") == id }
|
||||
fun mat4Lesson(id: Int) = mat4.firstOrNull { it.int("id") == id }
|
||||
fun mluv(n: Int) = mluvnice.firstOrNull { it.int("id") == n }
|
||||
fun book(id: String) = books.firstOrNull { it.str("id") == id }
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,16 @@ class ProgressStore(context: Context) {
|
|||
fun markFyz3(id: Int) { prefs.edit().putBoolean("fyz3.$id", true).apply() }
|
||||
fun fyz4Done(id: Int) = prefs.getBoolean("fyz4.$id", false)
|
||||
fun markFyz4(id: Int) { prefs.edit().putBoolean("fyz4.$id", true).apply() }
|
||||
fun mat0Done(id: Int) = prefs.getBoolean("mat0.$id", false)
|
||||
fun markMat0(id: Int) { prefs.edit().putBoolean("mat0.$id", true).apply() }
|
||||
fun matDone(id: Int) = prefs.getBoolean("mat.$id", false)
|
||||
fun markMat(id: Int) { prefs.edit().putBoolean("mat.$id", true).apply() }
|
||||
fun mat2Done(id: Int) = prefs.getBoolean("mat2.$id", false)
|
||||
fun markMat2(id: Int) { prefs.edit().putBoolean("mat2.$id", true).apply() }
|
||||
fun mat3Done(id: Int) = prefs.getBoolean("mat3.$id", false)
|
||||
fun markMat3(id: Int) { prefs.edit().putBoolean("mat3.$id", true).apply() }
|
||||
fun mat4Done(id: Int) = prefs.getBoolean("mat4.$id", false)
|
||||
fun markMat4(id: Int) { prefs.edit().putBoolean("mat4.$id", true).apply() }
|
||||
|
||||
fun mluvDone(n: Int) = prefs.getBoolean("mluv.$n", false)
|
||||
fun markMluv(n: Int) { prefs.edit().putBoolean("mluv.$n", true).apply() }
|
||||
|
|
@ -216,6 +226,46 @@ fun summarize(content: Content, p: ProgressStore): ProgressSum {
|
|||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
content.mat0.forEach { l ->
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if (p.mat0Done(l.int("id"))) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
content.mat.forEach { l ->
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if (p.matDone(l.int("id"))) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
content.mat2.forEach { l ->
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if (p.mat2Done(l.int("id"))) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
content.mat3.forEach { l ->
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if (p.mat3Done(l.int("id"))) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
content.mat4.forEach { l ->
|
||||
sum.totalEx += 1
|
||||
sum.openUnits += 1
|
||||
if (p.mat4Done(l.int("id"))) {
|
||||
sum.doneEx += 1
|
||||
sum.doneUnits += 1
|
||||
}
|
||||
}
|
||||
sum.totalEx += content.mluvnice.size
|
||||
sum.openUnits += 1
|
||||
val md = content.mluvnice.count { p.mluvDone(it.int("id")) }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package org.maturita.maturita.ui
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -12,6 +14,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
|
|
@ -45,18 +48,28 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.drawText
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.maturita.maturita.AppViewModel
|
||||
import org.maturita.maturita.data.J
|
||||
import org.maturita.maturita.data.answerAccepts
|
||||
import org.maturita.maturita.data.mathAnswerOk
|
||||
import org.maturita.maturita.data.mathDrawGiven
|
||||
import org.maturita.maturita.data.mathDrawNeed
|
||||
import org.maturita.maturita.data.mathDrawOk
|
||||
import org.maturita.maturita.data.mathIsDraw
|
||||
import org.maturita.maturita.data.answerIncomplete
|
||||
import org.maturita.maturita.data.hasUmlaut
|
||||
import org.maturita.maturita.data.netTxtEq
|
||||
|
|
@ -863,6 +876,198 @@ fun SciQuizScreen(vm: AppViewModel, area: String, id: Int) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MathPracticeScreen(vm: AppViewModel, year: Int, id: Int) {
|
||||
val lesson = when (year) {
|
||||
0 -> vm.content.mat0Lesson(id)
|
||||
2 -> vm.content.mat2Lesson(id)
|
||||
3 -> vm.content.mat3Lesson(id)
|
||||
4 -> vm.content.mat4Lesson(id)
|
||||
else -> vm.content.matLesson(id)
|
||||
} ?: return
|
||||
val problems = lesson.arr("problems")
|
||||
var texts by remember(year, id) { mutableStateOf(List(problems.size) { "" }) }
|
||||
var placed by remember(year, id) { mutableStateOf(List(problems.size) { emptyList<Pair<Int, Int>>() }) }
|
||||
var marks by remember(year, id) { mutableStateOf(List<Boolean?>(problems.size) { null }) }
|
||||
var revealed by remember(year, id) { mutableStateOf(false) }
|
||||
var feedback by remember(year, id) { mutableStateOf("") }
|
||||
var kind by remember(year, id) { mutableStateOf("") }
|
||||
ExerciseScaffold(
|
||||
vm,
|
||||
vm.tr(lesson.str("exTitleKey")),
|
||||
vm.tr(lesson.str("quizHeadKey")),
|
||||
vm.tr("check"),
|
||||
{
|
||||
var all = problems.isNotEmpty()
|
||||
val next = MutableList<Boolean?>(problems.size) { null }
|
||||
problems.forEachIndexed { i, problem ->
|
||||
val spec = problem.str("answer")
|
||||
val good = if (mathIsDraw(spec)) mathDrawOk(spec, placed[i]) else mathAnswerOk(spec, texts[i])
|
||||
next[i] = good
|
||||
if (!good) all = false
|
||||
}
|
||||
marks = next
|
||||
revealed = true
|
||||
if (all) {
|
||||
feedback = vm.tr("feedback_ok")
|
||||
kind = "ok"
|
||||
when (year) {
|
||||
0 -> vm.markMat0(id)
|
||||
2 -> vm.markMat2(id)
|
||||
3 -> vm.markMat3(id)
|
||||
4 -> vm.markMat4(id)
|
||||
else -> vm.markMat(id)
|
||||
}
|
||||
} else {
|
||||
feedback = vm.tr("feedback_retry_short")
|
||||
kind = "bad"
|
||||
}
|
||||
},
|
||||
feedback,
|
||||
kind,
|
||||
) {
|
||||
Text(
|
||||
vm.tr("math_quiz_intro"),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
problems.forEachIndexed { i, problem ->
|
||||
val spec = problem.str("answer")
|
||||
val border = when (marks.getOrNull(i)) {
|
||||
true -> Color(0xFF2E7D32)
|
||||
false -> MaterialTheme.colorScheme.error
|
||||
else -> MaterialTheme.colorScheme.outlineVariant
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 10.dp)
|
||||
.border(1.5.dp, border, MaterialTheme.shapes.medium)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Text(problem.str("prompt"), style = MaterialTheme.typography.bodyLarge)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
if (mathIsDraw(spec)) {
|
||||
Text(
|
||||
vm.tr("math_draw_hint"),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
MathBoard(mathDrawGiven(spec), placed[i], mathDrawNeed(spec), marks.getOrNull(i)) { next ->
|
||||
placed = placed.toMutableList().also { it[i] = next }
|
||||
}
|
||||
Row {
|
||||
OutlinedButton(onClick = {
|
||||
val next = placed[i].dropLast(1)
|
||||
placed = placed.toMutableList().also { it[i] = next }
|
||||
}) { Text(vm.tr("math_undo")) }
|
||||
Spacer(Modifier.width(8.dp))
|
||||
OutlinedButton(onClick = {
|
||||
placed = placed.toMutableList().also { it[i] = emptyList() }
|
||||
}) { Text(vm.tr("math_clear")) }
|
||||
}
|
||||
} else {
|
||||
OutlinedTextField(
|
||||
value = texts[i],
|
||||
onValueChange = { value -> texts = texts.toMutableList().also { it[i] = value } },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text(vm.tr("math_answer_ph")) },
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
if (revealed) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
problem.str("hint"),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MathBoard(
|
||||
given: List<Pair<Int, Int>>,
|
||||
user: List<Pair<Int, Int>>,
|
||||
need: Int,
|
||||
mark: Boolean?,
|
||||
onChange: (List<Pair<Int, Int>>) -> Unit,
|
||||
) {
|
||||
val measurer = rememberTextMeasurer()
|
||||
val accent = MaterialTheme.colorScheme.primary
|
||||
val ink = MaterialTheme.colorScheme.onSurface
|
||||
val grid = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val paper = MaterialTheme.colorScheme.surfaceVariant
|
||||
val ok = Color(0xFF2E7D32)
|
||||
val bad = MaterialTheme.colorScheme.error
|
||||
Canvas(
|
||||
Modifier
|
||||
.size(316.dp, 260.dp)
|
||||
.pointerInput(given, user, need) {
|
||||
detectTapGestures { pos ->
|
||||
val cell = size.width.toFloat() / (10f + 36f / 28f)
|
||||
val pad = cell * 18f / 28f
|
||||
val cx = (pos.x - pad) / cell
|
||||
val cy = (size.height.toFloat() - pad - pos.y) / cell
|
||||
val ix = kotlin.math.round(cx)
|
||||
val iy = kotlin.math.round(cy)
|
||||
if (kotlin.math.abs(cx - ix) > 0.45f || kotlin.math.abs(cy - iy) > 0.45f) return@detectTapGestures
|
||||
val x = ix.toInt()
|
||||
val y = iy.toInt()
|
||||
if (x !in 0..10 || y !in 0..8) return@detectTapGestures
|
||||
val hit = x to y
|
||||
if (given.any { it == hit }) return@detectTapGestures
|
||||
val next = user.toMutableList()
|
||||
val idx = next.indexOf(hit)
|
||||
if (idx >= 0) next.removeAt(idx)
|
||||
else if (next.size < need) next.add(hit)
|
||||
else return@detectTapGestures
|
||||
onChange(next)
|
||||
}
|
||||
},
|
||||
) {
|
||||
val cell = size.width / (10f + 36f / 28f)
|
||||
val pad = cell * 18f / 28f
|
||||
fun at(x: Int, y: Int) = Offset(pad + x * cell, size.height - pad - y * cell)
|
||||
drawRect(paper)
|
||||
for (i in 0..10) {
|
||||
val x = pad + i * cell
|
||||
drawLine(grid.copy(alpha = 0.55f), Offset(x, pad), Offset(x, size.height - pad), strokeWidth = 1f)
|
||||
}
|
||||
for (i in 0..8) {
|
||||
val y = pad + i * cell
|
||||
drawLine(grid.copy(alpha = 0.55f), Offset(pad, y), Offset(size.width - pad, y), strokeWidth = 1f)
|
||||
}
|
||||
drawLine(ink, Offset(pad, size.height - pad), Offset(size.width - pad, size.height - pad), strokeWidth = 2.4f)
|
||||
drawLine(ink, Offset(pad, size.height - pad), Offset(pad, pad), strokeWidth = 2.4f)
|
||||
if (given.size >= 2) {
|
||||
drawLine(accent, at(given[0].first, given[0].second), at(given[1].first, given[1].second), strokeWidth = 3.5f)
|
||||
if (given.size >= 3) {
|
||||
drawLine(accent, at(given[0].first, given[0].second), at(given[2].first, given[2].second), strokeWidth = 3.5f)
|
||||
}
|
||||
}
|
||||
val names = listOf("A", "B", "C", "D")
|
||||
val label = TextStyle(color = ink, fontSize = 13.sp)
|
||||
given.forEachIndexed { i, g ->
|
||||
if (i >= names.size) return@forEachIndexed
|
||||
val pt = at(g.first, g.second)
|
||||
drawCircle(accent, radius = 7f, center = pt)
|
||||
drawText(measurer, names[i], topLeft = Offset(pt.x + 8f, pt.y - 22f), style = label)
|
||||
}
|
||||
val dot = if (mark == false) bad else ok
|
||||
user.forEachIndexed { i, u ->
|
||||
val pt = at(u.first, u.second)
|
||||
drawCircle(dot, radius = 7f, center = pt)
|
||||
drawText(measurer, "${i + 1}", topLeft = Offset(pt.x + 8f, pt.y - 22f), style = label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FyzQuizScreen(vm: AppViewModel, year: Int, id: Int) {
|
||||
val lesson = when (year) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuBook
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.Functions
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.FormatListNumbered
|
||||
|
|
@ -224,6 +225,7 @@ private fun SubjectIcon(icon: String, open: Boolean) {
|
|||
"cz" -> CzechFlag(28.dp)
|
||||
"flask" -> Icon(Icons.Filled.Science, null, tint = tint, modifier = Modifier.size(26.dp))
|
||||
"bolt" -> Icon(Icons.Filled.Bolt, null, tint = tint, modifier = Modifier.size(26.dp))
|
||||
"function" -> Icon(Icons.Filled.Functions, null, tint = tint, modifier = Modifier.size(26.dp))
|
||||
else -> Icon(Icons.Filled.Lock, null, tint = tint)
|
||||
}
|
||||
}
|
||||
|
|
@ -316,6 +318,14 @@ private fun subjectSum(vm: AppViewModel, s: J): ProgressSum {
|
|||
sum.doneEx = vm.content.chem.count { vm.progress.chemDone(it.int("id")) } +
|
||||
vm.content.bio.count { vm.progress.bioDone(it.int("id")) }
|
||||
}
|
||||
"matyears", "mat0map", "matmap", "mat2map", "mat3map", "mat4map" -> {
|
||||
sum.totalEx = vm.content.mat0.size + vm.content.mat.size + vm.content.mat2.size + vm.content.mat3.size + vm.content.mat4.size
|
||||
sum.doneEx = vm.content.mat0.count { vm.progress.mat0Done(it.int("id")) } +
|
||||
vm.content.mat.count { vm.progress.matDone(it.int("id")) } +
|
||||
vm.content.mat2.count { vm.progress.mat2Done(it.int("id")) } +
|
||||
vm.content.mat3.count { vm.progress.mat3Done(it.int("id")) } +
|
||||
vm.content.mat4.count { vm.progress.mat4Done(it.int("id")) }
|
||||
}
|
||||
"fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map" -> {
|
||||
sum.totalEx = vm.content.fyz.size + vm.content.fyz2.size + vm.content.fyz3.size + vm.content.fyz4.size
|
||||
sum.doneEx = vm.content.fyz.count { vm.progress.fyzDone(it.int("id")) } +
|
||||
|
|
@ -556,6 +566,32 @@ private fun buildSearch(vm: AppViewModel): List<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")
|
||||
vm.content.mat0.forEach { l ->
|
||||
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")}")
|
||||
}
|
||||
vm.content.mat.forEach { l ->
|
||||
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")}")
|
||||
}
|
||||
vm.content.mat2.forEach { l ->
|
||||
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")}")
|
||||
}
|
||||
vm.content.mat3.forEach { l ->
|
||||
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")}")
|
||||
}
|
||||
vm.content.mat4.forEach { l ->
|
||||
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")
|
||||
|
|
@ -713,6 +749,21 @@ fun LitYearsScreen(vm: AppViewModel) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MatYearsScreen(vm: AppViewModel) {
|
||||
DetailScaffold(vm, vm.tr("Matematika"), vm.tr("mat_years_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
item {
|
||||
NavRow(title = vm.tr("mat0_year"), subtitle = vm.tr("mat0_sub"), leading = { Text("ZŠ", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat0Map) }
|
||||
NavRow(title = vm.tr("mat_year1"), subtitle = vm.tr("mat_sub"), leading = { Text("1.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.MatMap) }
|
||||
NavRow(title = vm.tr("mat_year2"), subtitle = vm.tr("mat2_sub"), leading = { Text("2.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat2Map) }
|
||||
NavRow(title = vm.tr("mat_year3"), subtitle = vm.tr("mat3_sub"), leading = { Text("3.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat3Map) }
|
||||
NavRow(title = vm.tr("mat_year4"), subtitle = vm.tr("mat4_sub"), leading = { Text("4.", style = MaterialTheme.typography.titleMedium) }) { vm.go(Route.Mat4Map) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FyzYearsScreen(vm: AppViewModel) {
|
||||
DetailScaffold(vm, vm.tr("Fyzika"), vm.tr("fyz_years_sub")) { inner ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue