diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/LoTWRepository.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/LoTWRepository.kt index afc54f19..e0055629 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/LoTWRepository.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/LoTWRepository.kt @@ -212,7 +212,7 @@ class LoTWRepository : ILoTWRepository { line.startsWith(" cqz = adifValue(line).toIntOrNull() line.startsWith(" - state = adifValue(line).trim().ifBlank { null } + state = adifValue(line).trim().ifBlank { null }?.let { normalizeState(it) } line.startsWith(" { // VUCC_GRIDS holds a comma-separated list of grids // ("EN52en,EN53fa"), up to four for contacts spanning @@ -231,6 +231,16 @@ class LoTWRepository : ILoTWRepository { private fun adifValue(line: String): String = line.substringAfter('>').substringBefore("E<").trim() + /** + * LoTW returns STATE as "CODE // NAME" (e.g. "HB // Hubei" for China, + * "34 // Tottori-ken" for Japan, "CA // California" for the US). The award + * statistics match on the short CODE only (China 2-letter province pinyin, + * Japan 2-digit prefecture, US 2-letter state), so strip the " // NAME" + * suffix here once, storing the clean code for every downstream consumer + * (persistence, AwardCalculator). + */ + private fun normalizeState(raw: String): String = raw.substringBefore(" // ").trim() + /** "20260820" + "1130" (or "113000") -> UTC epoch ms; 0 when unparseable. */ private fun adifTimestampToEpoch(date: String, time: String): Long = try { val d = date.trim() diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/LoTWRepositoryTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/LoTWRepositoryTest.kt index 49f59ad7..1abc3a57 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/LoTWRepositoryTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/LoTWRepositoryTest.kt @@ -156,11 +156,13 @@ class LoTWRepositoryTest { @Test fun parseQsosExtractsAwardFields() { // Award statistics rely on the DXCC/CQZ/STATE fields coming straight - // from LoTW's qso_qsldetail report (STATE depends on DXCC). + // from LoTW's qso_qsldetail report (STATE depends on DXCC). LoTW + // writes STATE as "CODE // NAME" (verified with a real report 2026-09); + // the parser must strip the name suffix and keep the bare code. val qso = "BG7XYZ\n" + "20260820\n1130\n" + "SAT\nFO-29\nCW\n" + - "318\nCHINA\n24\nGD\n" + + "318\nCHINA\n24\nGD // Guangdong\n" + "OL62\n\n" val result = repo.parseConfirmedGridQsos(report(qso))!! val parsed = result["OL62"]!!.first() @@ -170,6 +172,21 @@ class LoTWRepositoryTest { assertEquals("GD", parsed.state) } + @Test + fun parseQsosStripsStateNameSuffixForAllEntities() { + // Japan prefecture: "34 // Tottori-ken" -> "34" (WAJA matching needs + // the 2-digit code). US states also arrive as "CODE // Name". + val jp = "JH0ABC\n20260820\n1130\n" + + "SAT\nRS-44\n339\nJAPAN\n" + + "34 // Tottori-ken\nPM95\n\n" + val us = "K1ABC\n20260820\n1130\n" + + "SAT\nAO-07\n291\nUNITED STATES\n" + + "CA // California\nEM40\n\n" + val result = repo.parseConfirmedGridQsos(report(jp, us))!! + assertEquals("34", result["PM95"]!!.first().state) + assertEquals("CA", result["EM40"]!!.first().state) + } + @Test fun parseQsosLeavesAwardFieldsNullWhenAbsent() { val qso = "JH0ABC\n20260820\n1130\n" +