package net.pokeranalytics.android.calculus import android.content.Context import net.pokeranalytics.android.R import net.pokeranalytics.android.exceptions.FormattingException import net.pokeranalytics.android.model.interfaces.Timed import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.extensions.formatted import net.pokeranalytics.android.util.extensions.formattedHourlyDuration import net.pokeranalytics.android.util.extensions.toCurrency import java.util.* import kotlin.math.exp class StatFormattingException(message: String) : Exception(message) { } class ObjectIdentifier(var id: String, var clazz: Class) { } /** * An enum representing all the types of Session statistics */ enum class Stat : RowRepresentable { NET_RESULT, BB_NET_RESULT, HOURLY_RATE, AVERAGE, NUMBER_OF_SETS, NUMBER_OF_GAMES, HOURLY_DURATION, AVERAGE_HOURLY_DURATION, NET_BB_PER_100_HANDS, HOURLY_RATE_BB, AVERAGE_NET_BB, WIN_RATIO, AVERAGE_BUYIN, ROI, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS, HANDS_PLAYED, LOCATIONS_PLAYED, LONGEST_STREAKS, MAXIMUM_NETRESULT, MINIMUM_NETRESULT, MAXIMUM_DURATION, DAYS_PLAYED, WINNING_SESSION_COUNT, BB_SESSION_COUNT, TOTAL_BUYIN, ; companion object { fun returnOnInvestment(netResult: Double, buyin: Double): Double? { if (buyin == 0.0) { return null } return netResult / buyin } fun netBBPer100Hands(netBB: Double, numberOfHands: Double): Double? { if (numberOfHands == 0.0) { return null } return netBB / numberOfHands * 100 } fun riskOfRuin(hourlyRate: Double, hourlyStandardDeviation: Double, bankrollValue: Double) : Double? { if (bankrollValue <= 0.0) { return null } val numerator = -2 * hourlyRate * bankrollValue val denominator = Math.pow(hourlyStandardDeviation, 2.0) val ratio = numerator / denominator return exp(ratio) } } override val resId: Int? get() { return when (this) { NET_RESULT -> R.string.net_result BB_NET_RESULT -> R.string.total_net_result_bb_ HOURLY_RATE -> R.string.average_hour_rate AVERAGE -> R.string.average NUMBER_OF_SETS -> R.string.number_of_sessions NUMBER_OF_GAMES -> R.string.number_of_records HOURLY_DURATION -> R.string.duration AVERAGE_HOURLY_DURATION -> R.string.average_hours_played NET_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands HOURLY_RATE_BB -> R.string.average_hour_rate_bb_ AVERAGE_NET_BB -> R.string.average_net_result_bb_ WIN_RATIO -> R.string.win_ratio AVERAGE_BUYIN -> R.string.average_buyin ROI -> R.string.tournament_roi STANDARD_DEVIATION -> R.string.standard_deviation 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 else -> throw IllegalStateException("Stat ${this.name} name required but undefined") } } /** * Formats the value of the stat to be suitable for display */ fun format(value: Double, secondValue: Double? = null, currency: Currency? = null): TextFormat { if (value.isNaN()) { return TextFormat(NULL_TEXT, R.color.white) } when (this) { // Amounts + red/green NET_RESULT, 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 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 NUMBER_OF_SETS, NUMBER_OF_GAMES, HANDS_PLAYED, LOCATIONS_PLAYED, DAYS_PLAYED -> { return TextFormat("${value.toInt()}") } // white durations HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> { return TextFormat(value.formattedHourlyDuration()) } // red/green percentages 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 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") } } val threshold: Double get() { return when (this) { WIN_RATIO -> 50.0 else -> 0.0 } } fun cumulativeLabelResId(context: Context): String { val resId = when (this) { AVERAGE, AVERAGE_HOURLY_DURATION, NET_BB_PER_100_HANDS, HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, HOURLY_RATE -> R.string.average NUMBER_OF_SETS -> R.string.number_of_sessions NUMBER_OF_GAMES -> R.string.number_of_records NET_RESULT -> R.string.total STANDARD_DEVIATION -> R.string.net_result STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands WIN_RATIO, HOURLY_DURATION -> return this.localizedTitle(context) else -> null } resId?.let { return context.getString(it) } ?: run { return NULL_TEXT } } val aggregationTypes: List get() { return when (this) { NET_RESULT -> listOf( AggregationType.SESSION, AggregationType.MONTH, AggregationType.YEAR, AggregationType.DURATION ) NUMBER_OF_GAMES, NUMBER_OF_SETS -> listOf(AggregationType.MONTH, AggregationType.YEAR) else -> listOf(AggregationType.SESSION, AggregationType.MONTH, AggregationType.YEAR) } } val hasEvolutionGraph: Boolean get() { return when (this) { HOURLY_DURATION, AVERAGE_HOURLY_DURATION, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> false else -> true } } val significantIndividualValue: Boolean get() { return when (this) { WIN_RATIO, NUMBER_OF_SETS, NUMBER_OF_GAMES, STANDARD_DEVIATION, HOURLY_DURATION -> false else -> true } } val shouldShowNumberOfSessions: Boolean get() { return when (this) { NUMBER_OF_GAMES, NUMBER_OF_SETS -> false else -> true } } val showXAxisZero: Boolean get() { return when (this) { HOURLY_DURATION -> true else -> false } } val showYAxisZero: Boolean get() { return when (this) { HOURLY_DURATION -> true else -> false } } override val viewType: Int = RowViewType.TITLE_VALUE.ordinal } /** * ComputedStat contains a [stat] and their associated [value] */ 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) { this.variation = (value - previousValue) / previousValue } } /** * The value used to get evolution dataset */ var progressValue: Double? = null /** * The variation of the stat */ var variation: Double? = null /** * Formats the value of the stat to be suitable for display */ fun format(): TextFormat { return this.stat.format(this.value, this.secondValue, this.currency) } }