diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt b/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt index 3d36fa494a..dfcfab28f5 100644 --- a/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt +++ b/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt @@ -1,22 +1,56 @@ package app.aaps.core.utils -import org.joda.time.DateTime +import java.time.Duration +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime -object MidnightUtils { - /* +/** * Midnight time conversion */ +object MidnightUtils { + + /** + * Actual passed seconds from midnight ignoring DST change + * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) + * + * @return seconds + */ fun secondsFromMidnight(): Int { - val passed = DateTime().millisOfDay.toLong() - return (passed / 1000).toInt() + val nowZoned = ZonedDateTime.now() + val localTime = nowZoned.toLocalTime() + val midnight: Instant = nowZoned.toLocalDate().atStartOfDay(nowZoned.zone).toInstant() + val duration: Duration = Duration.between(midnight, localTime) + return duration.seconds.toInt() } - fun secondsFromMidnight(date: Long): Int { - val passed = DateTime(date).millisOfDay.toLong() - return (passed / 1000).toInt() + /** + * Passed seconds from midnight for specified time ignoring DST change + * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) + * + * @param timestamp time + * @return seconds + */ + fun secondsFromMidnight(timestamp: Long): Int { + val timeZoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()) + val localTime = timeZoned.toLocalTime() + val midnight = timeZoned.toLocalDate().atStartOfDay(timeZoned.zone).toLocalTime() + val duration: Duration = Duration.between(midnight, localTime) + return duration.seconds.toInt() } - fun milliSecFromMidnight(date: Long): Long { - return DateTime(date).millisOfDay.toLong() + /** + * Passed milliseconds from midnight for specified time ignoring DST change + * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) + * + * @param timestamp time + * @return milliseconds + */ + fun milliSecFromMidnight(timestamp: Long): Long { + val timeZoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()) + val localTime = timeZoned.toLocalTime() + val midnight = timeZoned.toLocalDate().atStartOfDay(timeZoned.zone).toLocalTime() + val duration: Duration = Duration.between(midnight, localTime) + return duration.toMillis() } } \ No newline at end of file diff --git a/core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt b/core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt new file mode 100644 index 0000000000..46d4b46fae --- /dev/null +++ b/core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt @@ -0,0 +1,67 @@ +package app.aaps.core.utils + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.util.TimeZone + +class MidnightUtilsTest { + + @BeforeEach fun setUp() { + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Amsterdam")) + } + + @Test + fun secondsFromMidnight() { + val time = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(time)).isIn(0..24 * 3600) + } + + @Test + fun testSecondsFromMidnight() { + val midnight = LocalDate.now().atTime(LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(midnight)).isEqualTo(0) + val oneHourAfter = LocalDateTime.ofInstant(Instant.ofEpochMilli(midnight), ZoneId.systemDefault()).atZone(ZoneId.systemDefault()).plusHours(1).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(oneHourAfter)).isEqualTo(3600) + } + + @Test + fun milliSecFromMidnight() { + val midnight = LocalDate.now().atTime(LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(midnight)).isEqualTo(0) + val oneHourAfter = LocalDateTime.ofInstant(Instant.ofEpochMilli(midnight), ZoneId.systemDefault()).atZone(ZoneId.systemDefault()).plusHours(1).toInstant().toEpochMilli() + assertThat(MidnightUtils.milliSecFromMidnight(oneHourAfter)).isEqualTo(3600 * 1000) + } + + @Test fun testDateTimeToDuration() { + val dateTime = ZonedDateTime.of(1991, 8, 13, 23, 5, 1, 0, ZoneId.of("Europe/Amsterdam")).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101) + assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L) + } + + @Test fun testDateTimeToDurationAtDstChange() { + val dateTime = ZonedDateTime.of(2020, 10, 25, 23, 5, 1, 0, ZoneId.of("Europe/Amsterdam")).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101) + assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L) + } + + @Test fun testDateTimeToDurationAtDstReverseChange() { + val dateTime = ZonedDateTime.of(2020, 3, 29, 23, 5, 1, 0, ZoneId.of("Europe/Amsterdam")).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101) + assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L) + } + + @Test fun testDateTimeInOtherZone() { + TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles")) + assertThat(ZoneId.systemDefault().id).isEqualTo("America/Los_Angeles") + val dateTime = ZonedDateTime.of(2020, 3, 29, 23, 5, 1, 0, ZoneId.of("America/Los_Angeles")).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101) + assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L) + } +} \ No newline at end of file