From 929365fc4c9a6757f65b55876b3a232060eff41b Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 16 Dec 2022 15:57:30 +0100 Subject: [PATCH] RealmWriteService + FlatTimeInterval for better performance --- .../android/RealmWriteService.kt | 4 +- .../android/calculus/Calculator.kt | 1252 +++++++++-------- .../android/calculus/ComputableGroup.kt | 5 + .../android/calculus/ReportWhistleBlower.kt | 16 +- .../model/extensions/SessionExtensions.kt | 2 +- .../android/model/filter/Filterable.kt | 7 +- .../migrations/PokerAnalyticsMigration.kt | 13 + .../android/model/realm/FlatTimeInterval.kt | 61 + .../android/model/realm/Session.kt | 29 +- .../android/model/utils/SessionSetManager.kt | 257 +++- .../android/ui/fragment/StatisticsFragment.kt | 3 +- .../report/ComposableTableReportFragment.kt | 1 + .../ui/modules/calendar/CalendarFragment.kt | 8 +- .../android/ui/modules/feed/FeedFragment.kt | 27 +- .../android/util/extensions/DateExtension.kt | 26 + 15 files changed, 1061 insertions(+), 650 deletions(-) create mode 100644 app/src/main/java/net/pokeranalytics/android/model/realm/FlatTimeInterval.kt diff --git a/app/src/main/java/net/pokeranalytics/android/RealmWriteService.kt b/app/src/main/java/net/pokeranalytics/android/RealmWriteService.kt index 874fdbba..be2f692f 100644 --- a/app/src/main/java/net/pokeranalytics/android/RealmWriteService.kt +++ b/app/src/main/java/net/pokeranalytics/android/RealmWriteService.kt @@ -43,9 +43,9 @@ class RealmWriteService : Service() { this.realm.executeTransactionAsync({ asyncRealm -> handler(asyncRealm) - Timber.d(">> handler done") +// Timber.d(">> handler done") }, { - Timber.d(">> YEAAAAAAAAAAAH !!!") +// Timber.d(">> YEAAAAAAAAAAAH !!!") this.realm.refresh() }, { Timber.d(">> NOOOOO error = $it") diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt b/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt index 85c57c47..f78e8152 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt @@ -6,10 +6,10 @@ import net.pokeranalytics.android.calculus.Stat.* import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.combined -import net.pokeranalytics.android.model.extensions.hourlyDuration import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.filter import net.pokeranalytics.android.model.realm.* +import net.pokeranalytics.android.model.utils.SessionInterval import net.pokeranalytics.android.util.extensions.startOfDay import timber.log.Timber import java.util.* @@ -23,34 +23,44 @@ import kotlin.math.sqrt */ class Calculator { - /** - * The options used for calculations and display - */ - class Options( - var progressValues: ProgressValues = ProgressValues.NONE, - var stats: List = listOf(), - var criterias: List = listOf(), - var query: Query = Query(), - var filterId: String? = null, - private var aggregationType: AggregationType? = null, - var userGenerated: Boolean = false, - var reportSetupId: String? = null, - var includedTransactions: List = listOf() - ) { - - constructor( - progressValues: ProgressValues = ProgressValues.NONE, - stats: List = listOf(), - criterias: List = listOf(), - filter: Filter? = null, - aggregationType: AggregationType? = null, - userGenerated: Boolean = false, - reportSetupId: String? = null) : - this(progressValues, stats, criterias, filter?.query ?: Query(), filter?.id, aggregationType, userGenerated, reportSetupId) - - /** - * Specifies whether progress values should be added and their kind - */ + /** + * The options used for calculations and display + */ + class Options( + var progressValues: ProgressValues = ProgressValues.NONE, + var stats: List = listOf(), + var criterias: List = listOf(), + var query: Query = Query(), + var filterId: String? = null, + private var aggregationType: AggregationType? = null, + var userGenerated: Boolean = false, + var reportSetupId: String? = null, + var includedTransactions: List = listOf() + ) { + + constructor( + progressValues: ProgressValues = ProgressValues.NONE, + stats: List = listOf(), + criterias: List = listOf(), + filter: Filter? = null, + aggregationType: AggregationType? = null, + userGenerated: Boolean = false, + reportSetupId: String? = null + ) : + this( + progressValues, + stats, + criterias, + filter?.query ?: Query(), + filter?.id, + aggregationType, + userGenerated, + reportSetupId + ) + + /** + * Specifies whether progress values should be added and their kind + */ // var progressValues: ProgressValues = progressValues // get() { // if (field == ProgressValues.NONE && this.display.requireProgressValues) { @@ -59,248 +69,272 @@ class Calculator { // return field // } - init { - this.aggregationType?.let { - this.criterias = it.criterias - } - } - - - /** - * The type of evolution numericValues - */ - enum class ProgressValues { - NONE, - STANDARD, - TIMED - } - - /** - * This function determines whether the standard deviation should be computed - */ - val computeStandardDeviation: Boolean - get() { - this.stats.forEach { - if (it.isStandardDeviation) { - return true - } - } - return false - } - - /** - * Whether the longest streaks should be computed - */ - val computeLongestStreak: Boolean - get() { - return this.stats.contains(LONGEST_STREAKS) - } - - /** - * Whether the values should be sorted - */ - val shouldSortValues: Boolean - get() { - return this.progressValues != ProgressValues.NONE || this.computeLongestStreak - } - - /** - * Whether the number of locations played should be computed - */ - val computeLocationsPlayed: Boolean - get() { - return this.stats.contains(LOCATIONS_PLAYED) - } - /** - * Whether the number of days played should be computed - */ - val computeDaysPlayed: Boolean - get() { - return this.stats.contains(DAYS_PLAYED) - } - /** - * Whether progress values should be managed at the group level - */ - val shouldManageMultiGroupProgressValues: Boolean - get() { - return if (this.aggregationType != null) { - this.aggregationType == AggregationType.MONTH || this.aggregationType == AggregationType.YEAR - } else { - false - } - } - - /** - * Returns some default name - */ - fun getName(context: Context): String { - return when (this.stats.size) { - 1 -> this.stats.first().localizedTitle(context) - else -> this.query.getName(context) - } - } - - } - - companion object { - - fun computeStatsWithEvolutionByAggregationType( - realm: Realm, - stat: Stat, - group: ComputableGroup, - aggregationType: AggregationType, - stats: List? = null - ): Report { - - val options = Options( - progressValues = Options.ProgressValues.STANDARD, - stats = listOf(stat), - aggregationType = aggregationType - ) - - if (aggregationType == AggregationType.DURATION) { - options.progressValues = Options.ProgressValues.TIMED - } - - stats?.let { - options.stats = stats - } - - return when (aggregationType) { - AggregationType.SESSION, AggregationType.DURATION -> this.computeGroups(realm, listOf(group), options) - AggregationType.MONTH, AggregationType.YEAR -> { - this.computeStats(realm, options) - } - } - } - - fun computeStats(realm: Realm, options: Options = Options()): Report { - - val computableGroups: MutableList = mutableListOf() - - val combinations = options.criterias.combined(realm) + init { + this.aggregationType?.let { + this.criterias = it.criterias + } + } + + + /** + * The type of evolution numericValues + */ + enum class ProgressValues { + NONE, + STANDARD, + TIMED + } + + /** + * This function determines whether the standard deviation should be computed + */ + val computeStandardDeviation: Boolean + get() { + this.stats.forEach { + if (it.isStandardDeviation) { + return true + } + } + return false + } + + /** + * Whether the longest streaks should be computed + */ + val computeLongestStreak: Boolean + get() { + return this.stats.contains(LONGEST_STREAKS) + } + + /** + * Whether the values should be sorted + */ + val shouldSortValues: Boolean + get() { + return this.progressValues != ProgressValues.NONE || this.computeLongestStreak + } + + /** + * Whether the number of locations played should be computed + */ + val computeLocationsPlayed: Boolean + get() { + return this.stats.contains(LOCATIONS_PLAYED) + } + + /** + * Whether the number of days played should be computed + */ + val computeDaysPlayed: Boolean + get() { + return this.stats.contains(DAYS_PLAYED) + } + + /** + * Whether progress values should be managed at the group level + */ + val shouldManageMultiGroupProgressValues: Boolean + get() { + return if (this.aggregationType != null) { + this.aggregationType == AggregationType.MONTH || this.aggregationType == AggregationType.YEAR + } else { + false + } + } + + /** + * Returns some default name + */ + fun getName(context: Context): String { + return when (this.stats.size) { + 1 -> this.stats.first().localizedTitle(context) + else -> this.query.getName(context) + } + } + + } + + companion object { + + fun computeStatsWithEvolutionByAggregationType( + realm: Realm, + stat: Stat, + group: ComputableGroup, + aggregationType: AggregationType, + stats: List? = null + ): Report { + + val options = Options( + progressValues = Options.ProgressValues.STANDARD, + stats = listOf(stat), + aggregationType = aggregationType + ) + + if (aggregationType == AggregationType.DURATION) { + options.progressValues = Options.ProgressValues.TIMED + } + + stats?.let { + options.stats = stats + } + + return when (aggregationType) { + AggregationType.SESSION, AggregationType.DURATION -> this.computeGroups( + realm, + listOf(group), + options + ) + AggregationType.MONTH, AggregationType.YEAR -> { + this.computeStats(realm, options) + } + } + } + + fun computeStats(realm: Realm, options: Options = Options()): Report { + + val computableGroups: MutableList = mutableListOf() + + val combinations = options.criterias.combined(realm) // Timber.d("Combinations: ${ combinations.map { it.defaultName }}") - for (comparatorQuery in combinations) { - comparatorQuery.merge(options.query) - val group = ComputableGroup(comparatorQuery) - computableGroups.add(group) - } - - if (computableGroups.size == 0) { - val group = ComputableGroup(options.query) - computableGroups.add(group) - } - - return this.computeGroups(realm, computableGroups, options) - } - - /** - * Computes all statIds for list of Session sessionGroup - */ - fun computeGroups(realm: Realm, groups: List, options: Options = Options()): Report { - - val report = Report(options) - for (group in groups) { - - // Clean existing computables / sessionSets if group is reused - group.cleanup() - - // Computes actual sessionGroup statIds - val results: ComputedResults = this.compute(realm, group, options) - - // Computes the compared sessionGroup if existing - val comparedGroup = group.comparedGroup - if (comparedGroup != null) { - val comparedResults = this.compute(realm, comparedGroup, options) - group.comparedComputedResults = comparedResults - results.computeStatVariations(comparedResults) - } - - if (options.shouldManageMultiGroupProgressValues) { - group.comparedComputedResults = report.results.lastOrNull() - } - - results.finalize() // later treatment, such as evolution numericValues sorting - report.addResults(results) + for (comparatorQuery in combinations) { + comparatorQuery.merge(options.query) + val group = ComputableGroup(comparatorQuery) + computableGroups.add(group) + } + + if (computableGroups.size == 0) { + val group = ComputableGroup(options.query) + computableGroups.add(group) + } + + return this.computeGroups(realm, computableGroups, options) + } + + /** + * Computes all statIds for list of Session sessionGroup + */ + fun computeGroups( + realm: Realm, + groups: List, + options: Options = Options() + ): Report { + + val report = Report(options) + for (group in groups) { + + // Clean existing computables / sessionSets if group is reused + group.cleanup() + + // Computes actual sessionGroup statIds + val results: ComputedResults = this.compute(realm, group, options) + + // Computes the compared sessionGroup if existing + val comparedGroup = group.comparedGroup + if (comparedGroup != null) { + val comparedResults = this.compute(realm, comparedGroup, options) + group.comparedComputedResults = comparedResults + results.computeStatVariations(comparedResults) + } + + if (options.shouldManageMultiGroupProgressValues) { + group.comparedComputedResults = report.results.lastOrNull() + } + + results.finalize() // later treatment, such as evolution numericValues sorting + report.addResults(results) // val e = Date() // val duration = (e.time - s.time) / 1000.0 // Timber.d(">>> group ${group.name} in $duration seconds") - } + } - return report - } + return report + } - /** - * Computes statIds for a SessionSet - */ - fun compute(realm: Realm, computableGroup: ComputableGroup, options: Options = Options()): ComputedResults { + /** + * Computes statIds for a SessionSet + */ + fun compute( + realm: Realm, + computableGroup: ComputableGroup, + options: Options = Options() + ): ComputedResults { - val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues) + val results = + ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues) - val computables = computableGroup.computables(realm, options.shouldSortValues) + val computables = computableGroup.computables(realm, options.shouldSortValues) - if (computables.size == 0) { // we don't want to return stats with 0 as a value when comparing best performances - return results - } + if (computables.size == 0) { // we don't want to return stats with 0 as a value when comparing best performances + return results + } // Timber.d("#### Start computing group, ${computables.size} computables") - results.addStat(NUMBER_OF_GAMES, computables.size.toDouble()) + results.addStat(NUMBER_OF_GAMES, computables.size.toDouble()) // computables.forEach { // Timber.d("$$$ buyin = ${it.ratedBuyin} $$$ net result = ${it.ratedNet}") // } - val d1 = Date() + var ratedNet = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble() + if (options.includedTransactions.isNotEmpty()) { + for (transactionType in options.includedTransactions) { + val transactions = computableGroup.transactions( + realm, + transactionType, + options.shouldSortValues + ) + val transactionRatedAmount = + transactions.sum(Transaction.Field.RATED_AMOUNT.identifier).toDouble() + ratedNet += transactionRatedAmount + } + } - var ratedNet = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble() - if (options.includedTransactions.isNotEmpty()) { - for (transactionType in options.includedTransactions) { - val transactions = computableGroup.transactions(realm, transactionType, options.shouldSortValues) - val transactionRatedAmount = transactions.sum(Transaction.Field.RATED_AMOUNT.identifier).toDouble() - ratedNet += transactionRatedAmount - } - } + results.addStat(NET_RESULT, ratedNet) - results.addStat(NET_RESULT, ratedNet) + val totalHands = + computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble() + results.addStat(HANDS_PLAYED, totalHands) - val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble() - results.addStat(HANDS_PLAYED, totalHands) + val bbSum = computables.sum(ComputableResult.Field.BB_NET.identifier).toDouble() + results.addStat(BB_NET_RESULT, bbSum) - val bbSum = computables.sum(ComputableResult.Field.BB_NET.identifier).toDouble() - results.addStat(BB_NET_RESULT, bbSum) + val bbSessionCount = + computables.sum(ComputableResult.Field.HAS_BIG_BLIND.identifier).toInt() + results.addStat(BB_SESSION_COUNT, bbSessionCount.toDouble()) - val bbSessionCount = computables.sum(ComputableResult.Field.HAS_BIG_BLIND.identifier).toInt() - results.addStat(BB_SESSION_COUNT, bbSessionCount.toDouble()) + val winningSessionCount = + computables.sum(ComputableResult.Field.IS_POSITIVE.identifier).toInt() + results.addStat(WINNING_SESSION_COUNT, winningSessionCount.toDouble()) - val winningSessionCount = computables.sum(ComputableResult.Field.IS_POSITIVE.identifier).toInt() - results.addStat(WINNING_SESSION_COUNT, winningSessionCount.toDouble()) + val totalBuyin = + computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble() + results.addStat(TOTAL_BUYIN, totalBuyin) - val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble() - results.addStat(TOTAL_BUYIN, totalBuyin) - - val totalTips = computables.sum(ComputableResult.Field.RATED_TIPS.identifier).toDouble() - results.addStat(TOTAL_TIPS, totalTips) + val totalTips = computables.sum(ComputableResult.Field.RATED_TIPS.identifier).toDouble() + results.addStat(TOTAL_TIPS, totalTips) // Timber.d("########## totalBuyin = ${totalBuyin} ### sum = ${sum}") - val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble() - maxNetResult?.let { - results.addStat(MAXIMUM_NET_RESULT, it) - } - - val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble() - minNetResult?.let { - results.addStat(MINIMUM_NET_RESULT, it) - } - - Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 -> - results.addStat(NET_BB_PER_100_HANDS, netBB100) - } - Stat.returnOnInvestment(ratedNet, totalBuyin)?.let { roi -> - results.addStat(ROI, roi) - } + val maxNetResult = + computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble() + maxNetResult?.let { + results.addStat(MAXIMUM_NET_RESULT, it) + } + + val minNetResult = + computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble() + minNetResult?.let { + results.addStat(MINIMUM_NET_RESULT, it) + } + + Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 -> + results.addStat(NET_BB_PER_100_HANDS, netBB100) + } + Stat.returnOnInvestment(ratedNet, totalBuyin)?.let { roi -> + results.addStat(ROI, roi) + } // val shouldComputeITMRatio = options.stats.contains(TOURNAMENT_ITM_RATIO) || computableGroup.displayedStats?.contains(TOURNAMENT_ITM_RATIO) == true // if (shouldComputeITMRatio) { @@ -309,346 +343,406 @@ class Calculator { // results.addStat(TOURNAMENT_ITM_RATIO, itmRatio) // } - if (options.computeLocationsPlayed) { - results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble()) - } - - var average = 0.0 // also used for standard deviation later - if (computables.size > 0) { - average = ratedNet / computables.size.toDouble() - val winRatio = winningSessionCount.toDouble() / computables.size.toDouble() - val itmRatio = winningSessionCount.toDouble() / computables.size.toDouble() - val avgBuyin = totalBuyin / computables.size.toDouble() - - results.addStats( - setOf( - ComputedStat(AVERAGE, average), - ComputedStat(WIN_RATIO, winRatio), - ComputedStat(TOURNAMENT_ITM_RATIO, itmRatio), - ComputedStat(AVERAGE_BUYIN, avgBuyin) - ) - ) - } - var averageBB = 0.0 - if (bbSessionCount > 0) { - averageBB = bbSum / bbSessionCount - results.addStat(AVERAGE_NET_BB, averageBB) - } - val d2 = Date() - - val shouldIterateOverComputables = - (options.progressValues == Options.ProgressValues.STANDARD || options.computeLongestStreak) - - // Computable Result - if (shouldIterateOverComputables) { - - var index = 0 - var tSum = 0.0 - var tBBSum = 0.0 - var tBBSessionCount = 0 - var tWinningSessionCount = 0 - var tBuyinSum = 0.0 - var tHands = 0.0 - var longestWinStreak = 0 - var longestLoseStreak = 0 - var currentStreak = 0 - var tITMCount = 0 - - for (computable in computables) { - index++ - tSum += computable.ratedNet - tBBSum += computable.bbNet - tBBSessionCount += computable.hasBigBlind - tWinningSessionCount += computable.isPositive - tITMCount += computable.isPositive - tBuyinSum += computable.ratedBuyin - tHands += computable.estimatedHands - - if (computable.isPositive == 1) { // positive result - if (currentStreak >= 0) { // currently positive streak - currentStreak++ - } else { // currently negative streak - longestLoseStreak = min(longestLoseStreak, currentStreak) - currentStreak = 1 - } - } else { // negative result - if (currentStreak <= 0) { // currently negative streak - currentStreak-- - } else { // currently positive streak - longestWinStreak = max(longestWinStreak, currentStreak) - currentStreak = -1 - } - } - - val session = - computable.session ?: throw PAIllegalStateException("Computing lone ComputableResult") - results.addEvolutionValue(tSum, stat = NET_RESULT, data = session) - results.addEvolutionValue(tSum / index, stat = AVERAGE, data = session) - results.addEvolutionValue(index.toDouble(), stat = NUMBER_OF_GAMES, data = session) - results.addEvolutionValue(tBBSum / tBBSessionCount, stat = AVERAGE_NET_BB, data = session) - results.addEvolutionValue( - (tWinningSessionCount.toDouble() / index.toDouble()), - stat = WIN_RATIO, - data = session - ) - results.addEvolutionValue( - tITMCount.toDouble() / index.toDouble(), - stat = TOURNAMENT_ITM_RATIO, - data = session) - results.addEvolutionValue(tBuyinSum / index, stat = AVERAGE_BUYIN, data = session) - results.addEvolutionValue(computable.ratedNet, stat = STANDARD_DEVIATION, data = session) - - Stat.netBBPer100Hands(tBBSum, tHands)?.let { netBB100 -> - results.addEvolutionValue(netBB100, stat = NET_BB_PER_100_HANDS, data = session) - } - - Stat.returnOnInvestment(tSum, tBuyinSum)?.let { roi -> - results.addEvolutionValue(roi, stat = ROI, data = session) - } - - } - - if (currentStreak >= 0) { - longestWinStreak = max(longestWinStreak, currentStreak) - } else { - longestLoseStreak = min(longestLoseStreak, currentStreak) - } - - // loseStreak is negative and we want it positive - results.addStat(LONGEST_STREAKS, longestWinStreak.toDouble(), -longestLoseStreak.toDouble()) - - } - - val d3 = Date() - - val sessionSets = computableGroup.sessionSets(realm, options.shouldSortValues) - results.addStat(NUMBER_OF_SETS, sessionSets.size.toDouble()) - - var gHourlyDuration: Double? = null - var gBBSum: Double? = null - var maxDuration: Double? = null - - if (computableGroup.conditions.isEmpty()) { // SessionSets are fine - gHourlyDuration = - sessionSets.sum(SessionSet.Field.NET_DURATION.identifier).toDouble() / 3600000 // (milliseconds to hours) - gBBSum = sessionSets.sum(SessionSet.Field.BB_NET.identifier).toDouble() - - sessionSets.max(SessionSet.Field.NET_DURATION.identifier)?.let { - maxDuration = it.toDouble() / 3600000 - } - } - - val shouldIterateOverSets = computableGroup.conditions.isNotEmpty() - || options.progressValues != Options.ProgressValues.NONE - || options.computeDaysPlayed - - // Session Set - if (shouldIterateOverSets) { - - var tHourlyDuration = 0.0 - var tIndex = 0 - var tRatedNetSum = 0.0 - var tBBSum = 0.0 - var tTotalHands = 0.0 - var tHourlyRate: Double - var tHourlyRateBB: Double - val daysSet = mutableSetOf() - var tMaxDuration = 0.0 - - for (sessionSet in sessionSets) { - tIndex++ - - val setStats = SSStats(sessionSet, computableGroup.query) - - tRatedNetSum += setStats.ratedNet - tBBSum += setStats.bbSum - tHourlyDuration += setStats.hourlyDuration - tTotalHands += setStats.estimatedHands - tMaxDuration = max(tMaxDuration, setStats.hourlyDuration) - - tHourlyRate = tRatedNetSum / tHourlyDuration - tHourlyRateBB = tBBSum / tHourlyDuration - daysSet.add(sessionSet.startDate.startOfDay()) - - when (options.progressValues) { - Options.ProgressValues.STANDARD -> { - results.addEvolutionValue(tHourlyRate, stat = HOURLY_RATE, data = sessionSet) - results.addEvolutionValue(tIndex.toDouble(), stat = NUMBER_OF_SETS, data = sessionSet) - results.addEvolutionValue( - sessionSet.hourlyDuration, - tHourlyDuration, - HOURLY_DURATION, - sessionSet - ) - results.addEvolutionValue( - tHourlyDuration / tIndex, - stat = AVERAGE_HOURLY_DURATION, - data = sessionSet - ) - results.addEvolutionValue(tHourlyRateBB, stat = HOURLY_RATE_BB, data = sessionSet) - - } - Options.ProgressValues.TIMED -> { - results.addEvolutionValue(tRatedNetSum, tHourlyDuration, NET_RESULT, sessionSet) - results.addEvolutionValue(tHourlyRate, tHourlyDuration, HOURLY_RATE, sessionSet) - results.addEvolutionValue( - tIndex.toDouble(), - tHourlyDuration, - NUMBER_OF_SETS, - sessionSet - ) - results.addEvolutionValue( - sessionSet.hourlyDuration, - tHourlyDuration, - HOURLY_DURATION, - sessionSet - ) - results.addEvolutionValue( - tHourlyDuration / tIndex, - tHourlyDuration, - AVERAGE_HOURLY_DURATION, - sessionSet - ) - results.addEvolutionValue(tHourlyRateBB, tHourlyDuration, HOURLY_RATE_BB, sessionSet) - - Stat.netBBPer100Hands(tBBSum, tTotalHands)?.let { netBB100 -> - results.addEvolutionValue( - netBB100, - tHourlyDuration, - NET_BB_PER_100_HANDS, - sessionSet - ) - } - } - else -> { - // nothing - } - } - - results.addStat(DAYS_PLAYED, daysSet.size.toDouble()) - - } - - gHourlyDuration = tHourlyDuration - gBBSum = tBBSum - maxDuration = tMaxDuration - - } - - val d4 = Date() - - var hourlyRate = 0.0 - if (gHourlyDuration != null) { - - hourlyRate = ratedNet / gHourlyDuration - if (sessionSets.size > 0) { - val avgDuration = gHourlyDuration / sessionSets.size - results.addStat(HOURLY_RATE, hourlyRate) - results.addStat(AVERAGE_HOURLY_DURATION, avgDuration) - } - results.addStat(HOURLY_DURATION, gHourlyDuration) - } - - if (gBBSum != null) { - if (gHourlyDuration != null) { - results.addStat(HOURLY_RATE_BB, gBBSum / gHourlyDuration) - } - results.addStat(AVERAGE_NET_BB, gBBSum / bbSessionCount) - } - - maxDuration?.let { maxd -> - results.addStat(MAXIMUM_DURATION, maxd) // (milliseconds to hours) - } - - val bbPer100Hands = bbSum / totalHands * 100 - - // Standard Deviation - if (options.computeStandardDeviation) { - - // Session - var stdSum = 0.0 - var stdBBSum = 0.0 - var stdBBper100HandsSum = 0.0 - for (computable in computables) { - stdSum += (computable.ratedNet - average).pow(2.0) - stdBBSum += (computable.bbNet - averageBB).pow(2.0) - stdBBper100HandsSum += (computable.bbPer100Hands - bbPer100Hands).pow(2.0) - } - val standardDeviation = sqrt(stdSum / computables.size) - val standardDeviationBB = sqrt(stdBBSum / computables.size) - val standardDeviationBBper100Hands = sqrt(stdBBper100HandsSum / computables.size) - - results.addStat(STANDARD_DEVIATION, standardDeviation) - results.addStat(STANDARD_DEVIATION_BB, standardDeviationBB) - results.addStat(STANDARD_DEVIATION_BB_PER_100_HANDS, standardDeviationBBper100Hands) - - // Session Set - if (gHourlyDuration != null) { - var hourlyStdSum = 0.0 - for (set in sessionSets) { - val ssStats = SSStats(set, computableGroup.query) - val sHourlyRate = ssStats.hourlyRate - hourlyStdSum += (sHourlyRate - hourlyRate).pow(2.0) - } - val hourlyStandardDeviation = sqrt(hourlyStdSum / sessionSets.size) - - results.addStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation) - } - - } - val d5 = Date() - - val s1 = d2.time - d1.time - val s2 = d3.time - d2.time - val s3 = d4.time - d3.time - val s4 = d5.time - d4.time - Timber.d("Section 1 = $s1") - Timber.d("Section 2 = $s2") - Timber.d("Section 3 = $s3") - Timber.d("Section 4 = $s4") - - return results - } - - } + if (options.computeLocationsPlayed) { + results.addStat( + LOCATIONS_PLAYED, + computables.distinctBy { it.session?.location?.id }.size.toDouble() + ) + } + + var average = 0.0 // also used for standard deviation later + if (computables.size > 0) { + average = ratedNet / computables.size.toDouble() + val winRatio = winningSessionCount.toDouble() / computables.size.toDouble() + val itmRatio = winningSessionCount.toDouble() / computables.size.toDouble() + val avgBuyin = totalBuyin / computables.size.toDouble() + + results.addStats( + setOf( + ComputedStat(AVERAGE, average), + ComputedStat(WIN_RATIO, winRatio), + ComputedStat(TOURNAMENT_ITM_RATIO, itmRatio), + ComputedStat(AVERAGE_BUYIN, avgBuyin) + ) + ) + } + var averageBB = 0.0 + if (bbSessionCount > 0) { + averageBB = bbSum / bbSessionCount + results.addStat(AVERAGE_NET_BB, averageBB) + } + + val shouldIterateOverComputables = + (options.progressValues == Options.ProgressValues.STANDARD || options.computeLongestStreak) + + // Computable Result + if (shouldIterateOverComputables) { + + var index = 0 + var tSum = 0.0 + var tBBSum = 0.0 + var tBBSessionCount = 0 + var tWinningSessionCount = 0 + var tBuyinSum = 0.0 + var tHands = 0.0 + var longestWinStreak = 0 + var longestLoseStreak = 0 + var currentStreak = 0 + var tITMCount = 0 + + for (computable in computables) { + index++ + tSum += computable.ratedNet + tBBSum += computable.bbNet + tBBSessionCount += computable.hasBigBlind + tWinningSessionCount += computable.isPositive + tITMCount += computable.isPositive + tBuyinSum += computable.ratedBuyin + tHands += computable.estimatedHands + + if (computable.isPositive == 1) { // positive result + if (currentStreak >= 0) { // currently positive streak + currentStreak++ + } else { // currently negative streak + longestLoseStreak = min(longestLoseStreak, currentStreak) + currentStreak = 1 + } + } else { // negative result + if (currentStreak <= 0) { // currently negative streak + currentStreak-- + } else { // currently positive streak + longestWinStreak = max(longestWinStreak, currentStreak) + currentStreak = -1 + } + } + + val session = + computable.session + ?: throw PAIllegalStateException("Computing lone ComputableResult") + results.addEvolutionValue(tSum, stat = NET_RESULT, data = session) + results.addEvolutionValue(tSum / index, stat = AVERAGE, data = session) + results.addEvolutionValue( + index.toDouble(), + stat = NUMBER_OF_GAMES, + data = session + ) + results.addEvolutionValue( + tBBSum / tBBSessionCount, + stat = AVERAGE_NET_BB, + data = session + ) + results.addEvolutionValue( + (tWinningSessionCount.toDouble() / index.toDouble()), + stat = WIN_RATIO, + data = session + ) + results.addEvolutionValue( + tITMCount.toDouble() / index.toDouble(), + stat = TOURNAMENT_ITM_RATIO, + data = session + ) + results.addEvolutionValue( + tBuyinSum / index, + stat = AVERAGE_BUYIN, + data = session + ) + results.addEvolutionValue( + computable.ratedNet, + stat = STANDARD_DEVIATION, + data = session + ) + + Stat.netBBPer100Hands(tBBSum, tHands)?.let { netBB100 -> + results.addEvolutionValue( + netBB100, + stat = NET_BB_PER_100_HANDS, + data = session + ) + } + + Stat.returnOnInvestment(tSum, tBuyinSum)?.let { roi -> + results.addEvolutionValue(roi, stat = ROI, data = session) + } + + } + + if (currentStreak >= 0) { + longestWinStreak = max(longestWinStreak, currentStreak) + } else { + longestLoseStreak = min(longestLoseStreak, currentStreak) + } + + // loseStreak is negative and we want it positive + results.addStat( + LONGEST_STREAKS, + longestWinStreak.toDouble(), + -longestLoseStreak.toDouble() + ) + + } + + val sessionSets = computableGroup.sessionSets(realm, options.shouldSortValues) + results.addStat(NUMBER_OF_SETS, sessionSets.size.toDouble()) + +// var gHourlyDuration: Double? = null +//// var gBBSum: Double? = null + var maxDuration: Double? = null + + if (computableGroup.conditions.isEmpty()) { // SessionSets are fine +// gHourlyDuration = +// sessionSets.sum(SessionSet.Field.NET_DURATION.identifier).toDouble() / 3600000 // (milliseconds to hours) +// gBBSum = sessionSets.sum(SessionSet.Field.BB_NET.identifier).toDouble() + + sessionSets.max(SessionSet.Field.NET_DURATION.identifier)?.let { + maxDuration = it.toDouble() / 3600000 + } + } + + val shouldIterateOverSets = options.progressValues != Options.ProgressValues.NONE + || options.computeDaysPlayed + // || computableGroup.conditions.isNotEmpty() + + // Session Set + if (shouldIterateOverSets) { + + var tHourlyDuration = 0.0 + var tIndex = 0 + var tRatedNetSum = 0.0 + var tBBSum = 0.0 + var tTotalHands = 0.0 + var tHourlyRate: Double + var tHourlyRateBB: Double + val daysSet = mutableSetOf() + var tMaxDuration = 0.0 + + for (sessionSet in sessionSets) { + tIndex++ + + val setStats = SSStats(sessionSet, computableGroup.query) + + tRatedNetSum += setStats.ratedNet + tBBSum += setStats.bbSum + tHourlyDuration += setStats.hourlyDuration + tTotalHands += setStats.estimatedHands + tMaxDuration = max(tMaxDuration, setStats.hourlyDuration.toDouble()) + + tHourlyRate = tRatedNetSum / tHourlyDuration + tHourlyRateBB = tBBSum / tHourlyDuration + daysSet.add(sessionSet.startDate.startOfDay()) + + when (options.progressValues) { + Options.ProgressValues.STANDARD -> { + results.addEvolutionValue( + tHourlyRate, + stat = HOURLY_RATE, + data = sessionSet + ) + results.addEvolutionValue( + tIndex.toDouble(), + stat = NUMBER_OF_SETS, + data = sessionSet + ) + results.addEvolutionValue( + sessionSet.hourlyDuration, + tHourlyDuration, + HOURLY_DURATION, + sessionSet + ) + results.addEvolutionValue( + tHourlyDuration / tIndex, + stat = AVERAGE_HOURLY_DURATION, + data = sessionSet + ) + results.addEvolutionValue( + tHourlyRateBB, + stat = HOURLY_RATE_BB, + data = sessionSet + ) + + } + Options.ProgressValues.TIMED -> { + results.addEvolutionValue( + tRatedNetSum, + tHourlyDuration, + NET_RESULT, + sessionSet + ) + results.addEvolutionValue( + tHourlyRate, + tHourlyDuration, + HOURLY_RATE, + sessionSet + ) + results.addEvolutionValue( + tIndex.toDouble(), + tHourlyDuration, + NUMBER_OF_SETS, + sessionSet + ) + results.addEvolutionValue( + sessionSet.hourlyDuration, + tHourlyDuration, + HOURLY_DURATION, + sessionSet + ) + results.addEvolutionValue( + tHourlyDuration / tIndex, + tHourlyDuration, + AVERAGE_HOURLY_DURATION, + sessionSet + ) + results.addEvolutionValue( + tHourlyRateBB, + tHourlyDuration, + HOURLY_RATE_BB, + sessionSet + ) + + Stat.netBBPer100Hands(tBBSum, tTotalHands)?.let { netBB100 -> + results.addEvolutionValue( + netBB100, + tHourlyDuration, + NET_BB_PER_100_HANDS, + sessionSet + ) + } + } + else -> { + // nothing + } + } + + results.addStat(DAYS_PLAYED, daysSet.size.toDouble()) + + } + +// gHourlyDuration = tHourlyDuration +// gBBSum = tBBSum + maxDuration = tMaxDuration + + } + +// var hourlyRate = 0.0 +// if (gHourlyDuration != null) { +// +// hourlyRate = ratedNet / gHourlyDuration +// if (sessionSets.size > 0) { +// val avgDuration = gHourlyDuration / sessionSets.size +// results.addStat(HOURLY_RATE, hourlyRate) +// results.addStat(AVERAGE_HOURLY_DURATION, avgDuration) +// } +// results.addStat(HOURLY_DURATION, gHourlyDuration) +// } + + val timeIntervals = computableGroup.timeIntervals(realm) + val duration = timeIntervals.sum("duration").toDouble() + val breakDuration = sessionSets.sum("breakDuration").toDouble() + + val netHourlyDuration = (duration - breakDuration) / 3600000 + val hourlyRate = ratedNet / netHourlyDuration + + results.addStat(HOURLY_RATE, hourlyRate) + results.addStat(AVERAGE_HOURLY_DURATION, netHourlyDuration / sessionSets.size) + results.addStat(HOURLY_DURATION, netHourlyDuration) + + results.addStat(HOURLY_RATE_BB, bbSum / netHourlyDuration) + results.addStat(AVERAGE_NET_BB, bbSum / bbSessionCount) + +// if (gBBSum != null) { +// if (gHourlyDuration != null) { +// results.addStat(HOURLY_RATE_BB, gBBSum / gHourlyDuration) +// } +// results.addStat(AVERAGE_NET_BB, gBBSum / bbSessionCount) +// } + + maxDuration?.let { maxd -> + results.addStat(MAXIMUM_DURATION, maxd) // (milliseconds to hours) + } + + val bbPer100Hands = bbSum / totalHands * 100 + + // Standard Deviation + if (options.computeStandardDeviation) { + + // Session + var stdSum = 0.0 + var stdBBSum = 0.0 + var stdBBPer100HandsSum = 0.0 + for (computable in computables) { + stdSum += (computable.ratedNet - average).pow(2.0) + stdBBSum += (computable.bbNet - averageBB).pow(2.0) + stdBBPer100HandsSum += (computable.bbPer100Hands - bbPer100Hands).pow(2.0) + } + val standardDeviation = sqrt(stdSum / computables.size) + val standardDeviationBB = sqrt(stdBBSum / computables.size) + val standardDeviationBBper100Hands = sqrt(stdBBPer100HandsSum / computables.size) + + results.addStat(STANDARD_DEVIATION, standardDeviation) + results.addStat(STANDARD_DEVIATION_BB, standardDeviationBB) + results.addStat(STANDARD_DEVIATION_BB_PER_100_HANDS, standardDeviationBBper100Hands) + + // Session Set + var hourlyStdSum = 0.0 + for (set in sessionSets) { + val ssStats = SSStats(set, computableGroup.query) + val setHourlyRate = ssStats.hourlyRate + hourlyStdSum += (setHourlyRate - hourlyRate).pow(2.0) + } + val hourlyStandardDeviation = sqrt(hourlyStdSum / sessionSets.size) + results.addStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation) + } + + return results + } + + } } class SSStats(sessionSet: SessionSet, query: Query) { // Session Set Stats - var hourlyDuration: Double = 0.0 - var estimatedHands: Double = 0.0 - var bbSum: Double = 0.0 - var ratedNet: Double = 0.0 - - val hourlyRate: Double - get() { - return this.ratedNet / this.hourlyDuration - } - - init { - - if (sessionSet.sessions?.size == 1) { // use precomputed values - this.initStatsWithSet(sessionSet) - } else { // dynamically filter and compute subset - val setSessions = sessionSet.sessions!! - val filteredSessions = setSessions.filter(query) - if (setSessions.size == filteredSessions.size) { - this.initStatsWithSet(sessionSet) - } else { - ratedNet = filteredSessions.sumOf { it.computableResult?.ratedNet ?: 0.0 } - bbSum = filteredSessions.sumOf { it.bbNet } - hourlyDuration = filteredSessions.hourlyDuration - estimatedHands = filteredSessions.sumOf { it.estimatedHands } - } - } - } - - private fun initStatsWithSet(sessionSet: SessionSet) { - ratedNet = sessionSet.ratedNet - bbSum = sessionSet.bbNet - hourlyDuration = sessionSet.hourlyDuration - estimatedHands = sessionSet.estimatedHands - } + var hourlyDuration: Long = 0L + var estimatedHands: Double = 0.0 + var bbSum: Double = 0.0 + var ratedNet: Double = 0.0 + + val hourlyRate: Double + get() { + return if (this.hourlyDuration > 0L) { + this.ratedNet / this.hourlyDuration.toDouble() + } else { + 0.0 + } + } + + init { + + if (sessionSet.sessions?.size == 1) { // use precomputed values + this.initStatsWithSet(sessionSet) + } else { // dynamically filter and compute subset + val setSessions = sessionSet.sessions!! + val filteredSessions = setSessions.filter(query) + if (setSessions.size == filteredSessions.size) { + this.initStatsWithSet(sessionSet) + } else { + + ratedNet = filteredSessions.sumOf { it.computableResult?.ratedNet ?: 0.0 } + bbSum = filteredSessions.sumOf { it.bbNet } + estimatedHands = filteredSessions.sumOf { it.estimatedHands } + + val intervals = SessionInterval.intervalMap(filteredSessions.toSet()) + val netDuration = intervals.sumOf { it.duration } + val estimatedBreak = if (netDuration > 0.0) sessionSet.breakDuration * sessionSet.netDuration / netDuration else 0L + hourlyDuration = netDuration - estimatedBreak + } + } + } + + private fun initStatsWithSet(sessionSet: SessionSet) { + ratedNet = sessionSet.ratedNet + bbSum = sessionSet.bbNet + hourlyDuration = sessionSet.hourlyDuration.toLong() + estimatedHands = sessionSet.estimatedHands + } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt b/app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt index 2531b4a3..2aa7f01e 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt @@ -64,6 +64,11 @@ class ComputableGroup(val query: Query, var displayedStats: List? = null) return computables } + + fun timeIntervals(realm: Realm): RealmResults { + return Filter.queryOn(realm, this.query) + } + /** * The list of sets to compute */ diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt b/app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt index bc126f9e..5bfe91d5 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt @@ -55,18 +55,18 @@ class ReportWhistleBlower(var context: Context) { val realm = Realm.getDefaultInstance() - this.sessions = realm.where(Session::class.java).findAll() - this.sessions?.addChangeListener { _ -> + sessions = realm.where(Session::class.java).findAll() + sessions?.addChangeListener { _ -> requestReportLaunch() } - this.results = realm.where(Result::class.java).findAll() - this.results?.addChangeListener { _ -> + results = realm.where(Result::class.java).findAll() + results?.addChangeListener { _ -> requestReportLaunch() } - this.sessionSets = realm.where(SessionSet::class.java).findAll() - this.sessionSets?.addChangeListener { _ -> + sessionSets = realm.where(SessionSet::class.java).findAll() + sessionSets?.addChangeListener { _ -> requestReportLaunch() } @@ -156,13 +156,13 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co } fun cancel() { - Timber.d("Reportwhistleblower task CANCEL") +// Timber.d("Reportwhistleblower task CANCEL") this.cancelled = true } private fun launchReports() { - Timber.d("====== Report whistleblower launch batch...") +// Timber.d("====== Report whistleblower launch batch...") CoroutineScope(Dispatchers.Default).launch { diff --git a/app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt b/app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt index f490a97b..63516fda 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt @@ -133,7 +133,7 @@ val AbstractList.hourlyDuration: Double return intervals.sumOf { it.hourlyDuration } } -class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) { +class TimeInterval(var start: Date, var end: Date, var breakDuration: Long = 0L) { val hourlyDuration: Double get() { diff --git a/app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt b/app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt index eb09435d..93cf68e8 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt @@ -32,9 +32,9 @@ import net.pokeranalytics.android.util.CrashLogging * */ -class UnmanagedFilterField(message: String) : Exception(message) { - -} +//class UnmanagedFilterField(message: String) : Exception(message) { +// +//} /** * Companion-level Interface to indicate an RealmObject class can be filtered and to provide all the fieldNames (eg: parameter's path) needed to be query on. @@ -64,6 +64,7 @@ class FilterHelper { SessionSet::class.java -> SessionSet.fieldNameForQueryType(queryCondition) Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition) Result::class.java -> Result.fieldNameForQueryType(queryCondition) + FlatTimeInterval::class.java -> FlatTimeInterval.fieldNameForQueryType(queryCondition) else -> { CrashLogging.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}")) null diff --git a/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt b/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt index 3c97dc3d..9f0e5729 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt @@ -3,6 +3,7 @@ package net.pokeranalytics.android.model.migrations import io.realm.DynamicRealm import io.realm.RealmMigration import net.pokeranalytics.android.exceptions.PAIllegalStateException +import net.pokeranalytics.android.model.realm.FlatTimeInterval import timber.log.Timber import java.util.* @@ -341,6 +342,18 @@ class PokerAnalyticsMigration : RealmMigration { crs.addField("id", String::class.java).setRequired("id", true) crs.addPrimaryKey("id") } + schema.create("FlatTimeInterval")?.let { fs -> + fs.addField("id", String::class.java).setRequired("id", true) + fs.addPrimaryKey("id") + fs.addField("startDate", Date::class.java).setRequired("startDate", true) + fs.addField("endDate", Date::class.java).setRequired("endDate", true) + fs.addField("duration", Long::class.java) + + schema.get("Session")?.let { ss -> + ss.addRealmSetField("flatTimeIntervals", fs) + } + } + currentVersion++ } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/FlatTimeInterval.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/FlatTimeInterval.kt new file mode 100644 index 00000000..83ba5300 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/FlatTimeInterval.kt @@ -0,0 +1,61 @@ +package net.pokeranalytics.android.model.realm + +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey +import io.realm.annotations.RealmClass +import net.pokeranalytics.android.model.filter.Filterable +import net.pokeranalytics.android.model.filter.QueryCondition +import java.util.* + + +@RealmClass +open class FlatTimeInterval : RealmObject(), Filterable { + + @PrimaryKey + var id = UUID.randomUUID().toString() + + /** + * The start date of the session + */ + var startDate: Date = Date() + set(value) { + field = value + this.computeDuration() + } + + /** + * The start date of the session + */ + var endDate: Date = Date() + set(value) { + field = value + this.computeDuration() + } + + /** + * the net duration of the session, automatically calculated + */ + var duration: Long = 0L + + @LinkingObjects("flatTimeIntervals") + val sessions: RealmResults? = null + + private fun computeDuration() { + duration = endDate.time - startDate.time + } + + companion object { + + fun fieldNameForQueryType(queryCondition: Class ): String? { + Session.fieldNameForQueryType(queryCondition)?.let { + return "sessions.$it" + } + return null + } + + } + +} + diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt index 08d0c5ad..9f1dd87c 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt @@ -33,10 +33,7 @@ import net.pokeranalytics.android.ui.graph.Graph import net.pokeranalytics.android.ui.view.* import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow import net.pokeranalytics.android.util.* -import net.pokeranalytics.android.util.extensions.hourMinute -import net.pokeranalytics.android.util.extensions.shortDateTime -import net.pokeranalytics.android.util.extensions.toCurrency -import net.pokeranalytics.android.util.extensions.toMinutes +import net.pokeranalytics.android.util.extensions.* import java.text.DateFormat import java.text.NumberFormat import java.text.ParseException @@ -202,6 +199,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim */ var startDate: Date? = null set(value) { + val previous = this.startDate field = value if (value == null) { startDateHourMinuteComponent = null @@ -217,7 +215,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim if (value != null && this.endDate != null && value.after(this.endDate)) { this.endDate = null } - this.dateChanged() + + SessionSetManager.startChanged(this, min(previous, value)) + // this.computeStats() } @@ -227,6 +227,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim @Index var endDate: Date? = null set(value) { + val previous = this.endDate field = value if (value == null) { endDateHourMinuteComponent = null @@ -237,7 +238,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim } this.computeNetDuration() - this.dateChanged() + SessionSetManager.endChanged(this, max(previous, value)) this.defineDefaultTournamentBuyinIfNecessary() // this.computeStats() } @@ -373,6 +374,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim // The custom fields values var customFieldEntries: RealmList = RealmList() + // The list of opponents who participated to the session + var flatTimeIntervals: RealmList = RealmList() + // The number of hands played during the sessions var handsCount: Int? = null @@ -380,10 +384,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim this.generateStakes() } - private fun dateChanged() { - SessionSetManager.sessionDateChanged(this) - } - // /** // * Manages impacts on SessionSets // * Should be called when the start / end date are changed @@ -697,10 +697,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim CrashLogging.log("Deletes session. Id = ${this.id}") if (isValid) { -// realm.executeTransaction { - cleanup() - deleteFromRealm() -// } + cleanup() + deleteFromRealm() } else { CrashLogging.log("Attempt to delete an invalid session") } @@ -715,6 +713,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim this.sessionSet?.let { SessionSetManager.removeFromTimeline(this) } + + SessionSetManager.sessionDateChanged(this) + // cleanup unnecessary related objects this.result?.deleteFromRealm() this.computableResult?.deleteFromRealm() diff --git a/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt b/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt index 99e21550..550285ca 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt @@ -1,13 +1,18 @@ package net.pokeranalytics.android.model.utils import io.realm.Realm +import io.realm.RealmModel import io.realm.RealmQuery import io.realm.RealmResults import net.pokeranalytics.android.exceptions.ModelException +import net.pokeranalytics.android.model.realm.FlatTimeInterval import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.SessionSet import net.pokeranalytics.android.util.extensions.findById +import net.pokeranalytics.android.util.extensions.max +import net.pokeranalytics.android.util.extensions.min import timber.log.Timber +import java.util.* class CorruptSessionSetException(message: String) : Exception(message) @@ -17,12 +22,25 @@ class CorruptSessionSetException(message: String) : Exception(message) */ object SessionSetManager { - var sessions: RealmResults + var sessions: RealmResults? = null private val sessionIdsToProcess = mutableSetOf() + private var start: Date? = null + private var end: Date? = null + fun configure() {} // launch init + fun startChanged(session: Session, date: Date?) { + this.start = min(this.start, date) + this.sessionIdsToProcess.add(session.id) + } + + fun endChanged(session: Session, date: Date?) { + this.end = max(this.end, date) + this.sessionIdsToProcess.add(session.id) + } + fun sessionDateChanged(session: Session) { this.sessionIdsToProcess.add(session.id) } @@ -31,11 +49,12 @@ object SessionSetManager { val realm = Realm.getDefaultInstance() - this.sessions = realm.where(Session::class.java).findAllAsync() - this.sessions.addChangeListener { _, _ -> - if (this.sessionIdsToProcess.isNotEmpty()) { + sessions = realm.where(Session::class.java).findAllAsync() + sessions?.addChangeListener { _, _ -> + if (this.start != null && this.end != null) { realm.executeTransactionAsync { asyncRealm -> processSessions(asyncRealm) + cleanUp() } } } @@ -43,21 +62,38 @@ object SessionSetManager { realm.close() } + private fun cleanUp() { + this.start = null + this.end = null +// this.sessionIdsToProcess.clear() + } + private fun processSessions(realm: Realm) { - Timber.d("***** processSessions, process count = ${sessionIdsToProcess.size}") +// Timber.d("***** processSessions, process count = ${sessionIdsToProcess.size}") - for (sessionId in this.sessionIdsToProcess) { - realm.findById(sessionId)?.let { session -> - if (session.startDate != null && session.endDate != null) { - updateTimeline(session) - } else if (session.sessionSet != null) { - removeFromTimeline(session) - } + val start = this.start + val end = this.end + + val sessions = sessionIdsToProcess.mapNotNull { realm.findById(it) } + for (session in sessions) { + + // Session Sets + val startDate = session.startDate + val endDate = session.endDate + if (startDate != null && endDate != null) { + updateTimeline(session) + } else if (session.sessionSet != null) { + removeFromTimeline(session) } + + } + + // FlatTimeIntervals + if (start != null && end != null) { + processFlatTimeInterval(realm, start, end) } - this.sessionIdsToProcess.clear() } /** @@ -76,25 +112,37 @@ object SessionSetManager { throw ModelException("End date should never be null here") } - val sessionSets = this.matchingSets(session) - cleanupSessionSets(session, sessionSets) + val start = session.startDate!! + val end = session.endDate!! -// val sessionId = session.id -// realm.executeTransactionAsync { asyncRealm -> -// asyncRealm.findById(sessionId)?.let { s -> -// val sessionSets = this.matchingSets(session) -// cleanupSessionSets(session, sessionSets) -// } -// } + val sessionSets = this.matchingData(session.realm, start, end) + cleanupSessionSets(session, sessionSets) } - private fun matchingSets(session: Session): RealmResults { - val realm = session.realm - val endDate = session.endDate!! // tested above - val startDate = session.startDate!! - - val query: RealmQuery = realm.where(SessionSet::class.java) +// private fun matchingSets(session: Session): RealmResults { +// val realm = session.realm +// val endDate = session.endDate!! // tested above +// val startDate = session.startDate!! +// +// val query: RealmQuery = realm.where(SessionSet::class.java) +// +// query +// .lessThanOrEqualTo("startDate", startDate) +// .greaterThanOrEqualTo("endDate", startDate) +// .or() +// .lessThanOrEqualTo("startDate", endDate) +// .greaterThanOrEqualTo("endDate", endDate) +// .or() +// .greaterThanOrEqualTo("startDate", startDate) +// .lessThanOrEqualTo("endDate", endDate) +// +// return query.findAll() +// } + + private inline fun matchingData(realm: Realm, startDate: Date, endDate: Date): RealmResults { + + val query: RealmQuery = realm.where(T::class.java) query .lessThanOrEqualTo("startDate", startDate) @@ -127,7 +175,7 @@ object SessionSetManager { sessionSets.deleteAllFromRealm() allImpactedSessions.forEach { impactedSession -> - val sets = matchingSets(impactedSession) + val sets = matchingData(impactedSession.realm, impactedSession.startDate!!, impactedSession.endDate!!) this.updateTimeFrames(sets, impactedSession) } @@ -249,4 +297,155 @@ object SessionSetManager { } } + private fun processFlatTimeInterval(realm: Realm, start: Date, end: Date) { + + val sessions = matchingData(realm, start, end) + val intervalsStore = IntervalsStore(sessions.toSet()) + + intervalsStore.intervals.forEach { it.deleteFromRealm() } + + val intervals = SessionInterval.intervalMap(intervalsStore.sessions) + + for (interval in intervals) { + + val sortedDates = interval.dates.sorted() + for (i in (0 until sortedDates.size - 1)) { + + val s = sortedDates[i] + val e = sortedDates[i + 1] + + val matchingSessions = interval.sessions.filter { + val sd = it.startDate + val ed = it.endDate + (sd != null && ed != null && sd <= s && ed >= e) + } + if (matchingSessions.isNotEmpty()) { + Timber.d("**** Create FTI: $s - $e") + val fti = FlatTimeInterval() + fti.startDate = s + fti.endDate = e + matchingSessions.forEach { it.flatTimeIntervals.add(fti) } + realm.insertOrUpdate(fti) + } else { + Timber.w("The FTI has no sessions") + } + } + } + } + +} + +class IntervalsStore(sessions: Set) { + + var start: Date = Date() + var end: Date = Date(0L) + + val intervals = mutableSetOf() + + val sessions = mutableSetOf() + + private val sessionIds: MutableSet = mutableSetOf() + + init { + processSessions(sessions) + } + + private fun processSessions(sessions: Set) { + this.sessions.addAll(sessions) + for (session in sessions) { + loadIntervals(session) + } + } + + private fun loadIntervals(session: Session) { + + if (sessionIds.contains(session.id)) { + return + } + + session.startDate?.let { this.start = min(this.start, it) } + session.endDate?.let { this.end = max(this.end, it) } + + this.sessionIds.add(session.id) + + for (fti in session.flatTimeIntervals) { + this.intervals.add(fti) + + fti.sessions?.let { sessions -> + for (s in sessions) { + loadIntervals(s) + } + } + } + } + +} + +class SessionInterval(session: Session) { + + var start: Date + var end: Date? + + var sessions: MutableSet = mutableSetOf() + val dates: MutableSet = mutableSetOf() + + val duration: Long + get() { + val endDate = end ?: Date() + return endDate.time - start.time + } + + init { + this.start = session.startDate!! + this.end = session.endDate + + this.addSession(session) + } + + private fun addSession(session: Session) { + this.sessions.add(session) + + session.startDate?.let { this.dates.add(it) } + session.endDate?.let { endDate -> + this.dates.add(endDate) + if (endDate > end) { + end = endDate + } + } + } + + companion object { + + fun intervalMap(sessions: Set): List { + + val sorted = sessions.sortedBy { it.startDate } + val intervals = mutableListOf() + + sorted.firstOrNull()?.let { firstSession -> + + var currentInterval = SessionInterval(firstSession) + intervals.add(currentInterval) + + for (session in sessions.drop(1)) { + val start = session.startDate!! + val currentEnd = currentInterval.end + if (currentEnd != null && start > currentEnd) { + val interval = SessionInterval(session) + currentInterval = interval + intervals.add(interval) + } else { + currentInterval.addSession(session) + } + } + } + + intervals.forEach { + Timber.d("s = ${it.start}, e = ${it.end}") + } + + return intervals + } + + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt index 366adec3..3e2284f5 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt @@ -167,7 +167,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener { val async = async { val s = Date() - Timber.d(">>> start...") +// Timber.d(">>> start...") val realm = Realm.getDefaultInstance() realm.refresh() @@ -199,7 +199,6 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener { Timber.d(">>> Launch statistics computations") - val filter: Filter? = this.currentFilter(this.requireContext(), realm)?.let { if (it.filterableType == currentFilterable) { it } else { null } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt index 898fa1e1..ba1584fa 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt @@ -27,6 +27,7 @@ import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rows.StatRow import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.TextFormat +import timber.log.Timber open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt index 61d23e32..4474e1dd 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt @@ -388,7 +388,7 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource, val async = async { val s = Date() - Timber.d(">>> start...") +// Timber.d(">>> start...") val realm = Realm.getDefaultInstance() realm.refresh() @@ -436,6 +436,8 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource, private fun launchStatComputation(realm: Realm) { + return + Timber.d(">>> Launch calendar computations") val calendar = Calendar.getInstance() @@ -660,8 +662,8 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource, } } - Timber.d("Display data: ${System.currentTimeMillis() - startDate.time}ms") - Timber.d("Rows: ${rows.size}") +// Timber.d("Display data: ${System.currentTimeMillis() - startDate.time}ms") +// Timber.d("Rows: ${rows.size}") this.calendarAdapter.notifyDataSetChanged() diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt index 9776bde5..4d649407 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt @@ -19,9 +19,7 @@ import net.pokeranalytics.android.api.BlogPostApi import net.pokeranalytics.android.databinding.FragmentFeedBinding import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.LiveData -import net.pokeranalytics.android.model.realm.Filter -import net.pokeranalytics.android.model.realm.Session -import net.pokeranalytics.android.model.realm.Transaction +import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.ui.activity.BillingActivity import net.pokeranalytics.android.ui.activity.components.RequestCode @@ -87,10 +85,10 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis override fun asyncListenedEntityChange(realm: Realm, clazz: Class) { - Timber.d("asyncListenedEntityChange for $clazz") +// Timber.d("asyncListenedEntityChange for $clazz") when (clazz.kotlin) { Session::class -> { - Timber.d("WOWOWOOWOOWOWOWOWOWOWOWOWO") +// Timber.d("WOWOWOOWOOWOWOWOWOWOWOWOWO") this.sessionAdapter.refreshData() this.sessionAdapter.notifyDataSetChanged() } @@ -286,9 +284,20 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis displayBlogPostButton() binding.postButton.setOnClickListener { - Preferences.setBlogTipsTapped(requireContext()) - parentActivity?.openUrl(URL.BLOG_TIPS.value) - displayBlogPostButton() + + getRealm().executeTransactionAsync { realm -> + + realm.where().findAll().deleteAllFromRealm() + realm.where().findAll().deleteAllFromRealm() + realm.where().findAll().deleteAllFromRealm() + realm.where().findAll().deleteAllFromRealm() + realm.where().findAll().deleteAllFromRealm() + + } + +// Preferences.setBlogTipsTapped(requireContext()) +// parentActivity?.openUrl(URL.BLOG_TIPS.value) +// displayBlogPostButton() } binding.postButton.viewTreeObserver.addOnGlobalLayoutListener { @@ -632,7 +641,7 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis show = true this.badgeDrawable?.number = newCount } - this.binding.postButton.isVisible = show + this.binding.postButton.isVisible = true this.badgeDrawable?.isVisible = show } diff --git a/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt b/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt index 414d111d..075f2be1 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt @@ -7,6 +7,32 @@ import java.util.* // Calendar +fun min(d1: Date, d2: Date): Date { + return if (d1 < d2) d1 else d2 +} + +fun max(d1: Date, d2: Date): Date { + return if (d1 > d2) d1 else d2 +} + +@JvmName("min1") +fun min(d1: Date?, d2: Date?): Date? { + return if (d1 != null) { + if (d2 != null) min(d1, d2) else d1 + } else { + d2 + } +} + +@JvmName("max1") +fun max(d1: Date?, d2: Date?): Date? { + return if (d1 != null) { + if (d2 != null) max(d1, d2) else d1 + } else { + d2 + } +} + // Return a double representing the hour / minute of a date from a calendar fun Calendar.hourMinute(): Double { return (this.get(Calendar.HOUR_OF_DAY) + this.get(Calendar.MINUTE).toDouble() / 60.0).roundOffDecimal()