merge with dev

feature/top10
Laurent 7 years ago
commit af5d40997e
  1. 4
      app/build.gradle
  2. 3
      app/proguard-rules.pro
  3. BIN
      app/src/androidTest/assets/schema_3.realm
  4. 2
      app/src/androidTest/java/net/pokeranalytics/android/components/BaseFilterInstrumentedUnitTest.kt
  5. 2
      app/src/androidTest/java/net/pokeranalytics/android/components/RealmInstrumentedUnitTest.kt
  6. 44
      app/src/androidTest/java/net/pokeranalytics/android/components/SessionInstrumentedUnitTest.kt
  7. 104
      app/src/androidTest/java/net/pokeranalytics/android/model/CriteriaTest.kt
  8. 14
      app/src/androidTest/java/net/pokeranalytics/android/performanceTests/PerfsInstrumentedUnitTest.kt
  9. 125
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/BankrollInstrumentedUnitTest.kt
  10. 2
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/DeleteInstrumentedUnitTest.kt
  11. 34
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatPerformanceUnitTest.kt
  12. 335
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  13. 65
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/BlindFilterInstrumentedTest.kt
  14. 81
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/DateFilterInstrumentedUnitTest.kt
  15. 11
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/ExceptionFilterInstrumentedTest.kt
  16. 8
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/RealmFilterInstrumentedUnitTest.kt
  17. 119
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/SessionFilterInstrumentedUnitTest.kt
  18. 45
      app/src/main/AndroidManifest.xml
  19. 7
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  20. 568
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  21. 218
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  22. 496
      app/src/main/java/net/pokeranalytics/android/calculus/Report.kt
  23. 199
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  24. 0
      app/src/main/java/net/pokeranalytics/android/calculus/TextFormat.kt
  25. 79
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt
  26. 199
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt
  27. 17
      app/src/main/java/net/pokeranalytics/android/calculus/interfaces/Computable.kt
  28. 11
      app/src/main/java/net/pokeranalytics/android/calculus/interfaces/Datable.kt
  29. 11
      app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt
  30. 191
      app/src/main/java/net/pokeranalytics/android/model/Criteria.kt
  31. 21
      app/src/main/java/net/pokeranalytics/android/model/StatRepresentable.kt
  32. 8
      app/src/main/java/net/pokeranalytics/android/model/TableSize.kt
  33. 7
      app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt
  34. 58
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  35. 32
      app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt
  36. 831
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  37. 15
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Dated.kt
  38. 7
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Timed.kt
  39. 31
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  40. 58
      app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt
  41. 28
      app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt
  42. 11
      app/src/main/java/net/pokeranalytics/android/model/realm/Currency.kt
  43. 104
      app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt
  44. 145
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt
  45. 8
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterElementBlind.kt
  46. 4
      app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt
  47. 31
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  48. 399
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  49. 54
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  50. 37
      app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt
  51. 132
      app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt
  52. 29
      app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt
  53. 33
      app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollActivity.kt
  54. 51
      app/src/main/java/net/pokeranalytics/android/ui/activity/CalendarDetailsActivity.kt
  55. 33
      app/src/main/java/net/pokeranalytics/android/ui/activity/ComparisonChartActivity.kt
  56. 69
      app/src/main/java/net/pokeranalytics/android/ui/activity/GraphActivity.kt
  57. 72
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  58. 55
      app/src/main/java/net/pokeranalytics/android/ui/activity/ReportDetailsActivity.kt
  59. 33
      app/src/main/java/net/pokeranalytics/android/ui/activity/SettingsActivity.kt
  60. 59
      app/src/main/java/net/pokeranalytics/android/ui/activity/StatisticDetailsActivity.kt
  61. 69
      app/src/main/java/net/pokeranalytics/android/ui/adapter/ComparisonChartPagerAdapter.kt
  62. 4
      app/src/main/java/net/pokeranalytics/android/ui/adapter/HistorySessionRowRepresentableAdapter.kt
  63. 21
      app/src/main/java/net/pokeranalytics/android/ui/adapter/HomePagerAdapter.kt
  64. 78
      app/src/main/java/net/pokeranalytics/android/ui/adapter/ReportPagerAdapter.kt
  65. 16
      app/src/main/java/net/pokeranalytics/android/ui/extensions/ChipGroupExtension.kt
  66. 60
      app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt
  67. 16
      app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollDataFragment.kt
  68. 73
      app/src/main/java/net/pokeranalytics/android/ui/fragment/BankrollFragment.kt
  69. 224
      app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarDetailsFragment.kt
  70. 388
      app/src/main/java/net/pokeranalytics/android/ui/fragment/CalendarFragment.kt
  71. 123
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ComparisonChartFragment.kt
  72. 7
      app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt
  73. 9
      app/src/main/java/net/pokeranalytics/android/ui/fragment/EditableDataFragment.kt
  74. 91
      app/src/main/java/net/pokeranalytics/android/ui/fragment/FilterDetailsFragment.kt
  75. 15
      app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersFragment.kt
  76. 195
      app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt
  77. 13
      app/src/main/java/net/pokeranalytics/android/ui/fragment/HistoryFragment.kt
  78. 93
      app/src/main/java/net/pokeranalytics/android/ui/fragment/MoreFragment.kt
  79. 92
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportDetailsFragment.kt
  80. 140
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ReportsFragment.kt
  81. 18
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SessionFragment.kt
  82. 67
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt
  83. 197
      app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticDetailsFragment.kt
  84. 145
      app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt
  85. 247
      app/src/main/java/net/pokeranalytics/android/ui/fragment/StatsFragment.kt
  86. 199
      app/src/main/java/net/pokeranalytics/android/ui/fragment/TableReportFragment.kt
  87. 53
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/LoaderDialogFragment.kt
  88. 19
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/PokerAnalyticsFragment.kt
  89. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetFragment.kt
  90. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetListGameFragment.kt
  91. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/components/bottomsheet/BottomSheetTableSizeGridFragment.kt
  92. 32
      app/src/main/java/net/pokeranalytics/android/ui/graph/AxisFormatter.kt
  93. 30
      app/src/main/java/net/pokeranalytics/android/ui/graph/ChartDataSet.kt
  94. 117
      app/src/main/java/net/pokeranalytics/android/ui/graph/GraphExtensions.kt
  95. 38
      app/src/main/java/net/pokeranalytics/android/ui/graph/GraphUnderlyingEntry.kt
  96. 9
      app/src/main/java/net/pokeranalytics/android/ui/helpers/DateTimePickerManager.kt
  97. 29
      app/src/main/java/net/pokeranalytics/android/ui/view/CalendarTabs.kt
  98. 119
      app/src/main/java/net/pokeranalytics/android/ui/view/LegendView.kt
  99. 40
      app/src/main/java/net/pokeranalytics/android/ui/view/MultiLineLegendView.kt
  100. 4
      app/src/main/java/net/pokeranalytics/android/ui/view/NoPagingViewPager.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -28,14 +28,14 @@ android {
applicationId "net.pokeranalytics.android"
minSdkVersion 23
targetSdkVersion 28
versionCode 17
versionCode 16
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationVariants.all { variant ->
variant.outputs.all { output ->

@ -58,3 +58,6 @@
# Guava
-dontwarn com.google.j2objc.annotations.**
-keep class com.google.j2objc.annotations.** { *; }
# Enum
-optimizations !class/unboxing/enum

@ -30,7 +30,7 @@ open class BaseFilterInstrumentedUnitTest : RealmInstrumentedUnitTest() {
session.numberOfTables = numberOfTable
session.tableSize = tableSize
session.startDate = startDate
session.result?.netResult = netResult
session.result?.cashout = netResult
val cal = Calendar.getInstance() // creates calendar
cal.time = startDate // sets calendar time/date
cal.add(Calendar.HOUR_OF_DAY, endDate) // adds one hour

@ -11,6 +11,8 @@ import java.util.*
open class RealmInstrumentedUnitTest {
val EPSILON = 0.0001
lateinit var mockRealm: Realm
companion object {

@ -0,0 +1,44 @@
package net.pokeranalytics.android.components
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.realm.RealmList
import net.pokeranalytics.android.model.realm.*
import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class)
open class SessionInstrumentedUnitTest : RealmInstrumentedUnitTest() {
// convenience extension
fun Session.Companion.testInstance(
netResult: Double = 0.0,
isTournament: Boolean = false,
startDate: Date = Date(),
endDate: Int = 1,
bankroll: Bankroll? = null,
game: Game? = null,
location: Location? = null,
tournamentName: TournamentName? = null,
tournamentFeatures: RealmList<TournamentFeature> = RealmList(),
numberOfTable: Int = 1,
limit: Int? = null,
tableSize: Int? = null
): Session {
val session: Session = Session.newInstance(super.mockRealm, isTournament, bankroll)
session.game = game
session.location = location
session.tournamentFeatures = tournamentFeatures
session.tournamentName = tournamentName
session.limit = limit
session.numberOfTables = numberOfTable
session.tableSize = tableSize
session.startDate = startDate
session.result?.netResult = netResult
val cal = Calendar.getInstance() // creates calendar
cal.time = startDate // sets calendar time/date
cal.add(Calendar.HOUR_OF_DAY, endDate) // adds one hour
session.endDate = cal.time // returns new date object, one hour in the future
return session
}
}

@ -0,0 +1,104 @@
package net.pokeranalytics.android.model
import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.components.RealmInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.FilterCondition
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import org.junit.Assert
import org.junit.Test
import org.junit.Assert.*
import java.util.*
class CriteriaTest : BaseFilterInstrumentedUnitTest() {
@Test
fun getQueryConditions() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance()
cal.time = Date()
val s1 = Session.testInstance(100.0, false, cal.time)
cal.add(Calendar.YEAR, 1)
Session.testInstance(100.0, true, cal.time)
cal.add(Calendar.YEAR, -11)
val firstValue = cal.get(Calendar.YEAR)
Session.testInstance(100.0, true, cal.time)
cal.add(Calendar.YEAR, 7)
Session.testInstance(100.0, true, cal.time)
cal.add(Calendar.YEAR, -2)
Session.testInstance(100.0, true, cal.time)
cal.add(Calendar.YEAR, 10)
Session.testInstance(100.0, true, cal.time)
val lastValue = firstValue + 10
realm.commitTransaction()
val years = Criteria.Years.queryConditions as List<QueryCondition.AnyYear>
println("years = ${years.map { it.getDisplayName() }}")
assertEquals(11, years.size)
assertEquals(firstValue, years.first().listOfValues.first())
assertEquals(lastValue, years.last().listOfValues.first())
}
@Test
fun combined() {
val critierias = listOf(Criteria.MonthsOfYear, Criteria.DaysOfWeek)
val combined = critierias.combined()
combined.forEach {
it.forEach {qc->
println(qc.getDisplayName())
}
}
}
@Test
fun upToNow() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance()
cal.time = Date()
val s1 = Session.testInstance(100.0, false, cal.time)
cal.add(Calendar.YEAR, 1)
Session.testInstance(100.0, true, cal.time)
cal.add(Calendar.YEAR, -11)
val firstValue = cal.get(Calendar.YEAR)
Session.testInstance(100.0, true, cal.time)
cal.add(Calendar.YEAR, 7)
Session.testInstance(100.0, true, cal.time)
cal.add(Calendar.YEAR, -2)
Session.testInstance(100.0, true, cal.time)
cal.add(Calendar.YEAR, 10)
Session.testInstance(100.0, true, cal.time)
val lastValue = firstValue + 10
realm.commitTransaction()
val critierias = listOf(Criteria.Years, Criteria.MonthsOfYear)
val combined = critierias.combined()
combined.forEach {
it.forEach {qc->
println("<<<<< ${qc.getDisplayName()}")
}
}
println("<<<<< reduced")
val reduced= critierias.combined().upToNow()
reduced.forEach {
it.forEach {qc->
println("<<<<< ${qc.getDisplayName()}")
}
}
}
}

@ -3,11 +3,11 @@ package net.pokeranalytics.android.performanceTests
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import net.pokeranalytics.android.components.RealmInstrumentedUnitTest
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputableGroup
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.components.RealmInstrumentedUnitTest
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.SessionSet
@ -53,23 +53,23 @@ class PerfsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
Timber.d("computableResults: ${computableResults.size}")
Timber.d("sets: ${sets.size}")
val stats: List<Stat> = listOf(Stat.NETRESULT, Stat.AVERAGE)
val group = ComputableGroup("test", computableResults, sets, stats)
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group = ComputableGroup("test", listOf(), stats)
val options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val results: ComputedResults = Calculator.compute(realm, group, options)
Timber.d("*** ended in ${System.currentTimeMillis() - start} milliseconds")
val sum = results.computedStat(Stat.NETRESULT)
val sum = results.computedStat(Stat.NET_RESULT)
Timber.d("*** NET RESULT: ${sum?.value}")
val average = results.computedStat(Stat.AVERAGE)
Timber.d("*** AVERAGE: ${average?.value}")
val duration = results.computedStat(Stat.DURATION)
Timber.d("*** DURATION: ${duration?.value}")
val duration = results.computedStat(Stat.HOURLY_DURATION)
Timber.d("*** HOURLY_DURATION: ${duration?.value}")
}
}

@ -1,11 +1,10 @@
package net.pokeranalytics.android.unitTests
import androidx.test.ext.junit.runners.AndroidJUnit4
import net.pokeranalytics.android.components.RealmInstrumentedUnitTest
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputableGroup
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Stat
import io.realm.Realm
import net.pokeranalytics.android.calculus.bankroll.BankrollCalculator
import net.pokeranalytics.android.calculus.bankroll.BankrollReportSetup
import net.pokeranalytics.android.components.SessionInstrumentedUnitTest
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.Currency
import org.junit.Assert
@ -14,7 +13,17 @@ import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class)
class BankrollInstrumentedUnitTest : RealmInstrumentedUnitTest() {
class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() {
private fun createDefaultTransactionTypes(realm: Realm) {
TransactionType.Value.values().forEachIndexed { index, value ->
val type = TransactionType()
type.additive = value.additive
type.kind = index
type.lock = true
realm.insertOrUpdate(type)
}
}
// convenience extension
fun Session.Companion.testInstance(netResult: Double, startDate: Date, endDate: Date?): Session {
@ -26,56 +35,88 @@ class BankrollInstrumentedUnitTest : RealmInstrumentedUnitTest() {
}
@Test
fun testSessionStats() {
fun testReport() {
val realm = this.mockRealm
realm.beginTransaction()
val realm = mockRealm
val s1 = newSessionInstance(realm)
val s2 = newSessionInstance(realm)
realm.executeTransaction {
val br1 = realm.createObject(Bankroll::class.java, "1")
val br2 = realm.createObject(Bankroll::class.java, "2")
this.createDefaultTransactionTypes(realm)
val c1 = realm.createObject(Currency::class.java, "1")
val c2 = realm.createObject(Currency::class.java, "2")
c1.rate = 0.1
c2.rate = 2.0
br1.currency = c1
br2.currency = c2
var br1 = realm.createObject(Bankroll::class.java, "1")
var br2 = realm.createObject(Bankroll::class.java, "2")
s1.bankroll = br1
s2.bankroll = br2
br1.initialValue = 100.0
br2.initialValue = 1000.0
s1.result?.netResult = 100.0
s2.result?.netResult = 200.0
val t1 = realm.createObject(Transaction::class.java, UUID.randomUUID().toString())
t1.amount = 100.0
t1.bankroll = br1
t1.type = TransactionType.getByValue(TransactionType.Value.BONUS, realm)
val t2 = realm.createObject(Transaction::class.java, UUID.randomUUID().toString())
t2.amount = 500.0
t2.bankroll = br2
t2.type = TransactionType.getByValue(TransactionType.Value.BONUS, realm)
realm.commitTransaction()
val s1 = newSessionInstance(realm)
s1.bankroll = br1
s1.result?.cashout = 200.0
val s2 = newSessionInstance(realm)
s2.bankroll = br2
s2.result?.cashout = 500.0
val computableResults = realm.where(ComputableResult::class.java).findAll()
val sets = realm.where(SessionSet::class.java).findAll()
val stats: List<Stat> = listOf(Stat.NETRESULT, Stat.AVERAGE)
val group = ComputableGroup("test", computableResults, sets, stats)
val brSetup1 = BankrollReportSetup(br1)
val report1 = BankrollCalculator.computeReport(brSetup1)
Assert.assertEquals(400.0, report1.total, EPSILON)
val options = Calculator.Options()
val brSetup2 = BankrollReportSetup(br2)
val report2 = BankrollCalculator.computeReport(brSetup2)
Assert.assertEquals(2000.0, report2.total, EPSILON)
val results: ComputedResults = Calculator.compute(group, options)
val delta = 0.01
val brSetupAll = BankrollReportSetup()
val reportAll = BankrollCalculator.computeReport(brSetupAll)
Assert.assertEquals(2400.0, reportAll.total, EPSILON)
val sum = results.computedStat(Stat.NETRESULT)
if (sum != null) {
Assert.assertEquals(410.0, sum.value, delta)
} else {
Assert.fail("No Net result stat")
}
val average = results.computedStat(Stat.AVERAGE)
if (average != null) {
Assert.assertEquals(205.0, average.value, delta)
} else {
Assert.fail("No AVERAGE stat")
}
@Test
fun testReportWithRate() {
val realm = mockRealm
var br1: Bankroll? = null
realm.executeTransaction {
this.createDefaultTransactionTypes(realm)
val c1 = realm.createObject(Currency::class.java, UUID.randomUUID().toString())
c1.rate = 10.0
br1 = realm.createObject(Bankroll::class.java, "1")
br1?.currency = c1
br1?.initialValue = 100.0
val t1 = realm.createObject(Transaction::class.java, UUID.randomUUID().toString())
t1.amount = 100.0
t1.type = TransactionType.getByValue(TransactionType.Value.BONUS, realm)
br1?.transactions?.add(t1)
val s1 = newSessionInstance(realm)
s1.bankroll = br1
s1.result?.cashout = 200.0
}
}
val brSetup1 = BankrollReportSetup(br1)
val report1 = BankrollCalculator.computeReport(brSetup1)
Assert.assertEquals(400.0, report1.total, EPSILON)
val brSetupAll = BankrollReportSetup()
val reportAll = BankrollCalculator.computeReport(brSetupAll)
Assert.assertEquals(4000.0, reportAll.total, EPSILON)
}
}

@ -18,7 +18,9 @@ class DeleteInstrumentedUnitTest : RealmInstrumentedUnitTest() {
val s2 = newSessionInstance(realm)
val br1 = realm.createObject(Bankroll::class.java, "1")
br1.live = false
val br2 = realm.createObject(Bankroll::class.java, "2")
br2.live = false
val c1 = realm.createObject(Currency::class.java, "1")
val c2 = realm.createObject(Currency::class.java, "2")

@ -0,0 +1,34 @@
package net.pokeranalytics.android.unitTests
import androidx.test.ext.junit.runners.AndroidJUnit4
import net.pokeranalytics.android.components.SessionInstrumentedUnitTest
import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.model.realm.Session
import org.junit.Test
import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class)
class StatPerformanceUnitTest : SessionInstrumentedUnitTest() {
@Test
fun testSessionNetResultOnLoad() {
val realm = mockRealm
realm.beginTransaction()
for (index in 0..100) {
Session.testInstance((-2000..2000).random().toDouble())
println("*** creating $index")
}
realm.commitTransaction()
val d1 = Date()
realm.where(Result::class.java).sum("netResult")
val d2 = Date()
val duration = (d2.time - d1.time)
println("*** ended in $duration milliseconds")
}
}

@ -1,13 +1,13 @@
package net.pokeranalytics.android.unitTests
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.realm.RealmList
import io.realm.RealmResults
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputableGroup
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.components.RealmInstrumentedUnitTest
import net.pokeranalytics.android.components.SessionInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.Currency
import org.junit.Assert
@ -23,62 +23,10 @@ import java.util.*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
// convenience extension
private fun Session.Companion.testInstance(
netResult: Double = 0.0,
isTournament: Boolean = false,
startDate: Date = Date(),
endDate: Int = 1,
bankroll: Bankroll? = null,
game: Game? = null,
location: Location? = null,
tournamentName: TournamentName? = null,
tournamentFeatures: RealmList<TournamentFeature> = RealmList(),
numberOfTable: Int = 1,
limit: Int? = null,
tableSize: Int? = null
): Session {
val session: Session = Session.newInstance(super.mockRealm, isTournament, bankroll)
session.game = game
session.location = location
session.tournamentFeatures = tournamentFeatures
session.tournamentName = tournamentName
session.limit = limit
session.numberOfTables = numberOfTable
session.tableSize = tableSize
session.startDate = startDate
session.result?.netResult = netResult
val cal = Calendar.getInstance() // creates calendar
cal.time = startDate // sets calendar time/date
cal.add(Calendar.HOUR_OF_DAY, endDate) // adds one hour
session.endDate = cal.time // returns new date object, one hour in the future
return session
}
@Test
fun testSessionNetResultOnLoad() {
val realm = mockRealm
realm.beginTransaction()
for (index in 0..100) {
Session.testInstance((-2000..2000).random().toDouble())
println("*** creating $index")
}
realm.commitTransaction()
val d1 = Date()
realm.where(Result::class.java).sum("netResult")
val d2 = Date()
val duration = (d2.time - d1.time)
println("*** ended in $duration milliseconds")
}
class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
@Test
fun testSessionStats() {
fun testAllSessionStats() {
val realm = this.mockRealm
@ -112,6 +60,10 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
s2.startDate = sd2
s2.endDate = ed2
val l1 = realm.createObject(Location::class.java, UUID.randomUUID().toString())
s1.location = l1
s2.location = l1
realm.commitTransaction()
assertEquals(2, computableResults.size)
@ -120,17 +72,16 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
println(">>>>>> rated net = ${it.ratedNet} ")
}
val sets = realm.where(SessionSet::class.java).findAll()
val stats: List<Stat> = listOf(Stat.NETRESULT, Stat.AVERAGE)
val group = ComputableGroup("test", computableResults, sets, stats)
val group = ComputableGroup("test")
val options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION,
Stat.LONGEST_STREAKS, Stat.LOCATIONS_PLAYED, Stat.DAYS_PLAYED)
val results: ComputedResults = Calculator.compute(group, options)
val results: ComputedResults = Calculator.compute(realm, group, options)
val delta = 0.01
val sum = results.computedStat(Stat.NETRESULT)
val sum = results.computedStat(Stat.NET_RESULT)
if (sum != null) {
assertEquals(200.0, sum.value, delta)
} else {
@ -144,7 +95,7 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
Assert.fail("No AVERAGE stat")
}
val duration = results.computedStat(Stat.DURATION)
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(4.0, duration.value, delta)
} else {
@ -181,7 +132,7 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
} else {
Assert.fail("No avgBuyin stat")
}
val avgDuration = results.computedStat(Stat.AVERAGE_DURATION)
val avgDuration = results.computedStat(Stat.AVERAGE_HOURLY_DURATION)
if (avgDuration != null) {
assertEquals(2.0, avgDuration.value, delta)
} else {
@ -234,6 +185,42 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
Assert.fail("No std100 stat")
}
results.computedStat(Stat.MAXIMUM_NETRESULT)?.let {
assertEquals(300.0, it.value, delta)
} ?: run {
Assert.fail("No MAXIMUM_NETRESULT")
}
results.computedStat(Stat.MINIMUM_NETRESULT)?.let {
assertEquals(-100.0, it.value, delta)
} ?: run {
Assert.fail("No MINIMUM_NETRESULT")
}
results.computedStat(Stat.MAXIMUM_DURATION)?.let {
assertEquals(3.0, it.value, delta)
} ?: run {
Assert.fail("No MAXIMUM_DURATION")
}
results.computedStat(Stat.DAYS_PLAYED)?.let {
assertEquals(2.0, it.value, delta)
} ?: run {
Assert.fail("No DAYS_PLAYED")
}
results.computedStat(Stat.LOCATIONS_PLAYED)?.let {
assertEquals(1.0, it.value, delta)
} ?: run {
Assert.fail("No LOCATIONS_PLAYED")
}
results.computedStat(Stat.LONGEST_STREAKS)?.let {
assertEquals(1.0, it.value, delta)
assertEquals(1.0, it.secondValue!!, delta)
} ?: run {
Assert.fail("No LOCATIONS_PLAYED")
}
}
@Test
@ -263,18 +250,16 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
}
val computableResults = realm.where(ComputableResult::class.java).findAll()
val sets = realm.where(SessionSet::class.java).findAll()
val stats: List<Stat> = listOf(Stat.NETRESULT, Stat.AVERAGE)
val group = ComputableGroup("test", computableResults, sets, stats)
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group = ComputableGroup("test", listOf(), stats)
val options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val results: ComputedResults = Calculator.compute(realm, group, options)
val delta = 0.01
val duration = results.computedStat(Stat.DURATION)
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(3.0, duration.value, delta)
} else {
@ -332,18 +317,16 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
realm.commitTransaction()
val computableResults = realm.where(ComputableResult::class.java).findAll()
val sets = realm.where(SessionSet::class.java).findAll()
val stats: List<Stat> = listOf(Stat.NETRESULT, Stat.AVERAGE)
val group = ComputableGroup("test", computableResults, sets, stats)
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group = ComputableGroup("test", listOf(), stats)
val options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val results: ComputedResults = Calculator.compute(realm, group, options)
val delta = 0.01
val duration = results.computedStat(Stat.DURATION)
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(8.0, duration.value, delta)
} else {
@ -417,18 +400,16 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
realm.commitTransaction()
val computableResults = realm.where(ComputableResult::class.java).findAll()
val sets = realm.where(SessionSet::class.java).findAll()
val stats: List<Stat> = listOf(Stat.NETRESULT, Stat.AVERAGE)
val group = ComputableGroup("test", computableResults, sets, stats)
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group = ComputableGroup("test", listOf(), stats)
val options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val results: ComputedResults = Calculator.compute(realm, group, options)
val delta = 0.01
val duration = results.computedStat(Stat.DURATION)
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(8.0, duration.value, delta)
} else {
@ -439,14 +420,12 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
s1.deleteFromRealm()
}
val computableResults2 = realm.where(ComputableResult::class.java).findAll()
val sets2 = realm.where(SessionSet::class.java).findAll()
val stats2: List<Stat> = listOf(Stat.NETRESULT, Stat.AVERAGE)
val group2 = ComputableGroup("test", computableResults2, sets2, stats2)
val stats2: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group2 = ComputableGroup("test", listOf(), stats2)
val results2: ComputedResults = Calculator.compute(group2, options)
val results2: ComputedResults = Calculator.compute(realm, group2, options)
val duration2 = results2.computedStat(Stat.DURATION)
val duration2 = results2.computedStat(Stat.HOURLY_DURATION)
if (duration2 != null) {
assertEquals(7.0, duration2.value, delta)
} else {
@ -478,12 +457,12 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
val sets = realm.where(SessionSet::class.java).findAll()
Assert.assertEquals(1, sets.size)
assertEquals(1, sets.size)
val set = sets.first()
if (set != null) {
Assert.assertEquals(sd1.time, set.startDate.time)
Assert.assertEquals(ed1.time, set.endDate.time)
assertEquals(sd1.time, set.startDate.time)
assertEquals(ed1.time, set.endDate.time)
} else {
Assert.fail("No set")
}
@ -522,7 +501,7 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
val sets = realm.where(SessionSet::class.java).findAll()
Assert.assertEquals(2, sets.size)
assertEquals(2, sets.size)
}
@ -566,18 +545,16 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
s1.endDate = null
}
val computableResults = realm.where(ComputableResult::class.java).findAll()
val sets = realm.where(SessionSet::class.java).findAll()
val stats: List<Stat> = listOf(Stat.NETRESULT, Stat.AVERAGE)
val group = ComputableGroup("test", computableResults, sets, stats)
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group = ComputableGroup("test", listOf(), stats)
val options = Calculator.Options()
// options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val results: ComputedResults = Calculator.compute(realm, group, options)
val delta = 0.01
val duration = results.computedStat(Stat.DURATION)
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(7.0, duration.value, delta)
} else {
@ -594,7 +571,7 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
}
@Test
// @Test
fun testRatedNetResultSessions() {
val realm = this.mockRealm
@ -620,15 +597,13 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
}
val computableResults = realm.where(ComputableResult::class.java).findAll()
val sets = realm.where(SessionSet::class.java).findAll()
val stats: List<Stat> = listOf(Stat.NETRESULT)
val group = ComputableGroup("test", computableResults, sets, stats)
val stats: List<Stat> = listOf(Stat.NET_RESULT)
val group = ComputableGroup("test", listOf(), stats)
val options = Calculator.Options()
val results: ComputedResults = Calculator.compute(group, options)
val results: ComputedResults = Calculator.compute(realm, group, options)
val netResult = results.computedStat(Stat.NETRESULT)
Assert.assertEquals(250.0, netResult?.value)
val netResult = results.computedStat(Stat.NET_RESULT)
assertEquals(250.0, netResult?.value)
}
// @Test
@ -658,15 +633,13 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
}
val computableResults = realm.where(ComputableResult::class.java).findAll()
val sets = realm.where(SessionSet::class.java).findAll()
val stats: List<Stat> = listOf(Stat.NETRESULT)
val group = ComputableGroup("test", computableResults, sets, stats)
val stats: List<Stat> = listOf(Stat.NET_RESULT)
val group = ComputableGroup("test", listOf(), stats)
val options = Calculator.Options()
val results: ComputedResults = Calculator.compute(group, options)
val results: ComputedResults = Calculator.compute(realm, group, options)
val netResult = results.computedStat(Stat.NETRESULT)
Assert.assertEquals(250.0, netResult?.value)
val netResult = results.computedStat(Stat.NET_RESULT)
assertEquals(250.0, netResult?.value)
println("currency set rate real test starts here")
@ -679,13 +652,137 @@ class StatsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
}
}
val updatedComputableResults = realm.where(ComputableResult::class.java).findAll()
val updatedSets = realm.where(SessionSet::class.java).findAll()
val updatedGroup = ComputableGroup("test", updatedComputableResults, updatedSets, stats)
val updatedResults: ComputedResults = Calculator.compute(updatedGroup, options)
val updatedNetResult = updatedResults.computedStat(Stat.NETRESULT)
Assert.assertEquals(650.0, updatedNetResult?.value)
val updatedGroup = ComputableGroup("test", listOf(), stats)
val updatedResults: ComputedResults = Calculator.compute(realm, updatedGroup, options)
val updatedNetResult = updatedResults.computedStat(Stat.NET_RESULT)
assertEquals(650.0, updatedNetResult?.value)
}
@Test
fun testDaysPlayed() {
val realm = this.mockRealm
realm.executeTransaction {
val s1 = newSessionInstance(realm)
val s2 = newSessionInstance(realm)
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 09:00")
val ed1 = sdf.parse("01/1/2019 10:00")
val sd2 = sdf.parse("01/1/2019 19:00")
val ed2 = sdf.parse("01/1/2019 20:00")
s1.startDate = sd1
s1.endDate = ed1
s2.startDate = sd2
s2.endDate = ed2
}
val group = ComputableGroup("", listOf(), listOf())
val options = Calculator.Options(stats = listOf(Stat.DAYS_PLAYED))
val report = Calculator.computeGroups(realm, listOf(group), options)
report.results.firstOrNull()?.computedStat(Stat.DAYS_PLAYED)?.let {
assertEquals(1, it.value.toInt())
} ?: run {
Assert.fail("Missing DAYS_PLAYED")
}
}
@Test
fun testFilteredHourlyRate() {
val realm = this.mockRealm
realm.executeTransaction {
val s1 = newSessionInstance(realm, true)
val s2 = newSessionInstance(realm, true)
val s3 = newSessionInstance(realm, false)
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 09:00")
val ed1 = sdf.parse("01/1/2019 10:00")
val sd2 = sdf.parse("01/1/2019 04:00")
val ed2 = sdf.parse("01/1/2019 05:00")
val sd3 = sdf.parse("01/1/2019 03:00")
val ed3 = sdf.parse("01/1/2019 11:00")
s1.startDate = sd1
s1.endDate = ed1
s2.startDate = sd2
s2.endDate = ed2
s3.startDate = sd3
s3.endDate = ed3
}
val group = ComputableGroup("test", listOf(QueryCondition.IsCash))
val options = Calculator.Options()
options.displayedStats = listOf(Stat.HOURLY_DURATION)
val results: ComputedResults = Calculator.compute(realm, group, options)
val delta = 0.01
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(2.0, duration.value, delta)
} else {
Assert.fail("No Net result stat")
}
}
@Test
fun testFilteredHourlyRate2() {
val realm = this.mockRealm
realm.executeTransaction {
val s1 = newSessionInstance(realm, true)
val s2 = newSessionInstance(realm, true)
val s3 = newSessionInstance(realm, false)
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 06:00")
val ed1 = sdf.parse("01/1/2019 09:00")
val sd2 = sdf.parse("01/1/2019 07:00")
val ed2 = sdf.parse("01/1/2019 10:00")
val sd3 = sdf.parse("01/1/2019 03:00")
val ed3 = sdf.parse("01/1/2019 11:00")
s1.startDate = sd1
s1.endDate = ed1
s2.startDate = sd2
s2.endDate = ed2
s3.startDate = sd3
s3.endDate = ed3
}
val group = ComputableGroup("test", listOf(QueryCondition.IsCash))
val options = Calculator.Options()
options.displayedStats = listOf(Stat.HOURLY_DURATION)
val results: ComputedResults = Calculator.compute(realm, group, options)
val delta = 0.01
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(4.0, duration.value, delta)
} else {
Assert.fail("No Net result stat")
}
}
}

@ -6,7 +6,6 @@ import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.FilterCondition
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import org.junit.Assert
import org.junit.Test
@ -21,7 +20,7 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
realm.beginTransaction()
val currency = realm.createObject(net.pokeranalytics.android.model.realm.Currency::class.java, "1")
currency.code = "AUD"
currency.code = "USD"
val b1 = realm.createObject(Bankroll::class.java, "1")
val b2 = realm.createObject(Bankroll::class.java, "2")
@ -41,14 +40,16 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.AnyBlind()
val filter = QueryCondition.BLINDS
val blind = FilterElementRow.Blind(0.5, 1.0, null)
blind.filterSectionRow = FilterSectionRow.BLINDS
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind))
filter.updateValueMap(filterElement)
val blind = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!)
}
blind.filterSectionRow = FilterSectionRow.BLIND
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind))
filter.updateValueMap(filterElement)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
@ -86,12 +87,20 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.BLINDS
val blind = FilterElementRow.Blind(null, 1.0, null)
blind.filterSectionRow = FilterSectionRow.BLINDS
val filter = QueryCondition.AnyBlind()
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind))
filter.updateValueMap(filterElement)
val blind1 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!)
}
val blind2 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s2.blinds!!)
}
blind1.filterSectionRow = FilterSectionRow.BLIND
blind2.filterSectionRow = FilterSectionRow.BLIND
val filterElements = FilterCondition(filterElementRows = arrayListOf(blind1, blind2))
filter.updateValueMap(filterElements)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
@ -108,13 +117,14 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
realm.beginTransaction()
val currency = realm.createObject(net.pokeranalytics.android.model.realm.Currency::class.java, "1")
currency.code = "AUD"
currency.code = "USD"
val b1 = realm.createObject(Bankroll::class.java, "1")
val b2 = realm.createObject(Bankroll::class.java, "2")
b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
@ -129,13 +139,17 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.BLINDS
val blind = FilterElementRow.Blind(1.0, 2.0, "AUD")
blind.filterSectionRow = FilterSectionRow.BLINDS
val filter = QueryCondition.AnyBlind()
val blind = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s3.blinds!!)
}
blind.filterSectionRow = FilterSectionRow.BLIND
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind))
filter.updateValueMap(filterElement)
println("<<<< ${filter.listOfValues}")
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
Assert.assertEquals(1, sessions.size)
@ -172,12 +186,19 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.BLINDS
val blind1 = FilterElementRow.Blind(1.0, 2.0, null)
blind1.filterSectionRow = FilterSectionRow.BLINDS
val blind2 = FilterElementRow.Blind(0.5, 1.0, null)
blind2.filterSectionRow = FilterSectionRow.BLINDS
val filter = QueryCondition.AnyBlind()
val blind1 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!)
}
val blind2 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s2.blinds!!)
}
blind1.filterSectionRow = FilterSectionRow.BLIND
blind2.filterSectionRow = FilterSectionRow.BLIND
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind1, blind2))
filter.updateValueMap(filterElement)

@ -6,7 +6,6 @@ import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.FilterCondition
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.util.extensions.startOfDay
import org.junit.Assert
@ -30,19 +29,19 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(100.0, true, cal.time)
realm.commitTransaction()
val filter = QueryCondition.DAY_OF_WEEK
val filter = QueryCondition.AnyDayOfWeek()
cal.time = s1.startDate
val filterElementRow = FilterElementRow.Day(cal.get(Calendar.DAY_OF_WEEK))
val filterElementRow = QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(cal.get(Calendar.DAY_OF_WEEK)) }
filterElementRow.filterSectionRow = FilterSectionRow.DYNAMIC_DATE
val filterElement = FilterCondition(filterElementRow)
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueMap(filterElement)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this as Session).id)
Assert.assertEquals(s1.id, (this).id)
}
}
@ -59,19 +58,19 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(100.0, true, cal.time)
realm.commitTransaction()
val filter = QueryCondition.MONTH
val filter = QueryCondition.AnyMonthOfYear()
cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(cal.get(Calendar.MONTH)) }
val filterElementRow = FilterElementRow.Month(cal.get(Calendar.MONTH))
filterElementRow.filterSectionRow = FilterSectionRow.DYNAMIC_DATE
val filterElement = FilterCondition(filterElementRow)
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueMap(filterElement)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this as Session).id)
Assert.assertEquals(s1.id, (this).id)
}
}
@ -88,18 +87,18 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(100.0, true, cal.time)
realm.commitTransaction()
val filter = QueryCondition.YEAR
val filter = QueryCondition.AnyYear()
cal.time = s1.startDate
val filterElementRow = FilterElementRow.Year(cal.get(Calendar.YEAR))
val filterElementRow = QueryCondition.AnyYear().apply { listOfValues = arrayListOf(cal.get(Calendar.YEAR)) }
filterElementRow.filterSectionRow = FilterSectionRow.DYNAMIC_DATE
val filterElement = FilterCondition(filterElementRow)
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueMap(filterElement)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this as Session).id)
Assert.assertEquals(s1.id, (this).id)
}
}
@ -118,11 +117,11 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(100.0, true, cal.time)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.WEEK_END))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.IsWeekEnd))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this as Session).id)
Assert.assertEquals(s1.id, (this).id)
}
}
@ -143,11 +142,11 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.WEEK_DAY))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.IsWeekDay))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this as Session).id)
Assert.assertEquals(s1.id, (this).id)
}
}
@ -168,7 +167,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 2)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.TODAY))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.IsToday))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
@ -193,7 +192,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 2)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.TODAY))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.IsToday))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
@ -221,7 +220,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 3)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.YESTERDAY))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.WasYesterday))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
@ -248,7 +247,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 3)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.YESTERDAY))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.WasYesterday))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
@ -275,7 +274,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 3)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.TODAY_AND_YESTERDAY))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.WasTodayAndYesterday))
Assert.assertEquals(2, sessions.size)
Assert.assertTrue(sessions.containsAll(arrayListOf(s1,s2)))
@ -306,7 +305,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 5)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.THIS_YEAR))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.DuringThisYear))
Assert.assertEquals(3, sessions.size)
Assert.assertTrue(sessions.containsAll(arrayListOf(s1,s2, s3)))
@ -338,7 +337,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 5)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.THIS_MONTH))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.DuringThisMonth))
Assert.assertEquals(2, sessions.size)
Assert.assertTrue(sessions.containsAll(arrayListOf(s1,s2)))
@ -362,7 +361,7 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 2)
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.THIS_WEEK))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.DuringThisWeek))
Assert.assertEquals(1, sessions.size)
Assert.assertTrue(sessions.containsAll(arrayListOf(s1)))
@ -383,16 +382,16 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val s2 = Session.testInstance(100.0, true, cal.time, 1)
realm.commitTransaction()
val filter = QueryCondition.STARTED_FROM_DATE
val filterElementRow = FilterElementRow.From(s2.startDate!!)
val filter = QueryCondition.StartedFromDate()
val filterElementRow = QueryCondition.StartedFromDate().apply { singleValue = s2.startDate!!}
filterElementRow.filterSectionRow = FilterSectionRow.FIXED_DATE
filter.updateValueMap(FilterCondition(filterElementRow))
filter.updateValueMap(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s2.id, (this as Session).id)
Assert.assertEquals(s2.id, (this).id)
}
}
@ -411,16 +410,16 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.STARTED_TO_DATE
val filterElementRow = FilterElementRow.From(s1.startDate!!)
val filter = QueryCondition.StartedToDate()
val filterElementRow = QueryCondition.StartedToDate().apply { singleValue = s1.startDate!! }
filterElementRow.filterSectionRow = FilterSectionRow.FIXED_DATE
filter.updateValueMap(FilterCondition(filterElementRow))
filter.updateValueMap(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this as Session).id)
Assert.assertEquals(s1.id, (this).id)
}
}
@ -440,16 +439,16 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.ENDED_FROM_DATE
val filterElementRow = FilterElementRow.From(s2.endDate())
val filter = QueryCondition.EndedFromDate()
val filterElementRow = QueryCondition.EndedFromDate().apply { singleValue = s2.endDate() }
filterElementRow.filterSectionRow = FilterSectionRow.FIXED_DATE
filter.updateValueMap(FilterCondition(filterElementRow))
filter.updateValueMap(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s2.id, (this as Session).id)
Assert.assertEquals(s2.id, (this).id)
}
}
@ -469,16 +468,16 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction()
val filter = QueryCondition.ENDED_TO_DATE
val filterElementRow = FilterElementRow.From(s1.endDate())
val filter = QueryCondition.EndedToDate()
val filterElementRow = QueryCondition.EndedToDate().apply { singleValue = s1.endDate() }
filterElementRow.filterSectionRow = FilterSectionRow.FIXED_DATE
filter.updateValueMap(FilterCondition(filterElementRow))
filter.updateValueMap(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this as Session).id)
Assert.assertEquals(s1.id, (this).id)
}
}
}

@ -13,17 +13,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ExceptionFilterInstrumentedTest: BaseFilterInstrumentedUnitTest() {
@Test(expected = PokerAnalyticsException.FilterElementExpectedValueMissing::class)
fun testValueKeyFilterException() {
val filter = QueryCondition.STARTED_FROM_DATE
val filterElement = FilterCondition()
filter.updateValueMap(filterElement)
val realm = this.mockRealm
Filter.queryOn<Session>(realm, arrayListOf(filter))
}
@Test(expected = PokerAnalyticsException.FilterElementUnknownName::class)
fun testFilterException() {
FilterCondition().queryCondition

@ -2,12 +2,10 @@ package net.pokeranalytics.android.unitTests.filter
import androidx.test.ext.junit.runners.AndroidJUnit4
import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import org.junit.Assert
import org.junit.Test
@ -25,7 +23,7 @@ class RealmFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = Filter()
filter.name = "testSaveLoadCashFilter"
val filterElement = FilterElementRow.Cash
val filterElement = QueryCondition.IsCash
filterElement.filterSectionRow = FilterSectionRow.CASH_TOURNAMENT
filter.createOrUpdateFilterConditions(arrayListOf(filterElement))
@ -38,7 +36,7 @@ class RealmFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filterComponent = filter.filterConditions.first()
filterComponent?.let {
Assert.assertEquals(QueryCondition.CASH, QueryCondition.valueOf(it.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName))
Assert.assertTrue(it.queryCondition is QueryCondition.IsCash)
} ?: run {
Assert.fail()
}
@ -55,7 +53,7 @@ class RealmFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(Session.Type.CASH_GAME.ordinal, (this as Session).type)
Assert.assertEquals(Session.Type.CASH_GAME.ordinal, this.type)
} ?: run {
Assert.fail()
}

@ -3,10 +3,8 @@ package net.pokeranalytics.android.unitTests.filter
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.realm.RealmList
import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import org.junit.Assert
import org.junit.Test
@ -26,11 +24,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(100.0, true, Date(), 1)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.CASH))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.IsCash))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(Session.Type.CASH_GAME.ordinal, (this as Session).type)
Assert.assertEquals(Session.Type.CASH_GAME.ordinal, (this).type)
}
}
@ -44,11 +42,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(100.0, true, Date(), 1)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.TOURNAMENT))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.IsTournament))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(Session.Type.TOURNAMENT.ordinal, (this as Session).type)
Assert.assertEquals(Session.Type.TOURNAMENT.ordinal, (this).type)
}
}
@ -67,7 +65,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(100.0, true, Date(), 1, b2)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.LIVE))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.IsLive))
Assert.assertEquals(1, sessions.size)
(sessions[0] as Session).bankroll?.run {
@ -89,7 +87,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(100.0, true, Date(), 1, b2)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.ONLINE))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(QueryCondition.IsOnline))
Assert.assertEquals(1, sessions.size)
(sessions[0] as Session).bankroll?.run {
@ -109,8 +107,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(bankroll = b2)
realm.commitTransaction()
val filter = QueryCondition.BANKROLL
val filterElementRow = FilterElementRow.Bankroll(b1)
val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
filterElementRow.filterSectionRow = FilterSectionRow.BANKROLL
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
@ -140,11 +138,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(bankroll = b3)
realm.commitTransaction()
val filter = QueryCondition.BANKROLL
val filterElementRow = FilterElementRow.Bankroll(b1)
val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
filterElementRow.filterSectionRow = FilterSectionRow.BANKROLL
val filterElementRow2 = FilterElementRow.Bankroll(b2)
val filterElementRow2 = QueryCondition.AnyBankroll().apply { setObject(b2) }
filterElementRow2.filterSectionRow = FilterSectionRow.BANKROLL
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
@ -169,12 +167,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(game = g2)
realm.commitTransaction()
val filter = QueryCondition.GAME
val filterElementRow = FilterElementRow.Game(g2)
filterElementRow.filterSectionRow = FilterSectionRow.GAME
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
val anyGame = QueryCondition.AnyGame(g2)
val fc = FilterCondition(filterElementRows = arrayListOf(anyGame))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(fc.queryCondition))
Assert.assertEquals(1, sessions.size)
(sessions[0] as Session).game?.run {
@ -200,15 +195,13 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(game = g3)
realm.commitTransaction()
val filter = QueryCondition.GAME
val filterElementRow = FilterElementRow.Game(g2)
val filterElementRow = QueryCondition.AnyGame().apply { setObject(g2) }
filterElementRow.filterSectionRow = FilterSectionRow.GAME
val filterElementRow2 = FilterElementRow.Game(g3)
val filterElementRow2 = QueryCondition.AnyGame().apply { setObject(g3) }
filterElementRow2.filterSectionRow = FilterSectionRow.GAME
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
val filterCondition = FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2))
val queryCondition = filterCondition.queryCondition
val sessions = Filter.queryOn<Session>(realm, arrayListOf(queryCondition))
Assert.assertEquals(6, sessions.size)
@ -229,8 +222,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(location = l2)
realm.commitTransaction()
val filter = QueryCondition.LOCATION
val filterElementRow = FilterElementRow.Location(l1)
val filter = QueryCondition.AnyLocation()
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
filterElementRow.filterSectionRow = FilterSectionRow.LOCATION
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
@ -260,11 +253,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(location = l3)
realm.commitTransaction()
val filter = QueryCondition.LOCATION
val filter = QueryCondition.AnyLocation()
val filterElementRow = FilterElementRow.Location(l1)
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
filterElementRow.filterSectionRow = FilterSectionRow.LOCATION
val filterElementRow2 = FilterElementRow.Location(l3)
val filterElementRow2 = QueryCondition.AnyLocation().apply { setObject(l3) }
filterElementRow2.filterSectionRow = FilterSectionRow.LOCATION
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
@ -290,9 +283,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(tournamentName = t2)
realm.commitTransaction()
val filter = QueryCondition.TOURNAMENT_NAME
val filter = QueryCondition.AnyTournamentName()
val filterElementRow = FilterElementRow.TournamentName(t1)
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_NAME
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
@ -322,10 +315,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(tournamentName = t3)
realm.commitTransaction()
val filter = QueryCondition.TOURNAMENT_NAME
val filterElementRow = FilterElementRow.TournamentName(t1)
val filter = QueryCondition.AnyTournamentName()
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_NAME
val filterElementRow2 = FilterElementRow.TournamentName(t2)
val filterElementRow2 = QueryCondition.AnyTournamentName().apply { setObject(t2) }
filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_NAME
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
@ -358,12 +351,12 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(tournamentFeatures = RealmList(t1))
realm.commitTransaction()
val filter = QueryCondition.ALL_TOURNAMENT_FEATURES
val filterElementRow = FilterElementRow.AllTournamentFeature(t1)
val filter = QueryCondition.AllTournamentFeature()
val filterElementRow = QueryCondition.AllTournamentFeature().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE
val filterElementRow2 = FilterElementRow.AllTournamentFeature(t2)
val filterElementRow2 = QueryCondition.AllTournamentFeature().apply { setObject(t2) }
filterElementRow2.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE
val filterElementRow3 = FilterElementRow.AllTournamentFeature(t4)
val filterElementRow3 = QueryCondition.AllTournamentFeature().apply { setObject(t4) }
filterElementRow3.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2, filterElementRow3)))
@ -393,14 +386,14 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(tournamentFeatures = RealmList(t1))
realm.commitTransaction()
val filter = QueryCondition.ANY_TOURNAMENT_FEATURES
val filterElementRow = FilterElementRow.AnyTournamentFeature(t1)
val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE
val filterElementRow2 = FilterElementRow.AnyTournamentFeature(t2)
val filterElementRow2 = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
filterElementRow2.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE
val filterElementRow3 = FilterElementRow.AnyTournamentFeature(t3)
val filterElementRow3 = QueryCondition.AnyTournamentFeature().apply { setObject(t3) }
filterElementRow3.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE
val filterElementRow4 = FilterElementRow.AnyTournamentFeature(t4)
val filterElementRow4 = QueryCondition.AnyTournamentFeature().apply { setObject(t4) }
filterElementRow4.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2, filterElementRow3, filterElementRow4)))
@ -427,8 +420,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(tournamentFeatures = RealmList(t1))
realm.commitTransaction()
val filter = QueryCondition.ANY_TOURNAMENT_FEATURES
val filterElementRow = FilterElementRow.AnyTournamentFeature(t2)
val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
filterElementRow.filterSectionRow = FilterSectionRow.TOURNAMENT_FEATURE
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
@ -452,10 +445,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(tableSize = 10)
realm.commitTransaction()
val filter = QueryCondition.TABLE_SIZE
val filterElementRow = FilterElementRow.TableSize(TableSize(2))
val filter = QueryCondition.AnyTableSize()
val filterElementRow = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(2) }
filterElementRow.filterSectionRow = FilterSectionRow.TABLE_SIZE
val filterElementRow2 = FilterElementRow.TableSize(TableSize(4))
val filterElementRow2 = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(4) }
filterElementRow.filterSectionRow = FilterSectionRow.TABLE_SIZE
filter.updateValueMap(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
@ -479,12 +472,12 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val s2 = Session.testInstance(netResult = 570.0)
realm.commitTransaction()
val filter = QueryCondition.MORE_THAN_NET_RESULT
val filterElementRow = FilterElementRow.ResultMoreThan(204.0)
val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) }
filterElementRow.filterSectionRow = FilterSectionRow.VALUE
filter.updateValueMap(FilterCondition(filterElementRow))
filter.updateValueMap(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filterElementRow))
Assert.assertEquals(2, sessions.size)
@ -504,10 +497,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(netResult = 570.0)
realm.commitTransaction()
val filter = QueryCondition.LESS_THAN_NET_RESULT
val filterElementRow = FilterElementRow.ResultLessThan(540.0)
val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) }
filterElementRow.filterSectionRow = FilterSectionRow.VALUE
filter.updateValueMap(FilterCondition(filterElementRow))
filter.updateValueMap(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filter))
@ -529,15 +522,15 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
Session.testInstance(netResult = 570.0)
realm.commitTransaction()
val filterMore = QueryCondition.MORE_THAN_NET_RESULT
val filterElementRow = FilterElementRow.ResultMoreThan(200.0)
val filterMore = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(200.0) }
filterElementRow.filterSectionRow = FilterSectionRow.VALUE
filterMore.updateValueMap(FilterCondition(filterElementRow))
filterMore.updateValueMap(FilterCondition(arrayListOf(filterElementRow)))
val filterLess = QueryCondition.LESS_THAN_NET_RESULT
val filterElementRow2 = FilterElementRow.ResultLessThan(400.0)
val filterLess = QueryCondition.NetAmountWon()
val filterElementRow2 = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) }
filterElementRow2.filterSectionRow = FilterSectionRow.VALUE
filterLess.updateValueMap(FilterCondition(filterElementRow2))
filterLess.updateValueMap(FilterCondition(arrayListOf(filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, arrayListOf(filterMore, filterLess))

@ -34,6 +34,36 @@
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name=".ui.activity.BankrollActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name=".ui.activity.SettingsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name=".ui.activity.StatisticDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name=".ui.activity.ReportDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name=".ui.activity.CalendarDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name=".ui.activity.ComparisonChartActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name=".ui.activity.DataListActivity"
android:launchMode="singleTop"
@ -64,15 +94,20 @@
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity
android:name=".ui.activity.GraphActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>

@ -15,6 +15,7 @@ import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.util.FakeDataManager
import net.pokeranalytics.android.util.PokerAnalyticsLogs
import net.pokeranalytics.android.util.UserDefaults
import timber.log.Timber
@ -23,12 +24,13 @@ class PokerAnalyticsApplication : Application() {
override fun onCreate() {
super.onCreate()
UserDefaults.init(this)
// Realm
Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME)
.schemaVersion(2)
.schemaVersion(3)
.migration(PokerAnalyticsMigration())
.initialData(Seed(this))
.build()
@ -49,7 +51,8 @@ class PokerAnalyticsApplication : Application() {
}
if (BuildConfig.DEBUG) {
// this.createFakeSessions()
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
// this.createFakeSessions()
}
Patcher.patchBreaks()

@ -2,11 +2,20 @@ package net.pokeranalytics.android.calculus
import io.realm.Realm
import net.pokeranalytics.android.calculus.Stat.*
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.QueryCondition
import net.pokeranalytics.android.model.filter.filter
import net.pokeranalytics.android.model.filter.name
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.model.upToNow
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
@ -16,7 +25,12 @@ class Calculator {
/**
* The options used for calculations or display
*/
class Options {
class Options(
display: Display = Display.TABLE,
evolutionValues: EvolutionValues = EvolutionValues.NONE,
stats: List<Stat> = listOf(),
aggregationType: AggregationType? = null
) {
/**
* The way the stats are going to be displayed
@ -35,49 +49,122 @@ class Calculator {
enum class EvolutionValues {
NONE,
STANDARD,
DATED
TIMED
}
var display: Display = Display.TABLE
var evolutionValues: EvolutionValues = EvolutionValues.NONE
var displayedStats: List<Stat> = listOf()
var display: Display = display
var evolutionValues: EvolutionValues = evolutionValues
var displayedStats: List<Stat> = stats
var aggregationType: AggregationType? = aggregationType
/**
* This function determines whether the standard deviation should be computed
*/
fun shouldComputeStandardDeviation(): Boolean {
this.displayedStats.forEach { stat ->
return when (stat) {
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> true
else -> false
val computeStandardDeviation: Boolean
get() {
this.displayedStats.forEach {
if (it == STANDARD_DEVIATION_BB_PER_100_HANDS || it == STANDARD_DEVIATION || it == STANDARD_DEVIATION_HOURLY) {
return true
}
}
return false
}
return true
val computeLongestStreak: Boolean
get() {
return this.displayedStats.contains(LONGEST_STREAKS)
}
val shouldSortValues: Boolean
get() {
return this.evolutionValues != EvolutionValues.NONE || this.computeLongestStreak
}
val computeLocationsPlayed: Boolean
get() {
return this.displayedStats.contains(LOCATIONS_PLAYED)
}
val computeDaysPlayed: Boolean
get() {
return this.displayedStats.contains(DAYS_PLAYED)
}
val shouldManageMultiGroupProgressValues: Boolean
get() {
if (this.aggregationType != null) {
return this.aggregationType == AggregationType.MONTH || this.aggregationType == AggregationType.YEAR
} else {
return false
}
}
// var aggregation: Aggregation? = null
}
companion object {
fun computeStatsWithFilters(realm: Realm, filters: List<Filter>, options: Options): List<ComputedResults> {
fun computeStatsWithEvolutionByAggregationType(
realm: Realm,
stat: Stat,
group: ComputableGroup,
aggregationType: AggregationType,
stats: List<Stat>? = null
): Report {
val options = Options(evolutionValues = Options.EvolutionValues.STANDARD, aggregationType = aggregationType)
options.displayedStats = listOf(stat)
if (aggregationType == AggregationType.DURATION) {
options.evolutionValues = Options.EvolutionValues.TIMED
}
var computableGroups: MutableList<ComputableGroup> = mutableListOf()
filters.forEach { filter ->
stats?.let {
options.displayedStats = stats
}
val group = ComputableGroup(filter.name, filter.filterConditions.map { it.queryCondition })
return when (aggregationType) {
AggregationType.SESSION, AggregationType.DURATION -> this.computeGroups(realm, listOf(group), options)
AggregationType.MONTH -> {
val criteria: List<Criteria> = listOf(Criteria.Years, Criteria.MonthsOfYear)
this.computeStatsWithComparators(realm, criteria, group.conditions, options)
}
AggregationType.YEAR -> {
val criteria: List<Criteria> = listOf(Criteria.Years)
this.computeStatsWithComparators(realm, criteria, group.conditions, options)
}
}
}
fun computeStatsWithComparators(
realm: Realm,
criteria: List<Criteria> = listOf(),
conditions: List<QueryCondition> = listOf(),
options: Options = Options()
): Report {
val computableGroups: MutableList<ComputableGroup> = mutableListOf()
criteria.combined().upToNow().forEach { comparatorConditions ->
val allConditions = mutableListOf<QueryCondition>()
allConditions.addAll(conditions)
allConditions.addAll(comparatorConditions)
val group = ComputableGroup(allConditions.name(), allConditions)
computableGroups.add(group)
}
return Calculator.computeGroups(realm, computableGroups, options)
if (computableGroups.size == 0) {
val group = ComputableGroup(conditions.name(), conditions)
computableGroups.add(group)
}
return this.computeGroups(realm, computableGroups, options)
}
/**
* Computes all stats for list of Session sessionGroup
*/
fun computeGroups(realm: Realm, groups: List<ComputableGroup>, options: Options): List<ComputedResults> {
fun computeGroups(realm: Realm, groups: List<ComputableGroup>, options: Options = Options()): Report {
val computedResults = mutableListOf<ComputedResults>()
val report = Report(options)
groups.forEach { group ->
val s = Date()
@ -85,18 +172,22 @@ class Calculator {
group.cleanup()
// Computes actual sessionGroup stats
val results: ComputedResults = Calculator.compute(realm, group, options = options)
val results: ComputedResults = this.compute(realm, group, options)
// Computes the compared sessionGroup if existing
val comparedGroup = group.comparedComputables
val comparedGroup = group.comparedGroup
if (comparedGroup != null) {
val comparedResults = Calculator.compute(realm, comparedGroup, options = options)
val comparedResults = this.compute(realm, comparedGroup, options)
group.comparedComputedResults = comparedResults
results.computeStatVariations(comparedResults)
}
if (options.shouldManageMultiGroupProgressValues) {
group.comparedComputedResults = report.results.lastOrNull()
}
results.finalize(options) // later treatment, such as evolution numericValues sorting
computedResults.add(results)
report.addResults(results)
val e = Date()
val duration = (e.time - s.time) / 1000.0
@ -104,128 +195,64 @@ class Calculator {
}
return computedResults
return report
}
/**
* Computes stats for a SessionSet
*/
fun compute(realm: Realm, computableGroup: ComputableGroup, options: Options): ComputedResults {
fun compute(realm: Realm, computableGroup: ComputableGroup, options: Options = Options()): ComputedResults {
val computables = computableGroup.computables(realm)
Timber.d(">>>> Start computing group ${computableGroup.name}, ${computables.size} computables")
val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues)
val results: ComputedResults = ComputedResults(computableGroup)
val computables = computableGroup.computables(realm, options.shouldSortValues)
Timber.d(">>>> Start computing group ${computableGroup.name}, ${computables.size} computables")
results.addStat(NUMBER_OF_GAMES, computables.size.toDouble())
val sum = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
results.addStat(NET_RESULT, sum)
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 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()
val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble()
results.addStat(WINNING_SESSION_COUNT, winningSessionCount.toDouble())
// Compute for each session
when (options.evolutionValues) {
Options.EvolutionValues.STANDARD -> {
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, NETRESULT, session)
results.addEvolutionValue(tSum / index, AVERAGE, session)
results.addEvolutionValue(index.toDouble(), NUMBER_OF_GAMES, session)
results.addEvolutionValue(tBBSum / tBBSessionCount, AVERAGE_NET_BB, session)
results.addEvolutionValue((tWinningSessionCount / index).toDouble(), WIN_RATIO, session)
results.addEvolutionValue(tBuyinSum / index, AVERAGE_BUYIN, session)
Stat.netBBPer100Hands(tBBSum, tHands)?.let { netBB100 ->
results.addEvolutionValue(netBB100, NET_BB_PER_100_HANDS, session)
}
val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble()
results.addStat(TOTAL_BUYIN, totalBuyin)
Stat.returnOnInvestment(tSum, tBuyinSum)?.let { roi ->
results.addEvolutionValue(roi, ROI, session)
}
val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
maxNetResult?.let {
results.addStat(MAXIMUM_NETRESULT, it)
}
}
}
else -> {
// nothing
}
val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
minNetResult?.let {
results.addStat(MINIMUM_NETRESULT, it)
}
val sessionSets = computableGroup.sessionSets(realm)
// Compute for each serie
val gHourlyDuration =
sessionSets.sum(SessionSet.Field.NET_DURATION.identifier).toDouble() / 3600000 // (milliseconds to hours)
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 hourlyRate = gSum / gHourlyDuration
// var bbHourlyRate = gBBSum / gDuration
when (options.evolutionValues) {
Options.EvolutionValues.DATED -> {
var tHourlyDuration = 0.0
var tIndex = 0
var tSum = 0.0
var tTotalHands = 0.0
var tBBSum = 0.0
var tHourlyRate = 0.0
var tHourlyRateBB = 0.0
sessionSets.forEach { sessionSet ->
tIndex++
tHourlyDuration += sessionSet.hourlyDuration
tSum += sessionSet.ratedNet
tTotalHands += sessionSet.estimatedHands
tBBSum += sessionSet.bbNet
tHourlyRate = gSum / tHourlyDuration
tHourlyRateBB = gBBSum / tHourlyDuration
results.addEvolutionValue(tSum, tHourlyDuration, NETRESULT, sessionSet)
results.addEvolutionValue(tSum / tHourlyDuration, tHourlyDuration, HOURLY_RATE, sessionSet)
results.addEvolutionValue(tHourlyRate, tHourlyDuration, HOURLY_RATE, sessionSet)
results.addEvolutionValue(tIndex.toDouble(), tHourlyDuration, NUMBER_OF_SETS, sessionSet)
results.addEvolutionValue(sessionSet.netDuration.toDouble(), tHourlyDuration, DURATION, sessionSet)
results.addEvolutionValue(tHourlyDuration / tIndex, tHourlyDuration, AVERAGE_DURATION, sessionSet)
results.addEvolutionValue(tHourlyRateBB, tHourlyDuration, HOURLY_RATE_BB, sessionSet)
Stat.netBBPer100Hands(gBBSum, gTotalHands)?.let { netBB100 ->
results.addEvolutionValue(netBB100, tHourlyDuration, NET_BB_PER_100_HANDS, sessionSet)
}
Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 ->
results.addStat(NET_BB_PER_100_HANDS, netBB100)
}
Stat.returnOnInvestment(sum, totalBuyin)?.let { roi ->
results.addStat(ROI, roi)
}
}
}
else -> {
// nothing
}
if (options.computeLocationsPlayed) {
results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble())
}
var average = 0.0
var average = 0.0 // also used for standard deviation later
if (computables.size > 0) {
average = sum / computables.size.toDouble()
val winRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val avgBuyin = totalBuyin / computables.size
val avgBuyin = totalBuyin / computables.size.toDouble()
results.addStats(
setOf(
@ -236,41 +263,228 @@ class Calculator {
)
}
if (sessionSets.size > 0) {
val avgDuration = gHourlyDuration / sessionSets.size
results.addStats(
setOf(
ComputedStat(HOURLY_RATE, hourlyRate),
ComputedStat(AVERAGE_DURATION, avgDuration)
val shouldIterateOverComputables =
(options.evolutionValues == Options.EvolutionValues.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
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) { // 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 IllegalStateException("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(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())
}
// Create stats
results.addStats(
setOf(
ComputedStat(NETRESULT, sum),
ComputedStat(DURATION, gHourlyDuration),
ComputedStat(NUMBER_OF_SETS, sessionSets.size.toDouble()),
ComputedStat(NUMBER_OF_GAMES, computables.size.toDouble()),
ComputedStat(HOURLY_RATE_BB, gBBSum / gHourlyDuration),
ComputedStat(AVERAGE_NET_BB, gBBSum / bbSessionCount),
ComputedStat(HANDS_PLAYED, totalHands)
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
Stat.returnOnInvestment(sum, totalBuyin)?.let { roi ->
results.addStats(setOf(ComputedStat(ROI, roi)))
if (computableGroup.conditions.size == 0) { // 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
}
}
Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 ->
results.addStats(setOf(ComputedStat(NET_BB_PER_100_HANDS, netBB100)))
val shouldIterateOverSets = computableGroup.conditions.isNotEmpty() ||
options.evolutionValues != Options.EvolutionValues.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<Date>()
var tMaxDuration = 0.0
sessionSets.forEach { sessionSet ->
tIndex++
val setStats = SSStats(sessionSet, computableGroup.conditions)
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.evolutionValues) {
Options.EvolutionValues.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)
Stat.netBBPer100Hands(tBBSum, tTotalHands)?.let { netBB100 ->
results.addEvolutionValue(netBB100, stat = NET_BB_PER_100_HANDS, data = sessionSet)
}
}
Options.EvolutionValues.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 = sum / 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.shouldComputeStandardDeviation()) {
if (options.computeStandardDeviation) {
// Session
var stdSum = 0.0
@ -282,20 +496,22 @@ class Calculator {
val standardDeviation = Math.sqrt(stdSum / computables.size)
val standardDeviationBBper100Hands = Math.sqrt(stdBBper100HandsSum / computables.size)
results.addStat(STANDARD_DEVIATION, standardDeviation)
results.addStat(STANDARD_DEVIATION_BB_PER_100_HANDS, standardDeviationBBper100Hands)
// Session Set
var hourlyStdSum = 0.0
sessionSets.forEach { set ->
hourlyStdSum += Math.pow(set.hourlyRate - hourlyRate, 2.0)
if (gHourlyDuration != null) {
var hourlyStdSum = 0.0
sessionSets.forEach { set ->
val ssStats = SSStats(set, computableGroup.conditions)
val sHourlyRate = ssStats.hourlyRate
hourlyStdSum += Math.pow(sHourlyRate - hourlyRate, 2.0)
}
val hourlyStandardDeviation = Math.sqrt(hourlyStdSum / sessionSets.size)
results.addStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation)
}
val hourlyStandardDeviation = Math.sqrt(hourlyStdSum / sessionSets.size)
results.addStats(
setOf(
ComputedStat(STANDARD_DEVIATION, standardDeviation),
ComputedStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation),
ComputedStat(STANDARD_DEVIATION_BB_PER_100_HANDS, standardDeviationBBper100Hands)
)
)
}
return results
@ -303,5 +519,43 @@ class Calculator {
}
}
class SSStats(sessionSet: SessionSet, conditions: List<QueryCondition>) { // 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(conditions)
if (setSessions.size == filteredSessions.size) {
this.initStatsWithSet(sessionSet)
} else {
ratedNet = filteredSessions.sumByDouble { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumByDouble { it.bbNet }
hourlyDuration = filteredSessions.hourlyDuration
estimatedHands = filteredSessions.sumByDouble { it.estimatedHands }
}
}
}
private fun initStatsWithSet(sessionSet: SessionSet) {
ratedNet = sessionSet.ratedNet
bbSum = sessionSet.bbNet
hourlyDuration = sessionSet.hourlyDuration
estimatedHands = sessionSet.estimatedHands
}
}

@ -1,218 +0,0 @@
package net.pokeranalytics.android.calculus
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.Entry
import io.realm.Realm
import io.realm.RealmResults
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.Timed
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
/**
* A sessionGroup of computable items identified by a name
*/
class ComputableGroup(name: String, conditions: List<QueryCondition>, stats: List<Stat>? = null) {
/**
* The display name of the group
*/
var name: String = name
/**
* A list of conditions to get
*/
var conditions: List<QueryCondition> = conditions
/**
* The list of endedSessions to compute
*/
private var _computables: RealmResults<ComputableResult>? = null
/**
* Retrieves the computables on the relative [realm] filtered with the provided [conditions]
*/
fun computables(realm: Realm): RealmResults<ComputableResult> {
// if computables exists and is valid (previous realm not closed)
this._computables?.let {
if (it.isValid) {
return it
}
}
val computables: RealmResults<ComputableResult> = Filter.queryOn(realm, this.conditions)
this._computables = computables
return computables
}
/**
* The list of sets to compute
*/
private var _sessionSets: RealmResults<SessionSet>? = null
/**
* Retrieves the session sets on the relative [realm] filtered with the provided [conditions]
*/
fun sessionSets(realm: Realm): RealmResults<SessionSet> {
// if computables exists and is valid (previous realm not closed)
this._sessionSets?.let {
if (it.isValid) {
return it
}
}
val sets: RealmResults<SessionSet> = Filter.queryOn(realm, this.conditions)
this._sessionSets = sets
return sets
}
/**
* The list of stats to display
*/
var stats: List<Stat>? = stats
/**
* A subgroup used to compute stat variation
*/
var comparedComputables: ComputableGroup? = null
/**
* The computed stats of the comparable sessionGroup
*/
var comparedComputedResults: ComputedResults? = null
fun cleanup() {
this._computables = null
this._sessionSets = null
}
}
class ComputedResults(group: ComputableGroup) {
/**
* The session group used to computed the stats
*/
var group: ComputableGroup = group
// The computed stats of the sessionGroup
private var _computedStats: MutableMap<Stat, ComputedStat> = mutableMapOf()
// The map containing all evolution numericValues for all stats
private var _evolutionValues: MutableMap<Stat, MutableList<Point>> = mutableMapOf()
fun allStats(): Collection<ComputedStat> {
return this._computedStats.values
}
/**
* Adds a value to the evolution values
*/
fun addEvolutionValue(value: Double, stat: Stat, data: Any) {
this._addEvolutionValue(Point(value, data), stat = stat)
}
fun addEvolutionValue(value: Double, duration: Double, stat: Stat, data: Timed) {
stat.underlyingClass = data::class.java
this._addEvolutionValue(Point(value, y = duration, data = data.id), stat = stat)
}
private fun _addEvolutionValue(point: Point, stat: Stat) {
val evolutionValues = this._evolutionValues[stat]
if (evolutionValues != null) {
evolutionValues.add(point)
} else {
val values: MutableList<Point> = mutableListOf(point)
this._evolutionValues[stat] = values
}
}
fun addStats(computedStats: Set<ComputedStat>) {
computedStats.forEach {
this._computedStats[it.stat] = it
}
}
fun computedStat(stat: Stat): ComputedStat? {
return this._computedStats[stat]
}
fun computeStatVariations(resultsToCompare: ComputedResults) {
this._computedStats.keys.forEach { stat ->
val computedStat = this.computedStat(stat)
val comparedStat = resultsToCompare.computedStat(stat)
if (computedStat != null && comparedStat != null) {
computedStat.variation = (computedStat.value - comparedStat.value) / comparedStat.value
}
}
}
fun finalize(options: Calculator.Options) {
if (options.evolutionValues != Calculator.Options.EvolutionValues.NONE) {
// Sort points as a distribution
this._computedStats.keys.filter { it.hasDistributionSorting() }.forEach { _ ->
// @todo sort
// var evolutionValues = this._evolutionValues[stat]
// evolutionValues.so
}
}
}
/**
* Returns the number of computed stats
*/
fun numberOfStats(): Int {
return this._computedStats.size
}
// MPAndroidChart
fun defaultStatEntries(stat: Stat): List<Entry> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES -> this.barEntries(stat)
else -> this.singleLineEntries(stat)
}
}
fun singleLineEntries(stat: Stat): List<Entry> {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEachIndexed { index, p ->
entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
}
}
return entries
}
fun durationEntries(stat: Stat): List<Entry> {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return entries
}
fun barEntries(stat: Stat): List<BarEntry> {
val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return entries
}
}
class Point(val x: Double, val y: Double, val data: Any) {
constructor(y: Double, data: Any) : this(0.0, y, data)
}

@ -0,0 +1,496 @@
package net.pokeranalytics.android.calculus
import android.content.Context
import com.github.mikephil.charting.data.*
import io.realm.Realm
import io.realm.RealmResults
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.Timed
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.graph.PALineDataSet
import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent
import net.pokeranalytics.android.util.ColorUtils
import kotlin.math.abs
/**
* The class returned after performing calculation in the Calculator object
*/
class Report(var options: Calculator.Options) {
/**
* The mutable list of ComputedResults, one for each group of data
*/
private var _results: MutableList<ComputedResults> = mutableListOf()
val results: List<ComputedResults> = this._results
/**
* Adds a new result to the list of ComputedResults
*/
fun addResults(result: ComputedResults) {
this._results.add(result)
}
/**
* Returns the list of entries corresponding to the provided [stat]
* One value will be returned by result
*/
fun lineEntries(stat: Stat? = null, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
val statToUse = stat ?: options.displayedStats.firstOrNull()
val statName = statToUse?.name ?: ""
statToUse?.let {
this._results.forEachIndexed { index, results ->
results.computedStat(it)?.progressValue?.let { progressValue ->
entries.add(Entry(index.toFloat(), progressValue.toFloat(), results))
}
}
}
return PALineDataSet(entries, statName, context)
}
fun barEntries(stat: Stat? = null, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
val statToUse = stat ?: options.displayedStats.firstOrNull()
statToUse?.let {
this._results.forEachIndexed { index, results ->
val cs = results.computedStat(it)
cs?.let { computedStat ->
val barEntry = BarEntry(index.toFloat(), computedStat.value.toFloat(), results)
entries.add(barEntry)
}
}
}
val barDataSet = BarDataSet(entries, statToUse?.name)
barDataSet.color = context.getColor(R.color.green)
barDataSet.setDrawValues(false)
return barDataSet
}
fun multiLineEntries(context: Context): List<LineDataSet> {
val dataSets = mutableListOf<LineDataSet>()
options.displayedStats.forEach { stat ->
this._results.forEachIndexed { index, result ->
val ds = result.singleLineEntries(stat, context)
ds.color = ColorUtils.almostRandomColor(index, context)
dataSets.add(ds)
}
}
return dataSets
}
}
/**
* A sessionGroup of computable items identified by a name
*/
class ComputableGroup(name: String = "", conditions: List<QueryCondition> = listOf(), stats: List<Stat>? = null) {
/**
* The display name of the group
*/
var name: String = name
/**
* A list of conditions to get
*/
var conditions: List<QueryCondition> = conditions
/**
* The list of endedSessions to compute
*/
private var _computables: RealmResults<ComputableResult>? = null
/**
* Retrieves the computables on the relative [realm] filtered with the provided [conditions]
*/
fun computables(realm: Realm, sorted: Boolean = false): RealmResults<ComputableResult> {
// if computables exists and is valid (previous realm not closed)
this._computables?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) "session.startDate" else null
val computables = Filter.queryOn<ComputableResult>(realm, this.conditions, sortedField)
this._computables = computables
return computables
}
/**
* The list of sets to compute
*/
private var _sessionSets: RealmResults<SessionSet>? = null
/**
* Retrieves the session sets on the relative [realm] filtered with the provided [conditions]
*/
fun sessionSets(realm: Realm, sorted: Boolean = false): RealmResults<SessionSet> {
// if computables exists and is valid (previous realm not closed)
this._sessionSets?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) SessionSet.Field.START_DATE.identifier else null
val sets = Filter.queryOn<SessionSet>(realm, this.conditions, sortedField)
this._sessionSets = sets
return sets
}
/**
* The list of stats to display
*/
var stats: List<Stat>? = stats
/**
* A subgroup used to compute stat variation
*/
var comparedGroup: ComputableGroup? = null
/**
* The computed stats of the comparable sessionGroup
*/
var comparedComputedResults: ComputedResults? = null
fun cleanup() {
this._computables = null
this._sessionSets = null
}
val isEmpty: Boolean
get() {
return this._computables?.isEmpty() ?: true
}
}
class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValues: Boolean = false) : GraphUnderlyingEntry {
/**
* The session group used to computed the stats
*/
var group: ComputableGroup = group
// The computed stats of the sessionGroup
private var _computedStats: MutableMap<Stat, ComputedStat> = mutableMapOf()
// The map containing all evolution numericValues for all stats
private var _evolutionValues: MutableMap<Stat, MutableList<Point>> = mutableMapOf()
private var shouldManageMultiGroupProgressValues = shouldManageMultiGroupProgressValues
fun allStats(): Collection<ComputedStat> {
return this._computedStats.values
}
/**
* Adds a value to the evolution values
*/
fun addEvolutionValue(value: Double, duration: Double? = null, stat: Stat, data: Timed) {
val point = if (duration != null) {
Point(duration, y = value, data = data.objectIdentifier)
} else {
Point(value, data = data.objectIdentifier)
}
this._addEvolutionValue(point, stat = stat)
}
private fun _addEvolutionValue(point: Point, stat: Stat) {
val evolutionValues = this._evolutionValues[stat]
if (evolutionValues != null) {
evolutionValues.add(point)
} else {
val values: MutableList<Point> = mutableListOf(point)
this._evolutionValues[stat] = values
}
}
fun addStat(stat: Stat, value: Double, secondValue: Double? = null) {
val computedStat = ComputedStat(stat, value, secondValue = secondValue)
this.addComputedStat(computedStat)
}
fun addStats(computedStats: Set<ComputedStat>) {
computedStats.forEach {
this.addComputedStat(it)
}
}
/**
* Adds a [computedStat] to the list of stats
* Also computes evolution values using the previously computed values
*/
private fun addComputedStat(computedStat: ComputedStat) {
this._computedStats[computedStat.stat] = computedStat
}
private fun consolidateProgressStats() {
if (this.shouldManageMultiGroupProgressValues) {
this.group.comparedComputedResults?.let { previousResult ->
this.allStats().forEach { computedStat ->
val stat = computedStat.stat
previousResult.computedStat(stat)?.let { previousComputedStat ->
when (stat) {
Stat.NET_RESULT, Stat.HOURLY_DURATION, Stat.BB_NET_RESULT, Stat.BB_SESSION_COUNT,
Stat.WINNING_SESSION_COUNT, Stat.TOTAL_BUYIN, Stat.HANDS_PLAYED, Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_SETS -> {
val previousValue = previousComputedStat.progressValue ?: previousComputedStat.value
computedStat.progressValue = previousValue + computedStat.value
}
else -> {}
}
} ?: run {
computedStat.progressValue = computedStat.value
}
}
}
val netResult = this.computedStat(Stat.NET_RESULT)?.progressValue
val bbNetResult = this.computedStat(Stat.BB_NET_RESULT)?.progressValue
val duration = this.computedStat(Stat.HOURLY_DURATION)?.progressValue
val numberOfGames = this.computedStat(Stat.NUMBER_OF_GAMES)?.progressValue
val numberOfSets = this.computedStat(Stat.NUMBER_OF_SETS)?.progressValue
val handsPlayed = this.computedStat(Stat.HANDS_PLAYED)?.progressValue
val winningCount = this.computedStat(Stat.WINNING_SESSION_COUNT)?.progressValue
val bbSessionCount = this.computedStat(Stat.BB_SESSION_COUNT)?.progressValue
val totalBuyin = this.computedStat(Stat.TOTAL_BUYIN)?.progressValue
this.allStats().forEach { computedStat ->
when (computedStat.stat) {
Stat.HOURLY_RATE -> {
if (netResult != null && duration != null) {
computedStat.progressValue = netResult / duration
}
}
Stat.AVERAGE -> {
if (netResult != null && numberOfGames != null) {
computedStat.progressValue = netResult / numberOfGames
}
}
Stat.AVERAGE_HOURLY_DURATION -> {
if (duration != null && numberOfSets != null) {
computedStat.progressValue = duration / numberOfSets
}
}
Stat.NET_BB_PER_100_HANDS -> {
if (bbNetResult != null && handsPlayed != null) {
computedStat.progressValue = Stat.netBBPer100Hands(bbNetResult, handsPlayed)
}
}
Stat.HOURLY_RATE_BB -> {
if (bbNetResult != null && duration != null) {
computedStat.progressValue = bbNetResult / duration
}
}
Stat.AVERAGE_NET_BB -> {
if (bbNetResult != null && bbSessionCount != null) {
computedStat.progressValue = bbNetResult / bbSessionCount
}
}
Stat.WIN_RATIO -> {
if (winningCount != null && numberOfGames != null) {
computedStat.progressValue = winningCount / numberOfGames
}
}
Stat.AVERAGE_BUYIN -> {
if (totalBuyin != null && numberOfGames != null) {
computedStat.progressValue = totalBuyin / numberOfGames
}
}
Stat.ROI -> {
if (totalBuyin != null && netResult != null) {
computedStat.progressValue = Stat.returnOnInvestment(netResult, totalBuyin)
}
}
else -> {}
}
}
}
}
fun computedStat(stat: Stat): ComputedStat? {
return this._computedStats[stat]
}
fun computeStatVariations(resultsToCompare: ComputedResults) {
this._computedStats.keys.forEach { stat ->
val computedStat = this.computedStat(stat)
val comparedStat = resultsToCompare.computedStat(stat)
if (computedStat != null && comparedStat != null) {
computedStat.variation = (computedStat.value - comparedStat.value) / comparedStat.value
}
}
}
fun finalize(options: Calculator.Options) {
this.consolidateProgressStats()
}
// MPAndroidChart
fun defaultStatEntries(stat: Stat, context: Context): DataSet<out Entry> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HOURLY_DURATION -> this.barEntries(stat, context = context)
Stat.STANDARD_DEVIATION -> this.distributionEntries(stat, context)
else -> this.singleLineEntries(stat, context)
}
}
fun singleLineEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEachIndexed { index, p ->
entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
}
}
return PALineDataSet(entries, this.group.name, context)
}
fun durationEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return PALineDataSet(entries, stat.name, context)
}
fun barEntries(stat: Stat, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
val dataSet = BarDataSet(entries, stat.name)
dataSet.color = context.getColor(R.color.green)
// dataSet.barBorderWidth = 1.0f
dataSet.setDrawValues(false)
return dataSet
}
fun distributionEntries(stat: Stat, context: Context): BarDataSet {
val colors = mutableListOf<Int>()
val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points ->
val negative = mutableListOf<Point>()
val positive = mutableListOf<Point>()
points.sortByDescending { it.y }
points.forEach {
if (it.y < 0) {
negative.add(it)
} else {
positive.add(it)
}
}
negative.forEachIndexed { index, p ->
entries.add(BarEntry(index.toFloat(), abs(p.y.toFloat()), p.data))
colors.add(context.getColor(R.color.red))
}
positive.forEachIndexed { index, p ->
val x = negative.size + index.toFloat()
entries.add(BarEntry(x, p.y.toFloat(), p.data))
colors.add(context.getColor(R.color.green))
}
}
val dataSet = BarDataSet(entries, stat.name)
dataSet.colors = colors
dataSet.setDrawValues(false)
return dataSet
}
val isEmpty: Boolean
get() {
return this.group.isEmpty
}
// Stat Entry
override val entryTitle: String = this.group.name
override fun formattedValue(stat: Stat): TextFormat {
this.computedStat(stat)?.let {
return it.format()
} ?: run {
throw IllegalStateException("Missing stat in results")
}
}
override fun legendValues(
stat: Stat,
entry: Entry,
style: GraphFragment.Style,
groupName: String,
context: Context
): LegendContent {
when (style) {
GraphFragment.Style.BAR -> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
DefaultLegendValues(this.entryTitle, totalStatValue)
}
else -> {
val entryValue = this.formattedValue(stat)
val countValue = this.computedStat(Stat.NUMBER_OF_GAMES)?.format()
DefaultLegendValues(this.entryTitle, entryValue, countValue)
}
}
}
else -> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
DefaultLegendValues(this.entryTitle, totalStatValue)
}
else -> {
val entryValue = this.formattedValue(stat)
val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
DefaultLegendValues(this.entryTitle, entryValue, totalStatValue)
}
}
}
}
}
}
class Point(val x: Double, val y: Double, val data: Any) {
constructor(y: Double, data: Any) : this(0.0, y, data)
}

@ -1,40 +1,66 @@
package net.pokeranalytics.android.calculus
import android.content.Context
import io.realm.RealmModel
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.FormattingException
import net.pokeranalytics.android.model.interfaces.Timed
import net.pokeranalytics.android.ui.graph.AxisFormatting
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.CurrencyUtils
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) {
}
interface StatBase : RealmModel {
class ObjectIdentifier(var id: String, var clazz: Class<out Timed>) {
fun formattedValue(stat: Stat, context: Context): TextFormat
}
enum class AggregationType {
SESSION,
MONTH,
YEAR,
DURATION;
val resId: Int
get() {
return when (this) {
SESSION -> R.string.session
MONTH -> R.string.month
YEAR -> R.string.year
DURATION -> R.string.duration
}
}
val axisFormatting: AxisFormatting
get() {
return when (this) {
DURATION -> AxisFormatting.X_DURATION
else -> AxisFormatting.DEFAULT
}
}
}
/**
* An enum representing all the types of Session statistics
*/
enum class Stat(var underlyingClass: Class<out Timed>? = null) : RowRepresentable {
enum class Stat : RowRepresentable {
NETRESULT,
NET_RESULT,
BB_NET_RESULT,
HOURLY_RATE,
AVERAGE,
NUMBER_OF_SETS,
NUMBER_OF_GAMES,
DURATION,
AVERAGE_DURATION,
HOURLY_DURATION,
AVERAGE_HOURLY_DURATION,
NET_BB_PER_100_HANDS,
HOURLY_RATE_BB,
AVERAGE_NET_BB,
@ -44,17 +70,17 @@ enum class Stat(var underlyingClass: Class<out Timed>? = null) : RowRepresentabl
STANDARD_DEVIATION,
STANDARD_DEVIATION_HOURLY,
STANDARD_DEVIATION_BB_PER_100_HANDS,
HANDS_PLAYED;
/**
* Returns whether the stat evolution numericValues requires a distribution sorting
*/
fun hasDistributionSorting(): Boolean {
return when (this) {
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> true
else -> false
}
}
HANDS_PLAYED,
LOCATIONS_PLAYED,
LONGEST_STREAKS,
MAXIMUM_NETRESULT,
MINIMUM_NETRESULT,
MAXIMUM_DURATION,
DAYS_PLAYED,
WINNING_SESSION_COUNT,
BB_SESSION_COUNT,
TOTAL_BUYIN,
;
companion object {
@ -72,18 +98,32 @@ enum class Stat(var underlyingClass: Class<out Timed>? = null) : RowRepresentabl
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) {
NETRESULT -> R.string.net_result
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
DURATION -> R.string.duration
AVERAGE_DURATION -> R.string.average_hours_played
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_
@ -94,6 +134,13 @@ enum class Stat(var underlyingClass: Class<out Timed>? = null) : RowRepresentabl
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")
}
}
@ -101,7 +148,7 @@ enum class Stat(var underlyingClass: Class<out Timed>? = null) : RowRepresentabl
/**
* 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): TextFormat {
if (value.isNaN()) {
return TextFormat(NULL_TEXT, R.color.white)
@ -109,31 +156,32 @@ enum class Stat(var underlyingClass: Class<out Timed>? = null) : RowRepresentabl
when (this) {
// Amounts + red/green
Stat.NETRESULT, Stat.HOURLY_RATE, Stat.AVERAGE -> {
val numberFormat = CurrencyUtils.getCurrencyFormatter(context, currency)
NET_RESULT, HOURLY_RATE, AVERAGE, MAXIMUM_NETRESULT, MINIMUM_NETRESULT -> {
val color = if (value >= this.threshold) R.color.green else R.color.red
return TextFormat(numberFormat.format(value), color)
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 -> {
HOURLY_DURATION, AVERAGE_HOURLY_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 -> {
val numberFormat = CurrencyUtils.getCurrencyFormatter(context, currency)
return TextFormat(numberFormat.format(value))
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")
}
@ -148,14 +196,17 @@ enum class Stat(var underlyingClass: Class<out Timed>? = null) : RowRepresentabl
}
fun cumulativeLabelResId(context: Context) : String {
fun cumulativeLabelResId(context: Context): String {
val resId = when (this) {
AVERAGE, AVERAGE_DURATION, NET_BB_PER_100_HANDS,
HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, WIN_RATIO, HOURLY_RATE -> R.string.average
NETRESULT, DURATION -> R.string.total
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 {
@ -165,13 +216,67 @@ enum class Stat(var underlyingClass: Class<out Timed>? = null) : RowRepresentabl
}
}
val aggregationTypes: List<AggregationType>
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 -> 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 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) {
@ -180,22 +285,20 @@ class ComputedStat(var stat: Stat, var value: Double, var currency: Currency? =
}
/**
* The variation of the stat
* The value used to get evolution dataset
*/
var variation: Double? = null
var progressValue: Double? = null
/**
* Formats the value of the stat to be suitable for display
* The variation of the stat
*/
fun format(context: Context): TextFormat {
return this.stat.format(this.value, this.currency, context)
}
var variation: Double? = null
/**
* Returns a TextFormat instance for an evolution value located at the specified [index]
* Formats the value of the stat to be suitable for display
*/
fun evolutionValueFormat(index: Int): TextFormat {
return TextFormat("undef ${index}")
fun format(): TextFormat {
return this.stat.format(this.value, this.secondValue, this.currency)
}
}

@ -1,8 +1,11 @@
package net.pokeranalytics.android.calculus.bankroll
import io.realm.Realm
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputableGroup
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.realm.*
class BankrollCalculator {
@ -13,30 +16,80 @@ class BankrollCalculator {
val realm = Realm.getDefaultInstance()
val report = BankrollReport(setup)
val bankrolls: List<Bankroll> = if (setup.bankroll != null) listOf(setup.bankroll) else realm.where(Bankroll::class.java).findAll()
val sessionQuery = realm.where(Session::class.java)
if (setup.bankroll != null) {
sessionQuery.equalTo("bankroll.id", setup.bankroll.id)
}
// val sessions = sessionQuery.findAll()
val transactionQuery = realm.where(Transaction::class.java)
if (setup.bankroll != null) {
transactionQuery.equalTo("bankroll.id", setup.bankroll.id).findAll()
var initialValue = 0.0
var transactionNet = 0.0
bankrolls.forEach { bankroll ->
val rate = if (setup.virtualBankroll) bankroll.rate else 1.0
initialValue += bankroll.initialValue * rate
transactionNet += bankroll.transactions.sumByDouble { it.amount } * rate
}
val transactions = transactionQuery.findAll()
// val sessionsNet = sessions.sum("result.net")
val transactionsNet = transactions.sum("value")
report.transactionsNet = transactionNet
report.initial = initialValue
val queryConditions = setup.queryConditions
val transactions = Filter.queryOn<Transaction>(realm, queryConditions)
report.addDatedItems(transactions)
transactions.forEach {
report.addTransaction(it)
}
val sessions = Filter.queryOn<Session>(realm, queryConditions)
report.addDatedItems(sessions)
if (setup.virtualBankroll) {
val options = Calculator.Options(stats = listOf(Stat.NET_RESULT, Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY))
val group = ComputableGroup(conditions = queryConditions)
val result = Calculator.compute(realm, group, options)
result.computedStat(Stat.NET_RESULT)?.let {
report.netResult = it.value
}
this.computeRiskOfRuin(report, result)
} else {
val results = Filter.queryOn<Result>(realm, queryConditions)
report.netResult = results.sum("net").toDouble()
}
val depositType = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm)
report.transactionBuckets[depositType.id]?.let { bucket ->
report.depositTotal = bucket.transactions.sumByDouble { it.amount }
}
val withdrawalType = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm)
report.transactionBuckets[withdrawalType.id]?.let { bucket ->
report.withdrawalTotal = bucket.transactions.sumByDouble { it.amount }
}
report.generateGraphPointsIfNecessary()
realm.close()
return report
}
private fun computeRiskOfRuin(report: BankrollReport, results: ComputedResults) {
val hourlyRate = results.computedStat(Stat.HOURLY_RATE)?.value
val hourlyStandardDeviation = results.computedStat(Stat.STANDARD_DEVIATION_HOURLY)?.value
if (hourlyRate != null && hourlyStandardDeviation != null) {
report.riskOfRuin = Stat.riskOfRuin(hourlyRate, hourlyStandardDeviation, report.total)
}
}
}
}

@ -1,82 +1,74 @@
package net.pokeranalytics.android.calculus.bankroll
import net.pokeranalytics.android.calculus.interfaces.DatableValue
import android.content.Context
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.DatedValue
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.ui.graph.PALineDataSet
import java.util.*
import kotlin.collections.HashMap
/**
* A class describing the parameters required to launch a bankroll report
*
*/
class BankrollReportSetup(bankroll: Bankroll?, from: Date? = null, to: Date? = null) {
/**
* The bankroll to compute. If null, the virtual global bankroll
*/
val bankroll = bankroll
class BankrollReport(setup: BankrollReportSetup) {
/**
* The start of the report
* The setup used to compute the report
*/
val from = from
var setup: BankrollReportSetup = setup
/**
* The end of the report
* The value of the bankroll
*/
val to = to
}
class TransactionBucket(useRate: Boolean = false) {
var transactions: MutableList<Transaction> = mutableListOf()
private set
var total: Double = 0.0
private set
var useRate: Boolean = useRate
private set
fun addTransaction(transaction: Transaction) {
/**
* The initial value of the bankroll, or of all bankrolls if virtual is computed
*/
var initial: Double = 0.0
this.transactions.add(transaction)
var rate = 1.0
if (this.useRate) {
rate = transaction.bankroll?.currency?.rate ?: 1.0
/**
* The net result from poker computables
*/
var netResult: Double = 0.0
set(value) {
field = value
this.computeBankrollTotal()
}
val ratedAmount = rate * transaction.amount
this.total += ratedAmount
}
}
class BRGraphPoint {
var value: Double = 0.0
var variation: Double = 0.0
var date: Date? = null
}
class BankrollReport(setup: BankrollReportSetup) {
/**
* The setup used to compute the report
* The net result from transactions
*/
var setup: BankrollReportSetup = setup
var transactionsNet: Double = 0.0
set(value) {
field = value
this.computeBankrollTotal()
}
fun computeBankrollTotal() {
this.total = this.initial + this.netResult + this.transactionsNet
}
/**
* The value of the bankroll
* The sum of all deposits
*/
var total: Double = 0.0
private set
var depositTotal: Double = 0.0
set(value) {
field = value
this.netBanked = this.depositTotal + this.withdrawalTotal
}
/**
* The net result from poker computables
* The sum of all withdrawals
*/
var netResult: Double = 0.0
private set
var withdrawalTotal: Double = 0.0
set(value) {
field = value
this.netBanked = this.depositTotal + this.withdrawalTotal
}
/**
* The difference between withdrawals and deposits
@ -87,16 +79,21 @@ class BankrollReport(setup: BankrollReportSetup) {
/**
* The risk of ruin
*/
var riskOfRuin: Double = 0.0
private set
var riskOfRuin: Double? = null
var transactions: List<Transaction> = mutableListOf()
private set
var transactionBuckets: HashMap<String, TransactionBucket> = HashMap()
private set
var evolutionPoints: Array<BRGraphPoint> = arrayOf()
var evolutionItems: Array<DatableValue> = arrayOf()
var evolutionPoints: MutableList<BRGraphPoint> = mutableListOf()
var evolutionItems: MutableList<DatedValue> = mutableListOf()
private set
fun addDatedItems(items: Collection<DatedValue>) {
this.evolutionItems.addAll(items)
}
fun addTransaction(transaction: Transaction) {
@ -105,7 +102,7 @@ class BankrollReport(setup: BankrollReportSetup) {
var bucket = this.transactionBuckets[type.id]
if (bucket == null) {
val b = TransactionBucket(this.setup.bankroll == null)
val b = TransactionBucket(this.setup.virtualBankroll)
this.transactionBuckets[type.id] = b
bucket = b
}
@ -118,5 +115,93 @@ class BankrollReport(setup: BankrollReportSetup) {
}
fun generateGraphPointsIfNecessary() {
if (!this.setup.virtualBankroll) {
return
}
this.evolutionItems.sortBy { it.date }
this.evolutionItems.forEach {
val point = BRGraphPoint(it.amount, it.date, it)
this.evolutionPoints.add(point)
}
}
fun lineDataSet(context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this.evolutionPoints.forEach {
val entry = Entry(it.date.time.toFloat(), it.value.toFloat(), it.data)
entries.add(entry)
}
return PALineDataSet(entries, "", context)
}
}
/**
* A class describing the parameters required to launch a bankroll report
*
*/
class BankrollReportSetup(val bankroll: Bankroll? = null, val from: Date? = null, val to: Date? = null) {
val virtualBankroll: Boolean
get() {
return this.bankroll == null
}
val queryConditions: List<QueryCondition>
get() {
val conditions = mutableListOf<QueryCondition>()
this.bankroll?.let {
val bankrollCondition = QueryCondition.AnyBankroll(bankroll)
conditions.add(bankrollCondition)
}
this.from?.let {
val fromCondition = QueryCondition.StartedFromDate()
fromCondition.singleValue = it
conditions.add(fromCondition)
}
this.to?.let {
val toCondition = QueryCondition.StartedToDate()
toCondition.singleValue = it
conditions.add(toCondition)
}
return conditions
}
}
class TransactionBucket(useRate: Boolean = false) {
var transactions: MutableList<Transaction> = mutableListOf()
private set
var total: Double = 0.0
private set
var useRate: Boolean = useRate
private set
fun addTransaction(transaction: Transaction) {
this.transactions.add(transaction)
var rate = 1.0
if (this.useRate) {
rate = transaction.bankroll?.currency?.rate ?: 1.0
}
val ratedAmount = rate * transaction.amount
this.total += ratedAmount
}
}
data class BRGraphPoint(var value: Double, var date: Date, var data: Any? = null) {
var variation: Double = 0.0
}

@ -1,17 +0,0 @@
package net.pokeranalytics.android.calculus.interfaces
import net.pokeranalytics.android.model.realm.SessionSet
interface Computable {
var ratedNet: Double
var bbNet: Double
var hasBigBlind: Int
var isPositive: Int
var ratedBuyin: Double
var estimatedHands: Double
var bbPer100Hands: Double
var sessionSet: SessionSet?
}

@ -1,11 +0,0 @@
package net.pokeranalytics.android.calculus.interfaces
import java.util.*
interface Datable {
var date: Date
}
interface DatableValue : Datable {
var value: Double
}

@ -11,13 +11,14 @@ class ConfigurationException(message: String) : Exception(message)
sealed class PokerAnalyticsException(message: String) : Exception(message) {
object FilterElementUnknownName : PokerAnalyticsException(message = "No filterElement name was found to identify the queryCondition")
object FilterElementUnknownSectionName: PokerAnalyticsException(message = "No filterElement section name was found to identify the queryCondition")
object FilterMissingEntity: PokerAnalyticsException(message = "This filter has no entity initialized")
object FilterMissingEntity: PokerAnalyticsException(message = "This queryWith has no entity initialized")
object FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable")
object QueryValueMapUnknown: PokerAnalyticsException(message = "fieldName is missing")
object QueryTypeUnhandled: PokerAnalyticsException(message = "filter type not handled")
object QueryTypeUnhandled: PokerAnalyticsException(message = "queryWith type not handled")
object QueryValueMapUnexpectedValue: PokerAnalyticsException(message = "valueMap null not expected")
object FilterElementExpectedValueMissing : PokerAnalyticsException(message = "filter is empty or null")
data class FilterElementTypeMissing(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "filter element '$filterElementRow' type is missing")
object FilterElementExpectedValueMissing : PokerAnalyticsException(message = "queryWith is empty or null")
data class FilterElementTypeMissing(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "queryWith element '$filterElementRow' type is missing")
data class QueryValueMapMissingKeys(val missingKeys: List<String>) : PokerAnalyticsException(message = "valueMap does not contain $missingKeys")
data class UnknownQueryTypeForRow(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "no filter type for $filterElementRow")
data class UnknownQueryTypeForRow(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "no queryWith type for $filterElementRow")
data class MissingFieldNameForQueryCondition(val name: String) : PokerAnalyticsException(message = "Missing fieldname for QueryCondition ${name}")
}

@ -0,0 +1,191 @@
package net.pokeranalytics.android.model
import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.where
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.realm.*
import java.util.*
import kotlin.collections.ArrayList
fun List<Criteria>.combined(): List<List<QueryCondition>> {
val comparatorList = ArrayList<List<QueryCondition>>()
this.forEach {
comparatorList.add(it.queryConditions)
}
return getCombinations(comparatorList)
}
fun List<List<QueryCondition>>.upToNow(): List<List<QueryCondition>> {
val calendar = Calendar.getInstance()
calendar.time = Date()
val currentYear = calendar.get(Calendar.YEAR)
val currentMonth = calendar.get(Calendar.MONTH)
val toRemove = this.filter { list ->
list.any { it is QueryCondition.AnyYear && it.listOfValues.first() == currentYear }
}.filter { list ->
list.any {
it is QueryCondition.AnyMonthOfYear && it.listOfValues.first() > currentMonth
}
}
return this.filter{ list ->
var keep = true
toRemove.forEach {
if (list.containsAll(it)) {
keep = false
}
}
keep
}
}
fun <T> getCombinations(lists: List<List<T>>): List<List<T>> {
var combinations: LinkedHashSet<List<T>> = LinkedHashSet()
var newCombinations: LinkedHashSet<List<T>>
var index = 0
// extract each of the integers in the first list
// and add each to ints as a new list
if (lists.isNotEmpty()) {
for (i in lists[0]) {
val newList = ArrayList<T>()
newList.add(i)
combinations.add(newList)
}
index++
}
while (index < lists.size) {
val nextList = lists[index]
newCombinations = LinkedHashSet()
for (first in combinations) {
for (second in nextList) {
val newList = ArrayList<T>()
newList.addAll(first)
newList.add(second)
newCombinations.add(newList)
}
}
combinations = newCombinations
index++
}
return combinations.toList()
}
sealed class Criteria {
abstract class RealmCriteria : Criteria() {
inline fun <reified T: NameManageable> comparison(): List<QueryCondition> {
return compare<QueryCondition.QueryDataCondition<NameManageable>, T>()
.sorted()
}
}
abstract class SimpleCriteria(private val conditions:List<QueryCondition>): Criteria() {
fun comparison(): List<QueryCondition> {
return conditions
}
}
abstract class ListCriteria : Criteria() {
inline fun <reified T:QueryCondition.ListOfValues<S>, reified S:Comparable<S>> comparison(): List<QueryCondition> {
QueryCondition.distinct<Session, T, S>()?.let {
val values = it.mapNotNull { session ->
when (this) {
is Limits -> if (session.limit is S) { session.limit as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
is TournamentTypes -> if (session.tournamentType is S) { session.tournamentType as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
is TableSizes -> if (session.tableSize is S) { session.tableSize as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
is TournamentFees -> if (session.tournamentEntryFee is S) { session.tournamentEntryFee as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
is Blinds -> if (session.blinds is S) { session.blinds as S } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
else -> null
}
}.distinct()
return compareList<T, S>(values = values).sorted()
}
return listOf<T>()
}
}
object Bankrolls: RealmCriteria()
object Games: RealmCriteria()
object TournamentNames: RealmCriteria()
object Locations: RealmCriteria()
object TournamentFeatures: RealmCriteria()
object Limits: ListCriteria()
object TableSizes: ListCriteria()
object TournamentTypes: ListCriteria()
object MonthsOfYear: SimpleCriteria(List(12) { index -> QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(index)} })
object DaysOfWeek: SimpleCriteria(List(7) { index -> QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(index + 1) } })
object SessionTypes: SimpleCriteria(listOf(QueryCondition.IsCash, QueryCondition.IsTournament))
object BankrollTypes: SimpleCriteria(listOf(QueryCondition.IsLive, QueryCondition.IsOnline))
object DayPeriods: SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd))
object Years: ListCriteria()
object Blinds: ListCriteria()
object TournamentFees: ListCriteria()
object Cash: SimpleCriteria(listOf(QueryCondition.IsCash))
object Tournament: SimpleCriteria(listOf(QueryCondition.IsTournament))
val queryConditions: List<QueryCondition>
get() {
return when (this) {
is Bankrolls -> comparison<Bankroll>()
is Games -> comparison<Game>()
is TournamentFeatures -> comparison<TournamentFeature>()
is TournamentNames -> comparison<TournamentName>()
is Locations -> comparison<Location>()
is SimpleCriteria -> comparison()
is Limits -> comparison<QueryCondition.AnyLimit, Int>()
is TournamentTypes -> comparison<QueryCondition.AnyTournamentType, Int>()
is TableSizes -> comparison<QueryCondition.AnyTableSize, Int>()
is TournamentFees -> comparison<QueryCondition.TournamentFee, Double >()
is Years -> {
val years = arrayListOf<QueryCondition.AnyYear>()
val calendar = Calendar.getInstance()
calendar.time = Date()
val yearNow = calendar.get(Calendar.YEAR)
val realm = Realm.getDefaultInstance()
realm.where<Session>().sort("year", Sort.ASCENDING).findFirst()?.year?.let {
for (index in 0..(yearNow - it)) {
years.add(QueryCondition.AnyYear().apply {
listOfValues = arrayListOf(yearNow - index)
})
}
}
realm.close()
years.sorted()
}
is Blinds -> comparison<QueryCondition.AnyBlind, String >()
else -> throw PokerAnalyticsException.QueryTypeUnhandled
}
}
companion object {
inline fun < reified S : QueryCondition.QueryDataCondition<NameManageable >, reified T : NameManageable > compare(): List<S> {
val objects = arrayListOf<S>()
val realm = Realm.getDefaultInstance()
realm.where<T>().findAll().forEach {
objects.add((QueryCondition.getInstance<T>() as S).apply {
setObject(it)
})
}
realm.close()
return objects
}
inline fun < reified S : QueryCondition.ListOfValues<T>, T:Any > compareList(values:List<T>): List<S> {
val objects = arrayListOf<S>()
values.forEach {
objects.add((S::class.java.newInstance()).apply {
listOfValues = arrayListOf(it)
})
}
return objects
}
}
}

@ -1,21 +0,0 @@
package net.pokeranalytics.android.model
import net.pokeranalytics.android.calculus.ComputedStat
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
class StatRepresentable(stat: Stat, computedStat: ComputedStat?, groupName: String = "") : RowRepresentable {
var stat: Stat = stat
var computedStat: ComputedStat? = computedStat
var groupName: String = groupName
override val viewType: Int
get() = RowViewType.STAT.ordinal
override val resId: Int?
get() = this.stat.resId
}

@ -14,6 +14,14 @@ class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITL
}
}
override fun getDisplayName(): String {
return if (this.numberOfPlayer == 2) {
return "HU"
} else {
"${this.numberOfPlayer}-max"
}
}
override val resId: Int?
get() {
return if (this.numberOfPlayer == 2) {

@ -23,5 +23,12 @@ enum class TournamentType : RowRepresentable {
}
}
override fun getDisplayName(): String {
return when (this) {
MTT -> "MTT"
SNG -> "SNG"
}
}
override val viewType: Int = RowViewType.TITLE.ordinal
}

@ -25,9 +25,6 @@ enum class SessionState {
*/
fun Session.getState(): SessionState {
// if (timeFrame == null) {
// return SessionState.PENDING
// }
val start = this.startDate
if (start == null) {
return SessionState.PENDING
@ -44,3 +41,58 @@ fun Session.getState(): SessionState {
}
}
val AbstractList<Session>.hourlyDuration: Double
get() {
val intervals = mutableListOf<TimeInterval>()
this.forEach {
val interval = TimeInterval(it.startDate!!, it.endDate!!, it.breakDuration)
intervals.update(interval)
}
return intervals.sumByDouble { it.hourlyDuration }
}
class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) {
val hourlyDuration: Double
get() {
val netDuration = end.time - start.time - breakDuration
return (netDuration / 3600000).toDouble()
}
}
fun MutableList<TimeInterval>.update(timeInterval: TimeInterval): MutableList<TimeInterval> {
val overlapped = this.filter {
(it.start.before(timeInterval.start) && it.end.after(timeInterval.start)) ||
(it.start.before(timeInterval.end) && it.end.after(timeInterval.end)) ||
(it.start.after(timeInterval.start) && it.end.before(timeInterval.end))
}
if (overlapped.size == 0) { // add
this.add(timeInterval)
} else { // update
var start = timeInterval.start
var end = timeInterval.end
var breakDuration = timeInterval.breakDuration
overlapped.forEach {
if (it.start.before(start)) {
start = it.start
}
if (it.end.after(end)) {
end = it.end
}
breakDuration = kotlin.math.max(it.breakDuration, breakDuration)
}
this.removeAll(overlapped)
this.add(TimeInterval(start, end, breakDuration))
}
return this
}

@ -1,9 +1,13 @@
package net.pokeranalytics.android.model.filter
import io.realm.RealmModel
import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.Result
/**
* We want to be able to store filters in the database:
@ -11,7 +15,7 @@ import net.pokeranalytics.android.model.realm.SessionSet
* - filters can be applied to different type of objects: Sessions, Hands, Transactions...
* - filters can be applied to a list of different type of objects (feed)
*
* A filter is described by the following:
* A queryWith is described by the following:
* - a data type: Session, Hands...
* - a field: table size of a Session
* - an operator: equal, >=, <...
@ -27,7 +31,7 @@ import net.pokeranalytics.android.model.realm.SessionSet
* - multiple numericValues as 'OR'
*
* Also:
* A filter should be able to be converted into a Realm query
* A queryWith should be able to be converted into a Realm query
*
*/
@ -47,32 +51,46 @@ interface Filterable : RealmModel {
}
inline fun <reified T : Filterable> RealmResults<T>.filter(conditions: List<QueryCondition>) : RealmResults<T> {
return conditions.queryWith(this.where()).findAll()
}
class FilterHelper {
companion object {
inline fun <reified T : Filterable> fieldNameForQueryType(queryCondition: QueryCondition): String? {
inline fun <reified T : Filterable > fieldNameForQueryType(queryCondition: Class< out QueryCondition>): String? {
return when (T::class.java) {
val fieldName = when (T::class.java) {
Session::class.java -> Session.fieldNameForQueryType(queryCondition)
ComputableResult::class.java -> ComputableResult.fieldNameForQueryType(queryCondition)
SessionSet::class.java -> SessionSet.fieldNameForQueryType(queryCondition)
Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition)
Result::class.java -> Result.fieldNameForQueryType(queryCondition)
else -> {
throw UnmanagedFilterField("Filterable type fields are not defined for class ${T::class}")
}
}
fieldName?.let {
return fieldName
} ?: run {
throw PokerAnalyticsException.MissingFieldNameForQueryCondition(queryCondition.name)
}
}
}
}
//
//fun MutableList<Filterable>.filter(filter: FilterCondition) : List<Filterable> {
//fun MutableList<Filterable>.queryWith(queryWith: FilterCondition) : List<Filterable> {
//
// return this.filter { f ->
// return@filter true
// return this.queryWith { f ->
// return@queryWith true
// }
//}

@ -1,336 +1,605 @@
package net.pokeranalytics.android.model.filter
import io.realm.RealmList
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.realm.FilterCondition
import net.pokeranalytics.android.model.realm.FilterElementBlind
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.endOfDay
import net.pokeranalytics.android.util.extensions.startOfDay
import net.pokeranalytics.android.util.extensions.toCurrency
import java.text.DateFormatSymbols
import java.util.*
import kotlin.collections.ArrayList
fun List<QueryCondition>.name() : String {
return this.map { it.getDisplayName() }.joinToString(" : ")
}
//inline fun <reified T : Filterable> List<QueryCondition>.query(realm: Realm): RealmQuery<T> {
// return this.queryWith(realm.where())
//}
inline fun <reified T : Filterable> List<QueryCondition>.queryWith(query: RealmQuery<T>): RealmQuery<T> {
var realmQuery = query
this.forEach {
realmQuery = it.queryWith(realmQuery)
}
return realmQuery
}
/**
* Enum describing the way a query should be handled
* Some queries requires a value to be checked upon through equals, in, more, less, between
* To handle that, the enum has a public [valueMap] variable
* A new type should also set the expected numericValues required in the [filterValuesExpectedKeys]
*/
enum class QueryCondition(var operator: Operator? = null) {
LIVE,
CASH,
ONLINE,
TOURNAMENT,
BANKROLL,
GAME,
TOURNAMENT_NAME,
ANY_TOURNAMENT_FEATURES,
ALL_TOURNAMENT_FEATURES,
LOCATION,
LIMIT,
TABLE_SIZE,
TOURNAMENT_TYPE,
BLINDS,
LAST_GAMES,
LAST_SESSIONS,
MORE_NUMBER_OF_TABLE(Operator.MORE),
LESS_NUMBER_OF_TABLE(Operator.LESS),
BETWEEN_NUMBER_OF_TABLE(Operator.BETWEEN),
MORE_THAN_NET_RESULT(Operator.MORE),
LESS_THAN_NET_RESULT(Operator.LESS),
MORE_THAN_BUY_IN(Operator.MORE),
LESS_THAN_BUY_IN(Operator.LESS),
MORE_THAN_CASH_OUT(Operator.MORE),
LESS_THAN_CASH_OUT(Operator.LESS),
MORE_THAN_TIPS(Operator.MORE),
LESS_THAN_TIPS(Operator.LESS),
MORE_THAN_NUMBER_OF_PLAYER(Operator.MORE),
LESS_THAN_NUMBER_OF_PLAYER(Operator.LESS),
BETWEEN_NUMBER_OF_PLAYER(Operator.BETWEEN),
MORE_THAN_TOURNAMENT_FEE(Operator.MORE),
LESS_THAN_TOURNAMENT_FEE(Operator.LESS),
BETWEEN_TOURNAMENT_FEE(Operator.BETWEEN),
MIN_RE_BUY(Operator.MORE),
MAX_RE_BUY(Operator.LESS),
// Dates
STARTED_FROM_DATE,
STARTED_TO_DATE,
ENDED_FROM_DATE,
ENDED_TO_DATE,
DAY_OF_WEEK,
MONTH,
YEAR,
WEEK_DAY,
WEEK_END,
TODAY,
YESTERDAY,
TODAY_AND_YESTERDAY,
THIS_WEEK,
THIS_MONTH,
THIS_YEAR,
PAST_DAYS,
MORE_THAN_DURATION(Operator.MORE),
LESS_THAN_DURATION(Operator.LESS),
CURRENCY,
CURRENCY_CODE,
BIG_BLIND,
SMALL_BLIND,
COMMENT,
;
sealed class QueryCondition : FilterElementRow {
companion object {
inline fun < reified T:QueryCondition> more():T { return T::class.java.newInstance().apply { this.operator = Operator.MORE } }
inline fun < reified T:QueryCondition> less():T { return T::class.java.newInstance().apply { this.operator = Operator.LESS } }
inline fun < reified T:QueryCondition> moreOrLess():ArrayList<T> { return arrayListOf(more(), less()) }
fun <T:QueryCondition> valueOf(name:String) : T {
val kClass = Class.forName("${QueryCondition::class.qualifiedName}$$name").kotlin
val instance = kClass.objectInstance ?: kClass.java.newInstance()
return instance as T
}
inline fun <reified T:Identifiable>getInstance(): QueryCondition {
return when (T::class.java) {
Bankroll::class.java -> AnyBankroll()
Game::class.java -> AnyGame()
Location::class.java -> AnyLocation()
TournamentName::class.java -> AnyTournamentName()
TournamentFeature::class.java -> AnyTournamentFeature()
else -> throw PokerAnalyticsException.QueryTypeUnhandled
}
}
inline fun < reified T: Filterable, reified S: QueryCondition, reified U:Comparable<U>>distinct(): RealmResults<T>? {
FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let {
val realm = Realm.getDefaultInstance()
val distincts = when (T::class) {
String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING)
else -> realm.where<T>().isNotNull(it).findAll().sort(it, Sort.ASCENDING)
}
realm.close()
return distincts
}
return null
}
}
enum class Operator {
BETWEEN,
ANY,
ALL,
MORE,
LESS;
LESS,
EQUALS,
BETWEEN,
BETWEEN_RIGHT_EXCLUSIVE,
BETWEEN_LEFT_EXCLUSIVE,
;
}
var valueMap : Map<String, Any?>? = null
get() {
this.filterValuesExpectedKeys?.let { valueMapExceptedKeys ->
field?.let { map ->
val missingKeys = map.keys.filter { !valueMapExceptedKeys.contains(it) }
if (map.keys.size == valueMapExceptedKeys.size && missingKeys.isNotEmpty()) {
throw PokerAnalyticsException.QueryValueMapMissingKeys(missingKeys)
}
} ?: run {
throw PokerAnalyticsException.QueryValueMapUnexpectedValue
}
}
return field
val baseId = this::class.simpleName ?: throw PokerAnalyticsException.FilterElementUnknownName
val id: List<String> get() {
when (this.operator) {
Operator.MORE, Operator.LESS -> return listOf("$baseId+${this.operator.name}")
}
private set
private val filterValuesExpectedKeys : Array<String>?
get() {
this.operator?.let {
return when (it) {
Operator.BETWEEN -> arrayOf("leftValue", "rightValue")
else -> arrayOf("value")
}
return when (this) {
is SingleValue<*> -> listOf(baseId)
is ListOfValues<*> -> {
if (listOfValues.isEmpty()) { return listOf(baseId) }
this.listOfValues.map{ "$baseId+$it" }
}
return when (this) {
BANKROLL, GAME, LOCATION, ANY_TOURNAMENT_FEATURES, ALL_TOURNAMENT_FEATURES, TOURNAMENT_NAME -> arrayOf("ids")
LIMIT, TOURNAMENT_TYPE, TABLE_SIZE -> arrayOf("values")
BLINDS -> arrayOf("blinds")
STARTED_FROM_DATE, STARTED_TO_DATE, ENDED_FROM_DATE, ENDED_TO_DATE -> arrayOf("date")
DAY_OF_WEEK -> arrayOf("dayOfWeek")
MONTH -> arrayOf("month")
YEAR -> arrayOf("year")
else -> null
else -> listOf(baseId)
}
}
open var operator: Operator = Operator.ANY
abstract class ListOfValues<T>: QueryCondition(), Comparable<ListOfValues<T>> where T:Comparable<T> {
abstract var listOfValues: ArrayList<T>
abstract fun labelForValue(value:T): String
override fun getDisplayName(): String {
return when (listOfValues.size) {
0 -> return NULL_TEXT
1,2 -> listOfValues.map { labelForValue(it) }.joinToString(", ")
else -> "${listOfValues.size} $baseId"
}
}
/**
* main method of the enum
* providing a base RealmQuery [realmQuery], the method is able to attached the corresponding query and returns the newly formed [RealmQuery]
*/
inline fun <reified T : Filterable> filter(realmQuery: RealmQuery<T>): RealmQuery<T> {
when {
this == BLINDS -> {
val smallBlindFieldName = FilterHelper.fieldNameForQueryType<T>(SMALL_BLIND)
val bigBlindFieldName = FilterHelper.fieldNameForQueryType<T>(BIG_BLIND)
val currencyCodeFieldName = FilterHelper.fieldNameForQueryType<T>(CURRENCY_CODE)
smallBlindFieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown
bigBlindFieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown
currencyCodeFieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown
val blinds: RealmList<FilterElementBlind> by valueMap
blinds.forEachIndexed { index, blind ->
realmQuery
.beginGroup()
blind.sb?.let {
realmQuery
.equalTo(smallBlindFieldName, it)
.and()
}
override fun compareTo(other: ListOfValues<T>): Int {
return listOfValues.sorted().first().compareTo(other.listOfValues.sorted().first())
}
}
realmQuery
.equalTo(bigBlindFieldName, blind.bb)
.and()
abstract class SingleValue<T>: ListOfValues<T>() where T:Comparable<T> {
override var listOfValues = ArrayList<T>()
abstract var singleValue : T
}
blind.currencyCode?.let {
realmQuery.equalTo(currencyCodeFieldName, it)
} ?: run {
realmQuery.isNull(currencyCodeFieldName)
}
abstract class ListOfDouble: ListOfValues<Double>() {
open var sign: Int = 1
realmQuery.endGroup()
override var listOfValues = arrayListOf(0.0)
override fun updateValueMap(filterCondition: FilterCondition) {
super.updateValueMap(filterCondition)
listOfValues = filterCondition.getValues()
}
override fun labelForValue(value: Double): String {
return value.toCurrency(UserDefaults.currency)
}
}
if (index < blinds.size - 1) {
realmQuery.or()
}
}
return realmQuery
}
else -> {
abstract class ListOfInt: ListOfValues<Int>() {
override var listOfValues = arrayListOf(0)
override fun updateValueMap(filterCondition: FilterCondition) {
super.updateValueMap(filterCondition)
println("<<<< updateValueMap ${filterCondition.intValues}")
listOfValues = filterCondition.getValues()
}
override fun labelForValue(value: Int): String {
return value.toString()
}
}
abstract class ListOfString: ListOfValues<String>() {
override var listOfValues = ArrayList<String>()
override fun labelForValue(value: String): String { return value }
override fun updateValueMap(filterCondition: FilterCondition) {
super.updateValueMap(filterCondition)
listOfValues = filterCondition.getValues()
}
}
val fieldName = FilterHelper.fieldNameForQueryType<T>(this)
fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown
abstract class SingleDate: SingleValue<Date>() {
override fun labelForValue(value: Date): String {
return value.toString()
}
when (operator) {
Operator.LESS -> {
val value: Double by valueMap
return realmQuery.lessThanOrEqualTo(fieldName, value)
}
Operator.MORE -> {
val value: Double by valueMap
return realmQuery.greaterThanOrEqualTo(fieldName, value)
}
Operator.BETWEEN -> {
val leftValue: Double by valueMap
val rightValue: Double by valueMap
return realmQuery.between(fieldName, leftValue, rightValue)
}
}
override var singleValue: Date
get() { return listOfValues.firstOrNull() ?: Date() }
set(value) { listOfValues.add(value) }
return when (this) {
LIVE, ONLINE -> realmQuery.equalTo(fieldName, this == LIVE)
CASH -> realmQuery.equalTo(fieldName, Session.Type.CASH_GAME.ordinal)
TOURNAMENT -> realmQuery.equalTo(fieldName, Session.Type.TOURNAMENT.ordinal)
ALL_TOURNAMENT_FEATURES -> {
val ids: Array<String> by valueMap
ids.forEach {
realmQuery.equalTo(fieldName, it)
}
realmQuery
}
ANY_TOURNAMENT_FEATURES -> {
val ids: Array<String> by valueMap
realmQuery.`in`(fieldName, ids)
}
BANKROLL, GAME, LOCATION, TOURNAMENT_NAME -> {
val ids: Array<String> by valueMap
realmQuery.`in`(fieldName, ids)
}
LIMIT, TOURNAMENT_TYPE, TABLE_SIZE -> {
val values: Array<Int?>? by valueMap
realmQuery.`in`(fieldName, values)
}
STARTED_FROM_DATE -> {
val date: Date by valueMap
realmQuery.greaterThanOrEqualTo(fieldName, date)
}
STARTED_TO_DATE -> {
val date: Date by valueMap
realmQuery.lessThanOrEqualTo(fieldName, date)
}
ENDED_FROM_DATE -> {
val date: Date by valueMap
realmQuery.greaterThanOrEqualTo(fieldName, date)
}
ENDED_TO_DATE -> {
val date: Date by valueMap
realmQuery.lessThanOrEqualTo(fieldName, date)
}
DAY_OF_WEEK -> {
val dayOfWeek: Int by valueMap
realmQuery.equalTo(fieldName, dayOfWeek)
}
MONTH -> {
val month: Int by valueMap
realmQuery.equalTo(fieldName, month)
}
YEAR -> {
val year: Int by valueMap
realmQuery.equalTo(fieldName, year)
}
WEEK_END, WEEK_DAY -> {
var query = realmQuery
if (this == WEEK_DAY) {
query = realmQuery.not()
}
query.`in`(fieldName, arrayOf(Calendar.SATURDAY, Calendar.SUNDAY))
}
TODAY -> {
val startDate = Date()
realmQuery.between(fieldName, startDate.startOfDay(), startDate.endOfDay())
}
TODAY_AND_YESTERDAY-> {
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.add(Calendar.HOUR_OF_DAY, -24)
realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
}
YESTERDAY -> {
val calendar = Calendar.getInstance()
calendar.time = Date()
calendar.add(Calendar.HOUR_OF_DAY, -24)
realmQuery.between(fieldName, calendar.time.startOfDay(), calendar.time.endOfDay())
}
THIS_WEEK -> {
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.set(Calendar.DAY_OF_WEEK_IN_MONTH, Calendar.SUNDAY)
realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
}
THIS_MONTH -> {
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.set(Calendar.DAY_OF_MONTH, 1)
realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
}
THIS_YEAR -> {
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.set(Calendar.DAY_OF_YEAR, 1)
realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
}
else -> {
throw PokerAnalyticsException.QueryTypeUnhandled
}
override fun updateValueMap(filterCondition: FilterCondition) {
super.updateValueMap(filterCondition)
singleValue = filterCondition.getValue()
}
}
abstract class SingleInt: SingleValue<Int>() {
override fun labelForValue(value: Int): String {
return value.toString()
}
override var singleValue: Int
get() { return listOfValues.firstOrNull() ?: 0 }
set(value) { listOfValues.add(value) }
override fun updateValueMap(filterCondition: FilterCondition) {
super.updateValueMap(filterCondition)
singleValue = filterCondition.getValue()
}
}
override fun getDisplayName(): String { return baseId }
override var filterSectionRow: FilterSectionRow = FilterSectionRow.CASH_TOURNAMENT
abstract class QueryDataCondition < T: NameManageable > : ListOfString() {
fun setObject(dataObject: T) {
this.listOfValues.removeAll(this.listOfValues)
this.listOfValues.add(dataObject.id)
}
abstract val entity : Class<T>
override fun getDisplayName(): String {
val realm = Realm.getDefaultInstance()
val completeLabel = when (listOfValues.size) {
0 -> return NULL_TEXT
1,2 -> {
return listOfValues.map { labelForValue(realm, it) }.joinToString(", ")
}
else -> "${listOfValues.size} $baseId"
}
realm.close()
return completeLabel
}
private fun labelForValue(realm:Realm, value:String): String {
val query = realm.where(entity)
return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT
}
}
interface DateTime {
val showTime: Boolean
}
abstract class DateQuery: SingleDate(), DateTime {
override val showTime: Boolean = false
}
abstract class TimeQuery: DateQuery() {
override val showTime: Boolean = true
}
object IsLive : QueryCondition() {
override fun getDisplayName(): String { return "Live" }
}
object IsCash : QueryCondition() {
override fun getDisplayName(): String { return "Cash" }
}
object IsOnline : QueryCondition() {
override fun getDisplayName(): String { return "Online" }
}
object IsTournament : QueryCondition() {
override fun getDisplayName(): String { return "Tournament" }
}
class AnyBankroll(): QueryDataCondition<Bankroll>() {
override var entity: Class<Bankroll> = Bankroll::class.java
constructor(bankroll: Bankroll): this() {
this.setObject(bankroll)
}
}
class AnyGame(): QueryDataCondition<Game>() {
override val entity: Class<Game> = Game::class.java
constructor(game: Game): this() {
this.setObject(game)
}
}
class AnyTournamentName(): QueryDataCondition<TournamentName>() {
override val entity: Class<TournamentName> = TournamentName::class.java
constructor(tournamentName: TournamentName): this() {
this.setObject(tournamentName)
}
}
class AnyTournamentFeature(): QueryDataCondition<TournamentFeature>() {
override val entity: Class<TournamentFeature> = TournamentFeature::class.java
constructor(tournamentFeature: TournamentFeature): this() {
this.setObject(tournamentFeature)
}
}
class AllTournamentFeature(): QueryDataCondition<TournamentFeature>() {
override var operator = Operator.ALL
override val entity: Class<TournamentFeature> = TournamentFeature::class.java
constructor(tournamentFeature: TournamentFeature): this() {
this.setObject(tournamentFeature)
}
}
class AnyLocation(): QueryDataCondition<Location>() {
override val entity: Class<Location> = Location::class.java
constructor(location: Location): this() {
this.setObject(location)
}
}
class AnyLimit: ListOfInt() {
override fun labelForValue(value: Int): String {
return Limit.values()[value].getDisplayName()
}
}
class AnyTableSize: ListOfInt() {
override fun labelForValue(value: Int): String {
return TableSize(value).getDisplayName()
}
}
class AnyTournamentType: ListOfInt() {
override fun labelForValue(value: Int): String {
return TournamentType.values()[value].getDisplayName()
}
}
class AnyBlind: ListOfString()
class LastGame: SingleInt()
class LastSession: SingleInt()
class NumberOfTable: ListOfInt()
open class NetAmountWon: ListOfDouble()
class NetAmountLost: NetAmountWon() { override var sign: Int = -1 }
class NumberOfPlayer: ListOfInt()
class StartedFromDate: DateQuery() { override var operator = Operator.MORE }
class StartedToDate: DateQuery() { override var operator = Operator.LESS }
class EndedFromDate: DateQuery() { override var operator = Operator.MORE }
class EndedToDate: DateQuery() { override var operator = Operator.LESS }
class AnyDayOfWeek: ListOfInt() {
override fun labelForValue(value: Int): String {
return DateFormatSymbols.getInstance(Locale.getDefault()).weekdays[value]
}
}
class AnyMonthOfYear: ListOfInt() {
override fun labelForValue(value: Int): String {
return DateFormatSymbols.getInstance(Locale.getDefault()).months[value]
}
}
class AnyYear: ListOfInt() {
override fun labelForValue(value: Int): String {
return "$value"
}
}
object IsWeekDay: QueryCondition()
object IsWeekEnd: QueryCondition()
object IsToday: QueryCondition()
object WasYesterday: QueryCondition()
object WasTodayAndYesterday: QueryCondition()
object DuringThisWeek: QueryCondition()
object DuringThisMonth: QueryCondition()
object DuringThisYear: QueryCondition()
class TournamentFee: ListOfDouble() {
override fun labelForValue(value: Double): String {
return value.toCurrency(UserDefaults.currency)
}
}
class PastDay: SingleInt() {
override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
}
class Duration: SingleInt() {
var minutes:Int
get() { return singleValue }
set(value) { singleValue = value }
override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
override val bottomSheetType: BottomSheetType = BottomSheetType.DOUBLE_EDIT_TEXT
}
fun updateValueMap(filterCondition: FilterCondition) {
if (filterValuesExpectedKeys == null) {
return
class StartedFromTime: TimeQuery() {
override var operator = Operator.MORE
init {
this.singleValue = Date().startOfDay()
}
}
this.operator?.let {
valueMap = mapOf("value" to filterCondition.value)
return
class EndedToTime: TimeQuery() {
override var operator = Operator.LESS
init {
this.singleValue = Date().endOfDay()
}
}
/**
* main method of the enum
* providing a base RealmQuery [realmQuery], the method is able to attached the corresponding query and returns the newly formed [RealmQuery]
*/
inline fun <reified T : Filterable> queryWith(realmQuery: RealmQuery<T>): RealmQuery<T> {
val fieldName = FilterHelper.fieldNameForQueryType<T>(this::class.java)
fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown
when (this) {
ALL_TOURNAMENT_FEATURES, ANY_TOURNAMENT_FEATURES, BANKROLL, GAME, LOCATION, TOURNAMENT_NAME -> {
valueMap = mapOf("ids" to filterCondition.ids)
//is Between -> realmQuery.between(fieldName, leftValue, rightValue)
//is BetweenLeftExclusive -> realmQuery.greaterThan(fieldName, leftValue).and().lessThanOrEqualTo(fieldName, rightValue)
//is BetweenRightExclusive -> realmQuery.greaterThanOrEqualTo(fieldName, leftValue).and().lessThan(fieldName, rightValue)
IsLive, IsOnline -> return realmQuery.equalTo(fieldName, this == IsLive)
IsCash -> return realmQuery.equalTo(fieldName, Session.Type.CASH_GAME.ordinal)
IsTournament -> return realmQuery.equalTo(fieldName, Session.Type.TOURNAMENT.ordinal)
IsWeekEnd, IsWeekDay -> {
var query = realmQuery
if (this == IsWeekDay) {
query = realmQuery.not()
}
return query.`in`(fieldName, arrayOf(Calendar.SATURDAY, Calendar.SUNDAY))
}
LIMIT, TOURNAMENT_TYPE, TABLE_SIZE -> {
valueMap = mapOf("values" to filterCondition.values)
IsToday -> {
val startDate = Date()
return realmQuery.between(fieldName, startDate.startOfDay(), startDate.endOfDay())
}
BLINDS -> {
valueMap = mapOf("blinds" to filterCondition.blinds)
WasTodayAndYesterday-> {
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.add(Calendar.HOUR_OF_DAY, -24)
return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
}
STARTED_FROM_DATE, STARTED_TO_DATE, ENDED_FROM_DATE, ENDED_TO_DATE -> {
valueMap = mapOf("date" to filterCondition.date)
WasYesterday -> {
val calendar = Calendar.getInstance()
calendar.time = Date()
calendar.add(Calendar.HOUR_OF_DAY, -24)
return realmQuery.between(fieldName, calendar.time.startOfDay(), calendar.time.endOfDay())
}
DAY_OF_WEEK -> {
valueMap = mapOf("dayOfWeek" to filterCondition.dayOfWeek)
DuringThisWeek -> {
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.set(Calendar.DAY_OF_WEEK_IN_MONTH, Calendar.SUNDAY)
return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
}
MONTH -> {
valueMap = mapOf("month" to filterCondition.month)
DuringThisMonth -> {
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.set(Calendar.DAY_OF_MONTH, 1)
return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
}
YEAR -> {
valueMap = mapOf("year" to filterCondition.year)
DuringThisYear -> {
val startDate = Date()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.set(Calendar.DAY_OF_YEAR, 1)
return realmQuery.between(fieldName, calendar.time.startOfDay(), startDate.endOfDay())
}
else -> {
throw PokerAnalyticsException.QueryValueMapUnexpectedValue
}
return when (operator) {
Operator.EQUALS -> {
when (this) {
is SingleDate -> realmQuery.equalTo(fieldName, singleValue)
is SingleInt -> realmQuery.equalTo(fieldName, singleValue)
is ListOfInt -> realmQuery.equalTo(fieldName, listOfValues.first())
is ListOfDouble -> realmQuery.equalTo(fieldName, listOfValues.first() * sign)
is ListOfString -> realmQuery.equalTo(fieldName, listOfValues.first())
else -> realmQuery
}
}
Operator.MORE -> {
when (this) {
is SingleDate -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue)
is SingleInt -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue)
is ListOfInt -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
is ListOfDouble -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * sign)
else -> realmQuery
}
}
Operator.LESS -> {
when (this) {
is SingleDate -> realmQuery.lessThanOrEqualTo(fieldName, singleValue)
is SingleInt -> realmQuery.lessThanOrEqualTo(fieldName, singleValue)
is ListOfInt -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
is ListOfDouble -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first() * sign)
else -> realmQuery
}
}
Operator.ALL -> {
when (this) {
is ListOfInt -> {
listOfValues.forEach { realmQuery.equalTo(fieldName, it) }
realmQuery
}
is ListOfDouble -> {
listOfValues.forEach { realmQuery.equalTo(fieldName, it * sign) }
realmQuery
}
is ListOfString -> {
listOfValues.forEach { realmQuery.equalTo(fieldName, it) }
realmQuery
}
else -> realmQuery
}
}
Operator.ANY -> {
when (this) {
is ListOfInt -> realmQuery.`in`(fieldName, listOfValues.toTypedArray())
is ListOfDouble -> realmQuery.`in`(fieldName, listOfValues.toTypedArray())
is ListOfString -> realmQuery.`in`(fieldName, listOfValues.toTypedArray())
else -> realmQuery
}
}
else -> realmQuery
}
}
open fun updateValueMap(filterCondition: FilterCondition) {
filterCondition.operator?.let {
this.operator = Operator.values()[it]
}
}
override val viewType: Int
get() {
return when (this) {
is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal
is LastGame -> RowViewType.TITLE_VALUE_CHECK.ordinal
is LastSession -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> {
when (this.operator) {
Operator.MORE -> RowViewType.TITLE_VALUE_CHECK.ordinal
Operator.LESS -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> RowViewType.TITLE_CHECK.ordinal
}
}
}
}
override val bottomSheetType: BottomSheetType
get() {
return when (this) {
is PastDay -> BottomSheetType.EDIT_TEXT
is LastGame -> BottomSheetType.EDIT_TEXT
is LastSession -> BottomSheetType.EDIT_TEXT
else -> {
when (this.operator) {
Operator.MORE -> BottomSheetType.EDIT_TEXT
Operator.LESS -> BottomSheetType.EDIT_TEXT
else -> BottomSheetType.NONE
}
}
}
}
override val resId: Int?
get() {
return when (this) {
is IsCash -> R.string.cash_game
is IsTournament -> R.string.tournament
is IsToday -> R.string.today
is WasYesterday -> R.string.yesterday
is WasTodayAndYesterday -> R.string.yesterday_and_today
is DuringThisWeek -> R.string.current_week
is DuringThisMonth -> R.string.current_month
is DuringThisYear -> R.string.current_year
is StartedFromTime, is StartedFromDate -> R.string.from
is EndedToDate, is EndedToTime-> R.string.to
is IsLive -> R.string.live
is IsOnline -> R.string.online
is IsWeekDay -> R.string.week_days
is IsWeekEnd -> R.string.weekend
is PastDay -> R.string.period_in_days
is LastGame -> R.string.last_records
is LastSession -> R.string.last_sessions
is NetAmountWon -> {
when (this.operator) {
Operator.MORE -> R.string.won_amount_more_than
Operator.LESS -> R.string.won_amount_less_than
else -> null
}
}
is NetAmountLost -> {
when (this.operator) {
Operator.MORE -> R.string.lost_amount_more_than
Operator.LESS -> R.string.lost_amount_less_than
else -> null
}
}
else -> {
when (this.operator) {
Operator.MORE -> R.string.more_than
Operator.LESS -> R.string.less_than
else -> null
}
}
}
}
}

@ -0,0 +1,15 @@
package net.pokeranalytics.android.model.interfaces
import java.util.*
interface Dated {
var date: Date
}
interface DatedValue : Dated {
var amount: Double
}

@ -1,9 +1,10 @@
package net.pokeranalytics.android.model.interfaces
import net.pokeranalytics.android.calculus.StatBase
import net.pokeranalytics.android.calculus.ObjectIdentifier
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import java.util.*
interface Timed : StatBase, Identifiable {
interface Timed : GraphUnderlyingEntry, Identifiable {
fun startDate() : Date?
@ -29,4 +30,6 @@ interface Timed : StatBase, Identifiable {
val hourlyDuration: Double
get() = this.netDuration / 3600000.0
val objectIdentifier : ObjectIdentifier
}

@ -3,6 +3,7 @@ package net.pokeranalytics.android.model.migrations
import io.realm.DynamicRealm
import io.realm.RealmMigration
import timber.log.Timber
import java.util.*
class PokerAnalyticsMigration : RealmMigration {
@ -35,10 +36,10 @@ class PokerAnalyticsMigration : RealmMigration {
// Migrate to version 2
if (currentVersion == 1) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.rename("FilterElement", "FilterCondition")
schema.get("Filter")?.let {
it.renameField("filterElements", "filterConditions")
it.removeField("entityType")
}
schema.get("SessionSet")?.let {
it.addField("id", String::class.java).setRequired("id", true)
@ -47,6 +48,34 @@ class PokerAnalyticsMigration : RealmMigration {
currentVersion++
}
// Migrate to version 2
if (currentVersion == 2) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.rename("Report", "ReportSetup")
schema.get("Session")?.let {
it.addField("blinds", String::class.java)
}
schema.get("FilterCondition")?.let {
it.removeField("blindValues")
it.addField("operator", String::class.java).setNullable("operator", true)
it.addField("intValue", Int::class.java).setNullable("intValue", true)
it.addField("doubleValue", Double::class.java).setNullable("intValue", true)
it.addField("stringValue", String::class.java).setNullable("stringValue", true)
}
schema.get("ComputableResult")?.let {
it.removeField("sessionSet")
}
schema.get("Bankroll")?.let {
it.addField("initialValue", Double::class.java).setRequired("initialValue", true)
}
currentVersion++
}
}
override fun equals(other: Any?): Boolean {

@ -8,29 +8,12 @@ import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.BankrollRow
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.util.NULL_TEXT
import java.util.*
import kotlin.collections.ArrayList
open class Bankroll() : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable {
companion object {
val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.add(BankrollRow.LIVE)
rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.currency))
rows.add(BankrollRow.CURRENCY)
rows
}
}
open class Bankroll() : RealmObject(), NameManageable, RowRepresentable {
@PrimaryKey
override var id = UUID.randomUUID().toString()
@ -46,37 +29,16 @@ open class Bankroll() : RealmObject(), NameManageable, StaticRowRepresentableDat
// The currency of the bankroll
var currency: Currency? = null
override fun getDisplayName(): String {
return this.name
}
// Row Representable Datasource
override fun adapterRows(): List<RowRepresentable>? {
return Bankroll.rowRepresentation
}
// The initial value of the bankroll
var initialValue: Double = 0.0
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
else -> return super.stringForRow(row)
val rate: Double
get() {
return this.currency?.rate ?: 1.0
}
}
override fun boolForRow(row: RowRepresentable): Boolean {
return when (row) {
BankrollRow.LIVE -> !this.live
else -> super.boolForRow(row)
}
}
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
return when (row) {
SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.name))
BankrollRow.RATE -> row.editingDescriptors(mapOf())
else -> {
row.editingDescriptors(mapOf())
}
}
override fun getDisplayName(): String {
return this.name
}
override fun updateValue(value: Any?, row: RowRepresentable) {
@ -84,8 +46,12 @@ open class Bankroll() : RealmObject(), NameManageable, StaticRowRepresentableDat
SimpleRow.NAME -> this.name = value as String? ?: ""
BankrollRow.LIVE -> {
this.live = if (value is Boolean) !value else false
}
BankrollRow.INITIAL_VALUE -> {
this.initialValue = value as Double? ?: 0.0
}
BankrollRow.CURRENCY -> {
//TODO handle a use default currency option
this.currency?.code = value as String?
}
BankrollRow.RATE -> {

@ -1,33 +1,32 @@
package net.pokeranalytics.android.model.realm
import io.realm.RealmObject
import net.pokeranalytics.android.calculus.interfaces.Computable
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
open class ComputableResult() : RealmObject(), Computable, Filterable {
open class ComputableResult() : RealmObject(), Filterable {
override var ratedNet: Double = 0.0
var ratedNet: Double = 0.0
override var bbNet: BB = 0.0
var bbNet: BB = 0.0
override var hasBigBlind: Int = 0
var hasBigBlind: Int = 0
override var isPositive: Int = 0
var isPositive: Int = 0
override var ratedBuyin: Double = 0.0
var ratedBuyin: Double = 0.0
override var estimatedHands: Double = 0.0
var estimatedHands: Double = 0.0
override var bbPer100Hands: BB = 0.0
var bbPer100Hands: BB = 0.0
override var sessionSet: SessionSet? = null
// var sessionSet: SessionSet? = null
var session: Session? = null
fun updateWith(session: Session) {
this.sessionSet = session.sessionSet
// this.sessionSet = session.sessionSet
val rate = session.bankroll?.currency?.rate ?: 1.0
@ -56,8 +55,11 @@ open class ComputableResult() : RealmObject(), Computable, Filterable {
companion object {
fun fieldNameForQueryType(queryCondition: QueryCondition): String? {
return "session." + Session.fieldNameForQueryType(queryCondition)
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? {
Session.fieldNameForQueryType(queryCondition)?.let {
return "session.$it"
}
return null
}
}

@ -3,6 +3,7 @@ package net.pokeranalytics.android.model.realm
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.util.UserDefaults
import java.util.*
open class Currency : RealmObject() {
@ -13,7 +14,7 @@ open class Currency : RealmObject() {
@PrimaryKey
var id = UUID.randomUUID().toString()
/**
/**
* The currency code of the currency, i.e. USD, EUR...
*/
var code: String? = null
@ -37,7 +38,15 @@ open class Currency : RealmObject() {
computable.ratedBuyin = it * rate
}
computable.session?.bankrollHasBeenUpdated()
}
}
fun hasMainCurrencyCode() : Boolean {
this.code?.let { return it == UserDefaults.currency.currencyCode }
return false
}
}

@ -3,13 +3,9 @@ package net.pokeranalytics.android.model.realm
import io.realm.*
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import org.jetbrains.annotations.TestOnly
import timber.log.Timber
import java.util.*
@ -20,13 +16,6 @@ import java.util.*
*/
open class Filter : RealmObject() {
private var entityType: Int? = Entity.SESSION.ordinal
private enum class Entity {
SESSION,
;
}
companion object {
// Create a new instance
@ -35,16 +24,18 @@ open class Filter : RealmObject() {
return realm.copyToRealm(filter)
}
// Get a filter by its id
// Get a queryWith by its id
fun getFilterBydId(realm: Realm, filterId: String): Filter? {
return realm.where<Filter>().equalTo("id", filterId).findFirst()
}
@TestOnly
inline fun <reified T : Filterable> queryOn(realm: Realm, queries: List<QueryCondition>): RealmResults<T> {
inline fun <reified T : Filterable> queryOn(realm: Realm, queries: List<QueryCondition>, sortField: String? = null): RealmResults<T> {
var realmQuery = realm.where<T>()
queries.forEach {
realmQuery = it.filter(realmQuery)
realmQuery = it.queryWith(realmQuery)
}
sortField?.let {
realmQuery.sort(it)
}
Timber.d(">>> Filter query: ${realmQuery.description}")
return realmQuery.findAll()
@ -54,81 +45,76 @@ open class Filter : RealmObject() {
@PrimaryKey
var id = UUID.randomUUID().toString()
// the filter name
// the queryWith name
var name: String = ""
// the number of use of the filter,
// the number of use of the queryWith,
// for MutableRealmInteger, see https://realm.io/docs/java/latest/#counters
val usageCount: MutableRealmInteger = MutableRealmInteger.valueOf(0)
var filterConditions: RealmList<FilterCondition> = RealmList()
private set
fun createOrUpdateFilterConditions(filterConditionRows: ArrayList<FilterElementRow>) {
filterConditions.clear()
filterConditionRows
fun createOrUpdateFilterConditions(filterConditionRows: ArrayList<QueryCondition>) {
println("list of querys saving: ${filterConditionRows.map { it.id }}")
println("list of querys previous: ${this.filterConditions.map { it.queryCondition.id }}")
filterConditionRows
.map {
it.filterName
it.filterSectionRow
}
.distinct()
.forEach { filterName->
filterConditionRows
.filter {
it.filterName == filterName
it.filterSectionRow == filterName
}
.apply {
val casted = arrayListOf<FilterElementRow>()
println("list of querys: ${this.map { it.id }}")
val casted = arrayListOf<QueryCondition>()
casted.addAll(this)
filterConditions.add(FilterCondition(casted))
}
val newFilterCondition = FilterCondition(casted)
val previousCondition = filterConditions.filter {
it.filterName == newFilterCondition.filterName
}
filterConditions.removeAll(previousCondition)
filterConditions.add(newFilterCondition)
}
}
}
fun countBy(filterCategoryRow: FilterCategoryRow): Int {
val sections = filterCategoryRow.filterSectionRows
return filterConditions.count {
sections.contains(FilterSectionRow.valueOf(it.sectionName ?: throw PokerAnalyticsException.FilterElementUnknownSectionName))
}
}
fun remove(filterCategoryRow: FilterCategoryRow) {
val sections = filterCategoryRow.filterSectionRows.map { it.name }
val savedSections = filterConditions.filter { sections.contains(it.sectionName) }
this.filterConditions.removeAll(savedSections)
}
fun contains(filterElementRow: FilterElementRow): Boolean {
val filtered = filterConditions.filter {
it.filterName == filterElementRow.filterName
}
if (filtered.isEmpty()) {
return false
}
return filterElementRow.contains(filtered)
fun countBy(filterCategoryRow: FilterCategoryRow): Int {
val sections = filterCategoryRow.filterSectionRows.map { it.name }
println("list of sections $sections")
val savedSections = filterConditions.filter { sections.contains(it.sectionName) }.flatMap { it.queryCondition.id }
println("list of savedSections $savedSections")
return savedSections.size
}
/**
* Set the saved value in the filter for the given [filterElementRow]
*/
fun setSavedValueForElement(filterElementRow: FilterElementRow) {
when (filterElementRow) {
is FilterElementRow.PastDays -> {
val values = getSavedValueForElement(filterElementRow) as Array<*>
if (values.isNotEmpty() && values.first() is Int) {
filterElementRow.lastDays = values.first() as Int
}
}
is FilterElementRow.DateFilterElementRow -> filterElementRow.dateValue = getSavedValueForElement(filterElementRow) as Date? ?: Date()
}
fun contains(filterElementRow: QueryCondition): Boolean {
println("list of saved queries ${filterConditions.map { it.queryCondition.id }}")
println("list of contains ${filterElementRow.id}")
val contained = filterConditions.flatMap{ it.queryCondition.id }.contains(filterElementRow.id.first())
println("list of : $contained")
return contained
}
/**
* Get the saved value for the given [filterElementRow]
*/
private fun getSavedValueForElement(filterElementRow: FilterElementRow): Any? {
fun loadValueForElement(filterElementRow: QueryCondition) {
val filtered = filterConditions.filter {
it.filterName == filterElementRow.filterName
it.queryCondition.id == filterElementRow.id
}
if (filtered.isNotEmpty()) {
return filtered.first().getFilterConditionValue(filterElementRow)
return filterElementRow.updateValueMap(filtered.first())
}
return null
}
inline fun <reified T : Filterable> results(): RealmResults<T> {
@ -136,7 +122,7 @@ open class Filter : RealmObject() {
this.filterConditions.map {
it.queryCondition
}.forEach {
realmQuery = it.filter(realmQuery)
realmQuery = it.queryWith(realmQuery)
}
return realmQuery.findAll()

@ -4,9 +4,8 @@ import io.realm.RealmList
import io.realm.RealmObject
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow.*
import java.util.*
import kotlin.collections.ArrayList
open class FilterCondition() : RealmObject() {
@ -15,35 +14,16 @@ open class FilterCondition() : RealmObject() {
this.sectionName = sectionName
}
constructor(filterElementRows: ArrayList<FilterElementRow>) : this(filterElementRows.first().filterName, filterElementRows.first().filterSectionRow.name) {
constructor(filterElementRows: ArrayList<QueryCondition>) : this(filterElementRows.first().baseId, filterElementRows.first().filterSectionRow.name) {
val row = filterElementRows.first()
this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName
this.operator = row.operator.ordinal
when (row) {
is DateFilterElementRow -> {
this.dateValue = row.dateValue
}
is StringFilterElementRow -> {
this.stringValues = RealmList<String>().apply {
this.addAll(filterElementRows.map {
(it as StringFilterElementRow).stringValue
})
}
}
is NumericFilterElementRow -> {
this.numericValues = RealmList<Double>().apply {
this.addAll(filterElementRows.map {
(it as NumericFilterElementRow).doubleValue
})
}
}
is FilterElementBlind -> {
this.blindValues = RealmList<FilterElementBlind>().apply {
this.addAll(filterElementRows.map {
FilterElementBlind((it as FilterElementRow.Blind).sb, it.bb, it.code)
})
}
}
is QueryCondition.SingleInt -> this.setValue(row.singleValue)
is QueryCondition.SingleDate -> this.setValue(row.singleValue)
is QueryCondition.ListOfDouble -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfDouble).listOfValues })
is QueryCondition.ListOfInt -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfInt).listOfValues })
is QueryCondition.ListOfString -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfString).listOfValues })
}
}
@ -51,75 +31,62 @@ open class FilterCondition() : RealmObject() {
var sectionName: String? = null
val queryCondition : QueryCondition
get() = QueryCondition.valueOf(this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName)
get() = QueryCondition.valueOf<QueryCondition>(this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName)
.apply {
this.updateValueMap(this@FilterCondition)
}
private var numericValues: RealmList<Double>? = null
private var dateValue: Date? = null
private var stringValues: RealmList<String>? = null
private var blindValues: RealmList<FilterElementBlind>? = null
val ids: Array<String>
get() = stringValues?.toTypedArray() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
val blinds: RealmList<FilterElementBlind>
get() {
blindValues?.let {
if (it.isNotEmpty()) {
return it
} else {
throw PokerAnalyticsException.FilterElementExpectedValueMissing
}
}
throw PokerAnalyticsException.FilterElementExpectedValueMissing
}
val date: Date
get() = dateValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
val values: Array<Int>
get() = numericValues?.map {
it.toInt()
}?.toTypedArray() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
val value: Double
get() = numericValues?.first() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
val leftValue: Double
get() = numericValues?.first() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
val rightValue: Double
get() = numericValues?.last() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
val dayOfWeek: Int
get() = numericValues?.first()?.toInt() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
var doubleValues: RealmList<Double>? = null
var intValues: RealmList<Int>? = null
var stringValues: RealmList<String>? = null
var dateValue: Date? = null
var doubleValue: Double? = null
var intValue: Int? = null
var stringValue: String? = null
var operator: Int? = null
inline fun <reified T:Any > getValues(): ArrayList < T > {
println("<<<< r $stringValues")
return when (T::class) {
Int::class -> ArrayList<T>().apply { intValues?.map { add(it as T) } }
Double::class -> ArrayList<T>().apply { doubleValues?.map { add(it as T) } }
String::class -> ArrayList<T>().apply { stringValues?.map { add(it as T) } }
else -> throw PokerAnalyticsException.QueryValueMapUnexpectedValue
}
}
val month: Int
get() = numericValues?.first()?.toInt() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
inline fun <reified T:Any > getValue(): T {
return when (T::class) {
Int::class -> intValue ?: 0
Double::class -> doubleValue?: 0.0
Date::class -> dateValue ?: Date()
String::class -> stringValue ?: ""
else -> throw PokerAnalyticsException.QueryValueMapUnexpectedValue
} as T
}
private inline fun <reified T> setValues(values:List<T>) {
when (T::class) {
Int::class -> intValues = RealmList<Int>().apply { values.map { it as Int }.forEach { add(it) } }
Double::class -> doubleValues = RealmList<Double>().apply { values.map { it as Double }.forEach { add(it) } }
String::class -> stringValues = RealmList<String>().apply { values.map { it as String }.forEach { add(it) } }
else -> throw PokerAnalyticsException.QueryValueMapUnexpectedValue
}
}
val year: Int
get() = numericValues?.first()?.toInt() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
fun setValue(value:Double) {
doubleValue = value
}
fun setValue(value:Date) {
dateValue = value
}
/**
* Return the value associated with the given [filterElementRow]
*/
fun getFilterConditionValue(filterElementRow: FilterElementRow): Any? {
return when (filterElementRow) {
is From, is To -> dateValue //TODO: Probably change by 'date' (doesn't work now because the value isn't correctly saved
is PastDays -> values
else -> throw PokerAnalyticsException.FilterElementTypeMissing(filterElementRow)
}
}
fun setValue(value:Int) {
intValue= value
}
fun setValue(value:String) {
stringValue = value
}
}

@ -1,8 +0,0 @@
package net.pokeranalytics.android.model.realm
import io.realm.RealmObject
open class FilterElementBlind(var sb : Double? = null,
var bb : Double? = null,
var currencyCode : String? = null
) : RealmObject()

@ -11,7 +11,7 @@ enum class ReportDisplay {
MAP
}
open class Report : RealmObject() {
open class ReportSetup : RealmObject() {
@PrimaryKey
var id = UUID.randomUUID().toString()
@ -24,7 +24,7 @@ open class Report : RealmObject() {
// @todo define the configuration options
// var comparators: List<Int> = listOf()
// var criteria: List<Int> = listOf()
// var stats: List<Int> = listOf()
// The filters associated with the report

@ -6,16 +6,29 @@ import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects
import io.realm.annotations.RealmClass
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
@RealmClass
open class Result : RealmObject() {
open class Result : RealmObject(), Filterable {
companion object {
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition>): String? {
Session.fieldNameForQueryType(queryCondition)?.let {
return "sessions.$it"
}
return null
}
}
/**
* The buyin amount
*/
var buyin: Double? = null
set(value) {
field = value
this.computeNumberOfRebuy()
this.computeNet()
}
@ -36,6 +49,15 @@ open class Result : RealmObject() {
*/
var netResult: Double? = null
set(value) {
this.session?.bankroll?.let { bankroll ->
if (bankroll.live) {
throw IllegalStateException("Can't set net result on a live bankroll")
}
} ?: run {
throw IllegalStateException("Session doesn't have any bankroll")
}
field = value
this.computeNet()
if (value != null) {
@ -64,6 +86,9 @@ open class Result : RealmObject() {
// The tournament final position, if applicable
var tournamentFinalPosition: Int? = null
// Number of rebuys
//var numberOfRebuy: Double? = null
@LinkingObjects("result")
private val sessions: RealmResults<Session>? = null
@ -96,6 +121,10 @@ open class Result : RealmObject() {
this.session?.sessionSet?.computeStats()
}
// Computes the number of rebuy
private fun computeNumberOfRebuy() {
}
// @todo tips?
}

@ -1,6 +1,7 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import com.github.mikephil.charting.data.Entry
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject
@ -11,10 +12,7 @@ import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputedStat
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.StatFormattingException
import net.pokeranalytics.android.calculus.TextFormat
import net.pokeranalytics.android.calculus.*
import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.LiveData
@ -29,16 +27,15 @@ import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.adapter.UnmanagedRowRepresentableException
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SessionRow
import net.pokeranalytics.android.util.CurrencyUtils
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.*
import java.text.DateFormat
import java.util.*
import java.util.Currency
import kotlin.collections.ArrayList
@ -46,8 +43,7 @@ import kotlin.collections.ArrayList
typealias BB = Double
open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDataSource, RowRepresentable, Timed,
TimeFilterable, Filterable {
TimeFilterable, Filterable, DatedValue {
enum class Type {
CASH_GAME,
@ -67,36 +63,29 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
return realm.copyToRealm(session)
}
fun fieldNameForQueryType(queryCondition: QueryCondition): String? {
return when (queryCondition) {
LIVE, ONLINE -> "bankroll.live"
CASH, TOURNAMENT -> "type"
BANKROLL -> "bankroll.id"
GAME -> "game.id"
TOURNAMENT_NAME -> "tournamentName.id"
ANY_TOURNAMENT_FEATURES, ALL_TOURNAMENT_FEATURES -> "tournamentFeatures.id"
LOCATION -> "location.id"
LIMIT -> "limit"
TABLE_SIZE -> "tableSize"
TOURNAMENT_TYPE -> "tournamentType"
CURRENCY -> "bankroll.currency"
CURRENCY_CODE -> "bankroll.currency.code"
BIG_BLIND -> "cgBigBlind"
SMALL_BLIND -> "cgSmallBlind"
COMMENT -> "comment"
BETWEEN_NUMBER_OF_TABLE, MORE_NUMBER_OF_TABLE, LESS_NUMBER_OF_TABLE -> "numberOfTable"
MORE_THAN_NET_RESULT, LESS_THAN_NET_RESULT -> "computableResults.ratedNet"
MORE_THAN_BUY_IN, LESS_THAN_BUY_IN -> "result.buyin"
MORE_THAN_CASH_OUT, LESS_THAN_CASH_OUT -> "result.cashout"
MORE_THAN_TIPS, LESS_THAN_TIPS -> "result.tips"
MORE_THAN_NUMBER_OF_PLAYER, LESS_THAN_NUMBER_OF_PLAYER, BETWEEN_NUMBER_OF_PLAYER -> "tournamentNumberOfPlayers"
MORE_THAN_TOURNAMENT_FEE, LESS_THAN_TOURNAMENT_FEE, BETWEEN_TOURNAMENT_FEE -> "tournamentEntryFee"
STARTED_FROM_DATE, STARTED_TO_DATE -> "startDate"
ENDED_FROM_DATE, ENDED_TO_DATE -> "endDate"
DAY_OF_WEEK, WEEK_END, WEEK_DAY -> "dayOfWeek"
MONTH -> "month"
YEAR -> "year"
TODAY, YESTERDAY, TODAY_AND_YESTERDAY, THIS_YEAR, THIS_MONTH, THIS_WEEK -> "startDate"
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? {
return when (queryCondition) {
IsLive::class.java, IsOnline::class.java -> "bankroll.live"
IsCash::class.java, IsTournament::class.java -> "type"
AnyBankroll::class.java -> "bankroll.id"
AnyGame::class.java -> "game.id"
AnyTournamentName::class.java -> "tournamentName.id"
AnyTournamentFeature::class.java, AllTournamentFeature::class.java -> "tournamentFeatures.id"
AnyLocation::class.java -> "location.id"
AnyLimit::class.java -> "limit"
AnyTableSize::class.java -> "tableSize"
AnyTournamentType::class.java -> "tournamentType"
AnyBlind::class.java -> "blinds"
NumberOfTable::class.java -> "numberOfTable"
NetAmountWon::class.java -> "computableResults.ratedNet"
NumberOfPlayer::class.java -> "tournamentNumberOfPlayers"
TournamentFee::class.java -> "tournamentEntryFee"
StartedFromDate::class.java, StartedToDate::class.java -> "startDate"
EndedFromDate::class.java, EndedToDate::class.java -> "endDate"
AnyDayOfWeek::class.java, IsWeekEnd::class.java, IsWeekDay::class.java -> "dayOfWeek"
AnyMonthOfYear::class.java -> "month"
AnyYear::class.java -> "year"
IsToday::class.java, WasYesterday::class.java, WasTodayAndYesterday::class.java, DuringThisYear::class.java, DuringThisMonth::class.java, DuringThisWeek::class.java -> "startDate"
else -> null
}
}
@ -122,7 +111,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
// Timed interface
override var dayOfWeek : Int? = null
override var dayOfWeek: Int? = null
override var month: Int? = null
override var year: Int? = null
override var dayOfMonth: Int? = null
@ -221,14 +210,22 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
// The small blind value
var cgSmallBlind: Double? = null
set(value) {
field = value
formatBlinds()
}
// The big blind value
var cgBigBlind: Double? = null
set(value) {
field = value
this.computeStats()
formatBlinds()
}
var blinds: String? = null
private set
// Tournament
// The entry fee of the tournament
@ -246,6 +243,10 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
// The features of the tournament, like Knockout, Shootout, Turbo...
var tournamentFeatures: RealmList<TournamentFeature> = RealmList()
fun bankrollHasBeenUpdated() {
formatBlinds()
}
/**
* Manages impacts on SessionSets
* Should be called when the start / end date are changed
@ -299,11 +300,12 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
*/
val bbNet: BB
get() {
val bb = this.cgBigBlind; val result = this.result
if (bb != null && result != null) {
return result.net / bb
val bb = this.cgBigBlind
val result = this.result
return if (bb != null && result != null) {
result.net / bb
} else {
return 0.0
0.0
}
}
@ -318,6 +320,21 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
return noh * hd
}
// DatedValue
@Ignore
override var date: Date = Date()
get() {
return this.startDate ?: this.creationDate
}
@Ignore
override var amount: Double = 0.0
get() {
return this.computableResult?.ratedNet ?: 0.0
}
/**
* Pre-compute various stats
*/
@ -353,16 +370,6 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
return playerHandsPerHour / tableSize.toDouble()
}
@Ignore
var ratedBuyin: Double = 0.0
get() {
val rate = this.bankroll?.currency?.rate ?: 1.0
this.result?.buyin?.let { buyin ->
return buyin * rate
}
return 0.0
}
val hourlyRate: Double
get() {
this.result?.let { result ->
@ -483,14 +490,14 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
return NULL_TEXT
}
/**
* Return the formatted blinds
*/
fun getBlinds(context: Context): String {
val currencyCode = bankroll?.currency?.code ?: CurrencyUtils.getLocaleCurrency().currencyCode
val currencySymbol = Currency.getInstance(currencyCode).getSymbol(Preferences.getCurrencyLocale(context))
return if (cgSmallBlind == null) NULL_TEXT else "$currencySymbol ${cgSmallBlind?.formatted()}/${cgBigBlind?.round()}"
}
val currency: Currency
get() {
return bankroll?.currency?.code?.let {
Currency.getInstance(it)
} ?: run {
UserDefaults.currency
}
}
/**
* Return the game title
@ -509,6 +516,19 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
return if (gameTitle.isNotBlank()) gameTitle else NULL_TEXT
}
fun getFormattedBlinds(): String {
return blinds ?: NULL_TEXT
}
private fun formatBlinds() {
blinds = null
if (cgBigBlind == null) return
cgBigBlind?.let { bb ->
val sb = cgSmallBlind ?: bb / 2.0
blinds = "${currency.symbol} ${sb.formatted()}/${bb.round()}"
}
}
// LifeCycle
/**
@ -547,7 +567,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
}
@Ignore
private var rowRepresentationForCurrentState : List<RowRepresentable> = mutableListOf()
private var rowRepresentationForCurrentState: List<RowRepresentable> = mutableListOf()
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
@ -559,34 +579,34 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
CustomizableRowRepresentable(
RowViewType.HEADER_TITLE_AMOUNT_BIG,
title = getFormattedDuration(),
computedStat = ComputedStat(Stat.NETRESULT, result?.net ?: 0.0, CurrencyUtils.getCurrency(bankroll))
computedStat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency)
)
)
rows.add(SeparatorRowRepresentable())
rows.add(SeparatorRow())
}
SessionState.PAUSED -> {
rows.add(
CustomizableRowRepresentable(
RowViewType.HEADER_TITLE_AMOUNT_BIG,
resId = R.string.pause,
computedStat = ComputedStat(Stat.NETRESULT, result?.net ?: 0.0, CurrencyUtils.getCurrency(bankroll))
computedStat = ComputedStat(Stat.NET_RESULT, result?.net ?: 0.0, currency = currency)
)
)
rows.add(SeparatorRowRepresentable())
rows.add(SeparatorRow())
}
SessionState.FINISHED -> {
rows.add(
CustomizableRowRepresentable(
RowViewType.HEADER_TITLE_AMOUNT_BIG,
title = getFormattedDuration(),
computedStat = ComputedStat(Stat.NETRESULT, result?.net ?: 0.0, CurrencyUtils.getCurrency(bankroll))
computedStat = ComputedStat(Stat.NET_RESULT, 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, CurrencyUtils.getCurrency(bankroll))
computedStat = ComputedStat(Stat.HOURLY_RATE, this.hourlyRate, currency = currency)
)
)
@ -599,7 +619,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
// )
// )
// }
rows.add(SeparatorRowRepresentable())
rows.add(SeparatorRow())
}
else -> {
}
@ -625,21 +645,21 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
override fun stringForRow(row: RowRepresentable, context: Context): String {
return when (row) {
SessionRow.BANKROLL -> bankroll?.name ?: NULL_TEXT
SessionRow.BLINDS -> getBlinds(context)
SessionRow.BLINDS -> getFormattedBlinds()
SessionRow.BREAK_TIME -> if (this.breakDuration > 0.0) this.breakDuration.toMinutes() else NULL_TEXT
SessionRow.BUY_IN -> this.result?.buyin?.toCurrency(CurrencyUtils.getCurrency(bankroll)) ?: NULL_TEXT
SessionRow.CASHED_OUT, SessionRow.PRIZE -> this.result?.cashout?.toCurrency(CurrencyUtils.getCurrency(bankroll)) ?: NULL_TEXT
SessionRow.NET_RESULT -> this.result?.netResult?.toCurrency(CurrencyUtils.getCurrency(bankroll)) ?: NULL_TEXT
SessionRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT
SessionRow.CASHED_OUT, SessionRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT
SessionRow.NET_RESULT -> this.result?.netResult?.toCurrency(currency) ?: NULL_TEXT
SessionRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT
SessionRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT
SessionRow.GAME -> getFormattedGame()
SessionRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(CurrencyUtils.getCurrency(bankroll)) ?: NULL_TEXT
SessionRow.INITIAL_BUY_IN -> tournamentEntryFee?.toCurrency(currency) ?: NULL_TEXT
SessionRow.LOCATION -> location?.name ?: NULL_TEXT
SessionRow.PLAYERS -> tournamentNumberOfPlayers?.toString() ?: NULL_TEXT
SessionRow.POSITION -> result?.tournamentFinalPosition?.toString() ?: NULL_TEXT
SessionRow.START_DATE -> this.startDate?.shortDateTime() ?: NULL_TEXT
SessionRow.TABLE_SIZE -> this.tableSize?.let { TableSize(it).localizedTitle(context) } ?: NULL_TEXT
SessionRow.TIPS -> result?.tips?.toCurrency(CurrencyUtils.getCurrency(bankroll)) ?: NULL_TEXT
SessionRow.TIPS -> result?.tips?.toCurrency(currency) ?: NULL_TEXT
SessionRow.TOURNAMENT_TYPE -> this.tournamentType?.let {
TournamentType.values()[it].localizedTitle(context)
} ?: run {
@ -647,7 +667,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
}
SessionRow.TOURNAMENT_FEATURE -> {
if (tournamentFeatures.size > 2) {
"${tournamentFeatures.subList(0,2).joinToString {
"${tournamentFeatures.subList(0, 2).joinToString {
it.name
}}, ..."
} else if (tournamentFeatures.size > 0) {
@ -674,56 +694,98 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
return when (row) {
SessionRow.BANKROLL -> row.editingDescriptors(mapOf(
"defaultValue" to this.bankroll,
"data" to LiveData.BANKROLL.items(realm)))
SessionRow.GAME -> row.editingDescriptors(mapOf(
"limit" to this.limit,
"defaultValue" to this.game,
"data" to LiveData.GAME.items(realm)))
SessionRow.LOCATION -> row.editingDescriptors(mapOf(
"defaultValue" to this.location,
"data" to LiveData.LOCATION.items(realm)))
SessionRow.TOURNAMENT_FEATURE -> row.editingDescriptors(mapOf(
"defaultValue" to this.tournamentFeatures,
"data" to LiveData.TOURNAMENT_FEATURE.items(realm)))
SessionRow.TOURNAMENT_NAME -> row.editingDescriptors(mapOf(
"defaultValue" to this.tournamentName,
"data" to LiveData.TOURNAMENT_NAME.items(realm)))
SessionRow.TOURNAMENT_TYPE -> row.editingDescriptors(mapOf(
"defaultValue" to this.tournamentType))
SessionRow.TABLE_SIZE -> row.editingDescriptors(mapOf(
"defaultValue" to this.tableSize))
SessionRow.BLINDS -> row.editingDescriptors(mapOf(
"sb" to cgSmallBlind?.round(),
"bb" to cgBigBlind?.round()
))
SessionRow.BUY_IN -> row.editingDescriptors(mapOf(
"bb" to cgBigBlind,
"fee" to this.tournamentEntryFee,
"ratedBuyin" to result?.buyin
))
SessionRow.BANKROLL -> row.editingDescriptors(
mapOf(
"defaultValue" to this.bankroll,
"data" to LiveData.BANKROLL.items(realm)
)
)
SessionRow.GAME -> row.editingDescriptors(
mapOf(
"limit" to this.limit,
"defaultValue" to this.game,
"data" to LiveData.GAME.items(realm)
)
)
SessionRow.LOCATION -> row.editingDescriptors(
mapOf(
"defaultValue" to this.location,
"data" to LiveData.LOCATION.items(realm)
)
)
SessionRow.TOURNAMENT_FEATURE -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tournamentFeatures,
"data" to LiveData.TOURNAMENT_FEATURE.items(realm)
)
)
SessionRow.TOURNAMENT_NAME -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tournamentName,
"data" to LiveData.TOURNAMENT_NAME.items(realm)
)
)
SessionRow.TOURNAMENT_TYPE -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tournamentType
)
)
SessionRow.TABLE_SIZE -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tableSize
)
)
SessionRow.BLINDS -> row.editingDescriptors(
mapOf(
"sb" to cgSmallBlind?.round(),
"bb" to cgBigBlind?.round()
)
)
SessionRow.BUY_IN -> row.editingDescriptors(
mapOf(
"bb" to cgBigBlind,
"fee" to this.tournamentEntryFee,
"ratedBuyin" to result?.buyin
)
)
SessionRow.BREAK_TIME -> row.editingDescriptors(mapOf())
SessionRow.CASHED_OUT, SessionRow.PRIZE -> row.editingDescriptors(mapOf(
"defaultValue" to result?.cashout
))
SessionRow.NET_RESULT -> row.editingDescriptors(mapOf(
"defaultValue" to result?.netResult
))
SessionRow.COMMENT -> row.editingDescriptors(mapOf(
"defaultValue" to this.comment))
SessionRow.INITIAL_BUY_IN -> row.editingDescriptors(mapOf(
"defaultValue" to this.tournamentEntryFee
))
SessionRow.PLAYERS -> row.editingDescriptors(mapOf(
"defaultValue" to this.tournamentNumberOfPlayers))
SessionRow.POSITION -> row.editingDescriptors(mapOf(
"defaultValue" to this.result?.tournamentFinalPosition))
SessionRow.TIPS -> row.editingDescriptors(mapOf(
"sb" to cgSmallBlind?.round(),
"bb" to cgBigBlind?.round(),
"tips" to result?.tips
))
SessionRow.CASHED_OUT, SessionRow.PRIZE -> row.editingDescriptors(
mapOf(
"defaultValue" to result?.cashout
)
)
SessionRow.NET_RESULT -> row.editingDescriptors(
mapOf(
"defaultValue" to result?.netResult
)
)
SessionRow.COMMENT -> row.editingDescriptors(
mapOf(
"defaultValue" to this.comment
)
)
SessionRow.INITIAL_BUY_IN -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tournamentEntryFee
)
)
SessionRow.PLAYERS -> row.editingDescriptors(
mapOf(
"defaultValue" to this.tournamentNumberOfPlayers
)
)
SessionRow.POSITION -> row.editingDescriptors(
mapOf(
"defaultValue" to this.result?.tournamentFinalPosition
)
)
SessionRow.TIPS -> row.editingDescriptors(
mapOf(
"sb" to cgSmallBlind?.round(),
"bb" to cgBigBlind?.round(),
"tips" to result?.tips
)
)
else -> null
}
}
@ -760,13 +822,15 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
this.breakDuration = (value as Double? ?: 0.0).toLong() * 60 * 1000
}
SessionRow.BUY_IN -> {
val localResult = if (this.result != null) this.result as Result else realm.createObject(Result::class.java)
val localResult =
if (this.result != null) this.result as Result else realm.createObject(Result::class.java)
localResult.buyin = value as Double?
this.result = localResult
this.updateRowRepresentation()
}
SessionRow.CASHED_OUT, SessionRow.PRIZE -> {
val localResult = if (this.result != null) this.result as Result else realm.createObject(Result::class.java)
val localResult =
if (this.result != null) this.result as Result else realm.createObject(Result::class.java)
localResult.cashout = value as Double?
@ -846,14 +910,19 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
}
// Stat Base
// Stat Entry
override fun formattedValue(stat: Stat, context: Context) : TextFormat {
override val entryTitle: String
get() {
return DateFormat.getDateInstance(DateFormat.SHORT).format(this.startDate)
}
override fun formattedValue(stat: Stat): TextFormat {
this.result?.let { result ->
val value: Double? = when (stat) {
Stat.NETRESULT, Stat.AVERAGE, Stat.STANDARD_DEVIATION -> result.net
Stat.NET_RESULT, Stat.AVERAGE, Stat.STANDARD_DEVIATION -> result.net
Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_SETS -> 1.0
Stat.AVERAGE_BUYIN -> result.buyin
Stat.ROI -> {
@ -864,16 +933,20 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
}
}
Stat.HOURLY_RATE_BB -> this.bbHourlyRate
Stat.NET_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> Stat.netBBPer100Hands(this.bbNet, this.estimatedHands)
Stat.NET_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> Stat.netBBPer100Hands(
this.bbNet,
this.estimatedHands
)
Stat.AVERAGE_NET_BB -> this.bbNet
Stat.DURATION, Stat.AVERAGE_DURATION -> this.netDuration.toDouble()
Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> this.netDuration.toDouble()
Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY -> this.hourlyRate
Stat.HANDS_PLAYED -> this.estimatedHands
Stat.WIN_RATIO -> null
else -> throw StatFormattingException("format undefined for stat ${stat.name}")
}
value?.let {
return stat.format(it, CurrencyUtils.getCurrency(this.bankroll), context)
return stat.format(it, currency = currency)
} ?: run {
return TextFormat(NULL_TEXT)
}
@ -884,6 +957,56 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
}
override fun legendValues(
stat: Stat,
entry: Entry,
style: GraphFragment.Style,
groupName: String,
context: Context
) : LegendContent {
when (style) {
GraphFragment.Style.MULTILINE -> {
val secondTitle = stat.localizedTitle(context)
val entryValue = this.formattedValue(stat)
val dateValue = TextFormat(this.entryTitle)
}
return MultilineLegendValues(groupName, secondTitle, entryValue, dateValue)
}
else -> {
return when (stat) {
Stat.STANDARD_DEVIATION -> {
val left = this.formattedValue(Stat.NET_RESULT)
val hasMainCurrencyCode = this.bankroll?.currency?.hasMainCurrencyCode() ?: false
var right: TextFormat? = null
if (!hasMainCurrencyCode) {
this.computableResult?.ratedNet?.let { ratedNet ->
right = Stat.NET_RESULT.format(ratedNet)
}
}
DefaultLegendValues(this.entryTitle, left, right)
}
else -> {
super.legendValues(stat, entry, style, groupName, context)
}
}
}
}
}
// Timed
override val objectIdentifier: ObjectIdentifier
get() = ObjectIdentifier(this.id, Session::class.java)
}

@ -1,18 +1,20 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.Realm
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.ObjectIdentifier
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.StatFormattingException
import net.pokeranalytics.android.calculus.TextFormat
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.Timed
import net.pokeranalytics.android.util.NULL_TEXT
import java.text.DateFormat
import java.util.*
@ -79,17 +81,13 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
var bbNet: BB = 0.0
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)
else -> throw StatFormattingException("format undefined for stat ${stat.name}")
val bbHourlyRate: BB
get() {
return this.bbNet / this.hourlyDuration
}
}
enum class Field(val identifier: String) {
START_DATE("startDate"),
RATED_NET("ratedNet"),
HOURLY_RATE("hourlyRate"),
BB_NET("bbNet"),
@ -104,11 +102,45 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
return realm.copyToRealm(sessionSet)
}
fun fieldNameForQueryType(queryCondition: QueryCondition): String? {
return "sessions." + Session.fieldNameForQueryType(queryCondition)
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? {
Session.fieldNameForQueryType(queryCondition)?.let {
return "sessions.$it"
}
return null
}
}
// Stat Base
override val entryTitle: String
get() {
return DateFormat.getDateInstance(DateFormat.SHORT).format(this.startDate)
}
override fun formattedValue(stat: Stat) : TextFormat {
return when (stat) {
Stat.NET_RESULT, Stat.AVERAGE -> stat.format(this.ratedNet, currency = null)
Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> stat.format(this.hourlyDuration, currency = null)
Stat.HOURLY_RATE -> stat.format(this.hourlyRate, currency = null)
Stat.HANDS_PLAYED -> stat.format(this.estimatedHands, currency = null)
Stat.HOURLY_RATE_BB -> stat.format(this.bbHourlyRate, currency = null)
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, currency = null)
} else {
return TextFormat(NULL_TEXT)
}
}
else -> throw StatFormattingException("format undefined for stat ${stat.name}")
}
}
// Timed
override val objectIdentifier: ObjectIdentifier
get() = ObjectIdentifier(this.id, SessionSet::class.java)
}

@ -5,6 +5,9 @@ import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.DatedValue
import net.pokeranalytics.android.model.interfaces.Manageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
@ -15,39 +18,47 @@ import java.util.*
import kotlin.collections.ArrayList
open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSource, RowRepresentable {
open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSource, RowRepresentable, Filterable, DatedValue {
companion object {
val rowRepresentation : List<RowRepresentable> by lazy {
val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.addAll(TransactionRow.values())
rows
}
fun fieldNameForQueryType(queryCondition: Class<out QueryCondition>): String? {
return when (queryCondition) {
QueryCondition.AnyBankroll::class.java -> "bankroll.id"
QueryCondition.StartedFromDate::class.java, QueryCondition.StartedToDate::class.java -> "date"
else -> null
}
}
}
@PrimaryKey
@PrimaryKey
override var id = UUID.randomUUID().toString()
// The bankroll of the transaction
var bankroll: Bankroll? = null
// The amount of the transaction
var amount: Double = 0.0
// The amount of the transaction
override var amount: Double = 0.0
// The date of the transaction
var date: Date = Date()
// The date of the transaction
override var date: Date = Date()
// The type of the transaction
var type: TransactionType? = null
// The type of the transaction
var type: TransactionType? = null
// A user comment
var comment: String = ""
// A user comment
var comment: String = ""
@Ignore
override val viewType: Int = RowViewType.ROW_TRANSACTION.ordinal
override fun updateValue(value: Any?, row: RowRepresentable) {
when(row) {
when (row) {
TransactionRow.BANKROLL -> bankroll = value as Bankroll?
TransactionRow.TYPE -> type = value as TransactionType?
TransactionRow.AMOUNT -> amount = if (value == null) 0.0 else (value as String).toDouble()
@ -85,5 +96,5 @@ open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSo
override fun getFailedDeleteMessage(): Int {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
}

@ -14,65 +14,77 @@ import kotlin.collections.ArrayList
open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable {
companion object {
val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.addAll(TransactionTypeRow.values())
rows
}
}
@PrimaryKey
override var id = UUID.randomUUID().toString()
// The name of the transaction type
override var name: String = ""
// Whether or not the amount is added, or subtracted to the bankroll total
var additive: Boolean = false
// Whether or not the type can be deleted by the user
var lock: Boolean = false
// The predefined kind, if necessary, like: Withdrawal, deposit, or tips
var kind: Int? = null
override fun getDisplayName(): String {
return this.name
}
override fun adapterRows(): List<RowRepresentable>? {
return TransactionType.rowRepresentation
}
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
SimpleRow.NAME -> this.name
else -> return super.stringForRow(row)
}
}
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
return row.editingDescriptors(mapOf("defaultValue" to this.name))
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
}
}
override fun isValidForDelete(realm: Realm): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getFailedDeleteMessage(): Int {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
enum class TransactionKind {
WITHDRAWAL,
DEPOSIT
enum class Value(val additive: Boolean) {
WITHDRAWAL(false),
DEPOSIT(true),
BONUS(true)
}
companion object {
val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.addAll(TransactionTypeRow.values())
rows
}
fun getByValue(value: Value, realm: Realm): TransactionType {
val type = realm.where(TransactionType::class.java).equalTo("kind", value.ordinal).findFirst()
type?.let {
return it
}
throw IllegalStateException("Transaction type ${value.name} should exist in database!")
}
}
@PrimaryKey
override var id = UUID.randomUUID().toString()
// The name of the transaction type
override var name: String = ""
// Whether or not the amount is added, or subtracted to the bankroll total
var additive: Boolean = false
// Whether or not the type can be deleted by the user
var lock: Boolean = false
// The predefined kind, if necessary, like: Withdrawal, deposit, or tips
var kind: Int? = null
override fun getDisplayName(): String {
return this.name
}
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
SimpleRow.NAME -> this.name
else -> return super.stringForRow(row)
}
}
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
return row.editingDescriptors(mapOf("defaultValue" to this.name))
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
}
}
override fun isValidForDelete(realm: Realm): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getFailedDeleteMessage(): Int {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

@ -3,24 +3,23 @@ package net.pokeranalytics.android.model.utils
import android.content.Context
import io.realm.Realm
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.Currency
import net.pokeranalytics.android.util.CurrencyUtils
import net.pokeranalytics.android.util.CurrencyUtils.Companion.getLocaleCurrency
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.UserDefaults
import java.util.*
class Seed(var context:Context) : Realm.Transaction {
override fun execute(realm: Realm) {
this.createDefaultGames(realm)
this.createDefaultTournamentFeatures(realm)
this.createDefaultCurrencyAndBankroll(realm)
this.createDefaultTransactionTypes(realm)
}
private fun createDefaultTournamentFeatures(realm: Realm) {
context.resources.getStringArray(R.array.seed_tournament_features).forEach {
context.resources.getStringArray(net.pokeranalytics.android.R.array.seed_tournament_features).forEach {
val tournamentFeature = TournamentFeature()
tournamentFeature.id = UUID.randomUUID().toString()
tournamentFeature.name = it
@ -29,23 +28,24 @@ class Seed(var context:Context) : Realm.Transaction {
}
private fun createDefaultCurrencyAndBankroll(realm: Realm) {
// Currency
val localeCurrency = getLocaleCurrency()
val localeCurrency = UserDefaults.getLocaleCurrency()
val defaultCurrency = Currency()
defaultCurrency.code = localeCurrency.currencyCode
realm.insertOrUpdate(defaultCurrency)
// Bankroll
val bankroll = Bankroll()
bankroll.name = context.resources.getString(R.string.live)
bankroll.name = context.resources.getString(net.pokeranalytics.android.R.string.live)
bankroll.live = true
bankroll.currency = realm.where<Currency>().equalTo("code", localeCurrency.currencyCode).findFirst()
realm.insertOrUpdate(bankroll)
}
private fun createDefaultGames(realm: Realm) {
val gamesName = context.resources.getStringArray(R.array.seed_games)
val gamesShortName = context.resources.getStringArray(R.array.seed_games_short_name)
val gamesName = context.resources.getStringArray(net.pokeranalytics.android.R.array.seed_games)
val gamesShortName = context.resources.getStringArray(net.pokeranalytics.android.R.array.seed_games_short_name)
for ((index, name) in gamesName.withIndex()) {
val game = Game()
game.id = UUID.randomUUID().toString()
@ -54,4 +54,15 @@ class Seed(var context:Context) : Realm.Transaction {
realm.insertOrUpdate(game)
}
}
private fun createDefaultTransactionTypes(realm: Realm) {
TransactionType.Value.values().forEachIndexed { index, value ->
val type = TransactionType()
type.additive = value.additive
type.kind = index
type.lock = true
realm.insertOrUpdate(type)
}
}
}

@ -0,0 +1,33 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
class BankrollActivity : PokerAnalyticsActivity() {
companion object {
fun newInstance(context: Context) {
val intent = Intent(context, BankrollActivity::class.java)
context.startActivity(intent)
}
/**
* Create a new instance for result
*/
fun newInstanceForResult(fragment: Fragment, requestCode: Int) {
val intent = Intent(fragment.requireContext(), BankrollActivity::class.java)
fragment.startActivityForResult(intent, requestCode)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_bankroll)
}
}

@ -0,0 +1,51 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_calendar_details.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.CalendarDetailsFragment
class CalendarDetailsActivity : PokerAnalyticsActivity() {
companion object {
var computedResults: ComputedResults? = null
var sessionTypeCondition: QueryCondition? = null
var detailsTitle: String? = null
/**
* Default constructor
*/
fun newInstance(context: Context, computedResults: ComputedResults, sessionTypeCondition: QueryCondition?, title: String?) {
this.computedResults = computedResults
this.sessionTypeCondition = sessionTypeCondition
this.detailsTitle = title
val intent = Intent(context, CalendarDetailsActivity::class.java)
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_calendar_details)
initUI()
}
/**
* Init UI
*/
private fun initUI() {
val calendarDetailsFragment = calendarDetailsFragment as CalendarDetailsFragment
calendarDetailsFragment.setData(computedResults, sessionTypeCondition, detailsTitle)
}
}

@ -0,0 +1,33 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
class ComparisonChartActivity : PokerAnalyticsActivity() {
companion object {
fun newInstance(context: Context) {
val intent = Intent(context, ComparisonChartActivity::class.java)
context.startActivity(intent)
}
/**
* Create a new instance for result
*/
fun newInstanceForResult(fragment: Fragment, requestCode: Int) {
val intent = Intent(fragment.requireContext(), ComparisonChartActivity::class.java)
fragment.startActivityForResult(intent, requestCode)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_comparison_chart)
}
}

@ -1,69 +0,0 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.github.mikephil.charting.data.Entry
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.GraphFragment
class GraphParameters(stat: Stat, entries: List<Entry>) {
var stat: Stat = stat
var entries: List<Entry> = entries
}
class GraphActivity : PokerAnalyticsActivity() {
private enum class IntentKey(val keyName: String) {
STAT("STAT"),
ENTRIES("ENTRIES"),
}
companion object {
// Unparcel fails when setting a custom Parcelable object on Entry so we use a static reference to passe objects
var parameters: GraphParameters? = null
/**
* Default constructor
*/
fun newInstance(context: Context, stat: Stat, entries: List<Entry>) {
GraphActivity.parameters = GraphParameters(stat, entries)
val intent = Intent(context, GraphActivity::class.java)
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_editable_data)
initUI()
}
/**
* Init UI
*/
private fun initUI() {
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val fragment = GraphFragment()
fragmentTransaction.add(R.id.container, fragment)
fragmentTransaction.commit()
GraphActivity.parameters?.let {
fragment.setData(it.stat, it.entries)
GraphActivity.parameters = null
} ?: run {
throw Exception("Missing graph parameters")
}
}
}

@ -32,20 +32,35 @@ class HomeActivity : PokerAnalyticsActivity() {
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
net.pokeranalytics.android.R.id.navigation_history -> {
//CLEAN
/*
R.id.navigation_history -> {
displayFragment(0)
return@OnNavigationItemSelectedListener true
}
net.pokeranalytics.android.R.id.navigation_stats -> {
R.id.navigation_stats -> {
displayFragment(1)
return@OnNavigationItemSelectedListener true
}
net.pokeranalytics.android.R.id.navigation_settings -> {
R.id.navigation_settings -> {
displayFragment(2)
return@OnNavigationItemSelectedListener true
}
*/
R.id.navigation_history -> {
displayFragment(0)
}
R.id.navigation_stats -> {
displayFragment(1)
}
R.id.navigation_calendar -> {
displayFragment(2)
}
R.id.navigation_reports -> {
displayFragment(3)
}
R.id.navigation_more -> {
displayFragment(4)
}
}
false
return@OnNavigationItemSelectedListener true
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -58,10 +73,11 @@ class HomeActivity : PokerAnalyticsActivity() {
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.home_menu, menu)
menu?.clear()
menuInflater.inflate(R.menu.toolbar_home, menu)
this.homeMenu = menu
//TODO: Change filter button visibility
homeMenu?.findItem(R.id.filter)?.isVisible = false
//TODO: Change queryWith button visibility
homeMenu?.findItem(R.id.filter)?.isVisible = true
return super.onCreateOptionsMenu(menu)
}
@ -97,7 +113,7 @@ class HomeActivity : PokerAnalyticsActivity() {
setSupportActionBar(toolbar)
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
navigation.selectedItemId = net.pokeranalytics.android.R.id.navigation_history
navigation.selectedItemId = R.id.navigation_history
val homePagerAdapter = HomePagerAdapter(supportFragmentManager)
viewPager.offscreenPageLimit = 5
@ -134,17 +150,40 @@ class HomeActivity : PokerAnalyticsActivity() {
*/
private fun updateToolbar(index: Int) {
when (index) {
//CLEAN
/*
0 -> {
toolbar.title = getString(R.string.feed)
//TODO: Change filter button visibility
homeMenu?.findItem(R.id.filter)?.isVisible = false
homeMenu?.findItem(R.id.queryWith)?.isVisible = false
}
1 -> {
toolbar.title = getString(R.string.stats)
homeMenu?.findItem(R.id.filter)?.isVisible = false
homeMenu?.findItem(R.id.queryWith)?.isVisible = false
}
2 -> {
toolbar.title = getString(R.string.services)
homeMenu?.findItem(R.id.queryWith)?.isVisible = false
}
*/
0 -> {
toolbar.title = getString(R.string.feed)
homeMenu?.findItem(R.id.filter)?.isVisible = true
}
1 -> {
toolbar.title = getString(R.string.stats)
homeMenu?.findItem(R.id.filter)?.isVisible = true
}
2 -> {
toolbar.title = getString(R.string.calendar)
homeMenu?.findItem(R.id.filter)?.isVisible = false
}
3 -> {
toolbar.title = getString(R.string.reports)
homeMenu?.findItem(R.id.filter)?.isVisible = false
}
4 -> {
toolbar.title = getString(R.string.more)
homeMenu?.findItem(R.id.filter)?.isVisible = false
}
}
@ -171,18 +210,15 @@ class HomeActivity : PokerAnalyticsActivity() {
.setCancelable(true)
.setItems(choices.toTypedArray()) { _, which ->
Timber.d("Click on $which")
when(which) {
when (which) {
0 -> FiltersActivity.newInstance(this@HomeActivity)
}
}
.setNegativeButton(R.string.cancel) { _, _ ->
Timber.d("Click on cancel")
}
builder.show()
}
}

@ -0,0 +1,55 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.ReportDetailsFragment
class ReportDetailsActivity : PokerAnalyticsActivity() {
companion object {
// Unparcel fails when setting a custom Parcelable object on Entry so we use a static reference to passe objects
private var report: Report? = null
private var reportTitle: String = ""
/**
* Default constructor
*/
fun newInstance(context: Context, report: Report, reportTitle: String) {
//parameters = GraphParameters(stat, group, report)
this.report = report
this.reportTitle = reportTitle
val intent = Intent(context, ReportDetailsActivity::class.java)
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_report_details)
initUI()
}
/**
* Init UI
*/
private fun initUI() {
report?.let {
val fragmentTransaction = supportFragmentManager.beginTransaction()
val reportDetailsFragment = ReportDetailsFragment.newInstance(it, reportTitle)
fragmentTransaction.add(R.id.reportDetailsContainer, reportDetailsFragment)
fragmentTransaction.commit()
report = null
}
}
}

@ -0,0 +1,33 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
class SettingsActivity : PokerAnalyticsActivity() {
companion object {
fun newInstance(context: Context) {
val intent = Intent(context, SettingsActivity::class.java)
context.startActivity(intent)
}
/**
* Create a new instance for result
*/
fun newInstanceForResult(fragment: Fragment, requestCode: Int) {
val intent = Intent(fragment.requireContext(), SettingsActivity::class.java)
fragment.startActivityForResult(intent, requestCode)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
}
}

@ -0,0 +1,59 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputableGroup
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.StatisticDetailsFragment
class StatisticsDetailsParameters(var stat: Stat, var computableGroup: ComputableGroup, var report: Report)
class StatisticDetailsActivity : PokerAnalyticsActivity() {
companion object {
// Unparcel fails when setting a custom Parcelable object on Entry so we use a static reference to passe objects
private var parameters: StatisticsDetailsParameters? = null
private var displayAggregationChoices: Boolean = true
/**
* Default constructor
*/
fun newInstance(context: Context, stat: Stat, group: ComputableGroup, report: Report, displayAggregationChoices: Boolean = true) {
parameters = StatisticsDetailsParameters(stat, group, report)
this.displayAggregationChoices = displayAggregationChoices
val intent = Intent(context, StatisticDetailsActivity::class.java)
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_statistic_details)
initUI()
}
/**
* Init UI
*/
private fun initUI() {
val fragmentTransaction = supportFragmentManager.beginTransaction()
val statisticDetailsFragment = StatisticDetailsFragment()
fragmentTransaction.add(R.id.statisticDetailsContainer, statisticDetailsFragment)
fragmentTransaction.commit()
parameters?.let {
statisticDetailsFragment.setData(it.stat, it.computableGroup, it.report, displayAggregationChoices)
parameters = null
}
}
}

@ -0,0 +1,69 @@
package net.pokeranalytics.android.ui.adapter
import android.content.Context
import android.util.SparseArray
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.fragment.CalendarFragment
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.fragment.HistoryFragment
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import java.lang.ref.WeakReference
/**
* Comparison Chart Pager Adapter
*/
class ComparisonChartPagerAdapter(val context: Context, fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager) {
var weakReferences = SparseArray<WeakReference<PokerAnalyticsFragment>>()
override fun getItem(position: Int): PokerAnalyticsFragment {
return when (position) {
0 -> GraphFragment()
1 -> GraphFragment()
2 -> CalendarFragment.newInstance()
else -> HistoryFragment.newInstance()
}
}
override fun getCount(): Int {
return 3
}
override fun getPageTitle(position: Int): CharSequence? {
return when(position) {
0 -> context.getString(R.string.bar)
1 -> context.getString(R.string.line)
2-> context.getString(R.string.table)
else -> super.getPageTitle(position)
}
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
super.destroyItem(container, position, `object`)
weakReferences.remove(position)
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val fragment = super.instantiateItem(container, position) as PokerAnalyticsFragment
weakReferences.put(position, WeakReference(fragment))
return fragment
}
override fun getItemPosition(obj: Any): Int {
return POSITION_UNCHANGED
}
/**
* Return the fragment at the position key
*/
fun getFragment(key: Int): PokerAnalyticsFragment? {
if (weakReferences.get(key) != null) {
return weakReferences.get(key).get()
}
return null
}
}

@ -160,9 +160,9 @@ class HistorySessionRowRepresentableAdapter(
// Add headers if the date doesn't exist yet
for ((index, session) in realmResults.withIndex()) {
calendar.time = session.creationDate
calendar.time = session.startDate ?: session.creationDate
if (checkHeaderCondition(calendar, previousYear, previousMonth)) {
headersPositions[index + headersPositions.size + pendingRealmResults.size] = session.creationDate
headersPositions[index + headersPositions.size + pendingRealmResults.size] = session.startDate ?: session.creationDate
previousYear = calendar.get(Calendar.YEAR)
previousMonth = calendar.get(Calendar.MONTH)
}

@ -4,9 +4,7 @@ import android.util.SparseArray
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import net.pokeranalytics.android.ui.fragment.HistoryFragment
import net.pokeranalytics.android.ui.fragment.SettingsFragment
import net.pokeranalytics.android.ui.fragment.StatsFragment
import net.pokeranalytics.android.ui.fragment.*
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import java.lang.ref.WeakReference
@ -20,14 +18,16 @@ class HomePagerAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAda
override fun getItem(position: Int): PokerAnalyticsFragment {
return when (position) {
0 -> HistoryFragment.newInstance()
1 -> StatsFragment.newInstance()
2 -> SettingsFragment.newInstance()
1 -> StatisticsFragment.newInstance()
2 -> CalendarFragment.newInstance()
3 -> ReportsFragment.newInstance()
4 -> MoreFragment.newInstance()
else -> HistoryFragment.newInstance()
}
}
override fun getCount(): Int {
return 3
return 5
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
@ -42,11 +42,12 @@ class HomePagerAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAda
}
override fun getItemPosition(obj: Any): Int {
val fragment = obj as PokerAnalyticsFragment
return when (fragment) {
return when (obj) {
HistoryFragment::class.java -> 0
StatsFragment::class.java -> 1
SettingsFragment::class.java -> 2
StatisticsFragment::class.java -> 1
CalendarFragment::class.java -> 2
ReportsFragment::class.java -> 3
MoreFragment::class.java -> 4
else -> -1
}
}

@ -0,0 +1,78 @@
package net.pokeranalytics.android.ui.adapter
import android.content.Context
import android.util.SparseArray
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.viewpager.widget.PagerAdapter
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.fragment.TableReportFragment
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import java.lang.ref.WeakReference
/**
* Home Adapter
*/
class ReportPagerAdapter(val context: Context, val fragmentManager: FragmentManager, private val report: Report) : FragmentStatePagerAdapter(fragmentManager) {
var weakReferences = SparseArray<WeakReference<PokerAnalyticsFragment>>()
override fun getItem(position: Int): PokerAnalyticsFragment {
return when (position) {
0 -> {
val dataSetList = listOf(report.barEntries(null, context))
GraphFragment.newInstance(barDataSets = dataSetList, style = GraphFragment.Style.BAR)
}
1 -> {
val dataSetList = report.multiLineEntries(context = context)
GraphFragment.newInstance(lineDataSets = dataSetList, style = GraphFragment.Style.MULTILINE)
}
2 -> {
TableReportFragment.newInstance(report)
}
else -> PokerAnalyticsFragment()
}
}
override fun getCount(): Int {
return 3
}
override fun getPageTitle(position: Int): CharSequence? {
return when(position) {
0 -> context.getString(R.string.bar)
1 -> context.getString(R.string.line)
2 -> context.getString(R.string.table)
else -> ""
}
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
super.destroyItem(container, position, `object`)
weakReferences.remove(position)
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val fragment = super.instantiateItem(container, position) as PokerAnalyticsFragment
weakReferences.put(position, WeakReference(fragment))
return fragment
}
override fun getItemPosition(obj: Any): Int {
return PagerAdapter.POSITION_UNCHANGED
}
/**
* Return the fragment at the position key
*/
fun getFragment(key: Int): PokerAnalyticsFragment? {
if (weakReferences.get(key) != null) {
return weakReferences.get(key).get()
}
return null
}
}

@ -0,0 +1,16 @@
package net.pokeranalytics.android.ui.extensions
import com.google.android.material.chip.ChipGroup
class ChipGroupExtension {
open class SingleSelectionOnCheckedListener : ChipGroup.OnCheckedChangeListener {
override fun onCheckedChanged(group: ChipGroup, checkedId: Int) {
for (i in 0 until group.childCount) {
val chip = group.getChildAt(i)
chip.isClickable = chip.id != group.checkedChipId
}
}
}
}

@ -1,20 +1,30 @@
package net.pokeranalytics.android.util.extensions
package net.pokeranalytics.android.ui.extensions
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.net.Uri
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.AppCompatTextView
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.isVisible
import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.TextFormat
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.util.DeviceUtils
import net.pokeranalytics.android.util.URL
import java.io.File
// Sizes
@ -59,20 +69,33 @@ fun PokerAnalyticsActivity.openPlayStorePage() {
}
// Open email for "Contact us"
fun PokerAnalyticsActivity.openContactMail(subjectStringRes: Int) {
fun PokerAnalyticsActivity.openContactMail(subjectStringRes: Int, filePath: String?= null) {
val info = "v${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}), Android ${android.os.Build.VERSION.SDK_INT}, ${DeviceUtils.getDeviceName()}"
val intent = Intent(Intent.ACTION_SENDTO)
intent.data = Uri.parse("mailto:${URL.SUPPORT_EMAIL.value}")
intent.putExtra(Intent.EXTRA_SUBJECT, getString(subjectStringRes))
intent.putExtra(Intent.EXTRA_EMAIL, URL.SUPPORT_EMAIL.value)
intent.putExtra(Intent.EXTRA_TEXT, "\n\n$info")
startActivity(Intent.createChooser(intent, getString(R.string.contact)))
val emailIntent = Intent(Intent.ACTION_SEND)
filePath?.let {
val databaseFile = File(it)
val contentUri = FileProvider.getUriForFile(this, "net.pokeranalytics.android.fileprovider", databaseFile)
if (contentUri != null) {
emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
emailIntent.setDataAndType(contentUri, contentResolver.getType(contentUri))
emailIntent.putExtra(Intent.EXTRA_STREAM, contentUri)
}
}
emailIntent.type = "message/rfc822"
emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(subjectStringRes))
emailIntent.putExtra(Intent.EXTRA_TEXT, "\n\n$info")
emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(URL.SUPPORT_EMAIL.value))
startActivity(Intent.createChooser(emailIntent, getString(R.string.contact)))
}
// Open custom tab
fun PokerAnalyticsActivity.openUrl(url: String) {
val builder: CustomTabsIntent.Builder = CustomTabsIntent.Builder()
builder.setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary))
builder.setToolbarColor(ContextCompat.getColor(this, net.pokeranalytics.android.R.color.colorPrimary))
val customTabsIntent = builder.build()
customTabsIntent.launchUrl(this, Uri.parse(url))
}
@ -98,6 +121,23 @@ fun showAlertDialog(context: Context, title: Int? = null, message: Int? = null)
message?.let {
builder.setMessage(message)
}
builder.setPositiveButton(R.string.ok, null)
builder.setPositiveButton(net.pokeranalytics.android.R.string.ok, null)
builder.show()
}
fun AppCompatTextView.setTextFormat(textFormat: TextFormat, context: Context) {
this.setTextColor(textFormat.getColor(context))
this.text = textFormat.text
}
fun View.hideWithAnimation() {
isVisible = true
animate().cancel()
animate().alpha(0f).withEndAction { isVisible = false }.start()
}
fun View.showWithAnimation() {
isVisible = true
animate().cancel()
animate().alpha(1f).start()
}

@ -17,9 +17,10 @@ import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.BankrollRow
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.util.CurrencyUtils
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.toCurrency
import net.pokeranalytics.android.util.extensions.toRate
import retrofit2.Call
import retrofit2.Response
import java.util.*
@ -94,9 +95,12 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
NULL_TEXT
}
}
BankrollRow.INITIAL_VALUE -> {
this.bankroll.initialValue.toCurrency()
}
BankrollRow.RATE -> {
val rate = this.bankroll.currency?.rate ?: 1.0
CurrencyUtils.getCurrencyRateFormatter().format(rate)
rate.toRate()
}
else -> super.stringForRow(row)
}
@ -113,6 +117,9 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
return when (row) {
SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.bankroll.name))
BankrollRow.INITIAL_VALUE -> {
row.editingDescriptors(mapOf("defaultValue" to this.bankroll.initialValue))
}
BankrollRow.RATE -> {
val rate = this.bankroll.currency?.rate
row.editingDescriptors(mapOf("defaultValue" to rate))
@ -145,7 +152,7 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
*/
private fun initData() {
defaultCurrency = Currency.getInstance(Preferences.getCurrencyLocale(this.parentActivity))
defaultCurrency = UserDefaults.currency
if (!isUpdating) {
bankroll.currency = net.pokeranalytics.android.model.realm.Currency()
@ -162,6 +169,7 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS
rows.clear()
rows.add(SimpleRow.NAME)
rows.add(BankrollRow.LIVE)
rows.add(BankrollRow.INITIAL_VALUE)
rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.currency))
rows.add(BankrollRow.CURRENCY)
if (this.shouldShowCurrencyRate) {

@ -0,0 +1,73 @@
package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_bankroll.*
import kotlinx.android.synthetic.main.fragment_stats.recyclerView
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
class BankrollFragment : PokerAnalyticsFragment() {
companion object {
/**
* Create new instance
*/
fun newInstance(): BankrollFragment {
val fragment = BankrollFragment()
val bundle = Bundle()
fragment.arguments = bundle
return fragment
}
}
private lateinit var parentActivity: PokerAnalyticsActivity
// Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_bankroll, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initData()
initUI()
}
// Business
/**
* Init data
*/
private fun initData() {
}
/**
* Init UI
*/
private fun initUI() {
parentActivity = activity as PokerAnalyticsActivity
parentActivity.setSupportActionBar(toolbar)
parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
setHasOptionsMenu(true)
val viewManager = LinearLayoutManager(requireContext())
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
//adapter = statsAdapter
}
}
}

@ -0,0 +1,224 @@
package net.pokeranalytics.android.ui.fragment
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.tabs.TabLayout
import io.realm.Realm
import kotlinx.android.synthetic.main.fragment_calendar_details.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.activity.StatisticDetailsActivity
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.GraphRow
import net.pokeranalytics.android.ui.view.rowrepresentable.StatDoubleRow
import timber.log.Timber
import java.util.*
import kotlin.collections.ArrayList
class CalendarDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
companion object {
fun newInstance(context: Context) {
val intent = Intent(context, CalendarDetailsFragment::class.java)
context.startActivity(intent)
}
}
private lateinit var parentActivity: PokerAnalyticsActivity
private lateinit var statsAdapter: RowRepresentableAdapter
private var title: String? = ""
private var computedResults: ComputedResults? = null
private var sessionTypeCondition: QueryCondition? = null
private var rowRepresentables: ArrayList<RowRepresentable> = ArrayList()
//private var stat: Stat = Stat.NET_RESULT
//private var entries: List<Entry> = ArrayList()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_calendar_details, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUI()
}
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentables
}
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) {
when (row) {
is GraphRow -> {
//TODO: Open graph details
row.report.results.firstOrNull()?.group?.let { computableGroup ->
StatisticDetailsActivity.newInstance(requireContext(), row.stat, computableGroup, row.report, false)
}
}
}
}
/**
* Init UI
*/
private fun initUI() {
parentActivity = activity as PokerAnalyticsActivity
// Avoid a bug during setting the title
toolbar.title = ""
parentActivity.setSupportActionBar(toolbar)
parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
setHasOptionsMenu(true)
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
when (tab.position) {
0 -> sessionTypeCondition = null
1 -> sessionTypeCondition = QueryCondition.IsCash
2 -> sessionTypeCondition = QueryCondition.IsTournament
}
launchStatComputation()
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
statsAdapter = RowRepresentableAdapter(this, this)
val viewManager = LinearLayoutManager(requireContext())
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = statsAdapter
}
}
/**
* Display data
*/
private fun displayData() {
title?.let {
toolbar.title = it
}
sessionTypeCondition?.let {
when (it) {
QueryCondition.IsCash -> tabs.getTabAt(1)?.select()
QueryCondition.IsTournament -> tabs.getTabAt(2)?.select()
else -> tabs.getTabAt(0)?.select()
}
}
}
/**
* Launch stat computation
*/
private fun launchStatComputation() {
progressBar.isVisible = true
progressBar.animate().alpha(1f).start()
recyclerView.animate().alpha(0f).start()
computedResults?.let { computedResults ->
GlobalScope.launch {
val startDate = Date()
val realm = Realm.getDefaultInstance()
val conditions = ArrayList<QueryCondition>().apply {
addAll(computedResults.group.conditions)
// Remove session type conditions
removeAll(Criteria.Cash.queryConditions)
removeAll(Criteria.Tournament.queryConditions)
when (sessionTypeCondition) {
QueryCondition.IsCash -> addAll(Criteria.Cash.queryConditions)
QueryCondition.IsTournament -> addAll(Criteria.Tournament.queryConditions)
}
}
val requiredStats: List<Stat> = listOf(Stat.LOCATIONS_PLAYED, Stat.LONGEST_STREAKS, Stat.DAYS_PLAYED, Stat.STANDARD_DEVIATION_HOURLY)
val options = Calculator.Options(evolutionValues = Calculator.Options.EvolutionValues.STANDARD, stats = requiredStats)
val report = Calculator.computeStatsWithComparators(realm, conditions = conditions, options = options)
Timber.d("Report take: ${System.currentTimeMillis() - startDate.time}ms")
report.results.firstOrNull()?.let {
// Create rows
rowRepresentables.clear()
rowRepresentables.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = R.string.net_result))
rowRepresentables.add(GraphRow(report, Stat.NET_RESULT))
rowRepresentables.add(StatDoubleRow(it.computedStat(Stat.NET_RESULT), it.computedStat(Stat.HOURLY_RATE)))
rowRepresentables.add(StatDoubleRow(it.computedStat(Stat.LOCATIONS_PLAYED), it.computedStat(Stat.LONGEST_STREAKS)))
rowRepresentables.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = R.string.distribution))
rowRepresentables.add(GraphRow(report, Stat.STANDARD_DEVIATION))
rowRepresentables.add(StatDoubleRow(it.computedStat(Stat.WIN_RATIO), it.computedStat(Stat.MAXIMUM_NETRESULT)))
rowRepresentables.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, resId = R.string.volume))
rowRepresentables.add(GraphRow(report, Stat.HOURLY_DURATION))
rowRepresentables.add(StatDoubleRow(it.computedStat(Stat.HOURLY_DURATION), it.computedStat(Stat.AVERAGE_HOURLY_DURATION)))
rowRepresentables.add(StatDoubleRow(it.computedStat(Stat.DAYS_PLAYED), it.computedStat(Stat.MAXIMUM_DURATION)))
}
launch(Dispatchers.Main) {
statsAdapter.notifyDataSetChanged()
progressBar.animate().cancel()
progressBar.animate().alpha(0f).withEndAction { progressBar.isVisible = false }.start()
recyclerView.animate().cancel()
recyclerView.animate().alpha(1f).start()
}
}
}
}
/**
* Set data
*/
fun setData(computedResults: ComputedResults?, sessionTypeCondition: QueryCondition?, title: String?) {
this.computedResults = computedResults
this.sessionTypeCondition = sessionTypeCondition
this.title = title
displayData()
launchStatComputation()
}
}

@ -0,0 +1,388 @@
package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.tabs.TabLayout
import io.realm.Realm
import kotlinx.android.synthetic.main.fragment_calendar.*
import kotlinx.android.synthetic.main.fragment_stats.recyclerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.activity.CalendarDetailsActivity
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.hideWithAnimation
import net.pokeranalytics.android.ui.extensions.showWithAnimation
import net.pokeranalytics.android.ui.fragment.components.SessionObserverFragment
import net.pokeranalytics.android.ui.view.CalendarTabs
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.util.extensions.*
import timber.log.Timber
import java.util.*
import kotlin.coroutines.CoroutineContext
class CalendarFragment : SessionObserverFragment(), CoroutineScope, StaticRowRepresentableDataSource, RowRepresentableDelegate {
enum class TimeFilter {
MONTH, YEAR
}
companion object {
/**
* Create new instance
*/
fun newInstance(): CalendarFragment {
val fragment = CalendarFragment()
val bundle = Bundle()
fragment.arguments = bundle
return fragment
}
}
private lateinit var calendarAdapter: RowRepresentableAdapter
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
private var rows: ArrayList<CustomizableRowRepresentable> = ArrayList()
private var sortedMonthlyReports: SortedMap<Date, ComputedResults> = HashMap<Date, ComputedResults>().toSortedMap()
private var sortedYearlyReports: SortedMap<Date, ComputedResults> = HashMap<Date, ComputedResults>().toSortedMap()
private var datesForRows: HashMap<CustomizableRowRepresentable, Date> = HashMap()
private var sessionTypeCondition: QueryCondition? = null
private var currentTimeFilter: TimeFilter = TimeFilter.MONTH
private var currentStat = Stat.NET_RESULT
// Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(net.pokeranalytics.android.R.layout.fragment_calendar, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initData()
initUI()
launchStatComputation()
}
override fun adapterRows(): List<RowRepresentable>? {
return rows
}
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) {
when (currentTimeFilter) {
TimeFilter.MONTH -> {
val date = datesForRows[row]
sortedMonthlyReports[datesForRows[row]]?.let {
CalendarDetailsActivity.newInstance(requireContext(), it, sessionTypeCondition, date?.getMonthAndYear())
}
}
TimeFilter.YEAR -> {
val date = datesForRows[row]
sortedYearlyReports[datesForRows[row]]?.let {
CalendarDetailsActivity.newInstance(requireContext(), it, sessionTypeCondition, date?.getDateYear())
}
}
}
}
override fun sessionsChanged() {
launchStatComputation()
}
// Business
/**
* Init data
*/
private fun initData() {
}
/**
* Init UI
*/
private fun initUI() {
CalendarTabs.values().forEach {
val tab = tabs.newTab()
tab.text = getString(it.resId)
tabs.addTab(tab)
}
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
when (tab.position) {
0 -> currentStat = Stat.NET_RESULT
1 -> currentStat = Stat.HOURLY_RATE
2 -> currentStat = Stat.NUMBER_OF_GAMES
3 -> currentStat = Stat.WIN_RATIO
4 -> currentStat = Stat.STANDARD_DEVIATION_HOURLY
5 -> currentStat = Stat.AVERAGE
6 -> currentStat = Stat.AVERAGE_HOURLY_DURATION
7 -> currentStat = Stat.HOURLY_DURATION
}
displayData()
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
// Manage session type queryWith
filterSessionAll.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
sessionTypeCondition = null
filterSessionCash.isChecked = false
filterSessionTournament.isChecked = false
launchStatComputation()
} else if (sessionTypeCondition == null) {
filterSessionAll.isChecked = true
}
}
filterSessionCash.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
sessionTypeCondition = QueryCondition.IsCash
filterSessionAll.isChecked = false
filterSessionTournament.isChecked = false
launchStatComputation()
} else if (sessionTypeCondition == QueryCondition.IsCash) {
filterSessionCash.isChecked = true
}
}
filterSessionTournament.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
sessionTypeCondition = QueryCondition.IsTournament
filterSessionAll.isChecked = false
filterSessionCash.isChecked = false
launchStatComputation()
} else if (sessionTypeCondition == QueryCondition.IsTournament) {
filterSessionTournament.isChecked = true
}
}
// Manage time queryWith
filterTimeMonth.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
currentTimeFilter = TimeFilter.MONTH
filterTimeYear.isChecked = false
displayData()
} else if (currentTimeFilter == TimeFilter.MONTH) {
filterTimeMonth.isChecked = true
}
}
filterTimeYear.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
currentTimeFilter = TimeFilter.YEAR
filterTimeMonth.isChecked = false
displayData()
} else if (currentTimeFilter == TimeFilter.YEAR) {
filterTimeYear.isChecked = true
}
}
val viewManager = LinearLayoutManager(requireContext())
calendarAdapter = RowRepresentableAdapter(this, this)
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = calendarAdapter
}
}
/**
* Launch stat computation
*/
private fun launchStatComputation() {
progressBar?.showWithAnimation()
recyclerView?.hideWithAnimation()
GlobalScope.launch {
val calendar = Calendar.getInstance()
calendar.time = Date().startOfMonth()
val startDate = Date()
val realm = Realm.getDefaultInstance()
val monthlyReports: HashMap<Date, ComputedResults> = HashMap()
val yearlyReports: HashMap<Date, ComputedResults> = HashMap()
val requiredStats: List<Stat> = listOf(Stat.LOCATIONS_PLAYED, Stat.LONGEST_STREAKS, Stat.DAYS_PLAYED, Stat.STANDARD_DEVIATION_HOURLY )
val options = Calculator.Options(evolutionValues = Calculator.Options.EvolutionValues.STANDARD, stats = requiredStats)
// Compute data per AnyYear and AnyMonthOfYear
println(">>>> ${Criteria.MonthsOfYear.queryConditions.map { it.id }}")
val monthConditions = when (sessionTypeCondition) {
QueryCondition.IsCash -> listOf(Criteria.Years, Criteria.MonthsOfYear, Criteria.Cash).combined()
QueryCondition.IsTournament -> listOf(Criteria.Years, Criteria.MonthsOfYear, Criteria.Tournament).combined()
else -> listOf(Criteria.Years, Criteria.MonthsOfYear).combined()
}
monthConditions.forEach { conditions ->
val report = Calculator.computeStatsWithComparators(realm, conditions = conditions, options = options)
report.results.forEach { computedResults ->
if (!computedResults.isEmpty) {
// Set date data
conditions.forEach { condition ->
when (condition) {
is QueryCondition.AnyYear -> calendar.set(Calendar.YEAR, condition.listOfValues.first())
is QueryCondition.AnyMonthOfYear -> calendar.set(Calendar.MONTH, condition.listOfValues.first())
}
}
monthlyReports[calendar.time] = computedResults
}
}
}
calendar.time = Date().startOfYear()
// Compute data per AnyYear
val yearConditions = when (sessionTypeCondition) {
QueryCondition.IsCash -> listOf(Criteria.Years, Criteria.Cash).combined()
QueryCondition.IsTournament -> listOf(Criteria.Years, Criteria.Tournament).combined()
else -> listOf(Criteria.Years).combined()
}
yearConditions.forEach { conditions ->
val report = Calculator.computeStatsWithComparators(realm, conditions = conditions, options = options)
report.results.forEach { computedResults ->
if (!computedResults.isEmpty) {
// Set date data
conditions.forEach { condition ->
when (condition) {
is QueryCondition.AnyYear -> calendar.set(Calendar.YEAR, condition.listOfValues.first())
}
}
yearlyReports[calendar.time] = computedResults
}
}
}
sortedMonthlyReports = monthlyReports.toSortedMap(compareByDescending { it })
sortedYearlyReports = yearlyReports.toSortedMap(compareByDescending { it })
Timber.d("Computation: ${System.currentTimeMillis() - startDate.time}ms")
// Logs
/*
Timber.d("========== AnyYear x AnyMonthOfYear")
sortedMonthlyReports.keys.forEach {
Timber.d("$it => ${sortedMonthlyReports[it]?.computedStat(Stat.NET_RESULT)?.value}")
}
Timber.d("========== YEARLY")
sortedYearlyReports.keys.forEach {
Timber.d("$it => ${sortedYearlyReports[it]?.computedStat(Stat.NET_RESULT)?.value}")
}
*/
realm.close()
GlobalScope.launch(Dispatchers.Main) {
displayData()
}
}
}
/**
* Display data
*/
private fun displayData() {
val startDate = Date()
datesForRows.clear()
rows.clear()
when (currentTimeFilter) {
// Create monthly reports
TimeFilter.MONTH -> {
val years: ArrayList<String> = ArrayList()
sortedMonthlyReports.keys.forEach { date ->
if (!years.contains(date.getDateYear())) {
years.add(date.getDateYear())
rows.add(
CustomizableRowRepresentable(
customViewType = RowViewType.HEADER_TITLE,
title = date.getDateYear()
)
)
}
sortedMonthlyReports[date]?.computedStat(currentStat)?.let { computedStat ->
val row = CustomizableRowRepresentable(
customViewType = RowViewType.TITLE_VALUE_ARROW,
title = date.getDateMonth(),
computedStat = computedStat,
isSelectable = true
)
rows.add(row)
datesForRows.put(row, date)
}
}
}
// Create yearly reports
TimeFilter.YEAR -> {
sortedYearlyReports.keys.forEach { date ->
sortedYearlyReports[date]?.computedStat(currentStat)?.let { computedStat ->
val row = CustomizableRowRepresentable(
customViewType = RowViewType.TITLE_VALUE_ARROW,
title = date.getDateYear(),
computedStat = computedStat,
isSelectable = true
)
rows.add(row)
datesForRows.put(row, date)
}
}
}
}
Timber.d("Display data: ${System.currentTimeMillis() - startDate.time}ms")
Timber.d("Rows: ${rows.size}")
calendarAdapter.notifyDataSetChanged()
progressBar?.hideWithAnimation()
recyclerView?.showWithAnimation()
}
}

@ -0,0 +1,123 @@
package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.*
import kotlinx.android.synthetic.main.fragment_comparison_chart.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.BankrollActivity
import net.pokeranalytics.android.ui.activity.SettingsActivity
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.adapter.ComparisonChartPagerAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.toast
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.MoreTabRow
class ComparisonChartFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
companion object {
/**
* Create new instance
*/
fun newInstance(): ComparisonChartFragment {
val fragment = ComparisonChartFragment()
val bundle = Bundle()
fragment.arguments = bundle
return fragment
}
val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.addAll(MoreTabRow.values())
rows
}
}
private lateinit var parentActivity: PokerAnalyticsActivity
private lateinit var viewPagerAdapter: ComparisonChartPagerAdapter
private var comparisonChartMenu: Menu? = null
// Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_comparison_chart, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initData()
initUI()
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
menu?.clear()
inflater?.inflate(R.menu.toolbar_comparison_chart, menu)
this.comparisonChartMenu = menu
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item!!.itemId) {
R.id.settings -> openChangeStatistics()
}
return true
}
// Rows
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) {
super.onRowSelected(position, row, fromAction)
when(row) {
MoreTabRow.BANKROLL -> BankrollActivity.newInstance(requireContext())
MoreTabRow.SETTINGS -> SettingsActivity.newInstance(requireContext())
}
}
// Business
/**
* Init data
*/
private fun initData() {
}
/**
* Init UI
*/
private fun initUI() {
parentActivity = activity as PokerAnalyticsActivity
toolbar.title = ""
parentActivity.setSupportActionBar(toolbar)
parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
setHasOptionsMenu(true)
toolbar.title = "Comparison chart"
viewPagerAdapter = ComparisonChartPagerAdapter(requireContext(), parentActivity.supportFragmentManager)
viewPager.adapter = viewPagerAdapter
viewPager.offscreenPageLimit = 2
tabs.setupWithViewPager(viewPager)
}
/**
* Open change statistics
*/
private fun openChangeStatistics() {
//TODO
toast("Open change statistics")
}
}

@ -16,8 +16,7 @@ import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRowRepresentable
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.ui.view.rowrepresentable.SeparatorRow
import java.util.*
class CurrenciesFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
@ -29,7 +28,7 @@ class CurrenciesFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataS
val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.addAll(mostUsedCurrencies)
rows.add(SeparatorRowRepresentable())
rows.add(SeparatorRow())
rows.addAll(availableCurrencies)
rows
}
@ -69,7 +68,7 @@ class CurrenciesFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataS
}
var currencyCode: String = currency.currencyCode
var currencySymbole: String = currency.getSymbol(Preferences.currencyLocale)
var currencySymbole: String = currency.getSymbol(Locale.getDefault())
var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbole})"
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal

@ -52,7 +52,8 @@ open class EditableDataFragment : PokerAnalyticsFragment(), RowRepresentableDele
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.editable_data, menu)
menu?.clear()
inflater?.inflate(R.menu.toolbar_editable_data, menu)
this.editableMenu = menu
updateMenuUI()
super.onCreateOptionsMenu(menu, inflater)
@ -158,12 +159,6 @@ open class EditableDataFragment : PokerAnalyticsFragment(), RowRepresentableDele
finishActivityWithResult(uniqueIdentifier)
}
// if (managedItem is Bankroll) {
// managedItem.currency?.refreshRelatedRatedValues(it)
// }
//
// val uniqueIdentifier = (managedItem as Savable).id
// finishActivityWithResult(uniqueIdentifier)
}
}
else -> {

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_filter_details.*
import kotlinx.android.synthetic.main.fragment_filter_details.view.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.ui.activity.FilterDetailsActivity
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
@ -27,24 +28,25 @@ import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.shortDate
import net.pokeranalytics.android.util.extensions.shortTime
import net.pokeranalytics.android.util.extensions.toMinutes
import timber.log.Timber
import java.util.*
import kotlin.collections.ArrayList
open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
lateinit var parentActivity: PokerAnalyticsActivity
lateinit var rowRepresentableAdapter: RowRepresentableAdapter
private lateinit var primaryKey: String
private lateinit var filterCategoryRow: FilterCategoryRow
private var currentFilter: Filter? = null
private var rows: ArrayList<RowRepresentable> = ArrayList()
private var rowsForFilterSubcategoryRow: HashMap<FilterSectionRow, ArrayList<RowRepresentable>> = HashMap()
private var primaryKey: String? = null
private var filterMenu: Menu? = null
private var filterCategoryRow: FilterCategoryRow? = null
private val selectedRows = ArrayList<FilterElementRow>()
private val selectedRows = ArrayList<QueryCondition>()
private var isUpdating = false
private var shouldOpenKeyboard = true
@ -68,48 +70,33 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresenta
Timber.d("Row: $row")
if (row.viewType == RowViewType.TITLE_CHECK.ordinal) {
updateRowsSelection(row)
return
}
when (row) {
is FilterElementRow.DateFilterElementRow -> DateTimePickerManager.create(requireContext(), row, this, row.dateValue, onlyDate = true)
is FilterElementRow.PastDays -> {
val pastDays = if (row.lastDays > 0) row.lastDays.toString() else ""
val data = row.editingDescriptors(mapOf("pastDays" to pastDays))
BottomSheetFragment.create(fragmentManager, row, this, data, true)
}
is FilterElementRow.LastGames -> {
val lastGames = if (row.lastGames > 0) row.lastGames.toString() else ""
val data = row.editingDescriptors(mapOf("lastGames" to lastGames))
BottomSheetFragment.create(fragmentManager, row, this, data, true)
}
is FilterElementRow.LastSessions -> {
val lastSessions = if (row.lastSessions > 0) row.lastSessions.toString() else ""
val data = row.editingDescriptors(mapOf("lastSessions" to lastSessions))
BottomSheetFragment.create(fragmentManager, row, this, data, true)
}
is FilterElementRow.DurationFilterElement -> {
is QueryCondition.DateQuery -> DateTimePickerManager.create(requireContext(), row, this, row.singleValue, onlyDate = !row.showTime, onlyTime = row.showTime)
is QueryCondition.Duration -> {
val hours = if (row.minutes / 60 > 0) (row.minutes / 60).toString() else ""
val minutes = if (row.minutes % 60 > 0) (row.minutes % 60).toString() else ""
val data = row.editingDescriptors(mapOf("hours" to hours, "minutes" to minutes))
BottomSheetFragment.create(fragmentManager, row, this, data, true)
}
is FilterElementRow.AmountFilterElement -> {
val amount = if (row.amount > 0) row.amount.toString() else ""
val data = row.editingDescriptors(mapOf("amount" to amount))
BottomSheetFragment.create(fragmentManager, row, this, data, true)
}
else -> {
updateRowsSelection(row)
}
is QueryCondition.ListOfValues<*> -> {
val valueAsString = row.listOfValues.firstOrNull()?.toString() ?: ""
val data = row.editingDescriptors(mapOf("valueAsString" to valueAsString))
BottomSheetFragment.create(fragmentManager, row, this, data, true)
}
}
}
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
is FilterElementRow.PastDays -> if (row.lastDays > 0) row.lastDays.toString() else NULL_TEXT
is FilterElementRow.LastGames -> if (row.lastGames > 0) row.lastGames.toString() else NULL_TEXT
is FilterElementRow.LastSessions -> if (row.lastSessions > 0) row.lastSessions.toString() else NULL_TEXT
is FilterElementRow.DateFilterElementRow -> row.dateValue.shortDate()
is FilterElementRow.AmountFilterElement -> if (row.amount > 0) row.amount.toString() else NULL_TEXT
is FilterElementRow.DurationFilterElement -> row.minutes.toMinutes(requireContext())
is QueryCondition.DateQuery -> if (row.showTime) row.singleValue.shortTime() else row.singleValue.shortDate()
is QueryCondition.Duration -> row.minutes.toMinutes(requireContext())
is QueryCondition.ListOfValues<*> -> row.listOfValues.firstOrNull()?.toString() ?: NULL_TEXT
else -> super.stringForRow(row)
}
}
@ -123,12 +110,8 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresenta
Timber.d("onRowValueChanged: $row $value")
when (row) {
is FilterElementRow.DateFilterElementRow -> row.dateValue = if (value != null && value is Date) value else Date()
is FilterElementRow.PastDays -> row.lastDays = if (value != null && value is String) value.toInt() else 0
is FilterElementRow.LastGames -> row.lastGames = if (value != null && value is String) value.toInt() else 0
is FilterElementRow.LastSessions -> row.lastSessions = if (value != null && value is String) value.toInt() else 0
is FilterElementRow.AmountFilterElement -> row.amount = if (value != null && value is String) value.toDouble() else 0.0
is FilterElementRow.DurationFilterElement -> {
is QueryCondition.DateQuery -> row.singleValue = if (value != null && value is Date) value else Date()
is QueryCondition.Duration -> {
if (value is ArrayList<*>) {
val hours = try {
(value[0] as String? ?: "0").toInt()
@ -146,6 +129,9 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresenta
row.minutes = 0
}
}
is QueryCondition.SingleInt -> row.singleValue = if (value != null && value is String) value.toInt() else 0
is QueryCondition.ListOfDouble-> row.listOfValues = arrayListOf(if (value != null && value is String) value.toDouble() else 0.0)
is QueryCondition.ListOfInt-> row.listOfValues = arrayListOf(if (value != null && value is String) value.toInt() else 0)
}
// Remove the row before updating the selected rows list
@ -187,28 +173,24 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresenta
*/
private fun initData() {
primaryKey?.let {
currentFilter = Filter.getFilterBydId(getRealm(), it)
}
currentFilter = Filter.getFilterBydId(getRealm(), primaryKey)
filterCategoryRow?.let {
this.appBar.toolbar.title = it.localizedTitle(requireContext())
this.appBar.toolbar.title = filterCategoryRow.localizedTitle(requireContext())
this.rows.clear()
this.rowsForFilterSubcategoryRow.clear()
this.rows.addAll(it.filterElements)
this.rows.addAll(filterCategoryRow.filterElements)
this.rows.forEach { element ->
if (element is FilterElementRow && currentFilter?.contains(element) == true) {
currentFilter?.setSavedValueForElement(element)
if (element is QueryCondition && currentFilter?.contains(element) == true) {
currentFilter?.loadValueForElement(element)
this.selectedRows.add(element)
}
}
this.rowRepresentableAdapter = RowRepresentableAdapter(this, this)
this.recyclerView.adapter = rowRepresentableAdapter
}
}
/**
@ -229,10 +211,12 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresenta
rowRepresentableAdapter.refreshRow(it)
}
}
selectedRows.add(row)
selectedRows.add(row as QueryCondition)
}
}
println("list of selected rows : $selectedRows")
// Update UI
rowRepresentableAdapter.refreshRow(row)
}
@ -242,13 +226,14 @@ open class FilterDetailsFragment : PokerAnalyticsFragment(), StaticRowRepresenta
*/
private fun saveData() {
//TODO: Save currentFilter details data
Timber.d("Save data for filter: ${currentFilter?.id}")
selectedRows?.forEach {
Timber.d("Save data for queryWith: ${currentFilter?.id}")
selectedRows.forEach {
Timber.d("Selected rows: $it")
}
val realm = getRealm()
realm.beginTransaction()
currentFilter?.remove(filterCategoryRow)
currentFilter?.createOrUpdateFilterConditions(selectedRows)
realm.commitTransaction()

@ -60,7 +60,7 @@ open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDat
Timber.d("onActivityResult: $requestCode")
if (data != null && data.hasExtra(FilterDetailsActivity.IntentKey.FILTER_ID.keyName)) {
val filterId = data.getStringExtra(FilterDetailsActivity.IntentKey.FILTER_ID.keyName)
Timber.d("Updated filter: ${filterId}")
Timber.d("Updated queryWith: ${filterId}")
}
*/
@ -71,7 +71,8 @@ open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDat
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.editable_data, menu)
menu?.clear()
inflater?.inflate(R.menu.toolbar_editable_data, menu)
this.filterMenu = menu
updateMenuUI()
super.onCreateOptionsMenu(menu, inflater)
@ -179,19 +180,19 @@ open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDat
}
/**
* Valid the updates of the filter
* Valid the updates of the queryWith
*/
private fun validUpdates() {
Timber.d("Valid filter updates")
Timber.d("Valid queryWith updates")
val filterId = currentFilter?.id ?: ""
finishActivityWithResult(filterId)
}
/**
* Cancel the latest updates of the filter
* Cancel the latest updates of the queryWith
*/
private fun cancelUpdates() {
Timber.d("Cancel filter updates")
Timber.d("Cancel queryWith updates")
val filterId = filterCopy?.id ?: ""
@ -208,7 +209,7 @@ open class FiltersFragment : PokerAnalyticsFragment(), StaticRowRepresentableDat
* Delete data
*/
private fun deleteFilter() {
Timber.d("Delete filter")
Timber.d("Delete queryWith")
val realm = getRealm()
realm.beginTransaction()
currentFilter?.deleteFromRealm()

@ -4,38 +4,63 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.charts.BarLineChartBase
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.data.*
import com.github.mikephil.charting.highlight.Highlight
import com.github.mikephil.charting.listener.OnChartValueSelectedListener
import kotlinx.android.synthetic.main.fragment_graph.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ObjectIdentifier
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.ui.graph.AxisFormatting
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.graph.setStyle
import net.pokeranalytics.android.ui.view.LegendView
import net.pokeranalytics.android.ui.view.MultiLineLegendView
import timber.log.Timber
interface GraphDataSource {
}
class GraphFragment : PokerAnalyticsFragment(), OnChartValueSelectedListener {
lateinit var dataSource: GraphDataSource
lateinit var stat: Stat
lateinit var entries: List<Entry>
enum class Style {
LINE,
BAR,
MULTILINE,
}
companion object {
/**
* Create new instance
*/
fun newInstance(lineDataSets: List<LineDataSet>? = null, barDataSets: List<BarDataSet>? = null, style: Style = Style.LINE): GraphFragment {
val fragment = GraphFragment()
fragment.style = style
fragment.lineDataSetList = lineDataSets
fragment.barDataSetList = barDataSets
val bundle = Bundle()
fragment.arguments = bundle
return fragment
}
}
fun setData(stat: Stat, entries: List<Entry>) {
this.stat = stat
this.entries = entries
}
private lateinit var parentActivity: PokerAnalyticsActivity
private var style: Style = Style.LINE
private lateinit var legendView: LegendView
private var lineDataSetList: List<LineDataSet>? = null
private var barDataSetList: List<BarDataSet>? = null
private var chartView: BarLineChartBase<*>? = null
private var stat: Stat = Stat.NET_RESULT
private var axisFormatting: AxisFormatting = AxisFormatting.DEFAULT
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_graph, container, false)
@ -44,54 +69,144 @@ class GraphFragment : PokerAnalyticsFragment(), OnChartValueSelectedListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUI()
loadGraph()
}
private fun initUI() {
val dataSet = LineDataSet(this.entries, this.stat.name)
val colors = arrayOf(R.color.green_light).toIntArray()
dataSet.setColors(colors, context)
val lineData = LineData(listOf(dataSet))
/**
* Set data
*/
fun setLineData(lineDataSets: List<LineDataSet>, stat: Stat, axisFormatting: AxisFormatting = AxisFormatting.DEFAULT) {
this.lineDataSetList = lineDataSets
this.stat = stat
this.axisFormatting = axisFormatting
this.chart.setStyle()
if (isAdded && !isDetached) {
loadGraph()
}
}
this.chart.data = lineData
this.chart.setOnChartValueSelectedListener(this)
fun setBarData(barDataSets: List<BarDataSet>, stat: Stat, axisFormatting: AxisFormatting = AxisFormatting.DEFAULT) {
this.barDataSetList = barDataSets
this.stat = stat
this.axisFormatting = axisFormatting
if (isAdded && !isDetached) {
loadGraph()
}
}
// OnChartValueSelectedListener
/**
* Init UI
*/
private fun initUI() {
override fun onNothingSelected() {
// nothing to do
parentActivity = activity as PokerAnalyticsActivity
parentActivity.title = stat.localizedTitle(requireContext())
this.legendView = when (this.style) {
Style.MULTILINE -> MultiLineLegendView(requireContext())
else -> LegendView(requireContext())
}
this.legendContainer.addView(this.legendView)
}
override fun onValueSelected(e: Entry?, h: Highlight?) {
/**
* Load graph
*/
private fun loadGraph() {
e?.let { entry ->
h?.let { highlight ->
Timber.d("loadGraph")
this.chartContainer.removeAllViews()
var lastEntry: Entry? = null
var groupName: String? = null
val id = entry.data as String
val item = getRealm().where(this.stat.underlyingClass).equalTo("id", id).findAll().firstOrNull()
item?.let {
this.lineDataSetList?.let { dataSets ->
val date = it.startDate()
val lineChart = LineChart(context)
val lineData = LineData(dataSets)
lineChart.data = lineData
val entryStatName = this.stat.localizedTitle(requireContext())
val entryValue = it.formattedValue(this.stat, requireContext())
this.chartView = lineChart
val totalStatName = this.stat.cumulativeLabelResId(requireContext())
val totalStatValue = this.stat.format(e.y.toDouble(), null, requireContext())
dataSets.firstOrNull()?.let {
this.legendView.prepareWithStat(this.stat, it.entryCount, this.style)
lastEntry = it.getEntryForIndex(it.entryCount - 1)
groupName = it.label
}
}
this.barDataSetList?.let { dataSets ->
this.legendView.prepareWithStat(this.stat, style = this.style)
val barChart = BarChart(context)
if (stat.showXAxisZero) {
barChart.xAxis.axisMinimum = 0.0f
}
if (stat.showYAxisZero) {
barChart.axisLeft.axisMinimum = 0.0f
}
this.chartView = barChart
val barData = BarData(dataSets)
barChart.data = barData
dataSets.firstOrNull()?.let {
lastEntry = it.getEntryForIndex(it.entryCount - 1)
groupName = it.label
}
}
this.chartContainer.addView(this.chartView)
this.chartView?.let {
it.setStyle(false, axisFormatting, requireContext())
it.setOnChartValueSelectedListener(this)
}
lastEntry?.let {
this.selectValue(it, groupName ?: "")
}
}
}
this.text.text = ""
// OnChartValueSelectedListener
override fun onNothingSelected() {
// nothing to do
}
override fun onValueSelected(e: Entry?, h: Highlight?) {
var groupName = ""
h?.let { highlight ->
this.chartView?.data?.getDataSetByIndex(highlight.dataSetIndex)?.let {
groupName = it.label
}
}
e?.let { entry ->
this.selectValue(entry, groupName)
}
}
private fun selectValue(entry: Entry, groupName: String) {
val statEntry = when (entry.data) {
is ObjectIdentifier -> {
val identifier = entry.data as ObjectIdentifier
getRealm().where(identifier.clazz).equalTo("id", identifier.id).findAll().firstOrNull()
}
is GraphUnderlyingEntry -> entry.data as GraphUnderlyingEntry?
else -> null
}
statEntry?.let {
val legendValue = it.legendValues(stat, entry, this.style, groupName, requireContext())
this.legendView.setItemData(legendValue)
}
}
}

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import io.realm.RealmResults
import io.realm.Sort
import io.realm.kotlin.where
@ -56,12 +57,6 @@ class HistoryFragment : PokerAnalyticsFragment(), LiveRowRepresentableDataSource
realmSessions.removeAllChangeListeners()
}
override fun onResume() {
super.onResume()
// Old
//createSessionsHeaders()
}
/**
* Init UI
*/
@ -96,7 +91,11 @@ class HistoryFragment : PokerAnalyticsFragment(), LiveRowRepresentableDataSource
disclaimerDismiss.setOnClickListener {
Preferences.setStopShowingDisclaimer(requireContext())
disclaimerContainer.isVisible = false
disclaimerContainer.animate().translationY(disclaimerContainer.height.toFloat())
.setInterpolator(FastOutSlowInInterpolator())
.withEndAction { disclaimerContainer?.isVisible = false }
.start()
}
}

@ -0,0 +1,93 @@
package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_stats.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.BankrollActivity
import net.pokeranalytics.android.ui.activity.SettingsActivity
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.MoreTabRow
class MoreFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
companion object {
/**
* Create new instance
*/
fun newInstance(): MoreFragment {
val fragment = MoreFragment()
val bundle = Bundle()
fragment.arguments = bundle
return fragment
}
val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.addAll(MoreTabRow.values())
rows
}
}
private lateinit var moreAdapter: RowRepresentableAdapter
// Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_more, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initData()
initUI()
}
// Rows
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) {
super.onRowSelected(position, row, fromAction)
when(row) {
MoreTabRow.BANKROLL -> BankrollActivity.newInstance(requireContext())
MoreTabRow.SETTINGS -> SettingsActivity.newInstance(requireContext())
}
}
// Business
/**
* Init data
*/
private fun initData() {
}
/**
* Init UI
*/
private fun initUI() {
moreAdapter = RowRepresentableAdapter(this, this)
val viewManager = LinearLayoutManager(requireContext())
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = moreAdapter
}
}
}

@ -0,0 +1,92 @@
package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.tabs.TabLayout
import kotlinx.android.synthetic.main.fragment_report_details.*
import kotlinx.android.synthetic.main.fragment_statistic_details.toolbar
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.AggregationType
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.adapter.ReportPagerAdapter
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
class ReportDetailsFragment : PokerAnalyticsFragment() {
companion object {
fun newInstance(report: Report?, reportTitle: String): ReportDetailsFragment {
val fragment = ReportDetailsFragment()
fragment.reportTitle = reportTitle
report?.let {
fragment.selectedReport = it
}
val bundle = Bundle()
fragment.arguments = bundle
return fragment
}
}
private lateinit var parentActivity: PokerAnalyticsActivity
private lateinit var selectedReport: Report
private var reports: MutableMap<AggregationType, Report> = hashMapOf()
private var stat: Stat = Stat.NET_RESULT
private var displayAggregationChoices: Boolean = true
private var reportTitle: String = ""
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_report_details, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUI()
}
/**
* Init UI
*/
private fun initUI() {
parentActivity = activity as PokerAnalyticsActivity
// Avoid a bug during setting the title
toolbar.title = ""
parentActivity.setSupportActionBar(toolbar)
parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
setHasOptionsMenu(true)
toolbar.title = reportTitle
val reportPagerAdapter = ReportPagerAdapter(requireContext(), parentActivity.supportFragmentManager, selectedReport)
viewPager.adapter = reportPagerAdapter
viewPager.offscreenPageLimit = 3
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
viewPager.setCurrentItem(tab.position, false)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
}
/**
* Set data
*/
fun setData(report: Report) {
this.selectedReport = report
}
}

@ -0,0 +1,140 @@
package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm
import kotlinx.android.synthetic.main.fragment_stats.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.ui.activity.ReportDetailsActivity
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.ReportRow
import timber.log.Timber
import java.util.*
import kotlin.collections.ArrayList
class ReportsFragment : PokerAnalyticsFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
companion object {
/**
* Create new instance
*/
fun newInstance(): ReportsFragment {
val fragment = ReportsFragment()
val bundle = Bundle()
fragment.arguments = bundle
return fragment
}
val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.addAll(ReportRow.getRows())
rows
}
}
private lateinit var reportsAdapter: RowRepresentableAdapter
// Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_reports, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initData()
initUI()
}
// Rows
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) {
super.onRowSelected(position, row, fromAction)
if (row is ReportRow) {
val reportName = row.localizedTitle(requireContext())
launchComputation(row.criteria, reportName)
}
}
// Business
/**
* Init data
*/
private fun initData() {
}
/**
* Init UI
*/
private fun initUI() {
reportsAdapter = RowRepresentableAdapter(this, this)
val viewManager = LinearLayoutManager(requireContext())
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = reportsAdapter
}
}
/**
* Launch computation
*/
private fun launchComputation(criteria: List<Criteria>, reportName: String) {
if (criteria.combined().size < 2) {
Toast.makeText(context, R.string.less_then_2_values_for_comparison, Toast.LENGTH_LONG).show()
return
}
showLoader()
GlobalScope.launch {
val startDate = Date()
val realm = Realm.getDefaultInstance()
val requiredStats: List<Stat> = listOf(Stat.NET_RESULT)
val options = Calculator.Options(evolutionValues = Calculator.Options.EvolutionValues.STANDARD, stats = requiredStats)
val report = Calculator.computeStatsWithComparators(realm, criteria = criteria, options = options)
Timber.d("launchComputation: ${System.currentTimeMillis() - startDate.time}ms")
launch(Dispatchers.Main) {
if (!isDetached) {
hideLoader()
ReportDetailsActivity.newInstance(requireContext(), report, reportName)
}
}
realm.close()
}
}
}

@ -6,7 +6,6 @@ import android.view.*
import android.view.animation.OvershootInterpolator
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.DiffUtil
import io.realm.kotlin.where
@ -28,7 +27,6 @@ import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableDiffCallback
import net.pokeranalytics.android.ui.view.SmoothScrollLinearLayoutManager
import net.pokeranalytics.android.ui.view.rowrepresentable.SessionRow
import net.pokeranalytics.android.util.CurrencyUtils
import java.util.*
@ -45,8 +43,9 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate {
private val refreshTimer: Runnable = object : Runnable {
override fun run() {
// Refresh header each 30 seconds
currentSession.updateRowRepresentation()
sessionAdapter.notifyItemChanged(0)
handler.postDelayed(this, 30000)
handler.postDelayed(this, 60000)
}
}
@ -65,7 +64,8 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate {
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.session_toolbar, menu)
menu?.clear()
inflater?.inflate(R.menu.toolbar_session, menu)
this.sessionMenu = menu
updateMenuUI()
super.onCreateOptionsMenu(menu, inflater)
@ -105,9 +105,9 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate {
}
SessionRow.BANKROLL -> {
BottomSheetFragment.create(fragmentManager, row, this, data, false, CurrencyUtils.getCurrency(currentSession.bankroll))
BottomSheetFragment.create(fragmentManager, row, this, data, false, currentSession.currency)
}
else -> BottomSheetFragment.create(fragmentManager, row, this, data, currentCurrency = CurrencyUtils.getCurrency(currentSession.bankroll))
else -> BottomSheetFragment.create(fragmentManager, row, this, data, currentCurrency = currentSession.currency)
}
}
@ -168,7 +168,6 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate {
when (currentSession.getState()) {
SessionState.PENDING, SessionState.PLANNED -> {
state.setTextColor(ContextCompat.getColor(requireContext(), R.color.white))
sessionMenu?.findItem(R.id.restart)?.isVisible = false
floatingActionButton.setImageResource(R.drawable.ic_outline_play)
sessionMenu?.findItem(R.id.stop)?.isVisible = false
@ -177,17 +176,15 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate {
.setInterpolator(OvershootInterpolator()).start()
}
SessionState.STARTED -> {
state.setTextColor(ContextCompat.getColor(requireContext(), R.color.green))
sessionMenu?.findItem(R.id.restart)?.isVisible = true
floatingActionButton.setImageResource(R.drawable.ic_outline_pause)
sessionMenu?.findItem(R.id.stop)?.isVisible = true
floatingActionButton.animate().scaleX(1f).scaleY(1f).alpha(1f)
.setDuration(animationDuration)
.setInterpolator(OvershootInterpolator()).start()
handler.postDelayed(refreshTimer, 30000)
handler.postDelayed(refreshTimer, 60000)
}
SessionState.PAUSED -> {
state.setTextColor(ContextCompat.getColor(requireContext(), R.color.blue))
sessionMenu?.findItem(R.id.restart)?.isVisible = true
floatingActionButton.setImageResource(R.drawable.ic_outline_play)
sessionMenu?.findItem(R.id.stop)?.isVisible = true
@ -196,7 +193,6 @@ class SessionFragment : PokerAnalyticsFragment(), RowRepresentableDelegate {
.setInterpolator(OvershootInterpolator()).start()
}
SessionState.FINISHED -> {
state.setTextColor(ContextCompat.getColor(requireContext(), R.color.white))
sessionMenu?.findItem(R.id.restart)?.isVisible = true
sessionMenu?.findItem(R.id.stop)?.isVisible = false
floatingActionButton.animate().scaleX(0f).scaleY(0f).alpha(0f)

@ -7,9 +7,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm
import kotlinx.android.synthetic.main.fragment_settings.*
import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.activity.CurrenciesActivity
import net.pokeranalytics.android.ui.activity.DataListActivity
import net.pokeranalytics.android.ui.activity.GDPRActivity
@ -17,20 +19,20 @@ import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.openContactMail
import net.pokeranalytics.android.ui.extensions.openPlayStorePage
import net.pokeranalytics.android.ui.extensions.openUrl
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SettingRow
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.URL
import net.pokeranalytics.android.util.extensions.openContactMail
import net.pokeranalytics.android.util.extensions.openPlayStorePage
import net.pokeranalytics.android.util.extensions.openUrl
import net.pokeranalytics.android.util.UserDefaults
import java.util.*
class SettingsFragment : PokerAnalyticsFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource {
private lateinit var parentActivity: PokerAnalyticsActivity
companion object {
@ -53,49 +55,53 @@ class SettingsFragment : PokerAnalyticsFragment(), RowRepresentableDelegate, Sta
const val REQUEST_CODE_CURRENCY: Int = 100
}
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
SettingRow.VERSION -> BuildConfig.VERSION_NAME + if (BuildConfig.DEBUG) " (${BuildConfig.VERSION_CODE}) DEBUG" else ""
SettingRow.CURRENCY -> {
val locale = Preferences.getCurrencyLocale(this.parentActivity)
Currency.getInstance(locale).getSymbol(locale)
}
else -> ""
}
}
private lateinit var settingsAdapterRow: RowRepresentableAdapter
private lateinit var parentActivity: PokerAnalyticsActivity
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(net.pokeranalytics.android.R.layout.fragment_settings, container, false)
return inflater.inflate(R.layout.fragment_settings, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initData()
initUI()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == SettingsFragment.REQUEST_CODE_CURRENCY && resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_CURRENCY && resultCode == Activity.RESULT_OK) {
data?.let {
Preferences.setCurrencyCode(data.getStringExtra(CurrenciesFragment.INTENT_CURRENCY_CODE), requireContext())
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
it.where(Session::class.java).isNull("bankroll.currency.code").findAll().forEach {
it.bankrollHasBeenUpdated()
}
}
realm.close()
settingsAdapterRow.refreshRow(SettingRow.CURRENCY)
}
}
}
override fun adapterRows(): List<RowRepresentable>? {
return SettingsFragment.rowRepresentation
return rowRepresentation
}
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) {
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
SettingRow.VERSION -> BuildConfig.VERSION_NAME + if (BuildConfig.DEBUG) " (${BuildConfig.VERSION_CODE}) DEBUG" else ""
SettingRow.CURRENCY -> UserDefaults.currency.symbol
else -> ""
}
}
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) {
when (row) {
SettingRow.RATE_APP -> parentActivity.openPlayStorePage()
SettingRow.CONTACT_US -> parentActivity.openContactMail(R.string.contact)
SettingRow.BUG_REPORT -> parentActivity.openContactMail(R.string.bug_report_subject)
SettingRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, SettingsFragment.REQUEST_CODE_CURRENCY)
SettingRow.BUG_REPORT -> parentActivity.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path)
SettingRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, REQUEST_CODE_CURRENCY)
SettingRow.FOLLOW_US -> {
when (position) {
0 -> parentActivity.openUrl(URL.BLOG.value)
@ -115,24 +121,35 @@ class SettingsFragment : PokerAnalyticsFragment(), RowRepresentableDelegate, Sta
}
/**
* Init data
* Init UI
*/
private fun initData() {
private fun initUI() {
parentActivity = activity as PokerAnalyticsActivity
parentActivity.setSupportActionBar(toolbar)
parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
setHasOptionsMenu(true)
val viewManager = LinearLayoutManager(requireContext())
settingsAdapterRow = RowRepresentableAdapter(
this, this
)
customRecyclerView.apply {
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = settingsAdapterRow
}
}
/**
* Init data
*/
private fun initData() {
}
/**
* Open GDPR Activity
*/

@ -0,0 +1,197 @@
package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.LineDataSet
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import io.realm.Realm
import kotlinx.android.synthetic.main.fragment_statistic_details.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.*
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.extensions.ChipGroupExtension
import net.pokeranalytics.android.ui.extensions.hideWithAnimation
import net.pokeranalytics.android.ui.extensions.px
import net.pokeranalytics.android.ui.extensions.showWithAnimation
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import timber.log.Timber
import java.util.*
class StatisticDetailsFragment : PokerAnalyticsFragment() {
companion object {
/**
* Create new instance
*/
fun newInstance(): StatisticDetailsFragment {
val fragment = StatisticDetailsFragment()
val bundle = Bundle()
fragment.arguments = bundle
return fragment
}
}
private lateinit var parentActivity: PokerAnalyticsActivity
private lateinit var computableGroup: ComputableGroup
private lateinit var graphFragment: GraphFragment
private lateinit var selectedReport: Report
private var reports: MutableMap<AggregationType, Report> = hashMapOf()
private var stat: Stat = Stat.NET_RESULT
private var displayAggregationChoices: Boolean = true
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_statistic_details, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUI()
}
/**
* Init UI
*/
private fun initUI() {
parentActivity = activity as PokerAnalyticsActivity
// Avoid a bug during setting the title
toolbar.title = ""
parentActivity.setSupportActionBar(toolbar)
parentActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
setHasOptionsMenu(true)
val fragmentManager = parentActivity.supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
graphFragment = GraphFragment()
fragmentTransaction.add(R.id.graphContainer, graphFragment)
fragmentTransaction.commit()
stat.aggregationTypes.firstOrNull()?.let { aggregationType ->
reports[aggregationType]?.let { report ->
setGraphData(report, aggregationType)
}
}
toolbar.title = stat.localizedTitle(requireContext())
val aggregationTypes = stat.aggregationTypes
aggregationTypes.forEachIndexed { index, type ->
val chip = Chip(requireContext())
chip.id = index
chip.text = requireContext().getString(type.resId)
chip.chipStartPadding = 8f.px
chip.chipEndPadding = 8f.px
this.chipGroup.addView(chip)
}
this.chipGroup.isVisible = displayAggregationChoices
this.chipGroup.check(0)
this.chipGroup.setOnCheckedChangeListener(object : ChipGroupExtension.SingleSelectionOnCheckedListener() {
override fun onCheckedChanged(group: ChipGroup, checkedId: Int) {
super.onCheckedChanged(group, checkedId)
val aggregationType = aggregationTypes[checkedId]
reports[aggregationType]?.let { report ->
setGraphData(report, aggregationType)
} ?: run {
launchStatComputation(aggregationType)
}
}
})
}
/**
* Launch stat computation
*/
private fun launchStatComputation(aggregationType: AggregationType) {
graphContainer.hideWithAnimation()
progressBar.showWithAnimation()
GlobalScope.launch {
val s = Date()
Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
val report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, computableGroup, aggregationType)
reports[aggregationType] = report
realm.close()
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds")
launch(Dispatchers.Main) {
setGraphData(report, aggregationType)
progressBar.hideWithAnimation()
graphContainer.showWithAnimation()
}
}
}
/**
* Set the graph data set
*/
private fun setGraphData(report: Report, aggregationType: AggregationType) {
val dataSet = when (aggregationType) {
AggregationType.SESSION -> report.results.firstOrNull()?.defaultStatEntries(stat, requireContext())
AggregationType.DURATION -> {
report.results.firstOrNull()?.durationEntries(stat, requireContext())
}
AggregationType.MONTH, AggregationType.YEAR -> {
when (this.stat) {
Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_SETS -> report.barEntries(this.stat, requireContext())
else -> report.lineEntries(this.stat, requireContext())
}
}
}
dataSet?.let { ds ->
if (ds is LineDataSet) {
graphFragment.setLineData(listOf(ds), stat, aggregationType.axisFormatting)
}
if (ds is BarDataSet) {
graphFragment.setBarData(listOf(ds), stat, aggregationType.axisFormatting)
}
}
}
/**
* Set data
*/
fun setData(stat: Stat, computableGroup: ComputableGroup, report: Report, displayAggregationChoices: Boolean) {
this.stat = stat
this.computableGroup = computableGroup
this.selectedReport = report
this.displayAggregationChoices = displayAggregationChoices
stat.aggregationTypes.firstOrNull()?.let {
reports[it] = report
}
}
}

@ -0,0 +1,145 @@
package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.View
import io.realm.Realm
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputableGroup
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.StatRow
import timber.log.Timber
import java.util.*
import kotlin.coroutines.CoroutineContext
class StatisticsFragment : TableReportFragment() {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
private var stringAll = ""
private var stringCashGame = ""
private var stringTournament = ""
companion object {
/**
* Create new instance
*/
fun newInstance(report: Report? = null): StatisticsFragment {
val fragment = StatisticsFragment()
report?.let {
fragment.report = it
}
val bundle = Bundle()
fragment.arguments = bundle
return fragment
}
}
// Life Cycle
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
launchStatComputation()
}
override fun sessionsChanged() {
this.launchStatComputation()
this.statsAdapter?.notifyDataSetChanged()
}
override fun initData() {
super.initData()
this.stringAll = getString(R.string.all)
this.stringCashGame = getString(R.string.cash_game)
this.stringTournament = getString(R.string.tournament)
}
override fun convertReportIntoRepresentables(report: Report): ArrayList<RowRepresentable> {
val rows: ArrayList<RowRepresentable> = ArrayList()
report.results.forEach { result ->
rows.add(CustomizableRowRepresentable(title = result.group.name))
result.group.stats?.forEach { stat ->
rows.add(StatRow(stat, result.computedStat(stat), result.group.name))
}
}
return rows
}
// Business
/**
* Launch stat computation
*/
private fun launchStatComputation() {
GlobalScope.launch(coroutineContext) {
val test = GlobalScope.async {
val s = Date()
Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
report = createSessionGroupsAndStartCompute(realm)
realm.close()
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds")
}
test.await()
if (!isDetached) {
showResults()
}
}
}
/**
* Create session groups and start computations
*/
private fun createSessionGroupsAndStartCompute(realm: Realm): Report {
val allStats: List<Stat> = listOf(
Stat.NET_RESULT,
Stat.HOURLY_RATE,
Stat.AVERAGE,
Stat.NUMBER_OF_SETS,
Stat.AVERAGE_HOURLY_DURATION,
Stat.HOURLY_DURATION
)
val allSessionGroup = ComputableGroup(stringAll, listOf(), allStats)
val cgStats: List<Stat> = listOf(
Stat.NET_RESULT,
Stat.HOURLY_RATE,
Stat.NET_BB_PER_100_HANDS,
Stat.HOURLY_RATE_BB,
Stat.AVERAGE,
Stat.STANDARD_DEVIATION_HOURLY,
Stat.WIN_RATIO,
Stat.NUMBER_OF_GAMES,
Stat.AVERAGE_BUYIN
)
val cgSessionGroup = ComputableGroup(stringCashGame, listOf(QueryCondition.IsCash), cgStats)
val tStats: List<Stat> =
listOf(Stat.NET_RESULT, Stat.HOURLY_RATE, Stat.ROI, Stat.WIN_RATIO, Stat.NUMBER_OF_GAMES, Stat.AVERAGE_BUYIN)
val tSessionGroup = ComputableGroup(stringTournament, listOf(QueryCondition.IsTournament), tStats)
Timber.d(">>>>> Start computations...")
return Calculator.computeGroups(realm, listOf(allSessionGroup, cgSessionGroup, tSessionGroup), Calculator.Options())
}
}

@ -1,247 +0,0 @@
package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm
import kotlinx.android.synthetic.main.fragment_stats.*
import kotlinx.coroutines.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.*
import net.pokeranalytics.android.model.StatRepresentable
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.activity.GraphActivity
import net.pokeranalytics.android.ui.adapter.DisplayDescriptor
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.SessionObserverFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.util.NULL_TEXT
import timber.log.Timber
import java.util.*
import kotlin.coroutines.CoroutineContext
class StatsFragment : SessionObserverFragment(), StaticRowRepresentableDataSource, CoroutineScope,
RowRepresentableDelegate {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
private var rowRepresentables: ArrayList<RowRepresentable> = ArrayList()
private var stringAll = ""
private var stringCashGame = ""
private var stringTournament = ""
private lateinit var statsAdapter: RowRepresentableAdapter
private var computedResults : List<ComputedResults>? = null
companion object {
/**
* Create new instance
*/
fun newInstance(): StatsFragment {
val fragment = StatsFragment()
val bundle = Bundle()
fragment.arguments = bundle
return fragment
}
}
// Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_stats, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initData()
initUI()
launchStatComputation()
}
// Row Representable DS
override fun adapterRows(): List<RowRepresentable>? {
return this.rowRepresentables
}
override fun contentDescriptorForRow(row: RowRepresentable): DisplayDescriptor? {
val dc = DisplayDescriptor()
dc.textFormat = TextFormat(NULL_TEXT)
if (row is StatRepresentable) {
context?.let { context ->
row.computedStat?.let {
dc.textFormat = it.format(context)
}
}
}
return dc
}
override fun statFormatForRow(row: RowRepresentable): TextFormat {
if (row is StatRepresentable) {
context?.let { context ->
row.computedStat?.let { return it.format(context) }
}
}
return TextFormat(NULL_TEXT)
}
override fun onResume() {
super.onResume()
statsAdapter.notifyDataSetChanged()
}
// Override
override fun sessionsChanged() {
this.launchStatComputation()
this.statsAdapter.notifyDataSetChanged()
}
// Business
/**
* Init data
*/
private fun initData() {
this.stringAll = getString(R.string.all)
this.stringCashGame = getString(R.string.cash_game)
this.stringTournament = getString(R.string.tournament)
this.statsAdapter = RowRepresentableAdapter(this, this)
}
/**
* Init UI
*/
private fun initUI() {
val viewManager = LinearLayoutManager(requireContext())
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = statsAdapter
}
}
private fun launchStatComputation() {
GlobalScope.launch(coroutineContext) {
var results = listOf<ComputedResults>()
val test = GlobalScope.async {
val s = Date()
Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
results = createSessionGroupsAndStartCompute(realm)
computedResults = results
realm.close()
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in ${duration} seconds")
}
test.await()
if (!isDetached) {
showResults(results)
}
}
}
private fun createSessionGroupsAndStartCompute(realm: Realm) : List<ComputedResults> {
val allStats: List<Stat> = listOf(Stat.NETRESULT, Stat.HOURLY_RATE, Stat.AVERAGE, Stat.NUMBER_OF_SETS, Stat.AVERAGE_DURATION, Stat.DURATION)
val allSessionGroup = ComputableGroup(stringAll, listOf(), allStats)
val cgStats: List<Stat> = listOf(Stat.NETRESULT, Stat.HOURLY_RATE, Stat.NET_BB_PER_100_HANDS, Stat.HOURLY_RATE_BB, Stat.AVERAGE, Stat.STANDARD_DEVIATION_HOURLY, Stat.WIN_RATIO, Stat.NUMBER_OF_GAMES, Stat.AVERAGE_BUYIN)
val cgSessionGroup = ComputableGroup(stringCashGame, listOf(QueryCondition.CASH), cgStats)
val tStats: List<Stat> = listOf(Stat.NETRESULT, Stat.HOURLY_RATE, Stat.ROI, Stat.WIN_RATIO, Stat.NUMBER_OF_GAMES, Stat.AVERAGE_BUYIN)
val tSessionGroup = ComputableGroup(stringTournament, listOf(QueryCondition.TOURNAMENT), tStats)
Timber.d(">>>>> Start computations...")
return Calculator.computeGroups(realm, listOf(allSessionGroup, cgSessionGroup, tSessionGroup), Calculator.Options())
}
private fun showResults(results: List<ComputedResults>) {
this.rowRepresentables = this.convertResultsIntoRepresentables(results)
statsAdapter.notifyDataSetChanged()
}
private fun convertResultsIntoRepresentables(results: List<ComputedResults>) : ArrayList<RowRepresentable> {
val rows: ArrayList<RowRepresentable> = ArrayList()
results.forEach { result ->
rows.add(CustomizableRowRepresentable(title = result.group.name))
result.group.stats?.forEach { stat ->
rows.add(StatRepresentable(stat, result.computedStat(stat), result.group.name))
}
}
return rows
}
// RowRepresentableDelegate
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) {
// if (row is StatRepresentable) {
//
// // filter groups
// val groupResults = this.computedResults?.filter {
// it.group.name == row.groupName
// }
//
// groupResults?.firstOrNull()?.let {
// this.launchStatComputationWithEvolution(row.stat, it.group)
// }
// }
}
private fun launchStatComputationWithEvolution(stat: Stat, computableGroup: ComputableGroup) {
GlobalScope.launch(coroutineContext) {
var results = listOf<ComputedResults>()
val test = GlobalScope.async {
val s = Date()
Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
val options = Calculator.Options()
options.evolutionValues = Calculator.Options.EvolutionValues.STANDARD
results = Calculator.computeGroups(realm, listOf(computableGroup), options)
realm.close()
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in ${duration} seconds")
}
test.await()
if (!isDetached) {
results.firstOrNull()?.defaultStatEntries(stat)?.let { entries ->
GraphActivity.newInstance(requireContext(), stat, entries)
}
}
}
}
}

@ -0,0 +1,199 @@
package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm
import kotlinx.android.synthetic.main.fragment_stats.*
import kotlinx.coroutines.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.*
import net.pokeranalytics.android.ui.activity.StatisticDetailsActivity
import net.pokeranalytics.android.ui.adapter.DisplayDescriptor
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.SessionObserverFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.StatRow
import net.pokeranalytics.android.util.NULL_TEXT
import timber.log.Timber
import java.util.*
import kotlin.coroutines.CoroutineContext
open class TableReportFragment : SessionObserverFragment(), StaticRowRepresentableDataSource, CoroutineScope,
RowRepresentableDelegate {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
private var rowRepresentables: ArrayList<RowRepresentable> = ArrayList()
var statsAdapter: RowRepresentableAdapter? = null
var report : Report? = null
companion object {
/**
* Create new instance
*/
fun newInstance(report: Report? = null): TableReportFragment {
val fragment = TableReportFragment()
report?.let {
fragment.report = it
}
val bundle = Bundle()
fragment.arguments = bundle
return fragment
}
}
// Life Cycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_stats, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initData()
initUI()
report?.let {
showResults()
}
}
// Row Representable DS
override fun adapterRows(): List<RowRepresentable>? {
return this.rowRepresentables
}
override fun contentDescriptorForRow(row: RowRepresentable): DisplayDescriptor? {
val dc = DisplayDescriptor()
dc.textFormat = TextFormat(NULL_TEXT)
if (row is StatRow) {
context?.let { _ ->
row.computedStat?.let {
dc.textFormat = it.format()
}
}
}
return dc
}
override fun statFormatForRow(row: RowRepresentable): TextFormat {
if (row is StatRow) {
context?.let { _ ->
row.computedStat?.let { return it.format() }
}
}
return TextFormat(NULL_TEXT)
}
override fun onResume() {
super.onResume()
statsAdapter?.notifyDataSetChanged()
}
// Business
/**
* Init data
*/
open fun initData() {
this.statsAdapter = RowRepresentableAdapter(this, this)
}
/**
* Init UI
*/
open fun initUI() {
val viewManager = LinearLayoutManager(requireContext())
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = statsAdapter
}
}
/**
* Show results
*/
fun showResults() {
report?.let {
this.rowRepresentables = this.convertReportIntoRepresentables(it)
statsAdapter?.notifyDataSetChanged()
}
}
open fun convertReportIntoRepresentables(report: Report): ArrayList<RowRepresentable> {
val rows: ArrayList<RowRepresentable> = ArrayList()
report.options.displayedStats.forEach {stat ->
rows.add(CustomizableRowRepresentable(title = stat.localizedTitle(requireContext())))
report.results.forEach {
val title = it.group.name
rows.add(StatRow(stat, it.computedStat(stat), it.group.name, title))
}
}
return rows
}
// RowRepresentableDelegate
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) {
if (row is StatRow && row.stat.hasEvolutionGraph) {
// queryWith groups
val groupResults = this.report?.results?.filter {
it.group.name == row.groupName
}
groupResults?.firstOrNull()?.let {
this.launchStatComputationWithEvolution(row.stat, it.group)
}
}
}
private fun launchStatComputationWithEvolution(stat: Stat, computableGroup: ComputableGroup) {
showLoader()
GlobalScope.launch(coroutineContext) {
var report: Report? = null
val test = GlobalScope.async {
val s = Date()
Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
val aggregationType = stat.aggregationTypes.first()
report = Calculator.computeStatsWithEvolutionByAggregationType(realm, stat, computableGroup, aggregationType)
realm.close()
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>> ended in $duration seconds")
}
test.await()
if (!isDetached) {
hideLoader()
report?.let {
StatisticDetailsActivity.newInstance(requireContext(), stat, computableGroup, it)
}
}
}
}
}

@ -0,0 +1,53 @@
package net.pokeranalytics.android.ui.fragment.components
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import kotlinx.android.synthetic.main.fragment_loader.*
import net.pokeranalytics.android.R
class LoaderDialogFragment: DialogFragment() {
companion object {
const val ARGUMENT_MESSAGE_RES_ID = "ARGUMENT_MESSAGE_RES_ID"
/**
* Create new instance
*/
fun newInstance(resId: Int? = null, isCancelable: Boolean = false): LoaderDialogFragment {
val fragment = LoaderDialogFragment()
fragment.isCancelable = isCancelable
val bundle = Bundle()
resId?.let {
bundle.putInt(ARGUMENT_MESSAGE_RES_ID, resId)
}
fragment.arguments = bundle
return fragment
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_loader, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.let {bundle ->
if (bundle.containsKey(ARGUMENT_MESSAGE_RES_ID)) {
loadingMessage.text = getString(bundle.getInt(ARGUMENT_MESSAGE_RES_ID))
}
}
}
override fun onStart() {
super.onStart()
val window = dialog.window
window?.setBackgroundDrawableResource(android.R.color.transparent)
}
}

@ -7,6 +7,7 @@ import io.realm.Realm
open class PokerAnalyticsFragment: Fragment() {
private var realm: Realm? = null
private var loaderDialogFragment: LoaderDialogFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -36,4 +37,22 @@ open class PokerAnalyticsFragment: Fragment() {
*/
open fun onBackPressed(){}
/**
* Show the loader
*/
fun showLoader(resId: Int? = null, cancelable: Boolean? = false) {
loaderDialogFragment = LoaderDialogFragment.newInstance(resId, false)
activity?.let {
loaderDialogFragment?.show(it.supportFragmentManager, "loader")
}
}
/**
* Hide the loader
*/
fun hideLoader() {
loaderDialogFragment?.dismiss()
loaderDialogFragment = null
}
}

@ -97,7 +97,7 @@ open class BottomSheetFragment : BottomSheetDialogFragment() {
private fun initUI() {
row.let {
bottomSheetToolbar.title = row.localizedTitle(requireContext())
bottomSheetToolbar.inflateMenu(R.menu.bottom_sheet_menu)
bottomSheetToolbar.inflateMenu(R.menu.toolbar_bottom_sheet)
bottomSheetToolbar.setOnMenuItemClickListener {
false
}

@ -14,7 +14,7 @@ import net.pokeranalytics.android.exceptions.RowRepresentableEditDescriptorExcep
import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.extensions.px
import net.pokeranalytics.android.ui.extensions.px
/**
* Bottom Sheet List Game Fragment

@ -13,7 +13,7 @@ import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.GridSpacingItemDecoration
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.extensions.px
import net.pokeranalytics.android.ui.extensions.px
class BottomSheetTableSizeGridFragment : BottomSheetFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {

@ -0,0 +1,32 @@
package net.pokeranalytics.android.ui.graph
import com.github.mikephil.charting.components.AxisBase
import com.github.mikephil.charting.formatter.ValueFormatter
import net.pokeranalytics.android.util.extensions.kmbFormatted
import kotlin.math.roundToInt
class LargeNumberFormatter : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return value.kmbFormatted
}
override fun getAxisLabel(value: Float, axis: AxisBase?): String {
return value.roundToInt().kmbFormatted
}
}
class HourFormatter : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return value.kmbFormatted + "H"
}
override fun getAxisLabel(value: Float, axis: AxisBase?): String {
val test = value.kmbFormatted + "H"
return test
}
}

@ -0,0 +1,30 @@
package net.pokeranalytics.android.ui.graph
import android.content.Context
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
import net.pokeranalytics.android.R
class PALineDataSet(yVals: List<Entry>, label: String, context: Context) : LineDataSet(yVals, label) {
init {
this.highLightColor = context.getColor(R.color.chart_highlight_indicator)
this.setDrawValues(false)
this.setDrawCircles(false)
val colors = arrayOf(R.color.green_light).toIntArray()
this.setColors(colors, context)
}
}
//class PABarDataSet(yVals: List<BarEntry>, label: String, context: Context) : BarDataSet(yVals, label) {
//
// init {
// this.highLightColor = context.getColor(R.color.chart_highlight_indicator)
// }
//
//}

@ -1,66 +1,83 @@
package net.pokeranalytics.android.ui.graph
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.charts.BarLineChartBase
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.components.XAxis
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.extensions.px
fun BarChart.setStyle() {
GraphHelper.setStyle(this)
enum class AxisFormatting {
DEFAULT,
X_DURATION,
Y_DURATION,
}
fun LineChart.setStyle() {
GraphHelper.setStyle(this)
}
fun BarLineChartBase<*>.setStyle(
small: Boolean,
axisFormatting: AxisFormatting = AxisFormatting.DEFAULT,
context: Context
) {
this.legend.isEnabled = false
this.description.isEnabled = false
class GraphHelper {
// X Axis
this.xAxis.axisLineColor = ContextCompat.getColor(context, R.color.chart_default)
this.xAxis.enableGridDashedLine(3.0f.px, 5.0f.px, 1.0f.px)
this.xAxis.position = XAxis.XAxisPosition.BOTTOM
this.xAxis.setDrawGridLines(true)
this.xAxis.isGranularityEnabled = true
this.xAxis.granularity = 1.0f
this.xAxis.textColor = ContextCompat.getColor(context, R.color.chart_default)
this.xAxis.typeface = ResourcesCompat.getFont(context, R.font.roboto_medium)
this.xAxis.labelCount = 4
this.xAxis.textSize = 12f
this.xAxis.isEnabled = true
companion object {
when (this) {
is BarChart -> {
this.xAxis.setDrawLabels(false)
}
else -> {
this.xAxis.setDrawLabels(true)
}
}
fun setStyle(chart: BarLineChartBase<*>) {
// Y Axis
this.axisLeft.setDrawAxisLine(false)
this.axisLeft.setDrawGridLines(true)
this.axisLeft.enableGridDashedLine(3.0f.px, 5.0f.px, 1.0f.px)
// this.xAxis.axisLineColor = ContextCompat.getColor(context, R.color.) ChartAppearance.defaultColor
// this.xAxis.axisLineWidth = ChartAppearance.lineWidth
// this.xAxis.enableGridDashedLine(3.0f, 5.0f, 1.0f)
//
// this.xAxis.labelTextColor = ChartAppearance.defaultColor
// this.xAxis.labelFont = Fonts.graphAxis
// this.xAxis.labelCount = 4
// this.xAxis.labelPosition = .bottom
//
// this.xAxis.drawLabelsEnabled = true
// this.xAxis.drawGridLinesEnabled = true
// this.xAxis.granularity = 1.0
// this.xAxis.granularityEnabled = true
// this.xAxis.enabled = true
//
// // Y Axis
// this.leftAxis.drawAxisLineEnabled = false
// this.leftAxis.drawGridLinesEnabled = true
// this.leftAxis.gridLineDashLengths = [3.0, 5.0]
//
// this.leftAxis.drawZeroLineEnabled = true
// this.leftAxis.zeroLineWidth = ChartAppearance.lineWidth
// this.leftAxis.zeroLineColor = ChartAppearance.defaultColor
//
// this.leftAxis.granularityEnabled = true
// this.leftAxis.granularity = 1.0
//
// this.leftAxis.labelTextColor = ChartAppearance.defaultColor
// this.leftAxis.labelFont = Fonts.graphAxis
// this.leftAxis.labelCount = small ? 1 : 7 // @todo not great if interval is [0..2] for number of records as we get decimals
//
// if timeYAxis {
// this.leftAxis.valueFormatter = HourValueFormatter()
// } else {
// this.leftAxis.valueFormatter = LargeNumberFormatter()
// }
//
// this.rightAxis.enabled = false
//
// this.legend.enabled = false
this.axisLeft.setDrawZeroLine(true)
this.axisLeft.zeroLineColor = ContextCompat.getColor(context, R.color.chart_default)
}
this.axisLeft.isGranularityEnabled = true
this.axisLeft.granularity = 1.0f
this.axisLeft.textColor = ContextCompat.getColor(context, R.color.chart_default)
this.axisLeft.typeface = ResourcesCompat.getFont(context, R.font.roboto_medium)
this.axisLeft.labelCount =
if (small) 1 else 7 // @todo not great if interval is [0..2] for number of records as we get decimals
this.axisLeft.textSize = 12f
this.axisLeft.valueFormatter = LargeNumberFormatter()
this.axisRight.isEnabled = false
this.data?.isHighlightEnabled = !small
when (axisFormatting) {
AxisFormatting.DEFAULT -> {
this.axisLeft.valueFormatter = LargeNumberFormatter()
}
AxisFormatting.X_DURATION -> {
this.xAxis.valueFormatter = HourFormatter()
}
AxisFormatting.Y_DURATION -> {
this.axisLeft.valueFormatter = HourFormatter()
}
}
}

@ -0,0 +1,38 @@
package net.pokeranalytics.android.ui.graph
import android.content.Context
import com.github.mikephil.charting.data.Entry
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.TextFormat
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent
interface GraphUnderlyingEntry {
val entryTitle: String
fun formattedValue(stat: Stat): TextFormat
fun legendValues(
stat: Stat,
entry: Entry,
style: GraphFragment.Style,
groupName: String,
context: Context
): LegendContent {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
DefaultLegendValues(this.entryTitle, totalStatValue)
}
else -> {
val entryValue = this.formattedValue(stat)
val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
DefaultLegendValues(this.entryTitle, entryValue, totalStatValue)
}
}
}
}

@ -23,6 +23,7 @@ class DateTimePickerManager : DatePickerDialog.OnDateSetListener,
private lateinit var calendar: Calendar
private var minimumDate: Date? = null
private var onlyDate: Boolean = false
private var onlyTime: Boolean = false
private var isClearable: Boolean = true
companion object {
@ -33,6 +34,7 @@ class DateTimePickerManager : DatePickerDialog.OnDateSetListener,
date: Date?,
minimumDate: Date? = null,
onlyDate: Boolean? = false,
onlyTime: Boolean? = false,
isClearable: Boolean? = true
): DateTimePickerManager {
@ -46,9 +48,14 @@ class DateTimePickerManager : DatePickerDialog.OnDateSetListener,
dateTimePickerManager.calendar = calendar
dateTimePickerManager.minimumDate = minimumDate
dateTimePickerManager.onlyDate = onlyDate ?: false
dateTimePickerManager.onlyTime = onlyTime ?: false
dateTimePickerManager.isClearable = isClearable ?: true
dateTimePickerManager.showDatePicker()
if (dateTimePickerManager.onlyTime) {
dateTimePickerManager.showTimePicker()
} else {
dateTimePickerManager.showDatePicker()
}
return dateTimePickerManager
}

@ -0,0 +1,29 @@
package net.pokeranalytics.android.ui.view
import net.pokeranalytics.android.R
enum class CalendarTabs : Displayable {
NET_RESULTS,
NET_HOURLY_RATE,
NUMBER_OF_GAMES,
WIN_RATIO,
STANDARD_DEVIATION_PER_HOUR,
AVERAGE_NET_RESULT,
AVERAGE_DURATION,
DURATION_OF_PLAY;
override val resId: Int
get() {
return when (this) {
NET_RESULTS -> R.string.net_result
NET_HOURLY_RATE -> R.string.hour_rate_without_pauses
NUMBER_OF_GAMES -> R.string.number_of_records
WIN_RATIO -> R.string.win_ratio
STANDARD_DEVIATION_PER_HOUR -> R.string.standard_deviation_per_hour
AVERAGE_NET_RESULT -> R.string.average_net_result
AVERAGE_DURATION -> R.string.average_hours_played
DURATION_OF_PLAY -> R.string.total_hours_played
}
}
}

@ -0,0 +1,119 @@
package net.pokeranalytics.android.ui.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.layout_legend_default.view.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.TextFormat
import net.pokeranalytics.android.ui.extensions.setTextFormat
import net.pokeranalytics.android.ui.fragment.GraphFragment
interface LegendContent
data class DefaultLegendValues(
var title: String,
var leftFormat: TextFormat,
var rightFormat: TextFormat? = null
) : LegendContent
/**
* Display a row session
*/
open class LegendView : FrameLayout {
// open class Values(var title: String, var leftFormat: TextFormat, var rightFormat: TextFormat? = null)
// class MultiLineValues(
// var firstTitle: String,
// var secondTitle: String,
// leftFormat: TextFormat,
// rightFormat: TextFormat? = null
// ) : Values("", leftFormat, rightFormat)
private lateinit var legendLayout: ConstraintLayout
/**
* Constructors
*/
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init()
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}
open protected fun getResourceLayout(): Int {
return R.layout.layout_legend_default
}
/**
* Init
*/
private fun init() {
val layoutInflater = LayoutInflater.from(context)
legendLayout = layoutInflater.inflate(this.getResourceLayout(), this, false) as ConstraintLayout
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(legendLayout, layoutParams)
}
/**
* Set the stat data to the view
*/
open fun prepareWithStat(stat: Stat, counter: Int? = null, style: GraphFragment.Style) {
when (style) {
GraphFragment.Style.BAR -> {
this.stat1Name.text = stat.localizedTitle(context)
this.stat2Name.text = context.getString(R.string.sessions)
this.counter.isVisible = false
}
GraphFragment.Style.LINE -> {
if (stat.significantIndividualValue) {
this.stat1Name.text = stat.localizedTitle(context)
this.stat2Name.text = stat.cumulativeLabelResId(context)
} else {
this.stat1Name.text = stat.cumulativeLabelResId(context)
this.stat2Name.isVisible = false
}
counter?.let {
val counterText = "$it ${context.getString(R.string.sessions)}"
this.counter.text = counterText
this.counter.isVisible = stat.shouldShowNumberOfSessions
}
}
else -> {
}
}
}
/**
*
*/
open fun setItemData(content: LegendContent) {
if (content is DefaultLegendValues) {
this.title.text = content.title
this.stat1Value.setTextFormat(content.leftFormat, context)
content.rightFormat?.let {
this.stat2Value.setTextFormat(it, context)
}
}
}
}

@ -0,0 +1,40 @@
package net.pokeranalytics.android.ui.view
import android.content.Context
import kotlinx.android.synthetic.main.layout_legend_default.view.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.TextFormat
import net.pokeranalytics.android.ui.extensions.setTextFormat
import net.pokeranalytics.android.ui.fragment.GraphFragment
data class MultilineLegendValues(
var firstTitle: String,
var secondTitle: String,
var leftFormat: TextFormat,
var rightFormat: TextFormat
) : LegendContent
class MultiLineLegendView(context: Context) : LegendView(context = context) {
override fun getResourceLayout(): Int {
return R.layout.layout_legend_color
}
override fun prepareWithStat(stat: Stat, counter: Int?, style: GraphFragment.Style) {
}
override fun setItemData(content: LegendContent) {
if (content is MultilineLegendValues) {
this.stat1Name.text = content.firstTitle
this.stat2Name.text = content.secondTitle
this.stat1Value.setTextFormat(content.leftFormat, context)
this.stat2Value.setTextFormat(content.rightFormat, context)
}
}
}

@ -6,9 +6,9 @@ import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
/**
* Poker Analytics ViewPager
* ViewPager with paging disabled
*/
class HomeViewPager(context: Context, attrs: AttributeSet) : ViewPager(context, attrs) {
class NoPagingViewPager(context: Context, attrs: AttributeSet) : ViewPager(context, attrs) {
var enablePaging: Boolean = false

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save