Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions core/data/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library", "kt_jvm_test")

package(default_visibility = ["//visibility:public"])

kt_jvm_library(
name = "data",
srcs = glob(["src/main/kotlin/**/*.kt"]),
visibility = ["//visibility:public"],
deps = [
"//core/linguistics",
"@maven//:org_jetbrains_kotlinx_kotlinx_serialization_json",
"@maven//:org_xerial_sqlite_jdbc",
],
)

kt_jvm_test(
name = "seed_database_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/seed/SeedDatabaseTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.seed.SeedDatabaseTest",
deps = [
":data",
"//core/linguistics",
"@maven//:junit_junit",
"@maven//:org_xerial_sqlite_jdbc",
],
)

kt_jvm_test(
name = "seed_dictionary_repository_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/seed/SeedDictionaryRepositoryTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.seed.SeedDictionaryRepositoryTest",
deps = [
":data",
"//core/linguistics",
"@maven//:junit_junit",
"@maven//:org_xerial_sqlite_jdbc",
],
)

kt_jvm_test(
name = "seed_false_cognates_repository_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/seed/SeedFalseCognatesRepositoryTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.seed.SeedFalseCognatesRepositoryTest",
deps = [
":data",
"//core/linguistics",
"@maven//:junit_junit",
"@maven//:org_xerial_sqlite_jdbc",
],
)

kt_jvm_test(
name = "seed_curated_lessons_repository_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/seed/SeedCuratedLessonsRepositoryTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.seed.SeedCuratedLessonsRepositoryTest",
deps = [
":data",
"//core/linguistics",
"@maven//:junit_junit",
"@maven//:org_jetbrains_kotlinx_kotlinx_serialization_json",
"@maven//:org_xerial_sqlite_jdbc",
],
)

kt_jvm_test(
name = "seed_cognate_rules_repository_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/seed/SeedCognateRulesRepositoryTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.seed.SeedCognateRulesRepositoryTest",
deps = [
":data",
"//core/linguistics",
"@maven//:junit_junit",
"@maven//:org_jetbrains_kotlinx_kotlinx_serialization_json",
"@maven//:org_xerial_sqlite_jdbc",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.anysoftkeyboard.calque.data.models

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonPrimitive

/** Represents a single English-to-Spanish word pair in a cognate transformation rule. */
data class CognateRuleExample(
val english: String,
val spanish: String,
)

/** Domain model for a cognate transformation rule stored in `cognate_rules`. */
data class CognateRule(
val ruleId: String,
val englishSuffix: String,
val spanishSuffix: String,
val genderRule: String?,
val spellingNote: String,
val examplesJson: String,
) {
/** Parses the underlying [examplesJson] string into a list of [CognateRuleExample]s. */
fun parseExamples(): List<CognateRuleExample> {
if (examplesJson.isBlank()) return emptyList()

val parsedElement =
runCatching { Json.parseToJsonElement(examplesJson) }.getOrNull() ?: return emptyList()
val examplesArray: JsonArray =
when {
parsedElement is JsonArray -> parsedElement
parsedElement is JsonObject ->
parsedElement["examples"] as? JsonArray ?: return emptyList()
else -> return emptyList()
}

return examplesArray.mapNotNull { element ->
val obj = element as? JsonObject ?: return@mapNotNull null
val english = obj["english"]?.jsonPrimitive?.contentOrNull ?: ""
val spanish = obj["spanish"]?.jsonPrimitive?.contentOrNull ?: ""
CognateRuleExample(english = english, spanish = spanish)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.anysoftkeyboard.calque.data.models

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonPrimitive

/** Represents a single token in an interlinear word-for-word literal gloss. */
data class InterlinearGlossToken(
val es: String,
val en: String,
)

/** Represents a structured sentence inside a curated lesson's content. */
data class LessonSentence(
val sentenceEs: String,
val translationEn: String,
val interlinearGloss: List<InterlinearGlossToken> = emptyList(),
val grammarCallout: String? = null,
)

/** Domain model for a curated reading progression lesson stored in `curated_lessons`. */
data class CuratedLesson(
val lessonId: String,
val level: Int,
val sequenceOrder: Int,
val title: String,
val targetConcept: String,
val contentJson: String,
) {
/** Parses the underlying [contentJson] string into a list of [LessonSentence]s. */
fun parseSentences(): List<LessonSentence> {
if (contentJson.isBlank()) return emptyList()

val parsedElement =
runCatching { Json.parseToJsonElement(contentJson) }.getOrNull() ?: return emptyList()
val sentencesArray: JsonArray =
when {
parsedElement is JsonArray -> parsedElement
parsedElement is JsonObject ->
parsedElement["sentences"] as? JsonArray ?: return emptyList()
else -> return emptyList()
}

return sentencesArray.mapNotNull { element ->
val obj = element as? JsonObject ?: return@mapNotNull null
parseSentenceObject(obj)
}
}

private fun parseSentenceObject(obj: JsonObject): LessonSentence {
val sentenceEs =
obj["sentence_es"]?.jsonPrimitive?.contentOrNull
?: obj["sentenceEs"]?.jsonPrimitive?.contentOrNull
?: ""
val translationEn =
obj["translation_en"]?.jsonPrimitive?.contentOrNull
?: obj["translationEn"]?.jsonPrimitive?.contentOrNull
?: ""
val grammarCallout =
obj["grammar_callout"]?.jsonPrimitive?.contentOrNull
?: obj["grammarCallout"]?.jsonPrimitive?.contentOrNull

val rawGlossArray = (obj["interlinear_gloss"] ?: obj["interlinearGloss"]) as? JsonArray

val interlinearGloss =
rawGlossArray?.mapNotNull { glossElem ->
val glossObj = glossElem as? JsonObject ?: return@mapNotNull null
val es = glossObj["es"]?.jsonPrimitive?.contentOrNull.orEmpty()
val en = glossObj["en"]?.jsonPrimitive?.contentOrNull.orEmpty()
InterlinearGlossToken(es = es, en = en)
} ?: emptyList()

return LessonSentence(
sentenceEs = sentenceEs,
translationEn = translationEn,
interlinearGloss = interlinearGloss,
grammarCallout = grammarCallout,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.anysoftkeyboard.calque.data.seed

import com.anysoftkeyboard.calque.data.models.CognateRule
import java.sql.Connection
import java.sql.ResultSet
import java.util.Locale

/** Repository interface for accessing cognate transformation rules. */
interface CognateRulesRepository {
/** Retrieves all cognate rules. */
fun getAll(): List<CognateRule>

/** Retrieves a cognate rule by its unique identifier (e.g. "tion_to_cion"). */
fun getById(ruleId: String): CognateRule?

/**
* Finds the most specific cognate rule applicable to a Spanish word, matching the longest
* [CognateRule.spanishSuffix].
*/
fun findRuleForSpanishWord(spanishWord: String): CognateRule?
}

/** SQLite-backed implementation of [CognateRulesRepository]. */
class SqliteCognateRulesRepository(
private val connection: Connection,
) : CognateRulesRepository {

override fun getAll(): List<CognateRule> {
val sql =
"""
SELECT rule_id, english_suffix, spanish_suffix, gender_rule, spelling_note, examples_json
FROM cognate_rules
ORDER BY rowid ASC
"""
.trimIndent()

val results = mutableListOf<CognateRule>()
connection.createStatement().use { stmt ->
stmt.executeQuery(sql).use { rs ->
while (rs.next()) {
results.add(mapRow(rs))
}
}
}
return results
}

override fun getById(ruleId: String): CognateRule? {
val trimmed = ruleId.trim()
if (trimmed.isEmpty()) return null

val sql =
"""
SELECT rule_id, english_suffix, spanish_suffix, gender_rule, spelling_note, examples_json
FROM cognate_rules
WHERE rule_id = ?
LIMIT 1
"""
.trimIndent()

connection.prepareStatement(sql).use { stmt ->
stmt.setString(1, trimmed)
stmt.executeQuery().use { rs ->
return if (rs.next()) mapRow(rs) else null
}
}
}

override fun findRuleForSpanishWord(spanishWord: String): CognateRule? {
val normalized = spanishWord.lowercase(Locale.ROOT).trim()
if (normalized.isEmpty()) return null

val rules = getAll()
return rules
.filter { rule ->
val cleanSuffix = cleanSuffix(rule.spanishSuffix)
cleanSuffix.isNotEmpty() && normalized.endsWith(cleanSuffix)
}
.maxByOrNull { rule -> cleanSuffix(rule.spanishSuffix).length }
}

private fun cleanSuffix(suffix: String): String {
return suffix.lowercase(Locale.ROOT).trim().removePrefix("-")
}

private fun mapRow(rs: ResultSet): CognateRule {
return CognateRule(
ruleId = rs.getString("rule_id"),
englishSuffix = rs.getString("english_suffix"),
spanishSuffix = rs.getString("spanish_suffix"),
genderRule = rs.getString("gender_rule"),
spellingNote = rs.getString("spelling_note"),
examplesJson = rs.getString("examples_json"),
)
}
}
Loading
Loading