calculated day's difference over DST correctly

This commit is contained in:
Milos Kozak 2023-11-03 14:03:57 +01:00
parent a8fc73c8ad
commit 47758b6b27
11 changed files with 117 additions and 79 deletions

View file

@ -1,64 +1,86 @@
package app.aaps.core.interfaces.utils package app.aaps.core.interfaces.utils
import android.util.LongSparseArray import android.util.LongSparseArray
import java.util.Calendar import androidx.annotation.VisibleForTesting
import java.time.Instant
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
object MidnightTime { object MidnightTime {
@VisibleForTesting
val times = LongSparseArray<Long>() val times = LongSparseArray<Long>()
private var hits: Long = 0
private var misses: Long = 0
private const val THRESHOLD = 100000 private const val THRESHOLD = 100000
fun calc(): Long { /**
val c = Calendar.getInstance() * Epoch time of last midnight
c[Calendar.HOUR_OF_DAY] = 0 *
c[Calendar.MINUTE] = 0 * @return epoch millis
c[Calendar.SECOND] = 0 */
c[Calendar.MILLISECOND] = 0 fun calc(): Long =
return c.timeInMillis LocalDateTime.now().atZone(ZoneId.systemDefault())
} .with(LocalTime.of(0, 0, 0, 0))
.toInstant().toEpochMilli()
fun calcPlusMinutes(minutes: Int): Long { /**
val h = minutes / 60 * Today's time with 'minutes' from midnight
*
* @param minutes minutes to add
* @return epoch millis of today with hh:mm:00
*/
fun calcMidnightPlusMinutes(minutes: Int): Long {
val h = (minutes / 60) % 24
val m = minutes % 60 val m = minutes % 60
val c = Calendar.getInstance() return LocalDateTime.now().atZone(ZoneId.systemDefault())
c[Calendar.HOUR_OF_DAY] = h .with(LocalTime.of(h, m, 0, 0))
c[Calendar.MINUTE] = m .toInstant().toEpochMilli()
c[Calendar.SECOND] = 0
c[Calendar.MILLISECOND] = 0
return c.timeInMillis
} }
/**
* Epoch time of last midnight before 'time'
*
* @param time time of the day
* @return epoch millis
*/
fun calc(time: Long): Long { fun calc(time: Long): Long {
var m: Long?
synchronized(times) { synchronized(times) {
m = times[time] val m = times[time] ?: Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault())
if (m != null) { .with(LocalTime.of(0, 0, 0, 0))
++hits .toInstant().toEpochMilli()
return m!!
}
val c = Calendar.getInstance()
c.timeInMillis = time
c[Calendar.HOUR_OF_DAY] = 0
c[Calendar.MINUTE] = 0
c[Calendar.SECOND] = 0
c[Calendar.MILLISECOND] = 0
m = c.timeInMillis
times.append(time, m)
++misses
if (times.size() > THRESHOLD) resetCache() if (times.size() > THRESHOLD) resetCache()
return m
} }
return m!!
} }
/**
* Epoch time of last midnight 'days' back
*
* @param daysBack how many days back
* @return epoch millis of midnight
*/
fun calcDaysBack(daysBack: Long): Long =
LocalDateTime.now().atZone(ZoneId.systemDefault())
.with(LocalTime.of(0, 0, 0, 0))
.minusDays(daysBack)
.toInstant().toEpochMilli()
/**
* Epoch time of last midnight 'days' back from time
*
* @param time start time
* @param daysBack how many days back
* @return epoch millis of midnight
*/
fun calcDaysBack(time: Long, daysBack: Long): Long =
Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault())
.with(LocalTime.of(0, 0, 0, 0))
.minusDays(daysBack)
.toInstant().toEpochMilli()
@VisibleForTesting
fun resetCache() { fun resetCache() {
hits = 0
misses = 0
times.clear() times.clear()
} }
fun log(): String =
"Hits: " + hits + " misses: " + misses + " stored: " + times.size()
} }

View file

@ -25,24 +25,40 @@ class MidnightTimeTest {
assertThat(midnight).isAtMost(now) assertThat(midnight).isAtMost(now)
val c = Calendar.getInstance() val c = Calendar.getInstance()
c.timeInMillis = MidnightTime.calc(now) c.timeInMillis = MidnightTime.calc(now)
assertThat(c[Calendar.HOUR_OF_DAY].toLong()).isEqualTo(0L) assertThat(c[Calendar.HOUR_OF_DAY]).isEqualTo(0)
assertThat(c[Calendar.MINUTE].toLong()).isEqualTo(0L) assertThat(c[Calendar.MINUTE]).isEqualTo(0)
assertThat(c[Calendar.SECOND].toLong()).isEqualTo(0L) assertThat(c[Calendar.SECOND]).isEqualTo(0)
assertThat(c[Calendar.MILLISECOND].toLong()).isEqualTo(0L) assertThat(c[Calendar.MILLISECOND]).isEqualTo(0)
// Assure we get the same time from cache // Assure we get the same time from cache
assertThat(midnight).isEqualTo(MidnightTime.calc(now)) assertThat(midnight).isEqualTo(MidnightTime.calc(now))
} }
@Test fun calcMidnightPlusMinutesTest() {
val c = Calendar.getInstance()
c.timeInMillis = MidnightTime.calcMidnightPlusMinutes(121)
assertThat(c[Calendar.HOUR_OF_DAY]).isEqualTo(2)
assertThat(c[Calendar.MINUTE]).isEqualTo(1)
assertThat(c[Calendar.SECOND]).isEqualTo(0)
assertThat(c[Calendar.MILLISECOND]).isEqualTo(0)
}
@Test fun calcDaysBackTest() {
// We get real midnight
val now = System.currentTimeMillis()
val c = Calendar.getInstance()
c.timeInMillis = MidnightTime.calc(now)
c.add(Calendar.DAY_OF_MONTH, -5)
assertThat(c[Calendar.HOUR_OF_DAY]).isEqualTo(0)
assertThat(c[Calendar.MINUTE]).isEqualTo(0)
assertThat(c[Calendar.SECOND]).isEqualTo(0)
assertThat(c[Calendar.MILLISECOND]).isEqualTo(0)
assertThat(MidnightTime.calcDaysBack(5)).isEqualTo(c.timeInMillis)
}
@Test fun resetCache() { @Test fun resetCache() {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
MidnightTime.calc(now) MidnightTime.calc(now)
MidnightTime.resetCache() MidnightTime.resetCache()
assertThat(MidnightTime.times.size().toLong()).isEqualTo(0L) assertThat(MidnightTime.times.size().toLong()).isEqualTo(0L)
} }
@Test fun log() {
val now = System.currentTimeMillis()
MidnightTime.calc(now)
assertThat(MidnightTime.log()).startsWith("Hits:")
}
} }

View file

@ -9,7 +9,6 @@ import app.aaps.core.interfaces.stats.DexcomTIR
import app.aaps.core.interfaces.stats.DexcomTirCalculator import app.aaps.core.interfaces.stats.DexcomTirCalculator
import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DateUtil
import app.aaps.core.interfaces.utils.MidnightTime import app.aaps.core.interfaces.utils.MidnightTime
import app.aaps.core.interfaces.utils.T
import app.aaps.database.impl.AppRepository import app.aaps.database.impl.AppRepository
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -24,7 +23,7 @@ class DexcomTirCalculatorImpl @Inject constructor(
val days = 14L val days = 14L
override fun calculate(): DexcomTIR { override fun calculate(): DexcomTIR {
val startTime = MidnightTime.calc(dateUtil.now() - T.days(days).msecs()) val startTime = MidnightTime.calcDaysBack(days)
val endTime = MidnightTime.calc(dateUtil.now()) val endTime = MidnightTime.calc(dateUtil.now())
val bgReadings = repository.compatGetBgReadingsDataFromTime(startTime, endTime, true).blockingGet() val bgReadings = repository.compatGetBgReadingsDataFromTime(startTime, endTime, true).blockingGet()

View file

@ -39,9 +39,8 @@ class TddCalculatorImpl @Inject constructor(
) : TddCalculator { ) : TddCalculator {
override fun calculate(days: Long, allowMissingDays: Boolean): LongSparseArray<TotalDailyDose>? { override fun calculate(days: Long, allowMissingDays: Boolean): LongSparseArray<TotalDailyDose>? {
var startTime = MidnightTime.calc(dateUtil.now() - T.days(days).msecs()) var startTime = MidnightTime.calcDaysBack(days)
val endTime = MidnightTime.calc(dateUtil.now()) val endTime = MidnightTime.calc(dateUtil.now())
//val stepSize = T.hours(24).msecs() // this is not true on DST change
val result = LongSparseArray<TotalDailyDose>() val result = LongSparseArray<TotalDailyDose>()
// Try to load cached values // Try to load cached values

View file

@ -15,7 +15,6 @@ import app.aaps.core.interfaces.stats.TIR
import app.aaps.core.interfaces.stats.TirCalculator import app.aaps.core.interfaces.stats.TirCalculator
import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DateUtil
import app.aaps.core.interfaces.utils.MidnightTime import app.aaps.core.interfaces.utils.MidnightTime
import app.aaps.core.interfaces.utils.T
import app.aaps.database.impl.AppRepository import app.aaps.database.impl.AppRepository
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -31,7 +30,7 @@ class TirCalculatorImpl @Inject constructor(
override fun calculate(days: Long, lowMgdl: Double, highMgdl: Double): LongSparseArray<TIR> { override fun calculate(days: Long, lowMgdl: Double, highMgdl: Double): LongSparseArray<TIR> {
if (lowMgdl < 39) throw RuntimeException("Low below 39") if (lowMgdl < 39) throw RuntimeException("Low below 39")
if (lowMgdl > highMgdl) throw RuntimeException("Low > High") if (lowMgdl > highMgdl) throw RuntimeException("Low > High")
val startTime = MidnightTime.calc(dateUtil.now() - T.days(days).msecs()) val startTime = MidnightTime.calcDaysBack(days)
val endTime = MidnightTime.calc(dateUtil.now()) val endTime = MidnightTime.calc(dateUtil.now())
val bgReadings = repository.compatGetBgReadingsDataFromTime(startTime, endTime, true).blockingGet() val bgReadings = repository.compatGetBgReadingsDataFromTime(startTime, endTime, true).blockingGet()

View file

@ -331,18 +331,21 @@ class AutotunePlugin @Inject constructor(
jsonSettings.put("tune_insulin_curve", false) jsonSettings.put("tune_insulin_curve", false)
val peakTime: Int = insulinInterface.peak val peakTime: Int = insulinInterface.peak
if (insulinInterface.id === Insulin.InsulinType.OREF_ULTRA_RAPID_ACTING) when {
jsonSettings.put("curve", "ultra-rapid") insulinInterface.id === Insulin.InsulinType.OREF_ULTRA_RAPID_ACTING -> jsonSettings.put("curve", "ultra-rapid")
else if (insulinInterface.id === Insulin.InsulinType.OREF_RAPID_ACTING) insulinInterface.id === Insulin.InsulinType.OREF_RAPID_ACTING -> jsonSettings.put("curve", "rapid-acting")
jsonSettings.put("curve", "rapid-acting")
else if (insulinInterface.id === Insulin.InsulinType.OREF_LYUMJEV) { insulinInterface.id === Insulin.InsulinType.OREF_LYUMJEV -> {
jsonSettings.put("curve", "ultra-rapid") jsonSettings.put("curve", "ultra-rapid")
jsonSettings.put("useCustomPeakTime", true) jsonSettings.put("useCustomPeakTime", true)
jsonSettings.put("insulinPeakTime", peakTime) jsonSettings.put("insulinPeakTime", peakTime)
} else if (insulinInterface.id === Insulin.InsulinType.OREF_FREE_PEAK) { }
jsonSettings.put("curve", if (peakTime > 55) "rapid-acting" else "ultra-rapid")
jsonSettings.put("useCustomPeakTime", true) insulinInterface.id === Insulin.InsulinType.OREF_FREE_PEAK -> {
jsonSettings.put("insulinPeakTime", peakTime) jsonSettings.put("curve", if (peakTime > 55) "rapid-acting" else "ultra-rapid")
jsonSettings.put("useCustomPeakTime", true)
jsonSettings.put("insulinPeakTime", peakTime)
}
} }
jsonString = jsonSettings.toString(4).replace("\\/", "/") jsonString = jsonSettings.toString(4).replace("\\/", "/")
} catch (e: JSONException) { } catch (e: JSONException) {
@ -392,7 +395,7 @@ class AutotunePlugin @Inject constructor(
} }
} }
for (i in days.weekdays.indices) { for (i in days.weekdays.indices) {
json.put(WeekDay.DayOfWeek.values()[i].name, days.weekdays[i]) json.put(WeekDay.DayOfWeek.entries[i].name, days.weekdays[i])
} }
json.put("result", result) json.put("result", result)
json.put("updateButtonVisibility", updateButtonVisibility) json.put("updateButtonVisibility", updateButtonVisibility)
@ -429,7 +432,7 @@ class AutotunePlugin @Inject constructor(
} }
} }
for (i in days.weekdays.indices) for (i in days.weekdays.indices)
days.weekdays[i] = JsonHelper.safeGetBoolean(json, WeekDay.DayOfWeek.values()[i].name, true) days.weekdays[i] = JsonHelper.safeGetBoolean(json, WeekDay.DayOfWeek.entries[i].name, true)
result = JsonHelper.safeGetString(json, "result", "") result = JsonHelper.safeGetString(json, "result", "")
updateButtonVisibility = JsonHelper.safeGetInt(json, "updateButtonVisibility") updateButtonVisibility = JsonHelper.safeGetInt(json, "updateButtonVisibility")
lastRunSuccess = true lastRunSuccess = true
@ -440,8 +443,8 @@ class AutotunePlugin @Inject constructor(
fun calcDays(daysBack: Int): Int { fun calcDays(daysBack: Int): Int {
var endTime = MidnightTime.calc(dateUtil.now()) + autotuneStartHour * 60 * 60 * 1000L var endTime = MidnightTime.calc(dateUtil.now()) + autotuneStartHour * 60 * 60 * 1000L
if (endTime > dateUtil.now()) endTime -= T.days(1).msecs() // Check if 4 AM is before now if (endTime > dateUtil.now()) endTime = MidnightTime.calcDaysBack(1) // Check if 4 AM is before now
val startTime = endTime - daysBack * T.days(1).msecs() val startTime = MidnightTime.calcDaysBack(endTime, daysBack.toLong())
var result = 0 var result = 0
for (i in 0 until daysBack) { for (i in 0 until daysBack) {
if (days.isSet(startTime + i * T.days(1).msecs())) if (days.isSet(startTime + i * T.days(1).msecs()))

View file

@ -57,7 +57,7 @@ class InputTime(private val rh: ResourceHelper, private val dateUtil: DateUtil)
}) })
} }
private fun toMills(minutesSinceMidnight: Int): Long = MidnightTime.calcPlusMinutes(minutesSinceMidnight) private fun toMills(minutesSinceMidnight: Int): Long = MidnightTime.calcMidnightPlusMinutes(minutesSinceMidnight)
private fun getMinSinceMidnight(time: Long): Int = MidnightUtils.secondsFromMidnight(time) / 60 private fun getMinSinceMidnight(time: Long): Int = MidnightUtils.secondsFromMidnight(time) / 60

View file

@ -83,7 +83,7 @@ class InputTimeRange(private val rh: ResourceHelper, private val dateUtil: DateU
}) })
} }
private fun toMills(minutesSinceMidnight: Int): Long = MidnightTime.calcPlusMinutes(minutesSinceMidnight) private fun toMills(minutesSinceMidnight: Int): Long = MidnightTime.calcMidnightPlusMinutes(minutesSinceMidnight)
private fun getMinSinceMidnight(time: Long): Int = MidnightUtils.secondsFromMidnight(time) / 60 private fun getMinSinceMidnight(time: Long): Int = MidnightUtils.secondsFromMidnight(time) / 60

View file

@ -50,7 +50,7 @@ class TriggerRecurringTime(injector: HasAndroidInjector) : Trigger(injector) {
val data = JSONObject() val data = JSONObject()
.put("time", time.value) .put("time", time.value)
for (i in days.weekdays.indices) { for (i in days.weekdays.indices) {
data.put(WeekDay.DayOfWeek.values()[i].name, days.weekdays[i]) data.put(WeekDay.DayOfWeek.entries[i].name, days.weekdays[i])
} }
return data return data
} }
@ -58,7 +58,7 @@ class TriggerRecurringTime(injector: HasAndroidInjector) : Trigger(injector) {
override fun fromJSON(data: String): Trigger { override fun fromJSON(data: String): Trigger {
val o = JSONObject(data) val o = JSONObject(data)
for (i in days.weekdays.indices) for (i in days.weekdays.indices)
days.weekdays[i] = JsonHelper.safeGetBoolean(o, WeekDay.DayOfWeek.values()[i].name) days.weekdays[i] = JsonHelper.safeGetBoolean(o, WeekDay.DayOfWeek.entries[i].name)
if (o.has("hour")) { if (o.has("hour")) {
// do conversion from 2.5.1 format // do conversion from 2.5.1 format
val hour = JsonHelper.safeGetInt(o, "hour") val hour = JsonHelper.safeGetInt(o, "hour")
@ -90,7 +90,7 @@ class TriggerRecurringTime(injector: HasAndroidInjector) : Trigger(injector) {
override fun duplicate(): Trigger = TriggerRecurringTime(injector, this) override fun duplicate(): Trigger = TriggerRecurringTime(injector, this)
private fun toMills(minutesSinceMidnight: Int): Long = MidnightTime.calcPlusMinutes(minutesSinceMidnight) private fun toMills(minutesSinceMidnight: Int): Long = MidnightTime.calcMidnightPlusMinutes(minutesSinceMidnight)
private fun getMinSinceMidnight(time: Long): Int = MidnightUtils.secondsFromMidnight(time) / 60 private fun getMinSinceMidnight(time: Long): Int = MidnightUtils.secondsFromMidnight(time) / 60

View file

@ -70,7 +70,7 @@ class TriggerTimeRange(injector: HasAndroidInjector) : Trigger(injector) {
override fun duplicate(): Trigger = TriggerTimeRange(injector, range.start, range.end) override fun duplicate(): Trigger = TriggerTimeRange(injector, range.start, range.end)
private fun toMills(minutesSinceMidnight: Int): Long = MidnightTime.calcPlusMinutes(minutesSinceMidnight) private fun toMills(minutesSinceMidnight: Int): Long = MidnightTime.calcMidnightPlusMinutes(minutesSinceMidnight)
private fun getMinSinceMidnight(time: Long): Int = MidnightUtils.secondsFromMidnight(time) / 60 private fun getMinSinceMidnight(time: Long): Int = MidnightUtils.secondsFromMidnight(time) / 60

View file

@ -16,7 +16,7 @@ class TriggerTimeRangeTest : TriggerTestBase() {
@BeforeEach @BeforeEach
fun mock() { fun mock() {
now = 754 // in minutes from midnight now = 754 // in minutes from midnight
val nowMills = MidnightTime.calcPlusMinutes(now.toInt()) val nowMills = MidnightTime.calcMidnightPlusMinutes(now.toInt())
`when`(dateUtil.now()).thenReturn(nowMills) `when`(dateUtil.now()).thenReturn(nowMills)
`when`(rh.gs(R.string.timerange_value)).thenReturn("Time is between %1\$s and %2\$s") `when`(rh.gs(R.string.timerange_value)).thenReturn("Time is between %1\$s and %2\$s")
} }