From 5b439cfb751c7e5aa7f4296571e14d2c9a400378 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 16 Apr 2019 17:18:32 +0200 Subject: [PATCH] Added stats for Calendar report --- .../android/calculus/Calculator.kt | 142 ++++++++++++------ .../pokeranalytics/android/calculus/Report.kt | 4 + .../pokeranalytics/android/calculus/Stat.kt | 38 +++-- .../android/model/realm/Session.kt | 10 +- .../android/model/realm/SessionSet.kt | 12 +- .../android/ui/fragment/GraphFragment.kt | 2 +- 6 files changed, 140 insertions(+), 68 deletions(-) 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 ffd17417..efe7d906 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt @@ -8,8 +8,11 @@ import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.name import net.pokeranalytics.android.model.realm.ComputableResult import net.pokeranalytics.android.model.realm.SessionSet +import net.pokeranalytics.android.util.extensions.startOfDay import timber.log.Timber import java.util.* +import kotlin.math.max +import kotlin.math.min /** * The class performing stats computation @@ -62,7 +65,6 @@ class Calculator { return true } -// var aggregation: Aggregation? = null } companion object { @@ -161,7 +163,12 @@ class Calculator { val computables = computableGroup.computables(realm) Timber.d(">>>> Start computing group ${computableGroup.name}, ${computables.size} computables") - val results: ComputedResults = ComputedResults(computableGroup) + val computeLongestStreak = options.displayedStats.contains(LONGEST_STREAKS) + if (computeLongestStreak) { + computables.sort("session.startDate") + } + + val results = ComputedResults(computableGroup) val sum = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble() val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble() @@ -170,54 +177,85 @@ class Calculator { val winningSessionCount = computables.sum(ComputableResult.Field.IS_POSITIVE.identifier).toInt() val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble() - // Compute for each session + val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble() + val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble() - when (options.evolutionValues) { - Options.EvolutionValues.STANDARD -> { + if (options.displayedStats.contains(LOCATIONS_PLAYED)) { + results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble()) + } - var index: Int = 0 - var tSum = 0.0 - var tBBSum = 0.0 - var tBBSessionCount = 0 - var tWinningSessionCount = 0 - var tBuyinSum = 0.0 - var tHands = 0.0 - - computables.forEach { computable -> - index++ - tSum += computable.ratedNet - tBBSum += computable.bbNet - tBBSessionCount += computable.hasBigBlind - tWinningSessionCount += computable.isPositive - tBuyinSum += computable.ratedBuyin - tHands += computable.estimatedHands - - val session = - computable.session ?: throw IllegalStateException("Computing lone ComputableResult") - results.addEvolutionValue(tSum, stat = NETRESULT, 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(tBuyinSum / index, stat = AVERAGE_BUYIN, data = session) - - Stat.netBBPer100Hands(tBBSum, tHands)?.let { netBB100 -> - results.addEvolutionValue(netBB100, stat = NET_BB_PER_100_HANDS, data = session) + val shouldIterateOverComputables = + (options.evolutionValues == Options.EvolutionValues.STANDARD || computeLongestStreak) + + // Iterate for each session + if (shouldIterateOverComputables) { + + var index: Int = 0 + var tSum = 0.0 + var tBBSum = 0.0 + var tBBSessionCount = 0 + var tWinningSessionCount = 0 + var tBuyinSum = 0.0 + var tHands = 0.0 + var winStreak = 0; var loseStreak = 0; var currentStreak = 0 + + computables.forEach { computable -> + index++ + tSum += computable.ratedNet + tBBSum += computable.bbNet + tBBSessionCount += computable.hasBigBlind + tWinningSessionCount += computable.isPositive + tBuyinSum += computable.ratedBuyin + tHands += computable.estimatedHands + + if (computable.isPositive == 1) { + if (currentStreak >= 0) { + currentStreak++ + } else { + currentStreak = 1 + loseStreak = min(loseStreak, currentStreak) } - - Stat.returnOnInvestment(tSum, tBuyinSum)?.let { roi -> - results.addEvolutionValue(roi, stat = ROI, data = session) + } + if (computable.isPositive == 0) { + if (currentStreak <= 0) { + currentStreak-- + } else { + currentStreak = -1 + winStreak = max(winStreak, currentStreak) } + } + + val session = + computable.session ?: throw IllegalStateException("Computing lone ComputableResult") + results.addEvolutionValue(tSum, stat = NETRESULT, 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(tBuyinSum / index, stat = AVERAGE_BUYIN, 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) } + } - else -> { - // nothing + + if (currentStreak >= 0) { + winStreak = max(winStreak, currentStreak) + } else { + loseStreak = min(loseStreak, currentStreak) } + + results.addStat(LONGEST_STREAKS, winStreak.toDouble(), loseStreak.toDouble()) + } val sessionSets = computableGroup.sessionSets(realm) @@ -228,6 +266,7 @@ class Calculator { val gSum = sessionSets.sum(SessionSet.Field.RATED_NET.identifier).toDouble() val gTotalHands = sessionSets.sum(SessionSet.Field.ESTIMATED_HANDS.identifier).toDouble() val gBBSum = sessionSets.sum(SessionSet.Field.BB_NET.identifier).toDouble() + val maxDuration = sessionSets.max(SessionSet.Field.NET_DURATION.identifier)?.toDouble() val hourlyRate = gSum / gHourlyDuration // var bbHourlyRate = gBBSum / gDuration @@ -242,6 +281,7 @@ class Calculator { var tBBSum = 0.0 var tHourlyRate = 0.0 var tHourlyRateBB = 0.0 + val daysSet = mutableSetOf() sessionSets.forEach { sessionSet -> tIndex++ @@ -252,6 +292,7 @@ class Calculator { tHourlyRate = gSum / tHourlyDuration tHourlyRateBB = gBBSum / tHourlyDuration + daysSet.add(sessionSet.startDate.startOfDay()) when (options.evolutionValues) { Options.EvolutionValues.STANDARD -> { @@ -308,6 +349,8 @@ class Calculator { } } + results.addStat(DAYS_PLAYED, daysSet.size.toDouble()) + } } else -> { @@ -350,15 +393,24 @@ class Calculator { ComputedStat(HOURLY_RATE_BB, gBBSum / gHourlyDuration), ComputedStat(AVERAGE_NET_BB, gBBSum / bbSessionCount), ComputedStat(HANDS_PLAYED, totalHands) - ) ) Stat.returnOnInvestment(sum, totalBuyin)?.let { roi -> - results.addStats(setOf(ComputedStat(ROI, roi))) + results.addStat(ROI, roi) } Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 -> - results.addStats(setOf(ComputedStat(NET_BB_PER_100_HANDS, netBB100))) + results.addStat(NET_BB_PER_100_HANDS, netBB100) + } + + maxNetResult?.let { max -> + results.addStat(MAXIMUM_NETRESULT, max) + } + minNetResult?.let { min -> + results.addStat(MINIMUM_NETRESULT, min) + } + maxDuration?.let { maxd -> + results.addStat(MAXIMUM_DURATION, maxd) } val bbPer100Hands = bbSum / totalHands * 100 diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/Report.kt b/app/src/main/java/net/pokeranalytics/android/calculus/Report.kt index 348dce5b..72b7d331 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Report.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Report.kt @@ -182,6 +182,10 @@ class ComputedResults(group: ComputableGroup) { } } + fun addStat(stat: Stat, value: Double, secondValue: Double? = null) { + this._computedStats[stat] = ComputedStat(stat, value, secondValue) + } + fun addStats(computedStats: Set) { computedStats.forEach { this._computedStats[it.stat] = it diff --git a/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt b/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt index 2d72b901..4f1782da 100644 --- a/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt +++ b/app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt @@ -70,7 +70,14 @@ enum class Stat : RowRepresentable { STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS, - HANDS_PLAYED; + HANDS_PLAYED, + LOCATIONS_PLAYED, + LONGEST_STREAKS, + MAXIMUM_NETRESULT, + MINIMUM_NETRESULT, + MAXIMUM_DURATION, + DAYS_PLAYED + ; /** * Returns whether the stat evolution numericValues requires a distribution sorting @@ -120,6 +127,12 @@ enum class Stat : RowRepresentable { STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_per_hour STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.standard_deviation_bb_per_100_hands HANDS_PLAYED -> R.string.number_of_hands + LOCATIONS_PLAYED -> R.string.locations_played + LONGEST_STREAKS -> R.string.longest_streaks + MAXIMUM_NETRESULT -> R.string.max_net_result + MINIMUM_NETRESULT -> R.string.min_net_result + MAXIMUM_DURATION -> R.string.longest_session + DAYS_PLAYED -> R.string.days_played } } @@ -127,7 +140,7 @@ enum class Stat : RowRepresentable { /** * Formats the value of the stat to be suitable for display */ - fun format(value: Double, currency: Currency? = null, context: Context): TextFormat { + fun format(value: Double, secondValue: Double? = null, currency: Currency? = null, context: Context): TextFormat { if (value.isNaN()) { return TextFormat(NULL_TEXT, R.color.white) @@ -135,30 +148,33 @@ enum class Stat : RowRepresentable { when (this) { // Amounts + red/green - Stat.NETRESULT, Stat.HOURLY_RATE, Stat.AVERAGE -> { + NETRESULT, HOURLY_RATE, AVERAGE, MAXIMUM_NETRESULT, MINIMUM_NETRESULT -> { val color = if (value >= this.threshold) R.color.green else R.color.red return TextFormat(value.toCurrency(currency), color) } // Red/green numericValues - Stat.HOURLY_RATE_BB, Stat.AVERAGE_NET_BB, Stat.NET_BB_PER_100_HANDS -> { + HOURLY_RATE_BB, AVERAGE_NET_BB, NET_BB_PER_100_HANDS -> { val color = if (value >= this.threshold) R.color.green else R.color.red return TextFormat(value.formatted(), color) } // white integers - Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HANDS_PLAYED -> { + NUMBER_OF_SETS, NUMBER_OF_GAMES, HANDS_PLAYED, LOCATIONS_PLAYED, DAYS_PLAYED -> { return TextFormat("${value.toInt()}") } // white durations - Stat.DURATION, Stat.AVERAGE_DURATION -> { + DURATION, AVERAGE_DURATION, MAXIMUM_DURATION -> { return TextFormat(value.formattedHourlyDuration()) } // red/green percentages - Stat.WIN_RATIO, Stat.ROI -> { + WIN_RATIO, ROI -> { val color = if (value * 100 >= this.threshold) R.color.green else R.color.red return TextFormat("${(value * 100).formatted()}%", color) } // white amountsr - Stat.AVERAGE_BUYIN, Stat.STANDARD_DEVIATION, Stat.STANDARD_DEVIATION_HOURLY, - Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> { + AVERAGE_BUYIN, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, + STANDARD_DEVIATION_BB_PER_100_HANDS -> { return TextFormat(value.toCurrency(currency)) } + LONGEST_STREAKS -> { + return TextFormat("${value.toInt()}W / ${secondValue!!.toInt()}L") + } else -> throw FormattingException("Stat formatting of ${this.name} not handled") } } @@ -228,7 +244,7 @@ enum class Stat : RowRepresentable { /** * ComputedStat contains a [stat] and their associated [value] */ -class ComputedStat(var stat: Stat, var value: Double, var currency: Currency? = null) { +class ComputedStat(var stat: Stat, var value: Double, var secondValue: Double? = null, var currency: Currency? = null) { constructor(stat: Stat, value: Double, previousValue: Double?) : this(stat, value) { if (previousValue != null) { @@ -245,7 +261,7 @@ class ComputedStat(var stat: Stat, var value: Double, var currency: Currency? = * Formats the value of the stat to be suitable for display */ fun format(context: Context): TextFormat { - return this.stat.format(this.value, this.currency, context) + return this.stat.format(this.value, this.secondValue, this.currency, context) } } 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 eba8afc5..81a4d522 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 @@ -581,7 +581,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat CustomizableRowRepresentable( RowViewType.HEADER_TITLE_AMOUNT_BIG, title = getFormattedDuration(), - computedStat = ComputedStat(Stat.NETRESULT, result?.net ?: 0.0, currency) + computedStat = ComputedStat(Stat.NETRESULT, result?.net ?: 0.0, currency = currency) ) ) rows.add(SeparatorRow()) @@ -591,7 +591,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat CustomizableRowRepresentable( RowViewType.HEADER_TITLE_AMOUNT_BIG, resId = R.string.pause, - computedStat = ComputedStat(Stat.NETRESULT, result?.net ?: 0.0, currency) + computedStat = ComputedStat(Stat.NETRESULT, result?.net ?: 0.0, currency = currency) ) ) rows.add(SeparatorRow()) @@ -601,14 +601,14 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat CustomizableRowRepresentable( RowViewType.HEADER_TITLE_AMOUNT_BIG, title = getFormattedDuration(), - computedStat = ComputedStat(Stat.NETRESULT, result?.net ?: 0.0, currency) + computedStat = ComputedStat(Stat.NETRESULT, result?.net ?: 0.0, currency = currency) ) ) rows.add( CustomizableRowRepresentable( RowViewType.HEADER_TITLE_AMOUNT, resId = R.string.hour_rate_without_pauses, - computedStat = ComputedStat(Stat.HOURLY_RATE, this.hourlyRate, currency) + computedStat = ComputedStat(Stat.HOURLY_RATE, this.hourlyRate, currency = currency) ) ) @@ -896,7 +896,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat } value?.let { - return stat.format(it, currency, context) + return stat.format(it, currency = currency, context = context) } ?: run { return TextFormat(NULL_TEXT) } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt index cb3506c5..8842e1d7 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt @@ -88,15 +88,15 @@ open class SessionSet() : RealmObject(), Timed, Filterable { override fun formattedValue(stat: Stat, context: Context) : TextFormat { return when (stat) { - Stat.NETRESULT, Stat.AVERAGE -> stat.format(this.ratedNet, null, context) - Stat.DURATION, Stat.AVERAGE_DURATION -> stat.format(this.netDuration.toDouble(), null, context) - Stat.HOURLY_RATE -> stat.format(this.hourlyRate, null, context) - Stat.HANDS_PLAYED -> stat.format(this.estimatedHands, null, context) - Stat.HOURLY_RATE_BB -> stat.format(this.bbHourlyRate, null, context) + Stat.NETRESULT, Stat.AVERAGE -> stat.format(this.ratedNet, currency = null, context = context) + Stat.DURATION, Stat.AVERAGE_DURATION -> stat.format(this.netDuration.toDouble(), currency = null, context = context) + Stat.HOURLY_RATE -> stat.format(this.hourlyRate, currency = null, context = context) + Stat.HANDS_PLAYED -> stat.format(this.estimatedHands, currency = null, context = context) + Stat.HOURLY_RATE_BB -> stat.format(this.bbHourlyRate, currency = null, context = context) Stat.NET_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> { val netBBPer100Hands = Stat.netBBPer100Hands(this.bbNet, this.estimatedHands) if (netBBPer100Hands != null) { - return stat.format(this.estimatedHands, null, context) + return stat.format(this.estimatedHands, currency = null, context = context) } else { return TextFormat(NULL_TEXT) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt index 18ce6f6c..4738acf0 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt @@ -198,7 +198,7 @@ class GraphFragment : PokerAnalyticsFragment(), OnChartValueSelectedListener, Co val formattedDate = DateFormat.getDateInstance(DateFormat.SHORT).format(it.startDate()) val entryValue = it.formattedValue(this.stat, requireContext()) - val totalStatValue = this.stat.format(e.y.toDouble(), null, requireContext()) + val totalStatValue = this.stat.format(e.y.toDouble(), currency = null, context = requireContext()) this.legendView.setItemData(this.stat, formattedDate, entryValue, totalStatValue) }