feat(shift): add ShiftPatternStorage for persistence
This commit is contained in:
parent
eca6c61058
commit
e4834a52c9
79
core/src/main/kotlin/plus/rua/project/ShiftPatternStorage.kt
Normal file
79
core/src/main/kotlin/plus/rua/project/ShiftPatternStorage.kt
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package plus.rua.project
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 个人轮班设置持久化。照抄 [DateCheckerStorage] 模式。
|
||||||
|
*
|
||||||
|
* 编码格式(无 JSON 依赖):
|
||||||
|
* - 锚点:ISO 日期串 "2026-07-08"
|
||||||
|
* - 周期:逗号分隔的 1/0 串 "1,1,0,0"(1=WORK,0=OFF)
|
||||||
|
* - overrides/breaks:逗号分隔的 "日期:值" 对
|
||||||
|
* - overrides 值:1=WORK,0=OFF
|
||||||
|
* - breaks 值:cycleOffset(整数)
|
||||||
|
*/
|
||||||
|
class ShiftPatternStorage(private val prefs: SharedPreferences) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_ANCHOR = "shift_anchor"
|
||||||
|
private const val KEY_CYCLE = "shift_cycle"
|
||||||
|
private const val KEY_OVERRIDES = "shift_overrides"
|
||||||
|
private const val KEY_BREAKS = "shift_breaks"
|
||||||
|
private const val SEP = ","
|
||||||
|
|
||||||
|
fun fromContext(context: Context): ShiftPatternStorage =
|
||||||
|
ShiftPatternStorage(
|
||||||
|
context.getSharedPreferences("shift_pattern", Context.MODE_PRIVATE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(pattern: ShiftPattern) {
|
||||||
|
val cycleStr = pattern.cycle.joinToString(SEP) { if (it == ShiftKind.WORK) "1" else "0" }
|
||||||
|
val overridesStr = pattern.overrides.entries
|
||||||
|
.joinToString(SEP) { "${it.key}:${if (it.value == ShiftKind.WORK) 1 else 0}" }
|
||||||
|
val breaksStr = pattern.phaseBreaks
|
||||||
|
.joinToString(SEP) { "${it.date}:${it.cycleOffset}" }
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_ANCHOR, pattern.anchorDate.toString())
|
||||||
|
.putString(KEY_CYCLE, cycleStr)
|
||||||
|
.putString(KEY_OVERRIDES, overridesStr)
|
||||||
|
.putString(KEY_BREAKS, breaksStr)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(): ShiftPattern? {
|
||||||
|
val anchorStr = prefs.getString(KEY_ANCHOR, null) ?: return null
|
||||||
|
val cycleStr = prefs.getString(KEY_CYCLE, null) ?: return null
|
||||||
|
return try {
|
||||||
|
val anchor = LocalDate.parse(anchorStr)
|
||||||
|
val cycle = cycleStr.split(SEP).map { if (it.trim() == "1") ShiftKind.WORK else ShiftKind.OFF }
|
||||||
|
val overrides = parseOverrides(prefs.getString(KEY_OVERRIDES, null))
|
||||||
|
val breaks = parseBreaks(prefs.getString(KEY_BREAKS, null))
|
||||||
|
ShiftPattern(anchor, cycle, overrides, breaks)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseOverrides(s: String?): Map<LocalDate, ShiftKind> {
|
||||||
|
if (s.isNullOrBlank()) return emptyMap()
|
||||||
|
return s.split(SEP).associate { pair ->
|
||||||
|
val parts = pair.split(":")
|
||||||
|
LocalDate.parse(parts[0]) to (if (parts[1].trim() == "1") ShiftKind.WORK else ShiftKind.OFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseBreaks(s: String?): List<PhaseBreak> {
|
||||||
|
if (s.isNullOrBlank()) return emptyList()
|
||||||
|
return s.split(SEP).map { pair ->
|
||||||
|
val parts = pair.split(":")
|
||||||
|
PhaseBreak(LocalDate.parse(parts[0]), parts[1].trim().toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
prefs.edit().clear().apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
132
core/src/test/kotlin/plus/rua/project/ShiftPatternStorageTest.kt
Normal file
132
core/src/test/kotlin/plus/rua/project/ShiftPatternStorageTest.kt
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package plus.rua.project
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
class ShiftPatternStorageTest {
|
||||||
|
|
||||||
|
private val prefs = ShiftPatternTestPrefs()
|
||||||
|
private val storage = ShiftPatternStorage(prefs)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun load_noSavedData_returnsNull() {
|
||||||
|
storage.clear()
|
||||||
|
assertNull(storage.load())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveAndLoad_roundTrips_basicPattern() {
|
||||||
|
storage.clear()
|
||||||
|
val pattern = ShiftPattern(
|
||||||
|
anchorDate = LocalDate(2026, 5, 15),
|
||||||
|
cycle = listOf(ShiftKind.WORK, ShiftKind.WORK, ShiftKind.OFF, ShiftKind.OFF)
|
||||||
|
)
|
||||||
|
storage.save(pattern)
|
||||||
|
val result = storage.load()
|
||||||
|
assertEquals(pattern, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveAndLoad_roundTrips_withOverridesAndBreaks() {
|
||||||
|
storage.clear()
|
||||||
|
val pattern = ShiftPattern(
|
||||||
|
anchorDate = LocalDate(2026, 7, 8),
|
||||||
|
cycle = listOf(ShiftKind.WORK, ShiftKind.WORK, ShiftKind.OFF, ShiftKind.OFF),
|
||||||
|
overrides = mapOf(
|
||||||
|
LocalDate(2026, 7, 12) to ShiftKind.OFF,
|
||||||
|
LocalDate(2026, 7, 14) to ShiftKind.WORK
|
||||||
|
),
|
||||||
|
phaseBreaks = listOf(PhaseBreak(LocalDate(2026, 7, 18), 0))
|
||||||
|
)
|
||||||
|
storage.save(pattern)
|
||||||
|
val result = storage.load()
|
||||||
|
assertEquals(pattern, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun saveAndLoad_emptyOverridesAndBreaks() {
|
||||||
|
storage.clear()
|
||||||
|
val pattern = ShiftPattern(
|
||||||
|
anchorDate = LocalDate(2026, 5, 15),
|
||||||
|
cycle = listOf(ShiftKind.WORK, ShiftKind.OFF),
|
||||||
|
overrides = emptyMap(),
|
||||||
|
phaseBreaks = emptyList()
|
||||||
|
)
|
||||||
|
storage.save(pattern)
|
||||||
|
assertEquals(pattern, storage.load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制自 DateCheckerStorageTest(其为 private,无法共享);重命名以避免同包下 private 类同名冲突
|
||||||
|
private class ShiftPatternTestPrefs : SharedPreferences {
|
||||||
|
|
||||||
|
private val data = mutableMapOf<String, Any?>()
|
||||||
|
|
||||||
|
override fun getAll(): Map<String, *> = data.toMap()
|
||||||
|
|
||||||
|
override fun getString(key: String, defValue: String?): String? =
|
||||||
|
data[key] as? String ?: defValue
|
||||||
|
|
||||||
|
override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? =
|
||||||
|
data[key] as? Set<String> ?: defValues
|
||||||
|
|
||||||
|
override fun getInt(key: String, defValue: Int): Int =
|
||||||
|
data[key] as? Int ?: defValue
|
||||||
|
|
||||||
|
override fun getLong(key: String, defValue: Long): Long =
|
||||||
|
data[key] as? Long ?: defValue
|
||||||
|
|
||||||
|
override fun getFloat(key: String, defValue: Float): Float =
|
||||||
|
data[key] as? Float ?: defValue
|
||||||
|
|
||||||
|
override fun getBoolean(key: String, defValue: Boolean): Boolean =
|
||||||
|
data[key] as? Boolean ?: defValue
|
||||||
|
|
||||||
|
override fun contains(key: String): Boolean = data.containsKey(key)
|
||||||
|
|
||||||
|
override fun edit(): SharedPreferences.Editor = ShiftPatternTestPrefsEditor(data)
|
||||||
|
|
||||||
|
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {}
|
||||||
|
|
||||||
|
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ShiftPatternTestPrefsEditor(private val data: MutableMap<String, Any?>) : SharedPreferences.Editor {
|
||||||
|
|
||||||
|
private val pending = mutableMapOf<String, Any?>()
|
||||||
|
private var clearPending = false
|
||||||
|
|
||||||
|
override fun putString(key: String, value: String?): SharedPreferences.Editor = apply {
|
||||||
|
pending[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putStringSet(key: String, values: Set<String>?): SharedPreferences.Editor = apply {
|
||||||
|
pending[key] = values
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putInt(key: String, value: Int): SharedPreferences.Editor = apply { pending[key] = value }
|
||||||
|
override fun putLong(key: String, value: Long): SharedPreferences.Editor = apply { pending[key] = value }
|
||||||
|
override fun putFloat(key: String, value: Float): SharedPreferences.Editor = apply { pending[key] = value }
|
||||||
|
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor = apply { pending[key] = value }
|
||||||
|
|
||||||
|
override fun remove(key: String): SharedPreferences.Editor = apply { pending[key] = null }
|
||||||
|
|
||||||
|
override fun clear(): SharedPreferences.Editor = apply { clearPending = true }
|
||||||
|
|
||||||
|
override fun commit(): Boolean {
|
||||||
|
apply()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun apply() {
|
||||||
|
if (clearPending) {
|
||||||
|
data.clear()
|
||||||
|
clearPending = false
|
||||||
|
}
|
||||||
|
data.putAll(pending)
|
||||||
|
pending.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user