Replace java.time APIs with kotlin(x).(date)time (#3001)
* Replace java.time APIs with kotlin(x).(date)time kotlinx-datetime explicitly says in its README that it does not cover i18n, so we have to stick with using their toJavaX converters wherever we localise dates and times. Yes, some of these replacements are... questionable. But I wanted to try and maximise the replacement for now. * Bump kotlinx-datetime to 0.8.0 * More kotlin.time/kotlinx-datetime replacements * Remove redundant init block & make attributes val * Replace new use of java.time.Instant
This commit is contained in:
@@ -22,6 +22,8 @@ dependencies {
|
||||
implementation(libs.bundles.kotlinx.coroutines)
|
||||
implementation(libs.bundles.serialization)
|
||||
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
implementation(libs.unifile)
|
||||
|
||||
api(libs.sqldelight.androidxPaging)
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
package tachiyomi.domain.manga.interactor
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.atStartOfDayIn
|
||||
import kotlinx.datetime.daysUntil
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Instant
|
||||
|
||||
class FetchInterval(
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||
@@ -16,40 +22,42 @@ class FetchInterval(
|
||||
|
||||
suspend fun toMangaUpdate(
|
||||
manga: Manga,
|
||||
dateTime: ZonedDateTime,
|
||||
dateTime: LocalDateTime,
|
||||
timeZone: TimeZone,
|
||||
window: Pair<Long, Long>,
|
||||
): MangaUpdate {
|
||||
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
|
||||
chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true),
|
||||
zone = dateTime.zone,
|
||||
zone = timeZone,
|
||||
)
|
||||
val currentWindow = if (window.first == 0L && window.second == 0L) {
|
||||
getWindow(ZonedDateTime.now())
|
||||
getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone)
|
||||
} else {
|
||||
window
|
||||
}
|
||||
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
||||
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, timeZone, currentWindow)
|
||||
|
||||
return MangaUpdate(id = manga.id, nextUpdate = nextUpdate, fetchInterval = interval)
|
||||
}
|
||||
|
||||
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||
val lowerBound = today.minusDays(GRACE_PERIOD)
|
||||
val upperBound = today.plusDays(GRACE_PERIOD)
|
||||
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||
fun getWindow(localDateTime: LocalDate, timeZone: TimeZone): Pair<Long, Long> {
|
||||
val today = localDateTime.atStartOfDayIn(timeZone)
|
||||
val lowerBound = today - GRACE_PERIOD.days
|
||||
val upperBound = today + GRACE_PERIOD.days
|
||||
return Pair(lowerBound.toEpochMilliseconds(), upperBound.toEpochMilliseconds())
|
||||
}
|
||||
|
||||
internal fun calculateInterval(chapters: List<Chapter>, zone: ZoneId): Int {
|
||||
internal fun calculateInterval(chapters: List<Chapter>, zone: TimeZone): Int {
|
||||
val chapterWindow = if (chapters.size <= 8) 3 else 10
|
||||
|
||||
val uploadDates = chapters.asSequence()
|
||||
.filter { it.dateUpload > 0L }
|
||||
.sortedByDescending { it.dateUpload }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
Instant.fromEpochMilliseconds(it.dateUpload)
|
||||
.toLocalDateTime(zone)
|
||||
.date
|
||||
.atStartOfDayIn(zone)
|
||||
}
|
||||
.distinct()
|
||||
.take(chapterWindow)
|
||||
@@ -58,9 +66,10 @@ class FetchInterval(
|
||||
val fetchDates = chapters.asSequence()
|
||||
.sortedByDescending { it.dateFetch }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
Instant.fromEpochMilliseconds(it.dateFetch)
|
||||
.toLocalDateTime(zone)
|
||||
.date
|
||||
.atStartOfDayIn(zone)
|
||||
}
|
||||
.distinct()
|
||||
.take(chapterWindow)
|
||||
@@ -69,13 +78,13 @@ class FetchInterval(
|
||||
val interval = when {
|
||||
// Enough upload date from source
|
||||
uploadDates.size >= 3 -> {
|
||||
val ranges = uploadDates.windowed(2).map { x -> x[1].until(x[0], ChronoUnit.DAYS) }.sorted()
|
||||
ranges[(ranges.size - 1) / 2].toInt()
|
||||
val ranges = uploadDates.windowed(2).map { x -> x[1].daysUntil(x[0], zone) }.sorted()
|
||||
ranges[(ranges.size - 1) / 2]
|
||||
}
|
||||
// Enough fetch date from client
|
||||
fetchDates.size >= 3 -> {
|
||||
val ranges = fetchDates.windowed(2).map { x -> x[1].until(x[0], ChronoUnit.DAYS) }.sorted()
|
||||
ranges[(ranges.size - 1) / 2].toInt()
|
||||
val ranges = fetchDates.windowed(2).map { x -> x[1].daysUntil(x[0], zone) }.sorted()
|
||||
ranges[(ranges.size - 1) / 2]
|
||||
}
|
||||
// Default to 7 days
|
||||
else -> 7
|
||||
@@ -87,34 +96,34 @@ class FetchInterval(
|
||||
private fun calculateNextUpdate(
|
||||
manga: Manga,
|
||||
interval: Int,
|
||||
dateTime: ZonedDateTime,
|
||||
dateTime: LocalDateTime,
|
||||
timeZone: TimeZone,
|
||||
window: Pair<Long, Long>,
|
||||
): Long {
|
||||
if (manga.nextUpdate in window.first.rangeTo(window.second + 1)) {
|
||||
return manga.nextUpdate
|
||||
}
|
||||
|
||||
val latestDate = ZonedDateTime.ofInstant(
|
||||
if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(),
|
||||
dateTime.zone,
|
||||
)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
|
||||
val cycle = timeSinceLatest.floorDiv(
|
||||
val instant = if (manga.lastUpdate > 0) Instant.fromEpochMilliseconds(manga.lastUpdate) else Clock.System.now()
|
||||
val latestDate = instant.toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone)
|
||||
|
||||
val daysSinceLatest = (dateTime.toInstant(timeZone) - latestDate).inWholeDays
|
||||
val cycle = daysSinceLatest.floorDiv(
|
||||
interval.absoluteValue.takeIf { interval < 0 }
|
||||
?: increaseInterval(interval, timeSinceLatest, increaseWhenOver = 10),
|
||||
?: increaseInterval(interval, daysSinceLatest, increaseWhenOver = 10),
|
||||
)
|
||||
return latestDate.plusDays((cycle + 1) * interval.absoluteValue.toLong()).toEpochSecond(dateTime.offset) * 1000
|
||||
|
||||
val offsetDays = ((cycle + 1) * interval.absoluteValue.toLong()).days
|
||||
return latestDate.plus(offsetDays).toEpochMilliseconds()
|
||||
}
|
||||
|
||||
private fun increaseInterval(delta: Int, timeSinceLatest: Int, increaseWhenOver: Int): Int {
|
||||
private fun increaseInterval(delta: Int, daysSinceLatest: Long, increaseWhenOver: Int): Int {
|
||||
if (delta >= MAX_INTERVAL) return MAX_INTERVAL
|
||||
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
val cycle = daysSinceLatest.floorDiv(delta) + 1
|
||||
return if (cycle > increaseWhenOver) {
|
||||
increaseInterval(delta * 2, timeSinceLatest, increaseWhenOver)
|
||||
increaseInterval(delta * 2, daysSinceLatest, increaseWhenOver)
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import kotlinx.serialization.json.JsonObject
|
||||
import mihon.core.common.extensions.EMPTY
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import java.io.ObjectStreamException
|
||||
import java.time.Instant
|
||||
import kotlin.time.Instant
|
||||
import java.io.Serializable as JavaSerializable
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@@ -47,7 +47,7 @@ data class Manga(
|
||||
val expectedNextUpdate: Instant?
|
||||
get() = nextUpdate
|
||||
.takeIf { status != SManga.COMPLETED.toLong() }
|
||||
?.let { Instant.ofEpochMilli(it) }
|
||||
?.let { Instant.fromEpochMilliseconds(it) }
|
||||
|
||||
val sorting: Long
|
||||
get() = chapterFlags and CHAPTER_SORTING_MASK
|
||||
|
||||
@@ -3,7 +3,7 @@ package tachiyomi.domain.updates.interactor
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||
import tachiyomi.domain.updates.repository.UpdatesRepository
|
||||
import java.time.Instant
|
||||
import kotlin.time.Instant
|
||||
|
||||
class GetUpdates(
|
||||
private val repository: UpdatesRepository,
|
||||
@@ -21,7 +21,7 @@ class GetUpdates(
|
||||
hideExcludedScanlators: Boolean,
|
||||
): Flow<List<UpdatesWithRelations>> {
|
||||
return repository.subscribeAll(
|
||||
instant.toEpochMilli(),
|
||||
instant.toEpochMilliseconds(),
|
||||
limit = 500,
|
||||
unread = unread,
|
||||
started = started,
|
||||
|
||||
@@ -2,27 +2,27 @@ package tachiyomi.domain.manga.interactor
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.mockk
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.parallel.Execution
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class FetchIntervalTest {
|
||||
|
||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||
private val testZoneId = ZoneOffset.UTC
|
||||
private val testTime = LocalDateTime.parse("2020-01-01T00:00:00")
|
||||
private val testTimeZone = TimeZone.UTC
|
||||
private var chapter = Chapter.create().copy(
|
||||
dateFetch = testTime.toEpochSecond() * 1000,
|
||||
dateUpload = testTime.toEpochSecond() * 1000,
|
||||
dateFetch = testTime.toInstant(testTimeZone).toEpochMilliseconds(),
|
||||
dateUpload = testTime.toInstant(testTimeZone).toEpochMilliseconds(),
|
||||
)
|
||||
|
||||
private val fetchInterval = FetchInterval(mockk())
|
||||
@@ -32,12 +32,12 @@ class FetchIntervalTest {
|
||||
val chaptersWithUploadDate = (1..50).map {
|
||||
chapterWithTime(chapter, 1.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 7
|
||||
fetchInterval.calculateInterval(chaptersWithUploadDate, testTimeZone) shouldBe 7
|
||||
|
||||
val chaptersWithoutUploadDate = chaptersWithUploadDate.map {
|
||||
it.copy(dateUpload = 0L)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 7
|
||||
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testTimeZone) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -51,7 +51,7 @@ class FetchIntervalTest {
|
||||
|
||||
val chapters = oldChapters + newChapters
|
||||
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -66,7 +66,7 @@ class FetchIntervalTest {
|
||||
|
||||
val chapters = oldChapters + newChapters
|
||||
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -74,7 +74,7 @@ class FetchIntervalTest {
|
||||
val chapters = (1..10).map {
|
||||
chapterWithTime(chapter, 10.hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -84,7 +84,7 @@ class FetchIntervalTest {
|
||||
} + (1..5).map {
|
||||
chapterWithTime(chapter, 2.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,7 +92,7 @@ class FetchIntervalTest {
|
||||
val chapters = (1..20).map {
|
||||
chapterWithTime(chapter, it.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -100,7 +100,7 @@ class FetchIntervalTest {
|
||||
val chapters = (1..20).map {
|
||||
chapterWithTime(chapter, (15 * it).hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -108,7 +108,7 @@ class FetchIntervalTest {
|
||||
val chapters = (1..20).map {
|
||||
chapterWithTime(chapter, (2 * it).days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 2
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 2
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -116,12 +116,12 @@ class FetchIntervalTest {
|
||||
val chaptersWithUploadDate = (1..5).map {
|
||||
chapterWithTime(chapter, (25 * it).hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 1
|
||||
fetchInterval.calculateInterval(chaptersWithUploadDate, testTimeZone) shouldBe 1
|
||||
|
||||
val chaptersWithoutUploadDate = chaptersWithUploadDate.map {
|
||||
it.copy(dateUpload = 0L)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 1
|
||||
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testTimeZone) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -129,11 +129,11 @@ class FetchIntervalTest {
|
||||
val chapters = (1..20).map {
|
||||
chapterWithTime(chapter, (43 * it).hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 2
|
||||
fetchInterval.calculateInterval(chapters, testTimeZone) shouldBe 2
|
||||
}
|
||||
|
||||
private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter {
|
||||
val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000
|
||||
val newTime = testTime.toInstant(testTimeZone).plus(duration).toEpochMilliseconds()
|
||||
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user