Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/content/database/dao/AccountDailyStats.kt

154 lines
6.5 KiB
Kotlin

package org.mariotaku.twidere.content.database.dao
import android.arch.persistence.room.*
import org.mariotaku.twidere.content.database.converter.LocalDateConverter
import org.mariotaku.twidere.content.model.AccountStats
import org.mariotaku.twidere.extension.julianDay
import org.mariotaku.twidere.extension.time
import org.mariotaku.twidere.model.UserKey
import java.util.*
import kotlin.math.sign
@Dao
abstract class AccountDailyStats {
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insert(stats: AccountStats)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insert(stats: Collection<AccountStats>)
@Query("SELECT * FROM `account_daily_stats` WHERE `createdAt` = :date")
abstract fun list(@TypeConverters(LocalDateConverter::class) date: Date): List<AccountStats>
@Query("SELECT * FROM `account_daily_stats` WHERE `accountKey` = :accountKey AND `createdAt` BETWEEN :since AND :until ORDER BY `createdAt`")
abstract fun list(accountKey: UserKey, @TypeConverters(LocalDateConverter::class) since: Date,
@TypeConverters(LocalDateConverter::class) until: Date): List<AccountStats>
@Query("SELECT * FROM `account_daily_stats` WHERE `accountKey` = :accountKey AND `createdAt` >= :since ORDER BY `createdAt` LIMIT 1")
abstract fun firstSince(accountKey: UserKey, @TypeConverters(LocalDateConverter::class) since: Date): AccountStats?
fun listSparse(accountKey: UserKey, @TypeConverters(LocalDateConverter::class) since: Date,
@TypeConverters(LocalDateConverter::class) until: Date): Array<AccountStats?> {
val sinceJulianDay = Calendar.getInstance().time(since).julianDay()
val count = Calendar.getInstance().time(until).julianDay() - sinceJulianDay + 1
val result = arrayOfNulls<AccountStats>(count)
val tempCal = Calendar.getInstance()
list(accountKey, since, until).forEach {
tempCal.time = it.createdAt
val index = tempCal.julianDay() - sinceJulianDay
if (index in result.indices) {
result[index] = it
}
}
return result
}
fun monthlySummary(accountKey: UserKey, date: Date): AccountStats.Summaries {
val since = Calendar.getInstance().apply {
time = date
add(Calendar.DATE, -27)
}.time
val prevPeriodFirst = firstSince(accountKey, Calendar.getInstance().apply {
time = date
add(Calendar.DATE, -55)
}.time)
val currentPeriodStats = listSparse(accountKey, since, date)
val statuses = currentPeriodStats.statusesSummary(prevPeriodFirst)
val followers = currentPeriodStats.followersSummary()
return AccountStats.Summaries(statuses, followers)
}
private fun Array<AccountStats?>.statusesSummary(prevPeriodFirst: AccountStats?): AccountStats.DisplaySummary {
val firstNumber = numberAt(0, AccountStats::statusesCount)
val lastNumber = numberAt(lastIndex, AccountStats::statusesCount)
var growthText: String? = null
var growthSign = 0
if (prevPeriodFirst != null) {
val prevPeriodCount = firstNumber - prevPeriodFirst.statusesCount
val currPeriodCount = lastNumber - firstNumber
val growthPercent = currPeriodCount / prevPeriodCount.toFloat() - 1
growthText = String.format(Locale.US, "%.1f%%", Math.abs(growthPercent * 100))
growthSign = growthPercent.sign.toInt()
}
val firstNonNullIndex = indexOfFirst { it != null }
if (firstNonNullIndex < 0) throw DataNotReadyException()
val diffs = LongArray(size - firstNonNullIndex) item@{ index ->
val statIndex = index + firstNonNullIndex
if (statIndex == 0) return@item 0
return@item numberAt(statIndex, AccountStats::statusesCount) - numberAt(statIndex - 1,
AccountStats::statusesCount)
}
var maxDiff = diffs.max()!!
if (maxDiff <= 0L) {
maxDiff = Math.abs(diffs.min()!!)
}
if (maxDiff <= 0L) {
maxDiff = 10
}
val values = FloatArray(size - firstNonNullIndex) item@{ index ->
return@item diffs[index] / maxDiff.toFloat()
}
val periodSum = lastNumber - firstNumber
return AccountStats.DisplaySummary(periodSum, growthSign, growthText, size, values)
}
private fun Array<AccountStats?>.followersSummary(): AccountStats.DisplaySummary {
val firstNumber = numberAt(0, AccountStats::followersCount)
val lastNumber = numberAt(lastIndex, AccountStats::followersCount)
val maxNumber = maxBy { it?.followersCount ?: Long.MIN_VALUE }!!.followersCount
val valuesCount = size
val values = indices.map { index ->
val number = numberAt(index, AccountStats::followersCount)
return@map (number - firstNumber) / (maxNumber - firstNumber).toFloat()
}.toFloatArray()
val growth = lastNumber - firstNumber
return AccountStats.DisplaySummary(firstNumber, growth.sign, Math.abs(growth).toString(),
valuesCount, values)
}
private fun Array<AccountStats?>.numberAt(index: Int, selector: (AccountStats) -> Long): Long {
val itemAt = this[index]
if (itemAt != null) return selector(itemAt)
val firstNonNullIndex = indexOfFirst { it != null }
val lastNonNullIndex = indexOfLast { it != null }
if (firstNonNullIndex < 0 || lastNonNullIndex < 0 || firstNonNullIndex == lastNonNullIndex) {
throw DataNotReadyException()
}
val count = lastNonNullIndex - firstNonNullIndex
val firstNonNullNumber = selector(this[firstNonNullIndex]!!)
val lastNonNullNumber = selector(this[lastNonNullIndex]!!)
val delta = (lastNonNullNumber - firstNonNullNumber) / count
when {
index < firstNonNullIndex -> return firstNonNullNumber - delta * (firstNonNullIndex - index)
index > lastNonNullIndex -> return lastNonNullNumber + delta * (index - lastNonNullIndex)
else -> {
val startIndex = (0 until index).last { this[it] != null }
val endIndex = (index + 1..lastIndex).first { this[it] != null }
val start = selector(this[startIndex]!!)
val end = selector(this[endIndex]!!)
val delta2 = (end - start) / (endIndex - startIndex)
return start + delta2 * (index - startIndex)
}
}
}
class DataNotReadyException : Exception()
}