feat(shift): add ShiftPatternStorage for persistence

This commit is contained in:
xfy 2026-07-06 18:24:28 +08:00
parent eca6c61058
commit e4834a52c9
2 changed files with 211 additions and 0 deletions

View 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()
}
}

View 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()
}
}