Compare commits

..

1 Commits

Author SHA1 Message Date
Razmig Sarkissian 757da5623a merge interface wip 7 years ago
  1. 7
      .gitignore
  2. 139
      CLAUDE.md
  3. 167
      app/build.gradle
  4. 24
      app/google-services.json
  5. 68
      app/proguard-rules.pro
  6. BIN
      app/src/androidTest/assets/schema_0.realm
  7. BIN
      app/src/androidTest/assets/schema_2.realm
  8. BIN
      app/src/androidTest/assets/schema_3.realm
  9. 420
      app/src/androidTest/java/net/pokeranalytics/android/ExampleInstrumentedUnitTest.kt
  10. 26
      app/src/androidTest/java/net/pokeranalytics/android/RealmInstrumentedUnitTest.kt
  11. 40
      app/src/androidTest/java/net/pokeranalytics/android/components/BaseFilterInstrumentedUnitTest.kt
  12. 105
      app/src/androidTest/java/net/pokeranalytics/android/components/RealmInstrumentedUnitTest.kt
  13. 41
      app/src/androidTest/java/net/pokeranalytics/android/components/SessionInstrumentedUnitTest.kt
  14. 113
      app/src/androidTest/java/net/pokeranalytics/android/migrations/MigrationsInstrumentedUnitTest.kt
  15. 103
      app/src/androidTest/java/net/pokeranalytics/android/model/CriteriaTest.kt
  16. 79
      app/src/androidTest/java/net/pokeranalytics/android/performanceTests/PerfsInstrumentedUnitTest.kt
  17. 128
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/BankrollInstrumentedUnitTest.kt
  18. 53
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/DeleteInstrumentedUnitTest.kt
  19. 87
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/FavoriteSessionUnitTest.kt
  20. 38
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatPerformanceUnitTest.kt
  21. 838
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  22. 190
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/BlindFilterInstrumentedTest.kt
  23. 87
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/CustomFieldFilterInstrumentedUnitTest.kt
  24. 513
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/DateFilterInstrumentedUnitTest.kt
  25. 20
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/ExceptionFilterInstrumentedTest.kt
  26. 66
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/RealmFilterInstrumentedUnitTest.kt
  27. 597
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/SessionFilterInstrumentedUnitTest.kt
  28. 55
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/TransactionFilterInstrumentedUnitTest.kt
  29. 7
      app/src/debug/AndroidManifest.xml
  30. 290
      app/src/main/AndroidManifest.xml
  31. BIN
      app/src/main/ic_launcher-playstore.png
  32. 154
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  33. 75
      app/src/main/java/net/pokeranalytics/android/api/BackupApi.kt
  34. 49
      app/src/main/java/net/pokeranalytics/android/api/BlogPostApi.kt
  35. 60
      app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt
  36. 242
      app/src/main/java/net/pokeranalytics/android/api/MultipartRequest.kt
  37. 32
      app/src/main/java/net/pokeranalytics/android/calculus/AggregationType.kt
  38. 12
      app/src/main/java/net/pokeranalytics/android/calculus/Aggregator.kt
  39. 824
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  40. 106
      app/src/main/java/net/pokeranalytics/android/calculus/Computable.kt
  41. 111
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  42. 35
      app/src/main/java/net/pokeranalytics/android/calculus/ComputedStat.kt
  43. 8
      app/src/main/java/net/pokeranalytics/android/calculus/Format.kt
  44. 300
      app/src/main/java/net/pokeranalytics/android/calculus/Report.kt
  45. 338
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  46. 429
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  47. 128
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt
  48. 287
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt
  49. 118
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReportManager.kt
  50. 12
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/AggregationTypeExtensions.kt
  51. 84
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ComputedResultsExtensions.kt
  52. 64
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ReportDisplay.kt
  53. 64
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ReportExtensions.kt
  54. 81
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/StatRepresentable.kt
  55. 183
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  56. 32
      app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt
  57. 5
      app/src/main/java/net/pokeranalytics/android/exceptions/ModelException.kt
  58. 356
      app/src/main/java/net/pokeranalytics/android/model/Criteria.kt
  59. 54
      app/src/main/java/net/pokeranalytics/android/model/Limit.kt
  60. 200
      app/src/main/java/net/pokeranalytics/android/model/LiveData.kt
  61. 38
      app/src/main/java/net/pokeranalytics/android/model/LiveOnline.kt
  62. 13
      app/src/main/java/net/pokeranalytics/android/model/Stakes.kt
  63. 77
      app/src/main/java/net/pokeranalytics/android/model/TableSize.kt
  64. 43
      app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt
  65. 13
      app/src/main/java/net/pokeranalytics/android/model/blogpost/BlogPost.kt
  66. 172
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  67. 103
      app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt
  68. 128
      app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt
  69. 1065
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  70. 108
      app/src/main/java/net/pokeranalytics/android/model/handhistory/HandSetup.kt
  71. 53
      app/src/main/java/net/pokeranalytics/android/model/handhistory/Position.kt
  72. 50
      app/src/main/java/net/pokeranalytics/android/model/handhistory/Street.kt
  73. 22
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Dated.kt
  74. 129
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Manageable.kt
  75. 189
      app/src/main/java/net/pokeranalytics/android/model/interfaces/StakesHolder.kt
  76. 28
      app/src/main/java/net/pokeranalytics/android/model/interfaces/TimeFilterable.kt
  77. 34
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Timed.kt
  78. 14
      app/src/main/java/net/pokeranalytics/android/model/interfaces/UsageCountable.kt
  79. 234
      app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt
  80. 348
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  81. 256
      app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt
  82. 79
      app/src/main/java/net/pokeranalytics/android/model/realm/Comment.kt
  83. 68
      app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt
  84. 57
      app/src/main/java/net/pokeranalytics/android/model/realm/Currency.kt
  85. 340
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt
  86. 153
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomFieldEntry.kt
  87. 201
      app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt
  88. 111
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt
  89. 164
      app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt
  90. 16
      app/src/main/java/net/pokeranalytics/android/model/realm/HandHistory.kt
  91. 97
      app/src/main/java/net/pokeranalytics/android/model/realm/Location.kt
  92. 72
      app/src/main/java/net/pokeranalytics/android/model/realm/Performance.kt
  93. 94
      app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt
  94. 33
      app/src/main/java/net/pokeranalytics/android/model/realm/Report.kt
  95. 103
      app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt
  96. 167
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  97. 1160
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  98. 167
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  99. 518
      app/src/main/java/net/pokeranalytics/android/model/realm/TimeFrame.kt
  100. 94
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt
  101. Some files were not shown because too many files have changed in this diff Show More

7
.gitignore vendored

@ -6,11 +6,8 @@
# Built application files
*.apk
*.ap_
*.aab
/app/release
app/*2020/
# Files for the Dalvik VM
*.dex
@ -18,7 +15,7 @@ app/*2020/
*.class
# Generated files
bin
bin/
gen/
# Gradle files
@ -45,4 +42,4 @@ gen-external-apklibs
# Libraries
/libs/*/*/build
/captures
/captures

@ -1,139 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is **Poker Analytics**, a comprehensive Android application for tracking and analyzing poker sessions. The app supports both cash games and tournaments, providing detailed statistics, reporting, and data visualization capabilities.
### Key Features
- Session tracking (cash games and tournaments)
- Advanced filtering and reporting
- Bankroll management
- Hand history import and replay
- Statistical analysis and charting
- Data backup and export functionality
- Multi-currency support
## Development Commands
### Building the Project
```bash
./gradlew assembleStandardRelease # Build release APK
./gradlew assembleStandardDebug # Build debug APK
./gradlew build # Build all variants
```
### Running Tests
```bash
./gradlew test # Run unit tests
./gradlew connectedAndroidTest # Run instrumented tests (requires device/emulator)
./gradlew testStandardDebugUnitTest # Run specific unit tests
```
### Cleaning
```bash
./gradlew clean # Clean build artifacts
```
### Build Configuration
- **Target SDK**: 35 (Android 15)
- **Min SDK**: 23 (Android 6.0)
- **Build Tools**: 30.0.3
- **Kotlin Version**: 1.9.24
- **Realm Schema Version**: 14
## Architecture Overview
### Package Structure
The main source code is organized under `app/src/main/java/net/pokeranalytics/android/`:
#### Core Components
- **`model/`** - Data models and business logic
- `realm/` - Realm database models (Session, Bankroll, Result, etc.)
- `filter/` - Query system for filtering sessions
- `migrations/` - Database migration handling
- `handhistory/` - Hand history data structures
- **`ui/`** - User interface components
- `activity/` - Main activities (HomeActivity, SessionActivity, etc.)
- `fragment/` - UI fragments organized by feature
- `adapter/` - RecyclerView adapters and data sources
- `modules/` - Feature-specific UI modules
- **`calculus/`** - Statistics and calculation engine
- Core calculation logic for poker statistics
- Report generation system
- Performance tracking
- **`util/`** - Utility classes
- `csv/` - CSV import/export functionality
- `billing/` - In-app purchase handling
- `extensions/` - Kotlin extension functions
#### Key Classes
- **`Session`** (`model/realm/Session.kt`): Core session data model
- **`HomeActivity`** (`ui/activity/HomeActivity.kt`): Main app entry point with tab navigation
- **`PokerAnalyticsApplication`**: Application class handling initialization
### Database Architecture
The app uses **Realm** database with these key entities:
- `Session` - Individual poker sessions
- `Bankroll` - Bankroll management
- `Result` - Session results and statistics
- `ComputableResult` - Pre-computed statistics for performance
- `Filter` - Saved filter configurations
- `HandHistory` - Hand-by-hand game data
### UI Architecture
- **MVVM pattern** with Android Architecture Components
- **Fragment-based navigation** with bottom navigation tabs
- **Custom RecyclerView adapters** for data presentation
- **Material Design** components
## Key Technologies
### Core Dependencies
- **Realm Database** (10.15.1) - Local data storage
- **Kotlin Coroutines** - Asynchronous programming
- **Firebase Crashlytics** - Crash reporting and analytics
- **Material Design Components** - UI framework
- **MPAndroidChart** - Data visualization
- **CameraX** - Image capture functionality
### Testing
- **JUnit** for unit testing
- **Android Instrumented Tests** for integration testing
- Test files located in `app/src/androidTest/` and `app/src/test/`
## Development Guidelines
### Working with Sessions
- Sessions are the core data model representing individual poker games
- Use `Session.newInstance()` to create new sessions properly
- Always call `computeStats()` after modifying session data
- Sessions can be in various states: PENDING, STARTED, PAUSED, ENDED
### Database Operations
- Use Realm transactions for data modifications
- The app uses schema version 14 - increment when making schema changes
- Migration logic is in `PokerAnalyticsMigration.kt`
### Testing Data
- Use `FakeDataManager.createFakeSessions()` for generating test data
- Seed data is available through the `Seed` class
### Build Variants
- **standard** - Main production flavor
- Release builds are optimized and obfuscated with ProGuard
## Performance Considerations
- Sessions use `ComputableResult` for pre-computed statistics
- Large datasets are handled with Realm's lazy loading
- Chart rendering is optimized for large data sets
- Background processing uses Kotlin Coroutines
## Security & Privacy
- Sensitive data is encrypted in Realm database
- Crash logging excludes personal information
- Backup functionality includes data encryption

@ -1,183 +1,84 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
//apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
// Crashlytics
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
// Serialization
apply plugin: "kotlinx-serialization"
//apply plugin: 'io.fabric'
repositories {
maven { url 'https://jitpack.io' } // required for MPAndroidChart
jcenter() // for kotlin serialization
maven { url 'https://maven.fabric.io/public' }
}
android {
compileSdkVersion 35
buildToolsVersion "30.0.3"
compileSdkVersion 28
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "net.pokeranalytics.android"
minSdkVersion 23
targetSdkVersion 35
versionCode 180
versionName "6.0.38"
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
ext.enableCrashlytics = false
firebaseCrashlytics {
mappingFileUploadEnabled false // should help speed up build times: https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports?hl=en&platform=android
}
}
release {
minifyEnabled true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationVariants.all { variant ->
variant.outputs.all { output ->
def date = new Date()
def formattedDate = date.format('yyMMdd_HHmm')
def appName = "PokerAnalytics"
def buildType = variant.buildType.name
def newName
if (buildType == 'debug'){
newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_debug.apk"
} else {
newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_release.apk"
}
outputFileName = newName
}
}
}
}
flavorDimensions 'endOfUse'
productFlavors { // already used: 50000, 51000, 52000, 52130, 52110, 52120
standard {
dimension = 'endOfUse'
}
// oct2021 {
// dimension = 'endOfUse'
// versionNameSuffix = '_oct2021'
// versionCode = 52120 + android.defaultConfig.versionCode
// }
}
configurations {
release {
all*.exclude group: 'com.google.guava', module: 'listenablefuture'
}
}
buildFeatures {
viewBinding true
}
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Kotlin
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
// implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") // JVM dependency
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Android
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.1.0-alpha04'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
// implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
// Places
implementation 'com.google.android.libraries.places:places:2.3.0'
// Firebase
implementation 'com.google.firebase:firebase-core:16.0.7'
// Billing / Subscriptions
implementation 'com.android.billingclient:billing:7.0.0'
// Crashlytics
//implementation 'com.crashlytics.sdk.android:crashlytics:2.9.9'
// Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:26.1.0')
// Declare the dependencies for the Crashlytics and Analytics libraries
// When using the BoM, you don't specify versions in Firebase library dependencies
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
// Kotlin
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
// Logs
implementation 'com.jakewharton.timber:timber:4.7.1'
// MPAndroidChart
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
// CSV Parser: https://mvnrepository.com/artifact/org.apache.commons/commons-csv
implementation 'org.apache.commons:commons-csv:1.7'
// Polynomial Regression
implementation 'org.apache.commons:commons-math3:3.6.1'
// ffmpeg for encoding video (HH export)
// implementation 'com.arthenica:ffmpeg-kit-min-gpl:4.4.LTS'
// Camera
def camerax_version = "1.1.0"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
// Image picking and registerForActivityResult
implementation 'androidx.activity:activity-ktx:1.6.1'
implementation "androidx.fragment:fragment-ktx:1.4.1"
// Test
androidTestImplementation 'androidx.test:core:1.0.0'
androidTestImplementation 'androidx.test:runner:1.1.0'
androidTestImplementation 'androidx.test:rules:1.1.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Volley
implementation 'com.android.volley:volley:1.2.1'
// Instrumented Tests
androidTestImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
// Required -- JUnit 4 framework
testImplementation 'junit:junit:4.12'
// Optional -- Robolectric environment
// testImplementation 'androidx.test:core:1.1.0'
// Optional -- Mockito framework
testImplementation 'com.android.support.test:runner:1.0.1'
testImplementation 'com.android.support.test:rules:1.0.1'
// Test
testImplementation 'junit:junit:4.13.2'
testImplementation 'com.android.support.test:runner:1.0.2'
testImplementation 'com.android.support.test:rules:1.0.2'
// testImplementation 'androidx.test.espresso:espresso-core:3.1.0'
// gross, somehow needed to make the stop notif work
implementation 'com.google.guava:guava:27.0.1-android'
}
apply plugin: 'com.google.gms.google-services'

@ -8,12 +8,20 @@
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:245968016816:android:e5597a41d79df0a31d7275",
"mobilesdk_app_id": "1:245968016816:android:47f8b4f74b1296b4",
"android_client_info": {
"package_name": "net.pokeranalytics.android"
}
},
"oauth_client": [
{
"client_id": "245968016816-tr2mo4kbe1acn8u3ebbd9nk29iuk8fqr.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "net.pokeranalytics.android",
"certificate_hash": "e26278fa6db56acde23b0ff5981692f7f60408b9"
}
},
{
"client_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com",
"client_type": 3
@ -25,20 +33,20 @@
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 2,
"other_platform_oauth_client": [
{
"client_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "245968016816-8sklai9sq70m46anv550uttdic6cukn6.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "stax.SlashPoker.nosebleed"
}
}
]
},
"ads_service": {
"status": 2
}
}
}

@ -19,71 +19,3 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# Realm
-keep class io.realm.annotations.RealmModule
-keep @io.realm.annotations.RealmModule class *
-keep class io.realm.internal.Keep
-keep @io.realm.internal.Keep class *
-dontwarn javax.**
-dontwarn io.realm.**
-keep class net.pokeranalytics.android.model.** { *; }
-keep class net.pokeranalytics.android.ui.fragment.** { *; }
-keep class net.pokeranalytics.android.ui.modules.** { *; }
-keepattributes SourceFile,LineNumberTable # Keep file names and line numbers.
-keep public class * extends java.lang.Exception # Optional: Keep custom exceptions.
# Retrofit
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
# EnclosingMethod is required to use InnerClasses.
-keepattributes Signature, InnerClasses, EnclosingMethod
# Retrofit does reflection on method and parameter annotations.
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
# Ignore annotation used for build tooling.
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
# Ignore JSR 305 annotations for embedding nullability information.
-dontwarn javax.annotation.**
# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
-dontwarn kotlin.Unit
# Top-level functions that can only be used by Kotlin.
-dontwarn retrofit2.KotlinExtensions
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>
# Guava
-dontwarn com.google.j2objc.annotations.**
-keep class com.google.j2objc.annotations.** { *; }
# Enum
-optimizations !class/unboxing/enum
# Serialization
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class net.pokeranalytics.android.**$$serializer { *; }
-keepclassmembers class net.pokeranalytics.android.** {
*** Companion;
}
-keepclasseswithmembers class net.pokeranalytics.android.** {
kotlinx.serialization.KSerializer serializer(...);
}

@ -0,0 +1,420 @@
package net.pokeranalytics.android
import android.os.Looper
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.realm.RealmResults
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.SessionGroup
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.TimeFrame
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import java.text.SimpleDateFormat
import java.util.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedUnitTest : RealmInstrumentedUnitTest() {
// convenience extension
fun Session.Companion.testInstance(netResult: Double, startDate: Date, endDate: Date?): Session {
var session: Session = Session.newInstance()
session.result?.netResult = netResult
session.timeFrame?.setDate(startDate, endDate)
return session
}
@Test
fun testSessionStats() {
val realm = this.mockRealm
realm.beginTransaction()
var s1 = realm.createObject(Session::class.java, "1")
var s2 = realm.createObject(Session::class.java, "2")
s1.timeFrame = realm.createObject(TimeFrame::class.java)
s2.timeFrame = realm.createObject(TimeFrame::class.java)
s1.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)
s2.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)
s1.result?.buyin = 100.0 // net result = -100
s2.result?.buyin = 200.0
s2.result?.cashout = 500.0 // net result = 300
s1.cgBigBlind = 0.5 // bb net result = -200bb
s2.cgBigBlind = 2.0 // bb net result = 150bb
realm.insert(s1)
realm.insert(s2)
realm.commitTransaction()
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 10:00")
val ed1 = sdf.parse("01/1/2019 11:00")
val sd2 = sdf.parse("02/1/2019 08:00")
val ed2 = sdf.parse("02/1/2019 11:00")
realm.beginTransaction()
s1.timeFrame?.setDate(sd1, ed1) // duration = 1h, hourly = -100, bb100 = -200bb / 25hands * 100 = -800
s2.timeFrame?.setDate(sd2, ed2) // duration = 3h, hourly = 100, bb100 = 150 / 75 * 100 = +200
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
realm.commitTransaction()
val sessions = realm.where(Session::class.java).findAll()
val group = SessionGroup(name = "test", sessions = sessions)
var options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val delta = 0.01
val sum = results.computedStat(Stat.NETRESULT)
if (sum != null) {
assertEquals(200.0, sum.value, delta)
} else {
Assert.fail("No Net result stat")
}
val average = results.computedStat(Stat.AVERAGE)
if (average != null) {
assertEquals(100.0, average.value, delta)
} else {
Assert.fail("No AVERAGE stat")
}
val duration = results.computedStat(Stat.DURATION)
if (duration != null) {
assertEquals(4.0, duration.value, delta)
} else {
Assert.fail("No duration stat")
}
val hourlyRate = results.computedStat(Stat.HOURLY_RATE)
if (hourlyRate != null) {
assertEquals(50.0, hourlyRate.value, delta)
} else {
Assert.fail("No houry rate stat")
}
val handsPlayed = results.computedStat(Stat.HANDS_PLAYED)
if (handsPlayed != null) {
assertEquals(100.0, handsPlayed.value, delta)
} else {
Assert.fail("No hands played stat")
}
val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
if (numberOfGames != null) {
assertEquals(2, numberOfGames.value.toInt())
} else {
Assert.fail("No numberOfGames stat")
}
val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
if (numberOfSets != null) {
assertEquals(2, numberOfSets.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
val avgBuyin = results.computedStat(Stat.AVERAGE_BUYIN)
if (avgBuyin != null) {
assertEquals(150.0, avgBuyin.value, delta)
} else {
Assert.fail("No avgBuyin stat")
}
val avgDuration = results.computedStat(Stat.AVERAGE_DURATION)
if (avgDuration != null) {
assertEquals(2.0, avgDuration.value, delta)
} else {
Assert.fail("No avgDuration stat")
}
val roi = results.computedStat(Stat.ROI)
if (roi != null) {
assertEquals(200 / 300.0, roi.value, delta)
} else {
Assert.fail("No roi stat")
}
val avgBBNet = results.computedStat(Stat.AVERAGE_NET_BB)
if (avgBBNet != null) {
assertEquals(-25.0, avgBBNet.value, delta)
} else {
Assert.fail("No avgBBNet stat")
}
val bbHourlyRate = results.computedStat(Stat.HOURLY_RATE_BB)
if (bbHourlyRate != null) {
assertEquals(-12.5, bbHourlyRate.value, delta)
} else {
Assert.fail("No bbHourlyRate stat")
}
val netbbPer100Hands = results.computedStat(Stat.NET_BB_PER_100_HANDS)
if (netbbPer100Hands != null) {
assertEquals(-50.0, netbbPer100Hands.value, delta)
} else {
Assert.fail("No netbbPer100Hands stat")
}
val stdHourly = results.computedStat(Stat.STANDARD_DEVIATION_HOURLY)
if (stdHourly != null) {
assertEquals(141.42, stdHourly.value, delta)
} else {
Assert.fail("No stdHourly stat")
}
val std = results.computedStat(Stat.STANDARD_DEVIATION)
if (std != null) {
assertEquals(282.84, std.value, delta)
} else {
Assert.fail("No std stat")
}
val std100 = results.computedStat(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS)
if (std100 != null) {
assertEquals(707.1, std100.value, delta)
} else {
Assert.fail("No std100 stat")
}
}
@Test
fun testOverlappingSessions1() {
val realm = this.mockRealm
realm.beginTransaction()
var s1 = realm.createObject(Session::class.java, "1")
var s2 = realm.createObject(Session::class.java, "2")
s1.timeFrame = realm.createObject(TimeFrame::class.java)
s2.timeFrame = realm.createObject(TimeFrame::class.java)
s1.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)
s2.result = realm.createObject(net.pokeranalytics.android.model.realm.Result::class.java)
realm.insert(s1)
realm.insert(s2)
realm.commitTransaction()
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 08:00")
val ed2 = sdf.parse("01/1/2019 11:00")
realm.beginTransaction()
s1.timeFrame?.setDate(sd1, ed1) // duration = 1h, hourly = -100, bb100 = -200bb / 25hands * 100 = -800
s2.timeFrame?.setDate(sd2, ed2) // duration = 4h, hourly = 100, bb100 = 150 / 75 * 100 = +200
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
realm.commitTransaction()
val sessions = realm.where(Session::class.java).findAll()
val group = SessionGroup(name = "test", sessions = sessions)
var options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val delta = 0.01
val duration = results.computedStat(Stat.DURATION)
if (duration != null) {
assertEquals(3.0, duration.value, delta)
} else {
Assert.fail("No Net result stat")
}
val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
if (numberOfSets != null) {
assertEquals(1, numberOfSets.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
if (numberOfGames != null) {
assertEquals(2, numberOfGames.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
}
@Test
fun testOverlappingSessions2() {
val realm = this.mockRealm
realm.beginTransaction()
var s1 = realm.createObject(Session::class.java, "1")
var s2 = realm.createObject(Session::class.java, "2")
var s3 = realm.createObject(Session::class.java, "3")
s1.timeFrame = realm.createObject(TimeFrame::class.java)
s2.timeFrame = realm.createObject(TimeFrame::class.java)
s3.timeFrame = realm.createObject(TimeFrame::class.java)
realm.insert(s1)
realm.insert(s2)
realm.insert(s3)
realm.commitTransaction()
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 05: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 11:00")
val sd3 = sdf.parse("01/1/2019 03:00")
val ed3 = sdf.parse("01/1/2019 06:00")
realm.beginTransaction()
s1.timeFrame?.setDate(sd1, ed1) // duration = 4h
s2.timeFrame?.setDate(sd2, ed2) // duration = 4h
s3.timeFrame?.setDate(sd3, ed3) // duration = 3h
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
realm.copyToRealmOrUpdate(s3)
realm.commitTransaction()
val sessions = realm.where(Session::class.java).findAll()
val group = SessionGroup(name = "test", sessions = sessions)
var options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val delta = 0.01
val duration = results.computedStat(Stat.DURATION)
if (duration != null) {
assertEquals(8.0, duration.value, delta)
} else {
Assert.fail("No Net result stat")
}
val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
if (numberOfSets != null) {
assertEquals(1, numberOfSets.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
if (numberOfGames != null) {
assertEquals(3, numberOfGames.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
}
var sessions: RealmResults<Session>? = null
@Test
fun testOverlappingSessionDeletion() {
val realm = this.mockRealm
this.sessions = realm.where(Session::class.java).findAll() // monitor session deletions
Looper.prepare()
this.sessions?.addChangeListener { t, changeSet ->
val deletedSessions = realm.where(Session::class.java).`in`("id", changeSet.deletions.toTypedArray()).findAll()
deletedSessions.forEach { it.cleanup() }
}
Looper.loop()
realm.beginTransaction()
var s1 = realm.createObject(Session::class.java, "1")
var s2 = realm.createObject(Session::class.java, "2")
var s3 = realm.createObject(Session::class.java, "3")
s1.timeFrame = realm.createObject(TimeFrame::class.java)
s2.timeFrame = realm.createObject(TimeFrame::class.java)
s3.timeFrame = realm.createObject(TimeFrame::class.java)
realm.insert(s1)
realm.insert(s2)
realm.insert(s3)
realm.commitTransaction()
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 05: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 11:00")
val sd3 = sdf.parse("01/1/2019 03:00")
val ed3 = sdf.parse("01/1/2019 06:00")
realm.beginTransaction()
s1.timeFrame?.setDate(sd1, ed1) // duration = 4h
s2.timeFrame?.setDate(sd2, ed2) // duration = 4h
s3.timeFrame?.setDate(sd3, ed3) // duration = 3h
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
realm.copyToRealmOrUpdate(s3)
realm.commitTransaction()
val sessions = realm.where(Session::class.java).findAll()
val group = SessionGroup(name = "test", sessions = sessions)
var options = Calculator.Options()
options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(group, options)
val delta = 0.01
val duration = results.computedStat(Stat.DURATION)
if (duration != null) {
assertEquals(8.0, duration.value, delta)
} else {
Assert.fail("No duration stat")
}
realm.beginTransaction()
s1.deleteFromRealm()
realm.commitTransaction()
// realm.executeTransaction {
// s1.deleteFromRealm()
// }
val group2 = SessionGroup(name = "test", sessions = sessions)
val results2: ComputedResults = Calculator.compute(group2, options)
val duration2 = results2.computedStat(Stat.DURATION)
if (duration2 != null) {
assertEquals(7.0, duration2.value, delta)
} else {
Assert.fail("No duration2 stat")
}
}
}

@ -0,0 +1,26 @@
package net.pokeranalytics.android
import io.realm.Realm
import io.realm.RealmConfiguration
import org.junit.After
import org.junit.Before
open class RealmInstrumentedUnitTest {
lateinit var mockRealm: Realm
@Before
fun setup() {
val testConfig = RealmConfiguration.Builder().inMemory().name("test-realm").build()
Realm.setDefaultConfiguration(testConfig)
mockRealm = Realm.getDefaultInstance()
}
@After
@Throws(Exception::class)
public fun tearDown() {
mockRealm.close()
}
}

@ -1,40 +0,0 @@
package net.pokeranalytics.android.components
import io.realm.RealmList
import net.pokeranalytics.android.model.realm.*
import java.util.*
open class BaseFilterInstrumentedUnitTest : 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?.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
session.endDate = cal.time // returns new date object, one hour in the future
return session
}
}

@ -1,105 +0,0 @@
package net.pokeranalytics.android.components
import io.realm.Realm
import io.realm.RealmConfiguration
import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.model.realm.Session
import org.junit.After
import org.junit.Before
import java.util.*
open class RealmInstrumentedUnitTest {
val EPSILON = 0.0001
lateinit var mockRealm: Realm
companion object {
fun newSessionInstance(realm: Realm, isCashGame: Boolean = true) : Session {
val session = realm.createObject(Session::class.java, UUID.randomUUID().toString())
session.startDate = Date()
session.type = if (isCashGame) Session.Type.CASH_GAME.ordinal else Session.Type.TOURNAMENT.ordinal
session.result = realm.createObject(Result::class.java)
return session
}
}
@Before
fun setup() {
val testConfig = RealmConfiguration.Builder().inMemory().name("test-realm").build()
Realm.setDefaultConfiguration(testConfig)
this.mockRealm = Realm.getDefaultInstance()
this.mockRealm.beginTransaction()
this.mockRealm.deleteAll()
this.mockRealm.commitTransaction()
}
@After
@Throws(Exception::class)
fun tearDown() {
this.mockRealm.close()
}
}
/*
package net.pokeranalytics.android.components
import io.realm.Realm
import io.realm.RealmConfiguration
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Result
import net.pokeranalytics.android.model.realm.Session
import org.junit.AfterClass
import org.junit.Before
import java.util.*
interface RealmTestDataSource {
val realmName : String
}
open class RealmInstrumentedUnitTest {
companion object : RealmTestDataSource {
lateinit var mockRealm: Realm
override val realmName: String
get() = "RealmInstrumentedUnitTest"
init {
val testConfig = RealmConfiguration.Builder().inMemory().name(realmName).build()
Realm.setDefaultConfiguration(testConfig)
mockRealm = Realm.getDefaultInstance()
}
fun newSessionInstance(realm: Realm) : Session {
val session = realm.createObject(Session::class.java, UUID.randomUUID().toString())
val computableResult = realm.createObject(ComputableResult::class.java)
computableResult.session = session
session.result = realm.createObject(Result::class.java)
return session
}
@AfterClass
@Throws(Exception::class)
fun tearDown() {
mockRealm.close()
}
}
var mockRealm: Realm = Companion.mockRealm
@Before
fun setup() {
this.mockRealm.beginTransaction()
this.mockRealm.deleteAll()
this.mockRealm.commitTransaction()
}
}
*/

@ -1,41 +0,0 @@
package net.pokeranalytics.android.components
import io.realm.RealmList
import net.pokeranalytics.android.model.realm.*
import java.util.*
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
}
}

@ -1,113 +0,0 @@
package net.pokeranalytics.android.migrations
import android.os.Environment
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.where
import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration
import net.pokeranalytics.android.model.realm.Session
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.io.InputStream
/*
class MigrationsInstrumentedUnitTest {
@get:Rule
var permissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
private lateinit var mockRealm: Realm
private lateinit var directory: File
private lateinit var dbFileName: String
@Before
fun setup() {
Timber.d("*** Setup realm migration")
directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
dbFileName = "schema_0.realm"
val context = InstrumentationRegistry.getInstrumentation().context
val inputStream = context.assets.open(dbFileName)
copyBundledRealmFile(inputStream, dbFileName)
}
@Test
// TODO: Uncomment this when we'll have migrations to execute (should crash if we don't apply the migration)
//@Test(expected = io.realm.exceptions.RealmMigrationNeededException::class)
fun testMigrationNeeded() {
Timber.d("*** testMigrationNeeded")
val config0 = RealmConfiguration.Builder()
.directory(directory)
.name(dbFileName)
.schemaVersion(0)
.build()
Realm.migrateRealm(config0, PokerAnalyticsMigration())
}
@Test
fun testMigrationSuccess() {
Timber.d("*** testMigrationSuccess")
val config1 = RealmConfiguration.Builder()
.directory(directory)
.name(dbFileName)
// TODO: Set the correct schema version when we'll have migration to avoid crash
.schemaVersion(0)
.build()
Realm.migrateRealm(config1, PokerAnalyticsMigration())
this.mockRealm = Realm.getInstance(config1)
val sessions = this.mockRealm.where<Session>().findAll()
Timber.d("*** Sessions: ${sessions.size}")
this.mockRealm.close()
}
/**
* Copy [inputStream] to [outFileName]
*/
private fun copyBundledRealmFile(inputStream: InputStream, outFileName: String): String? {
try {
val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), outFileName)
val outputStream = file.outputStream()
inputStream.use { input ->
outputStream.use { fileOut ->
input.copyTo(fileOut)
}
}
inputStream.close()
outputStream.close()
Timber.d("File: ${file.length()}")
return file.absolutePath
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
}
*/

@ -1,103 +0,0 @@
package net.pokeranalytics.android.model
import android.content.Context
import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Session
import org.junit.Assert.assertEquals
import org.junit.Test
import java.util.*
import androidx.test.platform.app.InstrumentationRegistry
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)
val firstValue = cal.get(Calendar.YEAR)
cal.add(Calendar.YEAR, 1)
Session.testInstance(100.0, true, cal.time)
cal.add(Calendar.YEAR, -11)
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 + 5
realm.commitTransaction()
val yearQueries = Criteria.Years.queryConditions
assertEquals(16, yearQueries.size)
val firstYearCondition = yearQueries.first().conditions.first() as QueryCondition.AnyYear
assertEquals(firstValue - 10, firstYearCondition.listOfValues.first())
val lastYearCondition = yearQueries.last().conditions.first() as QueryCondition.AnyYear
assertEquals(lastValue, lastYearCondition.listOfValues.first())
// val years = Criteria.Years.queryConditions as List<QueryCondition.AnyYear>
// println("years = ${years.map { it.getDisplayName() }}")
//
// assertEquals(16, years.size)
// assertEquals(firstValue-10, years.first().listOfValues.first())
// assertEquals(lastValue, years.last().listOfValues.first())
}
@Test
fun combined() {
val criterias = listOf(Criteria.MonthsOfYear, Criteria.DaysOfWeek)
val combined = criterias.combined()
val context = InstrumentationRegistry.getInstrumentation().context
combined.forEach {
it.conditions.forEach {qc->
println(qc.getDisplayName(context))
}
}
}
@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 context = InstrumentationRegistry.getInstrumentation().context
val allMonths = Criteria.AllMonthsUpToNow.queries
allMonths.forEach {
it.conditions.forEach { qc->
println("<<<<< ${qc.getDisplayName(context)}")
}
}
}
}

@ -1,79 +0,0 @@
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.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.filter.Query
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.utils.Seed
import net.pokeranalytics.android.util.FakeDataManager
import org.junit.Test
import org.junit.runner.RunWith
import timber.log.Timber
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class PerfsInstrumentedUnitTest : RealmInstrumentedUnitTest() {
@Test
fun testGlobalPerfs() {
Timber.d("*** start global perfs ")
val realm = mockRealm
val app: Context = ApplicationProvider.getApplicationContext()
realm.beginTransaction()
Seed(app).execute(realm)
realm.commitTransaction()
FakeDataManager.createFakeSessions(5000) {success ->
if (success) {
val start = System.currentTimeMillis()
val sessions = realm.where(Session::class.java).findAll()
val computableResults = realm.where(ComputableResult::class.java).findAll()
val sets = realm.where(SessionSet::class.java).findAll()
Timber.d("sessions: ${sessions.size}")
Timber.d("computableResults: ${computableResults.size}")
Timber.d("sets: ${sets.size}")
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group = ComputableGroup(Query(), stats)
val options = Calculator.Options()
options.stats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(realm, group, options)
Timber.d("*** ended in ${System.currentTimeMillis() - start} milliseconds")
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.HOURLY_DURATION)
Timber.d("*** HOURLY_DURATION: ${duration?.value}")
}
}
}
}

@ -1,128 +0,0 @@
package net.pokeranalytics.android.unitTests
import androidx.test.ext.junit.runners.AndroidJUnit4
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
import org.junit.Test
import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class)
class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() {
private fun createDefaultTransactionTypes(realm: Realm) {
TransactionType.Value.values().forEachIndexed { index, value ->
val type = TransactionType()
type.additive = value.additive
type.kind = value.uniqueIdentifier
type.lock = true
realm.insertOrUpdate(type)
}
}
// convenience extension
fun Session.Companion.testInstance(netResult: Double, startDate: Date, endDate: Date?): Session {
val session: Session = newInstance(super.mockRealm, false)
session.result?.netResult = netResult
session.startDate = startDate
session.endDate = endDate
return session
}
@Test
fun testReport() {
val realm = mockRealm
realm.executeTransaction {
this.createDefaultTransactionTypes(realm)
val br1 = realm.createObject(Bankroll::class.java, "1")
br1.name = "br1"
val br2 = realm.createObject(Bankroll::class.java, "2")
br2.name = "br2"
br1.initialValue = 100.0
br2.initialValue = 1000.0
val t1 = realm.createObject(Transaction::class.java, UUID.randomUUID().toString())
t1.amount = 100.0
t1.type = TransactionType.getByValue(TransactionType.Value.BONUS, realm)
t1.bankroll = br1
val t2 = realm.createObject(Transaction::class.java, UUID.randomUUID().toString())
t2.amount = 500.0
t2.type = TransactionType.getByValue(TransactionType.Value.BONUS, realm)
t2.bankroll = br2
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 br1 = realm.where(Bankroll::class.java).equalTo("name", "br1").findFirst()
val brSetup1 = BankrollReportSetup(br1?.id)
val report1 = BankrollCalculator.computeReport(realm, brSetup1)
Assert.assertEquals(400.0, report1.total, EPSILON)
val br2 = realm.where(Bankroll::class.java).equalTo("name", "br2").findFirst()
val brSetup2 = BankrollReportSetup(br2?.id)
val report2 = BankrollCalculator.computeReport(realm, brSetup2)
Assert.assertEquals(2000.0, report2.total, EPSILON)
val brSetupAll = BankrollReportSetup()
val reportAll = BankrollCalculator.computeReport(realm, brSetupAll)
Assert.assertEquals(2400.0, reportAll.total, EPSILON)
}
@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)
t1.bankroll = br1
val s1 = newSessionInstance(realm)
s1.bankroll = br1
s1.result?.cashout = 200.0
}
val brSetup1 = BankrollReportSetup(br1?.id)
val report1 = BankrollCalculator.computeReport(realm, brSetup1)
Assert.assertEquals(400.0, report1.total, EPSILON)
val brSetupAll = BankrollReportSetup()
val reportAll = BankrollCalculator.computeReport(realm, brSetupAll)
Assert.assertEquals(4000.0, reportAll.total, EPSILON)
}
}

@ -1,53 +0,0 @@
package net.pokeranalytics.android.unitTests
import net.pokeranalytics.android.components.RealmInstrumentedUnitTest
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Currency
import net.pokeranalytics.android.util.extensions.findById
import org.junit.Assert
import org.junit.Test
class DeleteInstrumentedUnitTest : RealmInstrumentedUnitTest() {
@Test
fun testDeleteValidation() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = newSessionInstance(realm)
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")
c1.rate = 0.1
c2.rate = 2.0
br1.currency = c1
br2.currency = c2
s1.bankroll = br1
s2.bankroll = br2
s1.result?.netResult = 100.0
s2.result?.netResult = 200.0
realm.commitTransaction()
var isValidForDelete = br1.isValidForDelete(realm)
Assert.assertEquals(false, isValidForDelete)
realm.findById(Bankroll::class.java, "1")?.let {
isValidForDelete = it.isValidForDelete(realm)
Assert.assertEquals(false, isValidForDelete)
isValidForDelete = realm.copyFromRealm(it).isValidForDelete(realm)
Assert.assertEquals(false, isValidForDelete)
}
}
}

@ -1,87 +0,0 @@
package net.pokeranalytics.android.unitTests
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import net.pokeranalytics.android.components.RealmInstrumentedUnitTest
import net.pokeranalytics.android.model.realm.Location
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.utils.FavoriteSessionFinder
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class)
class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
@Test
fun testFavoriteWithoutLocation() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = newSessionInstance(realm)
val s2 = newSessionInstance(realm)
val s3 = newSessionInstance(realm)
s1.endDate = Date()
s2.endDate = Date()
s3.endDate = Date()
s1.cgBlinds = "2/4"
s2.cgBlinds = "2/4"
s3.cgBlinds = "1/2"
realm.insert(s1)
realm.insert(s2)
realm.insert(s3)
realm.commitTransaction()
val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, null, realm, InstrumentationRegistry.getInstrumentation().targetContext)
if (favSession != null) {
Assert.assertEquals(4.0, favSession.cgBiggestBet)
} else {
Assert.fail("session shouldn't be null")
}
}
@Test
fun testFavoriteWithLocation() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = newSessionInstance(realm)
val s2 = newSessionInstance(realm)
val s3 = newSessionInstance(realm)
val loc1 = realm.createObject(Location::class.java, "1")
val loc2 = realm.createObject(Location::class.java, "2")
s1.cgBiggestBet = 4.0
s2.cgBiggestBet = 4.0
s3.cgBiggestBet = 1.0
s1.location = loc1
s2.location = loc1
s3.location = loc2
realm.insert(s1)
realm.insert(s2)
realm.insert(s3)
realm.commitTransaction()
val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, loc2, realm, InstrumentationRegistry.getInstrumentation().targetContext)
if (favSession != null) {
Assert.assertEquals(1.0, favSession.cgBiggestBet)
} else {
Assert.fail("session shouldn't be null")
}
}
}

@ -1,38 +0,0 @@
package net.pokeranalytics.android.unitTests
import androidx.test.ext.junit.runners.AndroidJUnit4
import net.pokeranalytics.android.components.SessionInstrumentedUnitTest
import net.pokeranalytics.android.model.realm.Bankroll
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()
val bankroll = realm.createObject(Bankroll::class.java, "1")
bankroll.live = false
for (index in 0..100) {
Session.testInstance((-2000..2000).random().toDouble(), bankroll = bankroll)
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,838 +0,0 @@
package net.pokeranalytics.android.unitTests
import androidx.test.ext.junit.runners.AndroidJUnit4
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.SessionInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.Currency
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import java.text.SimpleDateFormat
import java.util.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
@Test
fun testAllSessionStats() {
val realm = this.mockRealm
val computableResults = realm.where(ComputableResult::class.java).findAll()
assertEquals(0, computableResults.size)
realm.beginTransaction()
val s1 = newSessionInstance(realm)
val s2 = newSessionInstance(realm)
s1.result?.buyin = 100.0 // net result = -100
s2.result?.buyin = 200.0
s2.result?.cashout = 500.0 // net result = 300
s1.cgBlinds = "0.5" // bb net result = -200bb
s2.cgBlinds = "2.0" // bb net result = 150bb
s2.tableSize = 5
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 10:00")
val ed1 = sdf.parse("01/1/2019 11:00") // 1 hour
val sd2 = sdf.parse("02/1/2019 08:00")
val ed2 = sdf.parse("02/1/2019 11:00") // 3 hours
s1.startDate = sd1
s1.endDate = ed1
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)
computableResults.forEach {
println(">>>>>> rated net = ${it.ratedNet} ")
}
val group = ComputableGroup(Query())
val options = Calculator.Options()
options.stats = 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(realm, group, options)
val delta = 0.01
val sum = results.computedStat(Stat.NET_RESULT)
if (sum != null) {
assertEquals(200.0, sum.value, delta)
} else {
Assert.fail("No Net result stat")
}
val average = results.computedStat(Stat.AVERAGE)
if (average != null) {
assertEquals(100.0, average.value, delta)
} else {
Assert.fail("No AVERAGE stat")
}
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(4.0, duration.value, delta)
} else {
Assert.fail("No netDuration stat")
}
val hourlyRate = results.computedStat(Stat.HOURLY_RATE)
if (hourlyRate != null) {
assertEquals(50.0, hourlyRate.value, delta)
} else {
Assert.fail("No houry rate stat")
}
val handsPlayed = results.computedStat(Stat.HANDS_PLAYED)
if (handsPlayed != null) {
assertEquals(177.77, handsPlayed.value, delta)
} else {
Assert.fail("No hands played stat")
}
val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
if (numberOfGames != null) {
assertEquals(2, numberOfGames.value.toInt())
} else {
Assert.fail("No numberOfGames stat")
}
val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
if (numberOfSets != null) {
assertEquals(2, numberOfSets.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
val avgBuyin = results.computedStat(Stat.AVERAGE_BUYIN)
if (avgBuyin != null) {
assertEquals(150.0, avgBuyin.value, delta)
} else {
Assert.fail("No avgBuyin stat")
}
val avgDuration = results.computedStat(Stat.AVERAGE_HOURLY_DURATION)
if (avgDuration != null) {
assertEquals(2.0, avgDuration.value, delta)
} else {
Assert.fail("No avgDuration stat")
}
val roi = results.computedStat(Stat.ROI)
if (roi != null) {
assertEquals(200 / 300.0, roi.value, delta)
} else {
Assert.fail("No roi stat")
}
val avgBBNet = results.computedStat(Stat.AVERAGE_NET_BB)
if (avgBBNet != null) {
assertEquals(-25.0, avgBBNet.value, delta)
} else {
Assert.fail("No avgBBNet stat")
}
val bbHourlyRate = results.computedStat(Stat.HOURLY_RATE_BB)
if (bbHourlyRate != null) {
assertEquals(-12.5, bbHourlyRate.value, delta)
} else {
Assert.fail("No bbHourlyRate stat")
}
val netbbPer100Hands = results.computedStat(Stat.NET_BB_PER_100_HANDS)
if (netbbPer100Hands != null) {
assertEquals(-28.12, netbbPer100Hands.value, delta)
} else {
Assert.fail("No netbbPer100Hands stat")
}
val std = results.computedStat(Stat.STANDARD_DEVIATION)
if (std != null) {
assertEquals(200.0, std.value, delta)
} else {
Assert.fail("No std stat")
}
val stdHourly = results.computedStat(Stat.STANDARD_DEVIATION_HOURLY)
if (stdHourly != null) {
assertEquals(111.8, stdHourly.value, delta)
} else {
Assert.fail("No stdHourly stat")
}
val std100 = results.computedStat(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS)
if (std100 != null) {
assertEquals(497.54, std100.value, delta)
} else {
Assert.fail("No std100 stat")
}
results.computedStat(Stat.MAXIMUM_NET_RESULT)?.let {
assertEquals(300.0, it.value, delta)
} ?: run {
Assert.fail("No MAXIMUM_NETRESULT")
}
results.computedStat(Stat.MINIMUM_NET_RESULT)?.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
fun testOverlappingSessions1() {
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 08:00")
val ed2 = sdf.parse("01/1/2019 11:00")
s1.startDate = sd1
s1.endDate = ed1
s2.startDate = sd2
s2.endDate = ed2
// netDuration = 1h, hourly = -100, bb100 = -200bb / 25hands * 100 = -800
// netDuration = 4h, hourly = 100, bb100 = 150 / 75 * 100 = +200
}
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group = ComputableGroup(Query(), stats)
val options = Calculator.Options()
options.stats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(realm, group, options)
val delta = 0.01
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(3.0, duration.value, delta)
} else {
Assert.fail("No Net result stat")
}
val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
if (numberOfSets != null) {
assertEquals(1, numberOfSets.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
if (numberOfGames != null) {
assertEquals(2, numberOfGames.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
}
@Test
fun testOverlappingSessions2() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = newSessionInstance(realm)
val s2 = newSessionInstance(realm)
val s3 = newSessionInstance(realm)
realm.insert(s1)
realm.insert(s2)
realm.insert(s3)
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 05:00")
val ed1 = sdf.parse("01/1/2019 09:00") // 4h
val sd2 = sdf.parse("01/1/2019 07:00")
val ed2 = sdf.parse("01/1/2019 11:00") // 4h
val sd3 = sdf.parse("01/1/2019 03:00")
val ed3 = sdf.parse("01/1/2019 06:00") // 3h
s1.startDate = sd1
s1.endDate = ed1
s2.startDate = sd2
s2.endDate = ed2
s3.startDate = sd3
s3.endDate = ed3
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
realm.copyToRealmOrUpdate(s3)
realm.commitTransaction()
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group = ComputableGroup(Query(), stats)
val options = Calculator.Options()
options.stats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(realm, group, options)
val delta = 0.01
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(8.0, duration.value, delta)
} else {
Assert.fail("No Net result stat")
}
val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
if (numberOfSets != null) {
assertEquals(1, numberOfSets.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
val numberOfGames = results.computedStat(Stat.NUMBER_OF_GAMES)
if (numberOfGames != null) {
assertEquals(3, numberOfGames.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
}
private var sessions: RealmResults<Session>? = null
// @Test
fun testOverlappingSessionDeletion() {
val realm = this.mockRealm
this.sessions = realm.where(Session::class.java).findAll() // monitor session deletions
this.sessions?.addChangeListener { _, changeSet ->
val deletedSessions =
realm.where(Session::class.java).`in`("id", changeSet.deletions.toTypedArray()).findAll()
deletedSessions.forEach { it.cleanup() }
}
realm.beginTransaction()
val s1 = realm.createObject(Session::class.java, "1")
val s2 = realm.createObject(Session::class.java, "2")
val s3 = realm.createObject(Session::class.java, "3")
realm.insert(s1)
realm.insert(s2)
realm.insert(s3)
realm.commitTransaction()
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 05: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 11:00")
val sd3 = sdf.parse("01/1/2019 03:00")
val ed3 = sdf.parse("01/1/2019 06:00")
realm.beginTransaction()
s1.startDate = sd1
s1.endDate = ed1
s2.startDate = sd2
s2.endDate = ed2
s3.startDate = sd3
s3.endDate = ed3
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
realm.copyToRealmOrUpdate(s3)
realm.commitTransaction()
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group = ComputableGroup(Query(), stats)
val options = Calculator.Options()
options.stats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(realm, group, options)
val delta = 0.01
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(8.0, duration.value, delta)
} else {
Assert.fail("No netDuration stat")
}
realm.executeTransaction {
s1.deleteFromRealm()
}
val stats2: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group2 = ComputableGroup(Query(), stats2)
val results2: ComputedResults = Calculator.compute(realm, group2, options)
val duration2 = results2.computedStat(Stat.HOURLY_DURATION)
if (duration2 != null) {
assertEquals(7.0, duration2.value, delta)
} else {
Assert.fail("No duration2 stat")
}
}
@Test
fun testSessionSetCount() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = newSessionInstance(realm)
realm.insert(s1)
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")
s1.startDate = sd1 //netDuration = 1h, hourly = -100, bb100 = -200bb / 25hands * 100 = -800
s1.endDate = ed1
realm.copyToRealmOrUpdate(s1)
realm.commitTransaction()
val sets = realm.where(SessionSet::class.java).findAll()
assertEquals(1, sets.size)
val set = sets.first()
if (set != null) {
assertEquals(sd1.time, set.startDate.time)
assertEquals(ed1.time, set.endDate.time)
} else {
Assert.fail("No set")
}
}
@Test
fun testSessionSetCount2() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = newSessionInstance(realm)
val s2 = newSessionInstance(realm)
realm.insert(s1)
realm.insert(s2)
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/2/2018 09:00")
val ed2 = sdf.parse("01/2/2018 10:00")
s1.startDate = sd1
s1.endDate = ed1
s2.startDate = sd2
s2.endDate = ed2
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
realm.commitTransaction()
val sets = realm.where(SessionSet::class.java).findAll()
assertEquals(2, sets.size)
}
@Test
fun testSessionSetCount3() {
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("02/1/2019 09:00")
val ed1 = sdf.parse("02/1/2019 10:00")
val sd2 = sdf.parse("01/1/2019 09:00")
val ed2 = sdf.parse("03/1/2019 10:00")
s1.startDate = sd1
s1.endDate = ed1
s2.startDate = sd2
s2.endDate = ed2
val ed22 = sdf.parse("01/1/2019 10:00")
s2.endDate = ed22
}
val sets = realm.where(SessionSet::class.java).findAll()
assertEquals(2, sets.size)
val group = ComputableGroup(Query(), listOf(Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_SETS, Stat.HOURLY_DURATION))
val options = Calculator.Options()
val results: ComputedResults = Calculator.compute(realm, group, options)
results.computedStat(Stat.NUMBER_OF_GAMES)?.let {
assertEquals(2, it.value.toInt())
}
results.computedStat(Stat.NUMBER_OF_SETS)?.let {
assertEquals(2, it.value.toInt())
}
results.computedStat(Stat.HOURLY_DURATION)?.let {
assertEquals(2.0, it.value, 0.001)
}
}
@Test
fun testSessionRestartInOverlappingSessions() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = newSessionInstance(realm)
val s2 = newSessionInstance(realm)
val s3 = newSessionInstance(realm)
realm.insert(s1)
realm.insert(s2)
realm.insert(s3)
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm")
val sd1 = sdf.parse("01/1/2019 05:00")
val ed1 = sdf.parse("01/1/2019 09:00") // 4h
val sd2 = sdf.parse("01/1/2019 07:00")
val ed2 = sdf.parse("01/1/2019 11:00") // 4h
val sd3 = sdf.parse("01/1/2019 03:00")
val ed3 = sdf.parse("01/1/2019 06:00") // 3h
s1.startDate = sd1
s1.endDate = ed1
s2.startDate = sd2
s2.endDate = ed2
s3.startDate = sd3
s3.endDate = ed3
realm.copyToRealmOrUpdate(s1)
realm.copyToRealmOrUpdate(s2)
realm.copyToRealmOrUpdate(s3)
realm.commitTransaction()
realm.executeTransaction {
s1.endDate = null
}
val stats: List<Stat> = listOf(Stat.NET_RESULT, Stat.AVERAGE)
val group = ComputableGroup(Query(), stats)
val options = Calculator.Options()
// options.displayedStats = listOf(Stat.STANDARD_DEVIATION_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION)
val results: ComputedResults = Calculator.compute(realm, group, options)
val delta = 0.01
val duration = results.computedStat(Stat.HOURLY_DURATION)
if (duration != null) {
assertEquals(7.0, duration.value, delta)
} else {
Assert.fail("No Net result stat")
}
val numberOfSets = results.computedStat(Stat.NUMBER_OF_SETS)
if (numberOfSets != null) {
assertEquals(2, numberOfSets.value.toInt())
} else {
Assert.fail("No numberOfSets stat")
}
}
// @Test
fun testRatedNetResultSessions() {
val realm = this.mockRealm
realm.executeTransaction {
val s1 = newSessionInstance(realm)
val s2 = newSessionInstance(realm)
val b1 = realm.createObject(Bankroll::class.java, "1")
val b2 = realm.createObject(Bankroll::class.java, "2")
val c1 = realm.createObject(Currency::class.java, "1")
val c2 = realm.createObject(Currency::class.java, "2")
c1.rate = 0.5
c2.rate = 1.0
b1.currency = c1
b2.currency = c2
s1.bankroll = b1
s2.bankroll = b2
s1.result?.netResult = 100.0
s2.result?.netResult = 200.0
}
val stats: List<Stat> = listOf(Stat.NET_RESULT)
val group = ComputableGroup(Query(), stats)
val options = Calculator.Options()
val results: ComputedResults = Calculator.compute(realm, group, options)
val netResult = results.computedStat(Stat.NET_RESULT)
assertEquals(250.0, netResult?.value)
}
// @Test
fun testUpdateRatedNetResultSessions() {
val realm = this.mockRealm
realm.executeTransaction {
val s1 = newSessionInstance(realm)
val s2 = newSessionInstance(realm)
val b1 = realm.createObject(Bankroll::class.java, "1")
val b2 = realm.createObject(Bankroll::class.java, "2")
val c1 = realm.createObject(Currency::class.java, "1")
val c2 = realm.createObject(Currency::class.java, "2")
c1.rate = 0.5
c2.rate = 1.0
b1.currency = c1
b2.currency = c2
s1.bankroll = b1
s2.bankroll = b2
s1.result?.netResult = 100.0
s2.result?.netResult = 200.0
}
val stats: List<Stat> = listOf(Stat.NET_RESULT)
val group = ComputableGroup(Query(), stats)
val options = Calculator.Options()
val results: ComputedResults = Calculator.compute(realm, group, options)
val netResult = results.computedStat(Stat.NET_RESULT)
assertEquals(250.0, netResult?.value)
println("currency set rate real test starts here")
val updatedC2 = realm.where(Currency::class.java).equalTo("id", "2").findFirst()
updatedC2?.let { currency ->
val newC2 = realm.copyFromRealm(currency)
newC2.rate = 3.0
realm.executeTransaction {
it.copyToRealmOrUpdate(newC2)
}
}
val updatedGroup = ComputableGroup(Query(), 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(Query(), 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(Query(QueryCondition.IsCash))
val options = Calculator.Options()
options.stats = 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(Query(QueryCondition.IsCash))
val options = Calculator.Options()
options.stats = 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")
}
}
}

@ -1,190 +0,0 @@
package net.pokeranalytics.android.unitTests.filter
import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
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.rows.FilterSectionRow
import org.junit.Assert
import org.junit.Test
import java.util.*
class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
@Test
fun testSingleBlindNoCurrencyFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val currency = realm.createObject(net.pokeranalytics.android.model.realm.Currency::class.java, "1")
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.cgBlinds = "0.5/1"
val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1"
val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBlinds = "1/2"
realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val blind = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s1.blinds!!)
}
// blind.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Stakes)
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(2, sessions.size)
sessions.map {
Assert.assertTrue(arrayListOf(s1.id, s2.id).contains((it as Session).id))
}
}
@Test
fun testSingleBlindNoSmallBlindNoCurrencyFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val currency = realm.createObject(net.pokeranalytics.android.model.realm.Currency::class.java, "1")
currency.code = "AUD"
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.cgBlinds = "0.5/1"
val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1"
val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBlinds = "1/2"
realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val blind1 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s1.blinds!!)
}
val blind2 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s2.blinds!!)
}
val filterElements = FilterCondition(arrayListOf(blind1, blind2), FilterSectionRow.Stakes)
filter.updateValueBy(filterElements)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(2, sessions.size)
sessions.map {
Assert.assertTrue(arrayListOf(s1.id, s2.id).contains((it as Session).id))
}
}
@Test
fun testSingleBlindCurrencyFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val currency = realm.createObject(net.pokeranalytics.android.model.realm.Currency::class.java, "1")
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.cgBlinds = "0.5/1"
val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1"
val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBlinds = "1/2"
realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val blind = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s3.blinds!!)
}
val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Stakes)
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
sessions.map {
Assert.assertEquals(s3.id, (it as Session).id)
}
}
@Test
fun testMultiBlindNoCurrencyFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val currency = realm.createObject(net.pokeranalytics.android.model.realm.Currency::class.java, "1")
currency.code = "AUD"
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.cgBlinds = "0.5/1"
val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1"
val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBlinds = "1/2"
realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val stake1 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s1.cgStakes!!)
}
val stake2 = QueryCondition.AnyStake().apply {
listOfValues = arrayListOf(s2.cgStakes!!)
}
val filterElement = FilterCondition(arrayListOf(stake1, stake2), FilterSectionRow.Stakes)
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(2, sessions.size)
sessions.map {
Assert.assertTrue(arrayListOf(s1.id, s2.id).contains((it as Session).id))
}
}
}

@ -1,87 +0,0 @@
package net.pokeranalytics.android.unitTests.filter
import androidx.test.ext.junit.runners.AndroidJUnit4
import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.CustomFieldEntry
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.Session
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class)
class CustomFieldFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
@Test
fun testCustomFieldListFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cf1 = realm.createObject(CustomField::class.java, "1")
cf1.type = CustomField.Type.LIST.ordinal
val cfe1 = realm.createObject(CustomFieldEntry::class.java, "9")
val cfe2 = realm.createObject(CustomFieldEntry::class.java, "8")
cfe1.value = "super"
cfe2.value = "nul"
cf1.entries.add(cfe1)
cf1.entries.add(cfe2)
val s1 = Session.testInstance(100.0, false, Date(), 1)
s1.customFieldEntries.add(cfe1)
val s2 = Session.testInstance(100.0, true, Date(), 1)
s2.customFieldEntries.add(cfe2)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.CustomFieldListQuery(cfe2)))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s2.id, (this).id)
}
}
@Test
fun testCustomFieldAmountFilter() {
val cfId = "1234"
var s2Id = ""
val realm = this.mockRealm
realm.executeTransaction {
val cf = realm.createObject(CustomField::class.java, cfId)
cf.type = CustomField.Type.AMOUNT.ordinal
val cfe1 = realm.createObject(CustomFieldEntry::class.java, "999")
cf.entries.add(cfe1)
cfe1.numericValue = 30.0
val cfe2 = realm.createObject(CustomFieldEntry::class.java, "888")
cf.entries.add(cfe2)
cfe2.numericValue = 100.0
val s1 = Session.testInstance(100.0, false, Date(), 1)
s1.customFieldEntries.add(cfe1)
val s2 = Session.testInstance(100.0, true, Date(), 1)
s2.customFieldEntries.add(cfe2)
s2Id = s2.id
}
val condition = QueryCondition.CustomFieldNumberQuery(cfId, 100.0)
val sessions = Filter.queryOn<Session>(realm, Query(condition))
Assert.assertEquals(1, sessions.size)
sessions.first()?.run {
Assert.assertEquals(s2Id, this.id)
}
}
}

@ -1,513 +0,0 @@
package net.pokeranalytics.android.unitTests.filter
import androidx.test.ext.junit.runners.AndroidJUnit4
import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.Query
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.rows.FilterSectionRow
import net.pokeranalytics.android.util.extensions.hourMinute
import net.pokeranalytics.android.util.extensions.startOfDay
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class)
class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
@Test
fun testDayOfWeekFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance() // creates calendar
cal.time = Date() // sets calendar time/date
val s1 = Session.testInstance(100.0, false, cal.time)
cal.add(Calendar.DAY_OF_MONTH, 1) // adds one hour
Session.testInstance(100.0, true, cal.time)
realm.commitTransaction()
val filter = QueryCondition.AnyDayOfWeek()
cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(cal.get(Calendar.DAY_OF_WEEK)) }
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this).id)
}
}
@Test
fun testMonthFilter() {
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.MONTH, 1)
Session.testInstance(100.0, true, cal.time)
realm.commitTransaction()
val filter = QueryCondition.AnyMonthOfYear()
cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(cal.get(Calendar.MONTH)) }
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this).id)
}
}
@Test
fun testYearFilter() {
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)
realm.commitTransaction()
val filter = QueryCondition.AnyYear()
cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyYear().apply { listOfValues = arrayListOf(cal.get(Calendar.YEAR)) }
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate)
filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this).id)
}
}
@Test
fun testWeekEndFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance()
cal.time = Date()
cal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY)
val s1 = Session.testInstance(100.0, false, cal.time)
cal.set(Calendar.DAY_OF_WEEK, Calendar.TUESDAY)
Session.testInstance(100.0, true, cal.time)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.IsWeekEnd))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this).id)
}
}
@Test
fun testWeekDayFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance()
cal.time = Date()
cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
val s1 = Session.testInstance(100.0, false, cal.time)
cal.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY)
Session.testInstance(100.0, true, cal.time)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.IsWeekDay))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this).id)
}
}
@Test
fun testTodayFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = Session.testInstance(100.0, false)
val cal = Calendar.getInstance()
cal.time = Date()
cal.add(Calendar.HOUR_OF_DAY, -72)
Session.testInstance(100.0, false, cal.time)
realm.commitTransaction()
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 2)
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.IsToday))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, this.id)
}
}
@Test
fun testTodayNoonFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance()
cal.time = Date().startOfDay()
val s1 = Session.testInstance(100.0, false, cal.time)
cal.add(Calendar.HOUR_OF_DAY, -72)
Session.testInstance(100.0, false, cal.time)
realm.commitTransaction()
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 2)
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.IsToday))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, this.id)
}
}
@Test
fun testYesterdayFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance()
cal.time = Date()
cal.add(Calendar.HOUR_OF_DAY, -24)
val s1 = Session.testInstance(100.0, false, cal.time)
Session.testInstance(100.0, false)
cal.add(Calendar.HOUR_OF_DAY, -72)
Session.testInstance(100.0, false, cal.time)
realm.commitTransaction()
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 3)
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.WasYesterday))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, this.id)
}
}
@Test
fun testYesterdayNoonFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance()
cal.time = Date()
cal.add(Calendar.HOUR_OF_DAY, -24)
val s1 = Session.testInstance(100.0, false, cal.time.startOfDay())
Session.testInstance(100.0, false)
cal.add(Calendar.HOUR_OF_DAY, -72)
Session.testInstance(100.0, false, cal.time)
realm.commitTransaction()
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 3)
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.WasYesterday))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, this.id)
}
}
@Test
fun testTodayAndYesterdayFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance()
cal.time = Date()
cal.add(Calendar.HOUR_OF_DAY, -24)
val s1 = Session.testInstance(100.0, false, cal.time)
val s2 = Session.testInstance(100.0, false)
cal.add(Calendar.HOUR_OF_DAY, -72)
Session.testInstance(100.0, false, cal.time)
realm.commitTransaction()
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 3)
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.WasTodayAndYesterday))
Assert.assertEquals(2, sessions.size)
Assert.assertTrue(sessions.containsAll(arrayListOf(s1,s2)))
}
@Test
fun testThisYear() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = Session.testInstance(100.0, false)
val cal = Calendar.getInstance()
cal.time = Date()
cal.add(Calendar.HOUR_OF_DAY, -24)
val s2 = Session.testInstance(100.0, false, cal.time)
cal.add(Calendar.HOUR_OF_DAY, -72)
val s3 = Session.testInstance(100.0, false, cal.time)
cal.add(Calendar.YEAR, -4)
Session.testInstance(100.0, false, cal.time)
cal.add(Calendar.YEAR, -1)
Session.testInstance(100.0, false, cal.time)
realm.commitTransaction()
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 5)
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.DuringThisYear))
Assert.assertEquals(3, sessions.size)
Assert.assertTrue(sessions.containsAll(arrayListOf(s1,s2, s3)))
}
@Test
fun testThisMonth() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = Session.testInstance(100.0, false)
val cal = Calendar.getInstance()
cal.time = Date()
cal.set(Calendar.DAY_OF_MONTH, 1)
cal.time = cal.time.startOfDay()
val s2 = Session.testInstance(100.0, false, cal.time)
cal.add(Calendar.HOUR_OF_DAY, -1)
Session.testInstance(100.0, false, cal.time)
cal.add(Calendar.MONTH, -1)
Session.testInstance(100.0, false, cal.time)
cal.add(Calendar.YEAR, -1)
Session.testInstance(100.0, false, cal.time)
realm.commitTransaction()
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 5)
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.DuringThisMonth))
Assert.assertEquals(2, sessions.size)
Assert.assertTrue(sessions.containsAll(arrayListOf(s1,s2)))
}
@Test
fun testThisWeek() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = Session.testInstance(100.0, false)
val cal = Calendar.getInstance()
cal.time = Date()
cal.set(Calendar.DAY_OF_WEEK_IN_MONTH, 1)
cal.time = cal.time.startOfDay()
cal.add(Calendar.HOUR_OF_DAY, -1)
Session.testInstance(100.0, false, cal.time)
realm.commitTransaction()
Assert.assertTrue(realm.where(Session::class.java).findAll().count() == 2)
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.DuringThisWeek))
Assert.assertEquals(1, sessions.size)
Assert.assertTrue(sessions.containsAll(arrayListOf(s1)))
}
@Test
fun testStartedFomDateFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance() // creates calendar
cal.time = Date() // sets calendar time/date
Session.testInstance(100.0, false, cal.time, 1)
cal.add(Calendar.DAY_OF_YEAR, 2) // adds one hour
val s2 = Session.testInstance(100.0, true, cal.time, 1)
realm.commitTransaction()
val filter = QueryCondition.StartedFromDate(Date())
val filterElementRow = QueryCondition.StartedFromDate(Date()).apply { singleValue = s2.startDate!!}
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s2.id, (this).id)
}
}
@Test
fun testStartedToDateFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance() // creates calendar
cal.time = Date() // sets calendar time/date
val s1 = Session.testInstance(100.0, false, cal.time, 1)
cal.add(Calendar.DAY_OF_YEAR, 2) // adds one hour
Session.testInstance(100.0, true, cal.time, 1)
realm.commitTransaction()
val filter = QueryCondition.StartedToDate(Date())
val filterElementRow = QueryCondition.StartedToDate(Date()).apply { singleValue = s1.startDate!! }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this).id)
}
}
@Test
fun testEndedFomDateFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance() // creates calendar
cal.time = Date() // sets calendar time/date
Session.testInstance(100.0, false, cal.time, 1)
cal.add(Calendar.DAY_OF_YEAR, 2) // adds one hour
val s2 = Session.testInstance(100.0, true, cal.time, 1)
realm.commitTransaction()
val filter = QueryCondition.EndedFromDate(Date())
val filterElementRow = QueryCondition.EndedFromDate(Date()).apply { singleValue = s2.endDate() }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s2.id, (this).id)
}
}
@Test
fun testEndedToDateFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance() // creates calendar
cal.time = Date() // sets calendar time/date
val s1 = Session.testInstance(100.0, false, cal.time, 1)
cal.add(Calendar.DAY_OF_YEAR, 2) // adds one hour
Session.testInstance(100.0, true, cal.time, 1)
realm.commitTransaction()
val filter = QueryCondition.EndedToDate(Date())
val filterElementRow = QueryCondition.EndedToDate(Date()).apply { singleValue = s1.endDate() }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s1.id, (this).id)
}
}
@Test
fun testFomTimeToTime() {
val realm = this.mockRealm
realm.beginTransaction()
val cal = Calendar.getInstance() // creates calendar
cal.time = Date() // sets calendar time/date
cal.set(Calendar.HOUR_OF_DAY, 14) // adds one hour
println("<<<<< s1 ${cal.hourMinute()}")
val s1 = Session.testInstance(100.0, false, cal.time, 1)
println("<<<<< s1 ${cal.hourMinute()}")
cal.add(Calendar.HOUR_OF_DAY, 2) // adds one hour
println("<<<<< s2 ${cal.hourMinute()}")
val s2 = Session.testInstance(100.0, true, cal.time, 1)
println("<<<<< s2 ${cal.hourMinute()}")
cal.set(Calendar.HOUR_OF_DAY, 23) // adds one hour
println("<<<<< s3 ${cal.hourMinute()}")
val s3 = Session.testInstance(100.0, true, cal.time, 2)
println("<<<<< s3 ${cal.hourMinute()}")
realm.commitTransaction()
val filter = QueryCondition.StartedFromTime(s2.startDate!!)
val filter2 = QueryCondition.EndedToTime(s2.endDate!!)
val sessions = Filter.queryOn<Session>(realm, Query(filter, filter2))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(s2.id, (this).id)
}
}
}

@ -1,20 +0,0 @@
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.FilterCondition
import net.pokeranalytics.android.model.realm.Session
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ExceptionFilterInstrumentedTest: BaseFilterInstrumentedUnitTest() {
@Test(expected = PokerAnalyticsException.FilterElementUnknownName::class)
fun testFilterException() {
FilterCondition().queryCondition
}
}

@ -1,66 +0,0 @@
package net.pokeranalytics.android.unitTests.filter
import androidx.test.ext.junit.runners.AndroidJUnit4
import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
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.rows.FixedValueFilterItemRow
import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class)
class RealmFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
@Test
fun testSaveLoadCashFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val filter = Filter()
filter.name = "testSaveLoadCashFilter"
val filterElement = QueryCondition.IsCash
val filterItemRow = FixedValueFilterItemRow(filterElement, FilterSectionRow.CashOrTournament)
filter.createOrUpdateFilterConditions(arrayListOf(filterItemRow))
val useCount = filter.countBy(FilterCategoryRow.GENERAL)
Assert.assertEquals(1, useCount)
val isCash = filter.contains(filterElement)
Assert.assertEquals(true, isCash)
val filterComponent = filter.filterConditions.first()
filterComponent?.let {
Assert.assertTrue(it.queryCondition is QueryCondition.IsCash)
} ?: run {
Assert.fail()
}
Session.testInstance(100.0, false, Date(), 1)
Session.testInstance(100.0, true, Date(), 1)
realm.copyToRealm(filter)
realm.commitTransaction()
val newRealm = this.mockRealm
newRealm.where(Filter::class.java).equalTo("name", "testSaveLoadCashFilter").findFirst()?.let { foundFilter ->
val sessions = foundFilter.results<Session>()
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(Session.Type.CASH_GAME.ordinal, this.type)
} ?: run {
Assert.fail()
}
} ?: run {
Assert.fail()
}
}
}

@ -1,597 +0,0 @@
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.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class)
class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
@Test
fun testCashFilter() {
val realm = this.mockRealm
realm.beginTransaction()
Session.testInstance(100.0, false, Date(), 1)
Session.testInstance(100.0, true, Date(), 1)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.IsCash))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(Session.Type.CASH_GAME.ordinal, (this).type)
}
}
@Test
fun testTournamentFilter() {
val realm = this.mockRealm
realm.beginTransaction()
Session.testInstance(100.0, false, Date(), 1)
Session.testInstance(100.0, true, Date(), 1)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.IsTournament))
Assert.assertEquals(1, sessions.size)
sessions[0]?.run {
Assert.assertEquals(Session.Type.TOURNAMENT.ordinal, (this).type)
}
}
@Test
fun testLiveFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val b1 = realm.createObject(Bankroll::class.java, "1")
val b2 = realm.createObject(Bankroll::class.java, "2")
b1.live = true
b2.live = false
Session.testInstance(100.0, false, Date(), 1, b1)
Session.testInstance(100.0, true, Date(), 1, b2)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.IsLive))
Assert.assertEquals(1, sessions.size)
(sessions[0] as Session).bankroll?.run {
Assert.assertEquals(true, this.live)
}
}
@Test
fun testOnlineFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val b1 = realm.createObject(Bankroll::class.java, "1")
val b2 = realm.createObject(Bankroll::class.java, "2")
b1.live = false
b2.live = true
Session.testInstance(100.0, false, Date(), 1, b1)
Session.testInstance(100.0, true, Date(), 1, b2)
realm.commitTransaction()
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.IsOnline))
Assert.assertEquals(1, sessions.size)
(sessions[0] as Session).bankroll?.run {
Assert.assertEquals(false, this.live)
}
}
@Test
fun testBankrollFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val b1 = realm.createObject(Bankroll::class.java, "1")
val b2 = realm.createObject(Bankroll::class.java, "2")
b1.live = false
b2.live = true
Session.testInstance(bankroll = b1)
Session.testInstance(bankroll = b2)
realm.commitTransaction()
val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Bankroll))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
(sessions[0] as Session).bankroll?.run {
Assert.assertEquals(b1.id, this.id)
}
}
@Test
fun testMultipleBankrollFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val b1 = realm.createObject(Bankroll::class.java, "1")
val b2 = realm.createObject(Bankroll::class.java, "2")
val b3 = realm.createObject(Bankroll::class.java, "3")
Session.testInstance(bankroll = b1)
Session.testInstance(bankroll = b2)
Session.testInstance(bankroll = b3)
Session.testInstance(bankroll = b1)
Session.testInstance(bankroll = b2)
Session.testInstance(bankroll = b3)
Session.testInstance(bankroll = b1)
Session.testInstance(bankroll = b2)
Session.testInstance(bankroll = b3)
realm.commitTransaction()
val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
val filterElementRow2 = QueryCondition.AnyBankroll().apply { setObject(b2) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Bankroll))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(6, sessions.size)
val result = arrayListOf(b1.id, b2.id)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).bankroll?.id))
}
}
@Test
fun testGameFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val g1 = realm.createObject(Game::class.java, "1")
val g2 = realm.createObject(Game::class.java, "2")
Session.testInstance(game = g1)
Session.testInstance(game = g2)
realm.commitTransaction()
val anyGame = QueryCondition.AnyGame(g2)
val fc = FilterCondition(arrayListOf(anyGame), FilterSectionRow.Game)
val sessions = Filter.queryOn<Session>(realm, Query(fc.queryCondition))
Assert.assertEquals(1, sessions.size)
(sessions[0] as Session).game?.run {
Assert.assertEquals(g2.id, this.id)
}
}
@Test
fun testMultipleGameFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val g1 = realm.createObject(Game::class.java, "1")
val g2 = realm.createObject(Game::class.java, "2")
val g3 = realm.createObject(Game::class.java, "3")
Session.testInstance(game = g1)
Session.testInstance(game = g2)
Session.testInstance(game = g3)
Session.testInstance(game = g1)
Session.testInstance(game = g2)
Session.testInstance(game = g3)
Session.testInstance(game = g1)
Session.testInstance(game = g2)
Session.testInstance(game = g3)
realm.commitTransaction()
val filterElementRow = QueryCondition.AnyGame().apply { setObject(g2) }
val filterElementRow2 = QueryCondition.AnyGame().apply { setObject(g3) }
val filterCondition = FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Game)
val queryCondition = filterCondition.queryCondition
val sessions = Filter.queryOn<Session>(realm, Query(queryCondition))
Assert.assertEquals(6, sessions.size)
val result = arrayListOf(g2.id, g3.id)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).game?.id))
}
}
@Test
fun testLocationFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val l1 = realm.createObject(Location::class.java, "1")
val l2 = realm.createObject(Location::class.java, "2")
Session.testInstance(location = l1)
Session.testInstance(location = l2)
realm.commitTransaction()
val filter = QueryCondition.AnyLocation()
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Location))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
(sessions[0] as Session).location?.run {
Assert.assertEquals(l1.id, this.id)
}
}
@Test
fun testMultipleLocationFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val l1 = realm.createObject(Location::class.java, "1")
val l2 = realm.createObject(Location::class.java, "2")
val l3 = realm.createObject(Location::class.java, "3")
Session.testInstance(location = l1)
Session.testInstance(location = l2)
Session.testInstance(location = l3)
Session.testInstance(location = l1)
Session.testInstance(location = l2)
Session.testInstance(location = l3)
Session.testInstance(location = l1)
Session.testInstance(location = l2)
Session.testInstance(location = l3)
realm.commitTransaction()
val filter = QueryCondition.AnyLocation()
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
val filterElementRow2 = QueryCondition.AnyLocation().apply { setObject(l3) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Location))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(6, sessions.size)
val result = arrayListOf(l1.id, l3.id)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).location?.id))
}
}
@Test
fun testTournamentNameFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val t1 = realm.createObject(TournamentName::class.java, "1")
val t2 = realm.createObject(TournamentName::class.java, "2")
Session.testInstance(tournamentName = t1)
Session.testInstance(tournamentName = t2)
realm.commitTransaction()
val filter = QueryCondition.AnyTournamentName()
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.TournamentName))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
(sessions[0] as Session).tournamentName?.run {
Assert.assertEquals(t1.id, this.id)
}
}
@Test
fun testMultipleTournamentNameFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val t1 = realm.createObject(TournamentName::class.java, "1")
val t2 = realm.createObject(TournamentName::class.java, "2")
val t3 = realm.createObject(TournamentName::class.java, "3")
Session.testInstance(tournamentName = t1)
Session.testInstance(tournamentName = t2)
Session.testInstance(tournamentName = t3)
Session.testInstance(tournamentName = t1)
Session.testInstance(tournamentName = t2)
Session.testInstance(tournamentName = t3)
Session.testInstance(tournamentName = t1)
Session.testInstance(tournamentName = t2)
Session.testInstance(tournamentName = t3)
realm.commitTransaction()
val filter = QueryCondition.AnyTournamentName()
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
val filterElementRow2 = QueryCondition.AnyTournamentName().apply { setObject(t2) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.TournamentName))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(6, sessions.size)
val result = arrayListOf(t1.id, t2.id)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).tournamentName?.id))
}
}
@Test
fun testAllTournamentFeatureFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val t1 = realm.createObject(TournamentFeature::class.java, "1")
val t2 = realm.createObject(TournamentFeature::class.java, "2")
val t3 = realm.createObject(TournamentFeature::class.java, "3")
val t4 = realm.createObject(TournamentFeature::class.java, "4")
Session.testInstance(tournamentFeatures = RealmList(t1,t2))
Session.testInstance(tournamentFeatures = RealmList(t2,t3))
Session.testInstance(tournamentFeatures = RealmList(t3,t4))
val s = Session.testInstance(tournamentFeatures = RealmList(t1,t2,t3,t4))
Session.testInstance(tournamentFeatures = RealmList(t1,t4))
Session.testInstance(tournamentFeatures = RealmList(t1,t3))
Session.testInstance(tournamentFeatures = RealmList(t2,t4, t3))
Session.testInstance(tournamentFeatures = RealmList(t1))
realm.commitTransaction()
val filter = QueryCondition.AllTournamentFeature()
val filterElementRow = QueryCondition.AllTournamentFeature().apply { setObject(t1) }
val filterElementRow2 = QueryCondition.AllTournamentFeature().apply { setObject(t2) }
val filterElementRow3 = QueryCondition.AllTournamentFeature().apply { setObject(t4) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2, filterElementRow3), FilterSectionRow.TournamentFeature))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(1, sessions.size)
(sessions[0] as Session).run {
Assert.assertEquals(s.id, this.id)
}
}
@Test
fun testAnyTournamentFeatureFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val t1 = realm.createObject(TournamentFeature::class.java, "1")
val t2 = realm.createObject(TournamentFeature::class.java, "2")
val t3 = realm.createObject(TournamentFeature::class.java, "3")
val t4 = realm.createObject(TournamentFeature::class.java, "4")
Session.testInstance(tournamentFeatures = RealmList(t1,t2))
Session.testInstance(tournamentFeatures = RealmList(t2,t3))
Session.testInstance(tournamentFeatures = RealmList(t3,t4))
Session.testInstance(tournamentFeatures = RealmList(t1,t2,t3,t4))
Session.testInstance(tournamentFeatures = RealmList(t1,t4))
Session.testInstance(tournamentFeatures = RealmList(t1,t3))
Session.testInstance(tournamentFeatures = RealmList(t2,t4, t3))
Session.testInstance(tournamentFeatures = RealmList(t1))
realm.commitTransaction()
val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t1) }
val filterElementRow2 = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
val filterElementRow3 = QueryCondition.AnyTournamentFeature().apply { setObject(t3) }
val filterElementRow4 = QueryCondition.AnyTournamentFeature().apply { setObject(t4) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2, filterElementRow3, filterElementRow4), FilterSectionRow.TournamentFeature))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(8, sessions.size)
}
@Test
fun testSingleAnyTournamentFeatureFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val t1 = realm.createObject(TournamentFeature::class.java, "1")
val t2 = realm.createObject(TournamentFeature::class.java, "2")
val t3 = realm.createObject(TournamentFeature::class.java, "3")
val t4 = realm.createObject(TournamentFeature::class.java, "4")
val s1 = Session.testInstance(tournamentFeatures = RealmList(t1,t2))
val s2 = Session.testInstance(tournamentFeatures = RealmList(t2,t3))
Session.testInstance(tournamentFeatures = RealmList(t3,t4))
val s3 = Session.testInstance(tournamentFeatures = RealmList(t1,t2,t3,t4))
Session.testInstance(tournamentFeatures = RealmList(t1,t4))
Session.testInstance(tournamentFeatures = RealmList(t1,t3))
val s4 = Session.testInstance(tournamentFeatures = RealmList(t2,t4, t3))
Session.testInstance(tournamentFeatures = RealmList(t1))
realm.commitTransaction()
val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.TournamentFeature))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
val result = arrayListOf(s1.id, s2.id, s3.id, s4.id)
Assert.assertEquals(4, sessions.size)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).id))
}
}
@Test
fun testTableSizeFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = Session.testInstance(tableSize = 2)
val s2 = Session.testInstance(tableSize = 4)
Session.testInstance(tableSize = 9)
Session.testInstance(tableSize = 10)
realm.commitTransaction()
val filter = QueryCondition.AnyTableSize()
val filterElementRow = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(2) }
val filterElementRow2 = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(4) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.TableSize))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(2, sessions.size)
val result = arrayListOf(s1.id, s2.id)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).id))
}
}
@Test
fun testMoreThanNetResultFilter() {
val realm = this.mockRealm
realm.beginTransaction()
Session.testInstance(netResult = 200.0)
val s1 = Session.testInstance(netResult = 500.0)
Session.testInstance(netResult = 50.0)
val s2 = Session.testInstance(netResult = 570.0)
realm.commitTransaction()
val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val sessions = Filter.queryOn<Session>(realm, Query(filterElementRow))
Assert.assertEquals(2, sessions.size)
val result = arrayListOf(s1.id, s2.id)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).id))
}
}
@Test
fun testLessThanNetResultFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = Session.testInstance(netResult = 200.0)
val s2 = Session.testInstance(netResult = 500.0)
val s3 = Session.testInstance(netResult = 50.0)
Session.testInstance(netResult = 570.0)
realm.commitTransaction()
val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val sessions = Filter.queryOn<Session>(realm, Query(filter))
Assert.assertEquals(3, sessions.size)
val result = arrayListOf(s1.id, s2.id, s3.id)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).id))
}
}
@Test
fun testNetResultFilter() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = Session.testInstance(netResult = 200.0)
Session.testInstance(netResult = 500.0)
Session.testInstance(netResult = 50.0)
Session.testInstance(netResult = 570.0)
realm.commitTransaction()
val filterMore = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(199.0) }
filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value))
val filterLess = QueryCondition.NetAmountWon()
val filterElementRow2 = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) }
filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2), FilterSectionRow.Value))
val sessions = Filter.queryOn<Session>(realm, Query(filterMore, filterLess))
Assert.assertEquals(1, sessions.size)
val result = arrayListOf(s1.id)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).id))
}
}
@Test
fun testNumberOfRebuys() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = Session.testInstance(netResult = 200.0, isTournament = true)
s1.result!!.buyin = 50.0
s1.tournamentEntryFee = 10.0
val s2 = Session.testInstance(netResult = 50.0, isTournament = true)
s2.result!!.buyin = 10.0
s2.tournamentEntryFee = 10.0
val s3 = Session.testInstance(netResult = 500.0)
s3.cgBlinds = "2.0"
s3.result!!.buyin = 1000.0
val s4 = Session.testInstance(netResult = 570.0)
s4.cgBlinds = "5.0"
s4.result!!.buyin = 200.0
realm.commitTransaction()
val filterMore = QueryCondition.NumberOfRebuy(QueryCondition.Operator.MORE, 4.0)
val sessions = Filter.queryOn<Session>(realm, Query(filterMore))
Assert.assertEquals(2, sessions.size)
val result = arrayListOf(s1.id, s3.id)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).id))
}
}
@Test
fun testTournamentFinalPosition() {
val realm = this.mockRealm
realm.beginTransaction()
val s1 = Session.testInstance(netResult = 200.0, isTournament = true)
s1.result!!.tournamentFinalPosition = 5
val s2 = Session.testInstance(netResult = 50.0, isTournament = true)
s2.result!!.tournamentFinalPosition = 15
val s3 = Session.testInstance(netResult = 500.0, isTournament = true)
s3.result!!.tournamentFinalPosition = 2
val s4 = Session.testInstance(netResult = 570.0, isTournament = true)
s4.result!!.tournamentFinalPosition = 1
realm.commitTransaction()
val filterLess = QueryCondition.TournamentFinalPosition(QueryCondition.Operator.LESS, finalPosition = 1)
var sessions = Filter.queryOn<Session>(realm, Query(filterLess))
Assert.assertEquals(1, sessions.size)
var result = arrayListOf(s4.id)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).id))
}
val filterMore = QueryCondition.TournamentFinalPosition(QueryCondition.Operator.MORE, finalPosition = 10)
sessions = Filter.queryOn(realm, Query(filterMore))
Assert.assertEquals(1, sessions.size)
result = arrayListOf(s2.id)
sessions.forEach {
Assert.assertTrue(result.contains((it as Session).id))
}
}
}

@ -1,55 +0,0 @@
package net.pokeranalytics.android.unitTests.filter
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.TransactionType
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TransactionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
@Test
fun testTransactionTypeFilter() {
val context = InstrumentationRegistry.getInstrumentation().context
val realm = this.mockRealm
realm.beginTransaction()
TransactionType.Value.values().forEachIndexed { index, value ->
val type = TransactionType()
val name = "test"
type.name = name
type.additive = value.additive
type.kind = value.uniqueIdentifier
type.lock = true
realm.insertOrUpdate(type)
}
val t1: Transaction = realm.createObject(Transaction::class.java, "1")
t1.type = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm)
val t2: Transaction = realm.createObject(Transaction::class.java, "2")
t2.type = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm)
val b1 = realm.createObject(Bankroll::class.java, "1")
t1.bankroll = b1
val b2 = realm.createObject(Bankroll::class.java, "2")
t2.bankroll = b2
realm.commitTransaction()
val transactions = Filter.queryOn<Transaction>(realm, Query(QueryCondition.AnyTransactionType(t1.type!!)))
Assert.assertEquals(1, transactions.size)
transactions[0]?.run {
Assert.assertEquals(t1.type!!.id, (this).type!!.id)
}
}
}

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Add WRITE_EXTERNAL_STORAGE only for debug / test -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

@ -1,262 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<!-- <uses-feature android:name="android.hardware.camera" android:required="false" />-->
<application
android:name=".PokerAnalyticsApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:largeHeap="true"
android:theme="@style/PokerAnalyticsTheme">
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.HomeActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="net.pokeranalytics.android.ui.activity.ImportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="text/comma-separated-values" />
<data android:mimeType="text/csv" />
</intent-filter>
</activity>
<!-- DatabaseCopyActivity is only used in development for now -->
<!-- <activity android:name=".ui.activity.DatabaseCopyActivity"-->
<!-- android:launchMode="singleTop"-->
<!-- android:screenOrientation="portrait"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <data android:scheme="content" />-->
<!-- <data android:scheme="file" />-->
<!-- <data android:mimeType="*/*" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
<activity
android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity
android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity"
android:launchMode="singleTop"
android:theme="@style/PokerAnalyticsTheme.MenuDialog"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="true"/>
<activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity"
android:launchMode="standard"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.settings.DealtHandsPerHourActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.calendar.GridCalendarActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.modules.settings.TransactionFilterActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity
android:name="net.pokeranalytics.android.ui.activity.ColorPickerActivity"
android:theme="@style/PokerAnalyticsTheme.AlertDialog"
android:launchMode="singleTop"
android:exported="true"/>
<activity
android:name="net.pokeranalytics.android.ui.activity.components.CameraActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true" />
<service
android:name="net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService"
android:exported="false"/>
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
package="net.pokeranalytics.android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".PokerAnalyticsApplication"
android:theme="@style/PokerAnalyticsTheme">
<activity
android:name=".ui.activity.HomeActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".ui.activity.DataListActivity" />
<activity
android:name=".ui.activity.SessionActivity"
android:launchMode="singleTop"/>
<activity android:name=".ui.activity.EditableDataActivity"/>
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

@ -1,116 +1,60 @@
package net.pokeranalytics.android
import android.app.Application
import android.content.Context
import android.os.Build
import com.google.firebase.FirebaseApp
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.where
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.model.migrations.Patcher
import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration
import io.realm.RealmResults
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.PokerAnalyticsLogs
import timber.log.Timber
import java.util.*
class PokerAnalyticsApplication : Application() {
var reportWhistleBlower: ReportWhistleBlower? = null
var backupOperator: BackupOperator? = null
companion object {
fun timeSinceInstall(context: Context): Long {
val installTime: Long = context.packageManager.getPackageInfo(context.packageName, 0).firstInstallTime
return System.currentTimeMillis() - installTime
}
}
override fun onCreate() {
super.onCreate()
if (!BuildConfig.DEBUG) {
FirebaseApp.initializeApp(this)
}
UserDefaults.init(this)
// AppGuard / Billing services
AppGuard.load(this.applicationContext)
// Realm
Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME)
.schemaVersion(14)
.allowWritesOnUiThread(true)
.migration(PokerAnalyticsMigration())
.initialData(Seed(this))
.build()
Realm.setDefaultConfiguration(realmConfiguration)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val locales = resources.configuration.locales
CrashLogging.log("App onCreate. Locales = $locales")
}
if (BuildConfig.DEBUG) {
// Logs
Timber.plant(PokerAnalyticsLogs())
}
Timber.d("SDK version = ${Build.VERSION.SDK_INT}")
if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
Timber.d("Realm path = ${Realm.getDefaultInstance().path}")
// this.createFakeSessions()
}
// Patch
Patcher.patchAll(this)
// Report
this.reportWhistleBlower = ReportWhistleBlower(this.applicationContext)
// Backups
this.backupOperator = BackupOperator(this.applicationContext)
// Infos
val locale = Locale.getDefault()
CrashLogging.log("Country: ${locale.country}, language: ${locale.language}")
// Realm.getDefaultInstance().executeTransaction {
// it.delete(Performance::class.java)
// }
}
/**
* Create fake sessions if we have less than 10 sessions
*/
private fun createFakeSessions() {
val realm = Realm.getDefaultInstance()
val sessionsCount = realm.where<Session>().count()
realm.close()
if (sessionsCount < 10) {
CoroutineScope(context = Dispatchers.IO).launch {
FakeDataManager.createFakeSessions(500)
}
}
}
class PokerAnalyticsApplication: Application() {
var sessions: RealmResults<Session>? = null
// private val listener: OrderedRealmCollectionChangeListener<RealmResults<Session>> =
// OrderedRealmCollectionChangeListener() { realmResults: RealmResults<Session>, changeSet: OrderedCollectionChangeSet ->
//
// if (changeSet == null) {
// return@OrderedRealmCollectionChangeListener
// }
//
// val realm: Realm = Realm.getDefaultInstance()
//
// val deletedSessions = realm.where(Session::class.java).`in`("id", changeSet.deletions.toTypedArray()).findAll()
// deletedSessions.forEach { it.cleanup() }
//
// }
override fun onCreate() {
super.onCreate()
// Realm
Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME)
.deleteRealmIfMigrationNeeded()
.build()
Realm.setDefaultConfiguration(realmConfiguration)
val realm: Realm = Realm.getDefaultInstance()
// Add observer on session time frame changes
this.sessions = realm.where(Session::class.java).findAll() // monitor session deletions
this.sessions?.addChangeListener { t, changeSet ->
val deletedSessions = realm.where(Session::class.java).`in`("id", changeSet.deletions.toTypedArray()).findAll()
deletedSessions.forEach { it.cleanup() }
}
if (BuildConfig.DEBUG) {
// Logs
Timber.plant(PokerAnalyticsLogs())
} else {
//Fabric.with(this, Crashlytics())
}
}
}

@ -1,75 +0,0 @@
package net.pokeranalytics.android.api
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.isNetworkAvailable
import okhttp3.MediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import timber.log.Timber
object RetrofitClient {
private const val BASE_URL = "https://www.pokeranalytics.net/backup/"
fun getClient(): Retrofit =
Retrofit.Builder()
.baseUrl(BASE_URL)
.build()
}
class BackupService {
private val retrofit = RetrofitClient.getClient()
val backupApi: MyBackupApi = retrofit.create(MyBackupApi::class.java)
}
interface MyBackupApi {
@Multipart
@POST("send")
fun postFile(@Part mail: MultipartBody.Part, @Part fileBody: MultipartBody.Part): Call<Void>
}
object BackupApi {
private val service = BackupService()
// curl -F recipient=laurent@staxriver.com -F file=@test.txt https://www.pokeranalytics.net/backup/send
suspend fun backupFile(context: Context, mail: String, fileName: String, fileContent: String): Boolean {
val filePart = MultipartBody.Part.createFormData(
"file",
fileName,
RequestBody.create(MediaType.parse("text/csv"), fileContent)
)
val mailPart = MultipartBody.Part.createFormData("recipient", mail)
return if (context.isNetworkAvailable()) {
var success = false
val job = CoroutineScope(context = Dispatchers.IO).async {
success = try {
val response = service.backupApi.postFile(mailPart, filePart).execute()
Timber.d("response code = ${response.code()}")
Timber.d("success = ${response.isSuccessful}")
true
} catch (e: Exception) {
Timber.d("!!! backup failed: ${e.message}")
CrashLogging.logException(e)
false
}
}
job.await()
return success
} else {
false
}
}
}

@ -1,49 +0,0 @@
package net.pokeranalytics.android.api
import android.content.Context
import com.android.volley.Request
import com.android.volley.toolbox.JsonArrayRequest
import com.android.volley.toolbox.Volley
import org.json.JSONArray
import timber.log.Timber
data class BlogPost(var id: Int, var content: String)
private fun JSONArray.toBlogPosts(): List<BlogPost> {
val posts = mutableListOf<BlogPost>()
(0 until this.length()).forEach { index ->
val jo = this.getJSONObject(index)
val post = BlogPost(jo.getInt("id"), jo.getJSONObject("content").getString("rendered"))
posts.add(post)
}
return posts
}
class BlogPostApi {
companion object {
private const val tipsLastPostsURL = "https://www.poker-analytics.net/blog/wp-json/wp/v2/posts/?categories=109\n"
fun getLatestPosts(context: Context, callback: (List<BlogPost>) -> (Unit)) {
val queue = Volley.newRequestQueue(context)
val jsonObjectRequest = JsonArrayRequest(
Request.Method.GET, tipsLastPostsURL, null,
{ response ->
// Timber.d("posts = $response")
callback(response.toBlogPosts())
},
{ error ->
Timber.w("Error while retrieving blog posts: $error")
}
)
queue.add(jsonObjectRequest)
}
}
}

@ -1,60 +0,0 @@
package net.pokeranalytics.android.api
import android.content.Context
import androidx.annotation.Keep
import com.android.volley.VolleyError
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import timber.log.Timber
@Keep
@Serializable
data class RateResponse(var info: RateInfo)
@Keep
@Serializable
data class RateInfo(var rate: Double)
class CurrencyConverterApi {
companion object {
val json = Json { ignoreUnknownKeys = true }
fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) {
val queue = Volley.newRequestQueue(context)
val url = "https://api.apilayer.com/exchangerates_data/convert?to=$toCurrency&from=$fromCurrency&amount=1"
Timber.d("Api call = $url")
val stringRequest = object : StringRequest(
Method.GET, url,
{ response ->
val o = json.decodeFromString<RateResponse>(response)
Timber.d("rate = ${o.info.rate}")
callback(o.info.rate, null)
},
{
Timber.d("Api call failed: ${it.message}")
callback(null, it)
}) {
override fun getHeaders(): MutableMap<String, String> {
val headers = HashMap<String, String>()
headers["apikey"] = "XnfeyID3PMKd3k4zTPW0XmZAbcZlZgqH"
return headers
}
}
queue.add(stringRequest)
}
}
}

@ -1,242 +0,0 @@
package net.pokeranalytics.android.api
import com.android.volley.*
import com.android.volley.toolbox.HttpHeaderParser
import java.io.*
open class VolleyMultipartRequest : Request<NetworkResponse?> {
private val twoHyphens = "--"
private val lineEnd = "\r\n"
private val boundary = "apiclient-" + System.currentTimeMillis()
private var mListener: Response.Listener<NetworkResponse>
private var mErrorListener: Response.ErrorListener
private var mHeaders: Map<String, String>? = null
private var byteData: Map<String, DataPart>? = null
/**
* Default constructor with predefined header and post method.
*
* @param url request destination
* @param headers predefined custom header
* @param listener on success achieved 200 code from request
* @param errorListener on error http or library timeout
*/
constructor(
url: String?, headers: Map<String, String>?,
byteData: Map<String, DataPart>,
listener: Response.Listener<NetworkResponse>,
errorListener: Response.ErrorListener
) : super(Method.POST, url, errorListener) {
mListener = listener
this.mErrorListener = errorListener
mHeaders = headers
this.byteData = byteData
}
/**
* Constructor with option method and default header configuration.
*
* @param method method for now accept POST and GET only
* @param url request destination
* @param listener on success event handler
* @param errorListener on error event handler
*/
constructor(
method: Int, url: String?,
listener: Response.Listener<NetworkResponse>,
errorListener: Response.ErrorListener
) : super(method, url, errorListener) {
mListener = listener
this.mErrorListener = errorListener
}
@Throws(AuthFailureError::class)
override fun getHeaders(): Map<String, String> {
return if (mHeaders != null) mHeaders!! else super.getHeaders()
}
override fun getBodyContentType(): String {
return "multipart/form-data;boundary=$boundary"
}
@Throws(AuthFailureError::class)
override fun getBody(): ByteArray? {
val bos = ByteArrayOutputStream()
val dos = DataOutputStream(bos)
try {
// populate text payload
val params = params
if (params != null && params.isNotEmpty()) {
textParse(dos, params, paramsEncoding)
}
// populate data byte payload
val data =
byteData
if (data != null && data.isNotEmpty()) {
dataParse(dos, data)
}
// close multipart form data after text and file data
dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd)
return bos.toByteArray()
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
override fun parseNetworkResponse(response: NetworkResponse?): Response<NetworkResponse?> {
return try {
Response.success(
response,
HttpHeaderParser.parseCacheHeaders(response)
)
} catch (e: Exception) {
Response.error(ParseError(e))
}
}
override fun deliverResponse(response: NetworkResponse?) {
mListener.onResponse(response)
}
override fun deliverError(error: VolleyError?) {
mErrorListener.onErrorResponse(error)
}
/**
* Parse string map into data output stream by key and value.
*
* @param dataOutputStream data output stream handle string parsing
* @param params string inputs collection
* @param encoding encode the inputs, default UTF-8
* @throws IOException
*/
@Throws(IOException::class)
private fun textParse(
dataOutputStream: DataOutputStream,
params: Map<String, String>,
encoding: String
) {
try {
for ((key, value) in params) {
buildTextPart(dataOutputStream, key, value)
}
} catch (uee: UnsupportedEncodingException) {
throw RuntimeException("Encoding not supported: $encoding", uee)
}
}
/**
* Parse data into data output stream.
*
* @param dataOutputStream data output stream handle file attachment
* @param data loop through data
* @throws IOException
*/
@Throws(IOException::class)
private fun dataParse(dataOutputStream: DataOutputStream, data: Map<String, DataPart>) {
for ((key, value) in data) {
buildDataPart(dataOutputStream, value, key)
}
}
/**
* Write string data into header and data output stream.
*
* @param dataOutputStream data output stream handle string parsing
* @param parameterName name of input
* @param parameterValue value of input
* @throws IOException
*/
@Throws(IOException::class)
private fun buildTextPart(
dataOutputStream: DataOutputStream,
parameterName: String,
parameterValue: String
) {
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd)
dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"$parameterName\"$lineEnd")
//dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
dataOutputStream.writeBytes(lineEnd)
dataOutputStream.writeBytes(parameterValue + lineEnd)
}
/**
* Write data file into header and data output stream.
*
* @param dataOutputStream data output stream handle data parsing
* @param dataFile data byte as DataPart from collection
* @param inputName name of data input
* @throws IOException
*/
@Throws(IOException::class)
private fun buildDataPart(
dataOutputStream: DataOutputStream,
dataFile: DataPart,
inputName: String
) {
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd)
dataOutputStream.writeBytes(
"Content-Disposition: form-data; name=\"" +
inputName + "\"; filename=\"" + dataFile.fileName + "\"" + lineEnd
)
if (dataFile.type != null && !dataFile.type!!.trim { it <= ' ' }.isEmpty()) {
dataOutputStream.writeBytes("Content-Type: " + dataFile.type + lineEnd)
}
dataOutputStream.writeBytes(lineEnd)
val fileInputStream = ByteArrayInputStream(dataFile.content)
var bytesAvailable: Int = fileInputStream.available()
val maxBufferSize = 1024 * 1024
var bufferSize = Math.min(bytesAvailable, maxBufferSize)
val buffer = ByteArray(bufferSize)
var bytesRead: Int = fileInputStream.read(buffer, 0, bufferSize)
while (bytesRead > 0) {
dataOutputStream.write(buffer, 0, bufferSize)
bytesAvailable = fileInputStream.available()
bufferSize = Math.min(bytesAvailable, maxBufferSize)
bytesRead = fileInputStream.read(buffer, 0, bufferSize)
}
dataOutputStream.writeBytes(lineEnd)
}
/**
* Simple data container use for passing byte file
*/
class DataPart {
var fileName: String? = null
var content: ByteArray? = null
var type: String? = null
/**
* Constructor with data.
*
* @param name label of data
* @param data byte data
*/
constructor(name: String?, data: ByteArray) {
fileName = name
content = data
}
/**
* Constructor with mime data type.
*
* @param name label of data
* @param data byte data
* @param mimeType mime data like "image/jpeg"
*/
constructor(name: String?, data: ByteArray, mimeType: String?) {
fileName = name
content = data
type = mimeType
}
}
}

@ -1,32 +0,0 @@
package net.pokeranalytics.android.calculus
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.ui.graph.Graph
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 criterias: List<Criteria>
get() {
return when (this) {
MONTH -> listOf(Criteria.AllMonthsUpToNow)
YEAR -> listOf(Criteria.Years)
else -> listOf()
}
}
}

@ -0,0 +1,12 @@
package net.pokeranalytics.android.calculus
class AggregationParameter<T> {
var values: List<T>? = null
}
class Aggregator {
var parameters: List<AggregationParameter<*>> = listOf()
}

@ -1,637 +1,207 @@
package net.pokeranalytics.android.calculus
import android.content.Context
import io.realm.Realm
import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.extensions.hourlyDuration
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.filter
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.util.extensions.startOfDay
import java.util.*
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
import net.pokeranalytics.android.model.realm.SessionSet
/**
* The class performing statIds computation
* The class performing stats computation
*/
class Calculator {
/**
* The options used for calculations and display
*/
class Options(
var progressValues: ProgressValues = ProgressValues.NONE,
var stats: List<Stat> = listOf(),
var criterias: List<Criteria> = listOf(),
var query: Query = Query(),
var filterId: String? = null,
private var aggregationType: AggregationType? = null,
var userGenerated: Boolean = false,
var reportSetupId: String? = null,
var includedTransactions: List<TransactionType> = listOf()
) {
constructor(
progressValues: ProgressValues = ProgressValues.NONE,
stats: List<Stat> = listOf(),
criterias: List<Criteria> = listOf(),
filter: Filter? = null,
aggregationType: AggregationType? = null,
userGenerated: Boolean = false,
reportSetupId: String? = null) :
this(progressValues, stats, criterias, filter?.query ?: Query(), filter?.id, aggregationType, userGenerated, reportSetupId)
/**
* Specifies whether progress values should be added and their kind
*/
// var progressValues: ProgressValues = progressValues
// get() {
// if (field == ProgressValues.NONE && this.display.requireProgressValues) {
// return ProgressValues.STANDARD
// }
// return field
// }
init {
this.aggregationType?.let {
this.criterias = it.criterias
}
}
/**
* The type of evolution numericValues
*/
enum class ProgressValues {
NONE,
STANDARD,
TIMED
}
/**
* This function determines whether the standard deviation should be computed
*/
val computeStandardDeviation: Boolean
get() {
this.stats.forEach {
if (it.isStandardDeviation) {
return true
}
}
return false
}
/**
* Whether the longest streaks should be computed
*/
val computeLongestStreak: Boolean
get() {
return this.stats.contains(LONGEST_STREAKS)
}
/**
* Whether the values should be sorted
*/
val shouldSortValues: Boolean
get() {
return this.progressValues != ProgressValues.NONE || this.computeLongestStreak
}
/**
* Whether the number of locations played should be computed
*/
val computeLocationsPlayed: Boolean
get() {
return this.stats.contains(LOCATIONS_PLAYED)
}
/**
* Whether the number of days played should be computed
*/
val computeDaysPlayed: Boolean
get() {
return this.stats.contains(DAYS_PLAYED)
}
/**
* Whether progress values should be managed at the group level
*/
val shouldManageMultiGroupProgressValues: Boolean
get() {
return if (this.aggregationType != null) {
this.aggregationType == AggregationType.MONTH || this.aggregationType == AggregationType.YEAR
} else {
false
}
}
/**
* Returns some default name
*/
fun getName(context: Context): String {
return when (this.stats.size) {
1 -> this.stats.first().localizedTitle(context)
else -> this.query.getName(context)
}
}
}
companion object {
fun computeStatsWithEvolutionByAggregationType(
realm: Realm,
stat: Stat,
group: ComputableGroup,
aggregationType: AggregationType,
stats: List<Stat>? = null
): Report {
val options = Options(
progressValues = Options.ProgressValues.STANDARD,
stats = listOf(stat),
aggregationType = aggregationType
)
if (aggregationType == AggregationType.DURATION) {
options.progressValues = Options.ProgressValues.TIMED
}
stats?.let {
options.stats = stats
}
return when (aggregationType) {
AggregationType.SESSION, AggregationType.DURATION -> this.computeGroups(realm, listOf(group), options)
AggregationType.MONTH, AggregationType.YEAR -> {
this.computeStats(realm, options)
}
}
}
fun computeStats(realm: Realm, options: Options = Options()): Report {
val computableGroups: MutableList<ComputableGroup> = mutableListOf()
val combinations = options.criterias.combined()
// Timber.d("Combinations: ${ combinations.map { it.defaultName }}")
for (comparatorQuery in combinations) {
comparatorQuery.merge(options.query)
val group = ComputableGroup(comparatorQuery)
computableGroups.add(group)
}
if (computableGroups.size == 0) {
val group = ComputableGroup(options.query)
computableGroups.add(group)
}
return this.computeGroups(realm, computableGroups, options)
}
/**
* Computes all statIds for list of Session sessionGroup
*/
fun computeGroups(realm: Realm, groups: List<ComputableGroup>, options: Options = Options()): Report {
val report = Report(options)
groups.forEach { group ->
// Clean existing computables / sessionSets if group is reused
group.cleanup()
// Computes actual sessionGroup statIds
val results: ComputedResults = this.compute(realm, group, options)
// Computes the compared sessionGroup if existing
val comparedGroup = group.comparedGroup
if (comparedGroup != null) {
val comparedResults = this.compute(realm, comparedGroup, options)
group.comparedComputedResults = comparedResults
results.computeStatVariations(comparedResults)
}
if (options.shouldManageMultiGroupProgressValues) {
group.comparedComputedResults = report.results.lastOrNull()
}
results.finalize() // later treatment, such as evolution numericValues sorting
report.addResults(results)
// val e = Date()
// val duration = (e.time - s.time) / 1000.0
// Timber.d(">>> group ${group.name} in $duration seconds")
}
return report
}
/**
* Computes statIds for a SessionSet
*/
fun compute(realm: Realm, computableGroup: ComputableGroup, options: Options = Options()): ComputedResults {
val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues)
val computables = computableGroup.computables(realm, options.shouldSortValues)
if (computables.size == 0) { // we don't want to return stats with 0 as a value when comparing best performances
return results
}
// Timber.d("#### Start computing group, ${computables.size} computables")
results.addStat(NUMBER_OF_GAMES, computables.size.toDouble())
// computables.forEach {
// Timber.d("$$$ buyin = ${it.ratedBuyin} $$$ net result = ${it.ratedNet}")
// }
var ratedNet = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
if (options.includedTransactions.isNotEmpty()) {
for (transactionType in options.includedTransactions) {
val transactions = computableGroup.transactions(realm, transactionType, options.shouldSortValues)
val transactionRatedAmount = transactions.sum(Transaction.Field.RATED_AMOUNT.identifier).toDouble()
ratedNet += transactionRatedAmount
}
}
results.addStat(NET_RESULT, ratedNet)
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()
results.addStat(WINNING_SESSION_COUNT, winningSessionCount.toDouble())
val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble()
results.addStat(TOTAL_BUYIN, totalBuyin)
val totalTips = computables.sum(ComputableResult.Field.RATED_TIPS.identifier).toDouble()
results.addStat(TOTAL_TIPS, totalTips)
// Timber.d("########## totalBuyin = ${totalBuyin} ### sum = ${sum}")
val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
maxNetResult?.let {
results.addStat(MAXIMUM_NET_RESULT, it)
}
val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
minNetResult?.let {
results.addStat(MINIMUM_NET_RESULT, it)
}
Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 ->
results.addStat(NET_BB_PER_100_HANDS, netBB100)
}
Stat.returnOnInvestment(ratedNet, totalBuyin)?.let { roi ->
results.addStat(ROI, roi)
}
// val shouldComputeITMRatio = options.stats.contains(TOURNAMENT_ITM_RATIO) || computableGroup.displayedStats?.contains(TOURNAMENT_ITM_RATIO) == true
// if (shouldComputeITMRatio) {
// val itmCount = computables.count { it.session?.result?.cashout ?: 0.0 > 0.0 } // should we add a property inside ComputableResult for better performance?
// val itmRatio = itmCount.toDouble() / computables.size.toDouble()
// results.addStat(TOURNAMENT_ITM_RATIO, itmRatio)
// }
if (options.computeLocationsPlayed) {
results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble())
}
var average = 0.0 // also used for standard deviation later
if (computables.size > 0) {
average = ratedNet / computables.size.toDouble()
val winRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val itmRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val avgBuyin = totalBuyin / computables.size.toDouble()
results.addStats(
setOf(
ComputedStat(AVERAGE, average),
ComputedStat(WIN_RATIO, winRatio),
ComputedStat(TOURNAMENT_ITM_RATIO, itmRatio),
ComputedStat(AVERAGE_BUYIN, avgBuyin)
)
)
}
var averageBB = 0.0
if (bbSessionCount > 0) {
averageBB = bbSum / bbSessionCount
results.addStat(AVERAGE_NET_BB, averageBB)
}
val shouldIterateOverComputables =
(options.progressValues == Options.ProgressValues.STANDARD || options.computeLongestStreak)
// Computable Result
if (shouldIterateOverComputables) {
var index = 0
var tSum = 0.0
var tBBSum = 0.0
var tBBSessionCount = 0
var tWinningSessionCount = 0
var tBuyinSum = 0.0
var tHands = 0.0
var longestWinStreak = 0
var longestLoseStreak = 0
var currentStreak = 0
var tITMCount = 0
computables.forEach { computable ->
index++
tSum += computable.ratedNet
tBBSum += computable.bbNet
tBBSessionCount += computable.hasBigBlind
tWinningSessionCount += computable.isPositive
tITMCount += computable.isPositive
tBuyinSum += computable.ratedBuyin
tHands += computable.estimatedHands
if (computable.isPositive == 1) { // positive result
if (currentStreak >= 0) { // currently positive streak
currentStreak++
} else { // currently negative streak
longestLoseStreak = min(longestLoseStreak, currentStreak)
currentStreak = 1
}
} else { // negative result
if (currentStreak <= 0) { // currently negative streak
currentStreak--
} else { // currently positive streak
longestWinStreak = max(longestWinStreak, currentStreak)
currentStreak = -1
}
}
val session =
computable.session ?: throw PAIllegalStateException("Computing lone ComputableResult")
results.addEvolutionValue(tSum, stat = NET_RESULT, data = session)
results.addEvolutionValue(tSum / index, stat = AVERAGE, data = session)
results.addEvolutionValue(index.toDouble(), stat = NUMBER_OF_GAMES, data = session)
results.addEvolutionValue(tBBSum / tBBSessionCount, stat = AVERAGE_NET_BB, data = session)
results.addEvolutionValue(tBBSum, stat = BB_NET_RESULT, data = session)
results.addEvolutionValue(
(tWinningSessionCount.toDouble() / index.toDouble()),
stat = WIN_RATIO,
data = session
)
results.addEvolutionValue(
tITMCount.toDouble() / index.toDouble(),
stat = TOURNAMENT_ITM_RATIO,
data = session)
results.addEvolutionValue(tBuyinSum / index, stat = AVERAGE_BUYIN, data = session)
results.addEvolutionValue(computable.ratedNet, stat = STANDARD_DEVIATION, data = session)
Stat.netBBPer100Hands(tBBSum, tHands)?.let { netBB100 ->
results.addEvolutionValue(netBB100, stat = NET_BB_PER_100_HANDS, data = session)
}
Stat.returnOnInvestment(tSum, tBuyinSum)?.let { roi ->
results.addEvolutionValue(roi, stat = ROI, data = session)
}
}
if (currentStreak >= 0) {
longestWinStreak = max(longestWinStreak, currentStreak)
} else {
longestLoseStreak = min(longestLoseStreak, currentStreak)
}
// loseStreak is negative and we want it positive
results.addStat(LONGEST_STREAKS, longestWinStreak.toDouble(), -longestLoseStreak.toDouble())
}
val sessionSets = computableGroup.sessionSets(realm, options.shouldSortValues)
results.addStat(NUMBER_OF_SETS, sessionSets.size.toDouble())
var gHourlyDuration: Double? = null
var gBBSum: Double? = null
var maxDuration: Double? = null
if (computableGroup.conditions.isEmpty()) { // SessionSets are fine
gHourlyDuration =
sessionSets.sum(SessionSet.Field.NET_DURATION.identifier).toDouble() / 3600000 // (milliseconds to hours)
gBBSum = sessionSets.sum(SessionSet.Field.BB_NET.identifier).toDouble()
sessionSets.max(SessionSet.Field.NET_DURATION.identifier)?.let {
maxDuration = it.toDouble() / 3600000
}
}
val shouldIterateOverSets = computableGroup.conditions.isNotEmpty()
|| options.progressValues != Options.ProgressValues.NONE
|| options.computeDaysPlayed
// Session Set
if (shouldIterateOverSets) {
var tHourlyDuration = 0.0
var tIndex = 0
var tRatedNetSum = 0.0
var tBBSum = 0.0
var tTotalHands = 0.0
var tHourlyRate: Double
var tHourlyRateBB: Double
val daysSet = mutableSetOf<Date>()
var tMaxDuration = 0.0
sessionSets.forEach { sessionSet ->
tIndex++
val setStats = SSStats(sessionSet, computableGroup.query)
tRatedNetSum += setStats.ratedNet
tBBSum += setStats.bbSum
tHourlyDuration += setStats.hourlyDuration
tTotalHands += setStats.estimatedHands
tMaxDuration = max(tMaxDuration, setStats.hourlyDuration)
tHourlyRate = tRatedNetSum / tHourlyDuration
tHourlyRateBB = tBBSum / tHourlyDuration
daysSet.add(sessionSet.startDate.startOfDay())
when (options.progressValues) {
Options.ProgressValues.STANDARD -> {
results.addEvolutionValue(tHourlyRate, stat = HOURLY_RATE, data = sessionSet)
results.addEvolutionValue(tIndex.toDouble(), stat = NUMBER_OF_SETS, data = sessionSet)
results.addEvolutionValue(
sessionSet.hourlyDuration,
tHourlyDuration,
HOURLY_DURATION,
sessionSet
)
results.addEvolutionValue(
tHourlyDuration / tIndex,
stat = AVERAGE_HOURLY_DURATION,
data = sessionSet
)
results.addEvolutionValue(tHourlyRateBB, stat = HOURLY_RATE_BB, data = sessionSet)
}
Options.ProgressValues.TIMED -> {
results.addEvolutionValue(tRatedNetSum, tHourlyDuration, NET_RESULT, sessionSet)
results.addEvolutionValue(tHourlyRate, tHourlyDuration, HOURLY_RATE, sessionSet)
results.addEvolutionValue(
tIndex.toDouble(),
tHourlyDuration,
NUMBER_OF_SETS,
sessionSet
)
results.addEvolutionValue(
sessionSet.hourlyDuration,
tHourlyDuration,
HOURLY_DURATION,
sessionSet
)
results.addEvolutionValue(
tHourlyDuration / tIndex,
tHourlyDuration,
AVERAGE_HOURLY_DURATION,
sessionSet
)
results.addEvolutionValue(tHourlyRateBB, tHourlyDuration, HOURLY_RATE_BB, sessionSet)
Stat.netBBPer100Hands(tBBSum, tTotalHands)?.let { netBB100 ->
results.addEvolutionValue(
netBB100,
tHourlyDuration,
NET_BB_PER_100_HANDS,
sessionSet
)
}
}
else -> {
// nothing
}
}
results.addStat(DAYS_PLAYED, daysSet.size.toDouble())
}
gHourlyDuration = tHourlyDuration
gBBSum = tBBSum
maxDuration = tMaxDuration
}
var hourlyRate = 0.0
if (gHourlyDuration != null) {
hourlyRate = ratedNet / gHourlyDuration
if (sessionSets.size > 0) {
val avgDuration = gHourlyDuration / sessionSets.size
results.addStat(HOURLY_RATE, hourlyRate)
results.addStat(AVERAGE_HOURLY_DURATION, avgDuration)
}
results.addStat(HOURLY_DURATION, gHourlyDuration)
}
if (gBBSum != null) {
if (gHourlyDuration != null) {
results.addStat(HOURLY_RATE_BB, gBBSum / gHourlyDuration)
}
results.addStat(AVERAGE_NET_BB, gBBSum / bbSessionCount)
}
maxDuration?.let { maxd ->
results.addStat(MAXIMUM_DURATION, maxd) // (milliseconds to hours)
}
val bbPer100Hands = bbSum / totalHands * 100
// Standard Deviation
if (options.computeStandardDeviation) {
// Session
var stdSum = 0.0
var stdBBSum = 0.0
var stdBBper100HandsSum = 0.0
computables.forEach { session ->
stdSum += (session.ratedNet - average).pow(2.0)
stdBBSum += (session.bbNet - averageBB).pow(2.0)
stdBBper100HandsSum += (session.bbPer100Hands - bbPer100Hands).pow(2.0)
}
val standardDeviation = sqrt(stdSum / computables.size)
val standardDeviationBB = sqrt(stdBBSum / computables.size)
val standardDeviationBBper100Hands = sqrt(stdBBper100HandsSum / computables.size)
results.addStat(STANDARD_DEVIATION, standardDeviation)
results.addStat(STANDARD_DEVIATION_BB, standardDeviationBB)
results.addStat(STANDARD_DEVIATION_BB_PER_100_HANDS, standardDeviationBBper100Hands)
// Session Set
if (gHourlyDuration != null) {
var hourlyStdSum = 0.0
sessionSets.forEach { set ->
val ssStats = SSStats(set, computableGroup.query)
val sHourlyRate = ssStats.hourlyRate
hourlyStdSum += (sHourlyRate - hourlyRate).pow(2.0)
}
val hourlyStandardDeviation = sqrt(hourlyStdSum / sessionSets.size)
results.addStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation)
}
}
return results
}
}
}
class SSStats(sessionSet: SessionSet, query: Query) { // Session Set Stats
var hourlyDuration: Double = 0.0
var estimatedHands: Double = 0.0
var bbSum: Double = 0.0
var ratedNet: Double = 0.0
val hourlyRate: Double
get() {
return this.ratedNet / this.hourlyDuration
}
init {
if (sessionSet.sessions?.size == 1) { // use precomputed values
this.initStatsWithSet(sessionSet)
} else { // dynamically filter and compute subset
val setSessions = sessionSet.sessions!!
val filteredSessions = setSessions.filter(query)
if (setSessions.size == filteredSessions.size) {
this.initStatsWithSet(sessionSet)
} else {
ratedNet = filteredSessions.sumOf { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumOf { it.bbNet }
hourlyDuration = filteredSessions.hourlyDuration
estimatedHands = filteredSessions.sumOf { it.estimatedHands }
}
}
}
private fun initStatsWithSet(sessionSet: SessionSet) {
ratedNet = sessionSet.ratedNet
bbSum = sessionSet.bbNet
hourlyDuration = sessionSet.hourlyDuration
estimatedHands = sessionSet.estimatedHands
}
/**
* The options used for calculations or display
*/
class Options {
/**
* The way the stats are going to be displayed
*/
enum class Display {
TABLE,
EVOLUTION,
COMPARISON,
MAP,
POLYNOMIAL
}
/**
* The type of evolution values
*/
enum class EvolutionValues {
NONE,
STANDARD,
DATED
}
var display: Display = Display.TABLE
var evolutionValues: EvolutionValues = EvolutionValues.NONE
var displayedStats: List<Stat> = listOf()
/**
* 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
}
}
return false
}
// var aggregation: Aggregation? = null
}
companion object {
fun computePreAggregation(sets: List<SessionSet>, options: Options): List<ComputedResults> {
return listOf()
}
/**
* Computes all stats for list of Session sessionGroup
*/
fun computeGroups(groups: List<SessionGroup>, options: Options): List<ComputedResults> {
var computedResults: MutableList<ComputedResults> = mutableListOf()
groups.forEach { group ->
// Computes actual sessionGroup stats
val results: ComputedResults = Calculator.compute(group, options = options)
// Computes the compared sessionGroup if existing
val comparedGroup = group.comparedSessions
if (comparedGroup != null) {
val comparedResults = Calculator.compute(comparedGroup, options = options)
group.comparedComputedResults = comparedResults
results.computeStatVariations(comparedResults)
}
results.finalize(options) // later treatment, such as evolution values sorting
computedResults.add(results)
}
return computedResults
}
/**
* Computes stats for a SessionSet
*/
fun compute(sessionGroup: SessionGroup, options: Options) : ComputedResults {
val sessions: List<SessionInterface> = sessionGroup.sessions
var sessionSets = sessionGroup.sessions.mapNotNull { it.sessionSet }.toHashSet()
var results: ComputedResults = ComputedResults()
var sum: Double = 0.0
var totalHands: Double = 0.0
var bbSum: Double = 0.0
var bbSessionCount: Int = 0
var winningSessionCount: Int = 0
var totalBuyin: Double = 0.0
// Compute for each session
var index: Int = 0
sessions.forEach { s ->
index++;
sum += s.value
bbSum += s.bbNetResult
bbSessionCount += s.bigBlindSessionCount
if (s.value >= 0) {
winningSessionCount++
}
totalBuyin += s.buyin
totalHands += s.estimatedHands
if (options.evolutionValues == Options.EvolutionValues.STANDARD) {
results.addEvolutionValue(sum, NETRESULT)
results.addEvolutionValue(sum / index, AVERAGE)
results.addEvolutionValue(index.toDouble(), NUMBER_OF_GAMES)
results.addEvolutionValue(bbSum / bbSessionCount, AVERAGE_NET_BB)
results.addEvolutionValue(Stat.netBBPer100Hands(bbSum, totalHands), NET_BB_PER_100_HANDS)
results.addEvolutionValue((winningSessionCount / index).toDouble(), WIN_RATIO)
results.addEvolutionValue(totalBuyin / index, AVERAGE_BUYIN)
results.addEvolutionValue(Stat.returnOnInvestment(sum, totalBuyin), ROI)
}
}
// Compute for each serie
var duration: Double = 0.0
var hourlyRate: Double = 0.0; var hourlyRateBB: Double = 0.0
var gIndex = 0; var gSum = 0.0; var gTotalHands = 0.0; var gBBSum = 0.0;
sessionSets.forEach { sessionSet ->
gIndex++
duration += sessionSet.hourlyDuration
gSum += sessionSet.netResult
gTotalHands += sessionSet.estimatedHands
gBBSum += sessionSet.bbNetResult
hourlyRate = gSum / duration
hourlyRateBB = gBBSum / duration
if (options.evolutionValues == Options.EvolutionValues.DATED) {
results.addEvolutionValue(gSum, duration, NETRESULT)
results.addEvolutionValue(gSum / duration, duration, HOURLY_RATE)
results.addEvolutionValue(Stat.netBBPer100Hands(gBBSum, gTotalHands), duration, NET_BB_PER_100_HANDS)
results.addEvolutionValue(hourlyRate, duration, HOURLY_RATE)
results.addEvolutionValue(gIndex.toDouble(), duration, NUMBER_OF_SETS)
results.addEvolutionValue(sessionSet.duration.toDouble(), duration, DURATION)
results.addEvolutionValue(duration / gIndex, duration, AVERAGE_DURATION)
results.addEvolutionValue(hourlyRateBB, duration, HOURLY_RATE_BB)
}
}
val average: Double = sum / sessions.size.toDouble()
// Create stats
results.addStats(setOf(
ComputedStat(NETRESULT, sum),
ComputedStat(HOURLY_RATE, hourlyRate),
ComputedStat(AVERAGE, average),
ComputedStat(DURATION, duration),
ComputedStat(NUMBER_OF_SETS, sessionSets.size.toDouble()),
ComputedStat(NUMBER_OF_GAMES, sessions.size.toDouble()),
ComputedStat(AVERAGE_DURATION, duration / sessions.size),
ComputedStat(NET_BB_PER_100_HANDS, Stat.netBBPer100Hands(bbSum, totalHands)),
ComputedStat(HOURLY_RATE_BB, bbSum / duration),
ComputedStat(AVERAGE_NET_BB, bbSum / bbSessionCount),
ComputedStat(WIN_RATIO, (winningSessionCount / sessions.size).toDouble()),
ComputedStat(AVERAGE_BUYIN, totalBuyin / sessions.size),
ComputedStat(ROI, Stat.returnOnInvestment(sum, totalBuyin)),
ComputedStat(HANDS_PLAYED, totalHands)
))
// Standard Deviation
if (options.shouldComputeStandardDeviation()) {
var stdSum: Double = 0.0
sessions.forEach { s ->
stdSum += Math.pow(s.value - average, 2.0)
}
val standardDeviation: Double = Math.sqrt(stdSum / sessions.size)
var hourlyStdSum: Double = 0.0
sessionSets.forEach { sg ->
hourlyStdSum += Math.pow(sg.hourlyRate - hourlyRate, 2.0)
}
val hourlyStandardDeviation: Double = Math.sqrt(hourlyStdSum / sessionSets.size)
results.addStats(setOf(
ComputedStat(STANDARD_DEVIATION, standardDeviation),
ComputedStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation)
))
}
return results
}
}
}

@ -0,0 +1,106 @@
package net.pokeranalytics.android.calculus
import net.pokeranalytics.android.model.realm.SessionSet
/**
* An interface to describe objects that can be summed
*/
interface Summable {
var value: Double
}
/**
* An interface describing some class that can be computed
*/
interface SessionInterface : Summable {
var sessionSet: SessionSet?
var estimatedHands: Double
var bbNetResult: Double
var bigBlindSessionCount: Int // 0 or 1
var buyin: Double
}
/**
* A sessionGroup of computable items identified by a name
*/
class SessionGroup(name: String, sessions: List<SessionInterface>) {
var name: String = name
var sessions: List<SessionInterface> = sessions
// A subgroup used to compute stat variation
var comparedSessions: SessionGroup? = null
// The computed stats of the comparable sessionGroup
var comparedComputedResults: ComputedResults? = null
}
class ComputedResults() {
// The computed stats of the sessionGroup
private var _computedStats: MutableMap<Stat, ComputedStat> = mutableMapOf()
// The map containing all evolution values for all stats
private var _evolutionValues: MutableMap<Stat, MutableList<Point>> = mutableMapOf()
fun addEvolutionValue(value: Double, stat: Stat) {
this._addEvolutionValue(Point(value), stat = stat)
}
fun addEvolutionValue(value: Double, duration: Double, stat: Stat) {
this._addEvolutionValue(Point(value, y = duration), stat = stat)
}
private fun _addEvolutionValue(point: Point, stat: Stat) {
var evolutionValues = this._evolutionValues[stat]
if (evolutionValues != null) {
evolutionValues.add(point)
} else {
var 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 ->
var 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 { stat ->
// @todo sort
// var evolutionValues = this._evolutionValues[stat]
// evolutionValues.so
}
}
}
}
class Point(x: Double, y: Double) {
val x: Double = x
val y: Double = y
constructor(x: Double) : this(x, 1.0)
}

@ -1,111 +0,0 @@
package net.pokeranalytics.android.calculus
import io.realm.Realm
import io.realm.RealmResults
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.*
import timber.log.Timber
/**
* A sessionGroup of computable items identified by a name
*/
class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null) {
/**
* A subgroup used to compute stat variation
*/
var comparedGroup: ComputableGroup? = null
/**
* The computed statIds of the comparable sessionGroup
*/
var comparedComputedResults: ComputedResults? = null
/**
* A list of _conditions to get
*/
val conditions: List<QueryCondition>
get() {
return this.query.conditions
}
/**
* The size of the retrieved computables list
*/
var size: Int = 0
/**
* 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.query, sortedField)
this.size = computables.size
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.query, sortedField)
this._sessionSets = sets
return sets
}
/**
* Retrieves the transactions on the relative [realm] filtered with the provided [conditions]
*/
fun transactions(realm: Realm, transactionType: TransactionType, sorted: Boolean = false): RealmResults<Transaction> {
val query = this.query.copy()
query.add(QueryCondition.AnyTransactionType(transactionType))
val sortedField = if (sorted) "date" else null
Timber.d("query = ${query.defaultName}")
return Filter.queryOn(realm, query, sortedField)
}
/**
* Nullifies used Realm results
*/
fun cleanup() {
this._computables = null
this._sessionSets = null
}
val isEmpty: Boolean
get() {
return this._computables?.isEmpty() ?: true
}
}

@ -1,35 +0,0 @@
package net.pokeranalytics.android.calculus
import net.pokeranalytics.android.util.TextFormat
import java.util.*
/**
* ComputedStat contains a [stat] and their associated [value]
*/
class ComputedStat(var stat: Stat, var value: Double, var secondValue: Double? = null, var currency: Currency? = null) {
constructor(stat: Stat, value: Double, previousValue: Double?) : this(stat, value) {
if (previousValue != null) {
this.variation = (value - previousValue) / previousValue
}
}
/**
* The value used to get evolution dataset
*/
var progressValue: Double? = null
/**
* The variation of the stat
*/
var variation: Double? = null
/**
* Formats the value of the stat to be suitable for display
*/
val textFormat: TextFormat
get() {
return this.stat.textFormat(this.value, this.secondValue, this.currency)
}
}

@ -0,0 +1,8 @@
package net.pokeranalytics.android.calculus
import android.graphics.Color
class StatFormat {
var text: String = ""
var color: Int = Color.BLUE
}

@ -1,300 +0,0 @@
package net.pokeranalytics.android.calculus
import android.content.Context
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.interfaces.GraphIdentifiableEntry
import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent
import net.pokeranalytics.android.util.TextFormat
/**
* 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>
get() {
return this._results
}
/**
* Adds a new result to the list of ComputedResults
*/
fun addResults(result: ComputedResults) {
this._results.add(result)
}
fun max(stat: Stat): ComputedResults? {
var computedResults: ComputedResults? = null
var count = 0
var max = Double.MIN_VALUE
for (cr in this._results) {
cr.computedStat(stat)?.value?.let { value ->
count += 1
if (value > max) {
computedResults = cr
max = value
}
}
}
return if (count >= 2) { computedResults } else { null }
}
}
class Point(val x: Double, val y: Double, val data: Any) {
constructor(y: Double, data: Any) : this(0.0, y, data)
}
class ComputedResults(group: ComputableGroup,
shouldManageMultiGroupProgressValues: Boolean = false) : GraphUnderlyingEntry {
/**
* The session group used to computed the statIds
*/
var group: ComputableGroup = group
// The computed statIds of the sessionGroup
private var _computedStats: MutableMap<Stat, ComputedStat> = mutableMapOf()
// The map containing all evolution numericValues for all statIds
private var _evolutionValues: MutableMap<Stat, MutableList<Point>> = mutableMapOf()
private var shouldManageMultiGroupProgressValues = shouldManageMultiGroupProgressValues
private val allStats: Collection<ComputedStat>
get() { return this._computedStats.values }
val evolutionValues: Map<Stat, MutableList<Point>>
get() {
return this._evolutionValues
}
/**
* Adds a value to the evolution values
*/
fun addEvolutionValue(value: Double, duration: Double? = null, stat: Stat, data: GraphIdentifiableEntry) {
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 statIds
* 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) {
fun computeProgressValues(computedResults: ComputedResults) {
this.allStats.forEach { computedStat ->
val stat = computedStat.stat
computedResults.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 ?: 0.0
computedStat.progressValue = previousValue + computedStat.value
}
else -> {
}
}
} ?: run {
computedStat.progressValue = computedStat.value
}
}
}
this.group.comparedComputedResults?.let { previousResult ->
computeProgressValues(previousResult)
} ?: run {
computeProgressValues(this)
}
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() {
this.consolidateProgressStats()
}
val isEmpty: Boolean
get() {
return this.group.isEmpty
}
// Stat Entry
override fun entryTitle(context: Context): String {
return this.group.query.getName(context)
}
override fun formattedValue(stat: Stat): TextFormat {
this.computedStat(stat)?.let {
return it.textFormat
} ?: run {
throw PAIllegalStateException("Missing stat in results")
}
}
override fun legendValues(
stat: Stat,
total: Double,
style: Graph.Style,
groupName: String,
context: Context
): LegendContent {
when (style) {
Graph.Style.BAR -> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.textFormat(total, currency = null)
DefaultLegendValues(this.entryTitle(context), totalStatValue)
}
else -> {
val entryValue = this.formattedValue(stat)
val countValue = this.computedStat(Stat.NUMBER_OF_GAMES)?.textFormat
DefaultLegendValues(this.entryTitle(context), entryValue, countValue)
}
}
}
else -> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.textFormat(total, currency = null)
DefaultLegendValues(this.entryTitle(context), totalStatValue)
}
else -> {
val entryValue = this.formattedValue(stat)
val totalStatValue = stat.textFormat(total, currency = null)
DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue)
}
}
}
}
}
}

@ -1,338 +0,0 @@
package net.pokeranalytics.android.calculus
import android.content.Context
import android.os.CountDownTimer
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.optimalduration.CashGameOptimalDurationCalculator
import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.CrashLogging
import net.pokeranalytics.android.util.extensions.formattedHourlyDuration
import timber.log.Timber
import kotlin.coroutines.CoroutineContext
interface NewPerformanceListener {
fun newBestPerformanceHandler()
}
class ReportWhistleBlower(var context: Context) {
private var sessions: RealmResults<Session>? = null
private var results: RealmResults<Result>? = null
private var currentTask: ReportTask? = null
private val currentNotifications: MutableList<String> = mutableListOf() // Performance.id
private val listeners: MutableList<NewPerformanceListener> = mutableListOf()
var paused: Boolean = false
private var timer: CountDownTimer? = null
init {
val realm = Realm.getDefaultInstance()
this.sessions = realm.where(Session::class.java).findAll()
this.sessions?.addChangeListener { _ ->
requestReportLaunch()
}
this.results = realm.where(Result::class.java).findAll()
this.results?.addChangeListener { _ ->
requestReportLaunch()
}
realm.close()
}
fun addListener(newPerformanceListener: NewPerformanceListener) {
this.listeners.add(newPerformanceListener)
}
fun removeListener(listener: NewPerformanceListener) {
this.listeners.remove(listener)
}
fun requestReportLaunch() {
// Timber.d(">>> Launch report")
if (paused) {
CrashLogging.log("can't start reports comparisons because of paused state")
return
}
this.timer?.cancel()
val launchStart = 100L
val timer = object: CountDownTimer(launchStart, launchStart) {
override fun onTick(p0: Long) { }
override fun onFinish() {
launchReportTask()
}
}
this.timer = timer
timer.start()
}
private fun launchReportTask() {
synchronized(this) {
this.currentTask?.cancel()
val reportTask = ReportTask(this, this.context)
this.currentTask = reportTask
reportTask.start()
}
}
/**
* Pauses the whistleblower, for example when importing data
*/
fun pause() {
this.paused = true
this.currentTask?.cancel()
this.currentTask = null
}
fun resume() {
this.paused = false
this.requestReportLaunch()
}
fun has(performanceId: String): Boolean {
return this.currentNotifications.contains(performanceId)
}
fun notify(performance: Performance) {
this.currentNotifications.add(performance.id)
for (listener in this.listeners) {
listener.newBestPerformanceHandler()
}
}
fun clearNotifications() {
this.currentNotifications.clear()
}
}
class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Context) {
private var cancelled = false
var handler: (() -> Unit)? = null
private val coroutineContext: CoroutineContext
get() = Dispatchers.Default
fun start() {
messages.add("Starting task...")
launchReports()
}
fun cancel() {
this.cancelled = true
}
var messages: MutableList<String> = mutableListOf()
private fun launchReports() {
CoroutineScope(coroutineContext).launch {
val realm = Realm.getDefaultInstance()
// Basic
for (basicReport in StaticReport.basicReports) {
if (cancelled) {
break
}
launchReport(realm, basicReport)
}
// CustomField
val customFields = realm.where(CustomField::class.java)
.equalTo("type", CustomField.Type.LIST.uniqueIdentifier).findAll()
for (customField in customFields) {
if (cancelled) {
break
}
launchReport(realm, StaticReport.CustomFieldList(customField))
}
realm.close()
}
}
private fun launchReport(realm: Realm, report: StaticReport) {
// Timber.d(">>> launch report = $report")
when (report) {
StaticReport.OptimalDuration -> launchOptimalDuration(realm, report)
else -> launchDefaultReport(realm, report)
}
}
private fun launchDefaultReport(realm: Realm, report: StaticReport) {
val options = Calculator.Options(
stats = report.stats,
criterias = report.criteria
)
val result = Calculator.computeStats(realm, options = options)
analyseDefaultReport(realm, report, result)
}
private fun launchOptimalDuration(realm: Realm, report: StaticReport) {
LiveOnline.entries.forEach { key ->
val duration = CashGameOptimalDurationCalculator.start(key.isLive)
analyseOptimalDuration(realm, report, key, duration)
}
this.handler?.let { it() }
}
private fun analyseDefaultReport(realm: Realm, staticReport: StaticReport, result: Report) {
messages.add("Analyse report $staticReport...")
val nameSeparator = " "
for (stat in result.options.stats) {
// Timber.d("analyse stat: $stat for report: $staticReport")
// Get current performance
var query = performancesQuery(realm, staticReport, stat)
val customField: CustomField? =
(staticReport as? StaticReport.CustomFieldList)?.customField
customField?.let {
query = query.equalTo("customFieldId", it.id)
}
val currentPerf = query.findFirst()
// Store if necessary, delete if necessary
val bestComputedResults = result.max(stat)
bestComputedResults?.let { computedResults ->
messages.add("found new perf...")
val performanceQuery = computedResults.group.query
val performanceName = performanceQuery.getName(this.context, nameSeparator)
Timber.d("Best computed = $performanceName, ${computedResults.computedStat(Stat.NET_RESULT)?.value}")
var storePerf = true
currentPerf?.let {
messages.add("has current perf...")
currentPerf.name?.let { name ->
if (computedResults.group.query.getName(this.context, nameSeparator) == name) {
storePerf = false
}
}
currentPerf.objectId?.let { objectId ->
if (computedResults.group.query.objectId == objectId) {
storePerf = false
}
}
if (storePerf) {
realm.executeTransaction {
currentPerf.name = performanceName
currentPerf.objectId = performanceQuery.objectId
currentPerf.customFieldId = customField?.id
}
this.whistleBlower.notify(currentPerf)
}
}
messages.add("storePerf = $storePerf...")
if (currentPerf == null && storePerf) {
val performance = Performance(
staticReport,
stat,
performanceName,
performanceQuery.objectId,
customField?.id,
null
)
realm.executeTransaction { it.copyToRealm(performance) }
this.whistleBlower.notify(performance)
}
} ?: run { // if there is no max but a now irrelevant Performance, we delete it
messages.add("deletes current perf if necessary: $currentPerf...")
// Timber.d("NO best computed value, current perf = $currentPerf ")
currentPerf?.let { perf ->
realm.executeTransaction {
// Timber.d("Delete perf: stat = ${perf.stat}, report = ${perf.reportId}")
perf.deleteFromRealm()
}
}
}
}
}
private fun analyseOptimalDuration(realm: Realm, staticReport: StaticReport, key: PerformanceKey, duration: Double?) {
val performance = performancesQuery(realm, staticReport, key).findFirst()
duration?.let {
var storePerf = true
val formattedDuration = (duration / 3600 / 1000).formattedHourlyDuration()
performance?.let { perf ->
if (perf.value == duration) {
storePerf = false
}
if (storePerf) {
realm.executeTransaction {
perf.name = formattedDuration
perf.value = duration
}
}
}
if (storePerf) {
val perf = Performance(staticReport, key, name = formattedDuration, value = duration)
realm.executeTransaction { it.copyToRealm(perf) }
this.whistleBlower.notify(perf)
}
} ?: run { // no duration
performance?.let { perf ->
realm.executeTransaction {
perf.deleteFromRealm() // delete if the perf exists
}
}
}
}
private fun performancesQuery(realm: Realm, staticReport: StaticReport, key: PerformanceKey): RealmQuery<Performance> {
return realm.where(Performance::class.java)
.equalTo("reportId", staticReport.uniqueIdentifier)
.equalTo("key", key.value)
}
}

@ -1,335 +1,118 @@
package net.pokeranalytics.android.calculus
import android.content.Context
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.FormattingException
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.PerformanceKey
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.TextFormat
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
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
import kotlin.math.pow
class StatFormattingException(message: String) : Exception(message)
/**
* An enum representing all the types of Session statistics
*/
enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable, PerformanceKey {
NET_RESULT(1),
BB_NET_RESULT(2),
HOURLY_RATE(3),
AVERAGE(4),
NUMBER_OF_SETS(5),
NUMBER_OF_GAMES(6),
HOURLY_DURATION(7),
AVERAGE_HOURLY_DURATION(8),
NET_BB_PER_100_HANDS(9),
HOURLY_RATE_BB(10),
AVERAGE_NET_BB(11),
WIN_RATIO(12),
AVERAGE_BUYIN(13),
ROI(14),
STANDARD_DEVIATION(15),
STANDARD_DEVIATION_HOURLY(16),
STANDARD_DEVIATION_BB_PER_100_HANDS(17),
HANDS_PLAYED(18),
LOCATIONS_PLAYED(19),
LONGEST_STREAKS(20),
MAXIMUM_NET_RESULT(21),
MINIMUM_NET_RESULT(22),
MAXIMUM_DURATION(23),
DAYS_PLAYED(24),
WINNING_SESSION_COUNT(25),
BB_SESSION_COUNT(26),
TOTAL_BUYIN(27),
RISK_OF_RUIN(28),
STANDARD_DEVIATION_BB(29),
TOURNAMENT_ITM_RATIO(30),
TOTAL_TIPS(31)
;
companion object : IntSearchable<Stat> {
override fun valuesInternal(): Array<Stat> {
return values()
}
val userSelectableList: List<Stat>
get() {
return values().filter { it.canBeUserSelected }
}
val evolutionValuesList: List<Stat>
get() {
return values().filter { it.hasProgressValues }
}
fun returnOnInvestment(netResult: Double, buyin: Double): Double? {
if (buyin == 0.0) {
return null
}
return netResult / buyin
}
fun netBBPer100Hands(netBB: Double, numberOfHands: Double): Double? {
if (numberOfHands == 0.0) {
return null
}
return netBB / numberOfHands * 100
}
fun riskOfRuin(hourlyRate: Double, hourlyStandardDeviation: Double, bankrollValue: Double): Double? {
if (bankrollValue <= 0.0) {
return null
}
val numerator = -2 * hourlyRate * bankrollValue
val denominator = hourlyStandardDeviation.pow(2.0)
val ratio = numerator / denominator
return exp(ratio)
}
}
override val value: Int = this.uniqueIdentifier
override val resId: Int?
get() {
return when (this) {
NET_RESULT -> R.string.net_result
BB_NET_RESULT -> R.string.total_net_result_bb_
HOURLY_RATE -> R.string.average_hour_rate
AVERAGE -> R.string.average
NUMBER_OF_SETS -> R.string.number_of_sessions
NUMBER_OF_GAMES -> R.string.number_of_records
HOURLY_DURATION -> R.string.duration
AVERAGE_HOURLY_DURATION -> R.string.average_hours_played
NET_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
HOURLY_RATE_BB -> R.string.average_hour_rate_bb_
AVERAGE_NET_BB -> R.string.average_net_result_bb_
WIN_RATIO -> R.string.win_ratio
AVERAGE_BUYIN -> R.string.average_buyin
ROI -> R.string.tournament_roi
STANDARD_DEVIATION -> R.string.standard_deviation
STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_per_hour
STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.standard_deviation_bb_per_100_hands
STANDARD_DEVIATION_BB -> R.string.bb_standard_deviation
HANDS_PLAYED -> R.string.number_of_hands
LOCATIONS_PLAYED -> R.string.locations_played
LONGEST_STREAKS -> R.string.longest_streaks
MAXIMUM_NET_RESULT -> R.string.max_net_result
MINIMUM_NET_RESULT -> R.string.min_net_result
MAXIMUM_DURATION -> R.string.longest_session
DAYS_PLAYED -> R.string.days_played
TOTAL_BUYIN -> R.string.total_buyin
TOURNAMENT_ITM_RATIO -> R.string.itm_ratio
TOTAL_TIPS -> R.string.total_tips
else -> throw PAIllegalStateException("Stat ${this.name} name required but undefined")
}
}
/**
* Formats the value of the stat to be suitable for display
*/
fun textFormat(value: Double, secondValue: Double? = null, currency: Currency? = null): TextFormat {
if (value.isNaN()) {
return TextFormat(NULL_TEXT, R.color.white)
}
when (this) {
// Amounts + red/green
NET_RESULT, HOURLY_RATE, AVERAGE, MAXIMUM_NET_RESULT, MINIMUM_NET_RESULT -> {
val color = if (value >= this.threshold) R.color.green else R.color.red
return TextFormat(value.toCurrency(currency), color)
}
// Red/green numericValues
HOURLY_RATE_BB, AVERAGE_NET_BB, NET_BB_PER_100_HANDS, BB_NET_RESULT -> {
val color = if (value >= this.threshold) R.color.green else R.color.red
return TextFormat(value.formatted, color)
}
// white integers
NUMBER_OF_SETS, NUMBER_OF_GAMES, HANDS_PLAYED, LOCATIONS_PLAYED, DAYS_PLAYED -> {
return TextFormat("${value.toInt()}")
} // white durations
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> {
return TextFormat(value.formattedHourlyDuration())
} // red/green percentages
WIN_RATIO, ROI, TOURNAMENT_ITM_RATIO -> {
val color = if (value * 100 >= this.threshold) R.color.green else R.color.red
return TextFormat("${(value * 100).formatted}%", color)
}
RISK_OF_RUIN -> {
val color = if (value * 100 <= this.threshold) R.color.green else R.color.red
return TextFormat("${(value * 100).formatted}%", color)
}
// white amounts
AVERAGE_BUYIN, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY,
STANDARD_DEVIATION_BB_PER_100_HANDS, STANDARD_DEVIATION_BB, TOTAL_BUYIN, TOTAL_TIPS -> {
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")
}
}
private val threshold: Double
get() {
return when (this) {
RISK_OF_RUIN -> 5.0
TOURNAMENT_ITM_RATIO -> 10.0
WIN_RATIO -> 50.0
else -> 0.0
}
}
/**
* Returns a label used to display the legend right value, typically a total or an average
*/
fun cumulativeLabelResId(context: Context): String {
val resId = when (this) {
AVERAGE, AVERAGE_HOURLY_DURATION, NET_BB_PER_100_HANDS,
HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, HOURLY_RATE -> R.string.average
NUMBER_OF_SETS -> R.string.number_of_sessions
NUMBER_OF_GAMES -> R.string.number_of_records
NET_RESULT, BB_NET_RESULT -> R.string.total
STANDARD_DEVIATION -> R.string.net_result
STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_
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, TOURNAMENT_ITM_RATIO -> return this.localizedTitle(context)
else -> null
}
resId?.let {
return context.getString(it)
} ?: run {
return NULL_TEXT
}
}
/**
* Returns the different available aggregation type for each statistic
*/
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)
}
}
/**
* Returns if the stat has an evolution graph
*/
val hasProgressGraph: Boolean
get() {
return when (this) {
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, STANDARD_DEVIATION_BB,
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, HANDS_PLAYED,
STANDARD_DEVIATION_BB_PER_100_HANDS, TOTAL_TIPS -> false
else -> true
}
}
val isStandardDeviation: Boolean
get() {
return when (this) {
STANDARD_DEVIATION, STANDARD_DEVIATION_BB,
STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> true
else -> false
}
}
val legendHideRightValue: Boolean
get() {
return when (this) {
AVERAGE, NUMBER_OF_SETS, NUMBER_OF_GAMES, WIN_RATIO, TOURNAMENT_ITM_RATIO,
HOURLY_DURATION, AVERAGE_HOURLY_DURATION -> true
else -> false
}
}
/**
* Returns if the stat has a significant value to display in a progress graph
*/
val graphSignificantIndividualValue: Boolean
get() {
return when (this) {
AVERAGE, WIN_RATIO, TOURNAMENT_ITM_RATIO,
NUMBER_OF_SETS, NUMBER_OF_GAMES,
STANDARD_DEVIATION, HOURLY_DURATION -> false
else -> true
}
}
/**
* Returns if the stat graph should show the number of sessions
*/
val graphShouldShowNumberOfSessions: Boolean
get() {
return when (this) {
NUMBER_OF_GAMES, NUMBER_OF_SETS -> false
else -> true
}
}
val graphShowsXAxisZero: Boolean
get() {
return when (this) {
HOURLY_DURATION -> true
else -> false
}
}
val graphShowsYAxisZero: Boolean
get() {
return when (this) {
HOURLY_DURATION -> true
else -> false
}
}
private val canBeUserSelected: Boolean
get() {
return when (this) {
WINNING_SESSION_COUNT, BB_SESSION_COUNT, RISK_OF_RUIN -> false
else -> true
}
}
enum class Stat : RowRepresentable {
NETRESULT,
HOURLY_RATE,
AVERAGE,
NUMBER_OF_SETS,
NUMBER_OF_GAMES,
DURATION,
AVERAGE_DURATION,
NET_BB_PER_100_HANDS,
HOURLY_RATE_BB,
AVERAGE_NET_BB,
WIN_RATIO,
AVERAGE_BUYIN,
ROI,
STANDARD_DEVIATION,
STANDARD_DEVIATION_HOURLY,
STANDARD_DEVIATION_BB_PER_100_HANDS,
HANDS_PLAYED;
/**
* Returns whether the stat evolution values requires a distribution sorting
*/
fun hasDistributionSorting() : Boolean {
when (this) {
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> return true
else -> return false
}
}
companion object {
fun returnOnInvestment(netResult: Double, buyin: Double) : Double {
return netResult / buyin
}
fun netBBPer100Hands(netBB: Double, numberOfHands: Double) : Double {
return netBB / numberOfHands * 100
}
}
override val resId: Int?
get() {
return when (this) {
NETRESULT -> R.string.net_result
HOURLY_RATE -> R.string.average_hour_rate
AVERAGE -> R.string.average
NUMBER_OF_SETS -> R.string.number_of_groups
NUMBER_OF_GAMES -> R.string.number_of_games
DURATION -> R.string.duration
AVERAGE_DURATION -> R.string.average_duration
NET_BB_PER_100_HANDS -> R.string.net_bb_per_100_hands
HOURLY_RATE_BB -> R.string.hourly_rate_bb
AVERAGE_NET_BB -> R.string.average_net_bb
WIN_RATIO -> R.string.win_ratio
AVERAGE_BUYIN -> R.string.average_buyin
ROI -> R.string.roi
STANDARD_DEVIATION -> R.string.standard_deviation
STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_hourly
STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.standard_deviation_bb_per_100_hands
HANDS_PLAYED -> R.string.hands_played
}
}
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
}
private val hasProgressValues: Boolean
get() {
return when (this) {
NET_RESULT, NET_BB_PER_100_HANDS, HOURLY_RATE_BB,
AVERAGE_HOURLY_DURATION, HOURLY_DURATION,
NUMBER_OF_SETS, ROI, AVERAGE_BUYIN,
WIN_RATIO, TOURNAMENT_ITM_RATIO,
AVERAGE_NET_BB, NUMBER_OF_GAMES, AVERAGE -> true
else -> false
}
}
/**
* ComputedStat contains a [stat] and their associated [value]
*/
class ComputedStat(stat: Stat, value: Double) {
constructor(stat: Stat, value: Double, previousValue: Double?) : this(stat, value) {
if (previousValue != null) {
this.variation = (value - previousValue) / previousValue
}
}
/**
* The statistic type
*/
var stat: Stat = stat
/**
* The stat value
*/
var value: Double = value
/**
* The variation of the stat
*/
var variation: Double? = null
/**
* Formats the value of the stat to be suitable for display
*/
fun format() : StatFormat {
return StatFormat()
}
/**
* Returns a StatFormat instance for an evolution value located at the specified [index]
*/
fun evolutionValueFormat(index: Int) : StatFormat {
return StatFormat()
}
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
}

@ -1,128 +0,0 @@
package net.pokeranalytics.android.calculus.bankroll
import io.realm.Realm
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.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.util.extensions.findById
class BankrollCalculator {
companion object {
fun computeReport(realm: Realm, setup: BankrollReportSetup) : BankrollReport {
val report = BankrollReport(setup)
realm.refresh() // fixes an issue where a newly created bankroll is not found, throwing an exception
val bankrolls: List<Bankroll> =
if (setup.bankrollId != null) {
val bankroll = realm.findById<Bankroll>(setup.bankrollId) ?: throw PAIllegalStateException("Bankroll not found with id=${setup.bankrollId}")
report.currency = bankroll.utilCurrency
listOf(bankroll)
}
else realm.where(Bankroll::class.java).findAll()
var initialValue = 0.0
var transactionNet = 0.0
bankrolls.forEach { bankroll ->
val rate = if (setup.virtualBankroll) bankroll.rate else 1.0
if (setup.shouldAddInitialValue) {
initialValue += bankroll.initialValue * rate
}
if (setup.virtualBankroll) {
val sum = realm.where(Transaction::class.java)
.equalTo("bankroll.id", bankroll.id)
.notEqualTo("type.kind", TransactionType.Value.TRANSFER.uniqueIdentifier)
.sum("amount") // we remove transfer as they don't impact the overall bankroll
transactionNet += rate * sum.toDouble()
} else {
bankroll.transactions?.let { transactions ->
val sum = transactions.sum("amount")
transactionNet += rate * sum.toDouble()
}
bankroll.destinationTransactions?.let { transactions ->
for (transaction in transactions) {
val transferRate = transaction.transferRate ?: 1.0
transactionNet += transferRate * transaction.amount * -1
}
}
}
}
report.transactionsNet = transactionNet
report.initial = initialValue
val baseQuery = setup.query(realm)
val transactions = Filter.queryOn<Transaction>(realm, baseQuery)
report.addDatedItems(transactions)
transactions.forEach {
report.addTransaction(it)
}
val sessionQuery = Query(QueryCondition.DateNotNull).merge(baseQuery)
val sessions = Filter.queryOn<Session>(realm, sessionQuery)
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(baseQuery)
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, baseQuery)
report.netResult = results.sum("net").toDouble()
}
val depositType = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm)
report.transactionBuckets[depositType.id]?.let { bucket ->
report.depositTotal = bucket.transactions.sumOf { it.amount }
}
val withdrawalType = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm)
report.transactionBuckets[withdrawalType.id]?.let { bucket ->
report.withdrawalTotal = bucket.transactions.sumOf { 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,287 +0,0 @@
package net.pokeranalytics.android.calculus.bankroll
import android.content.Context
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
import io.realm.Realm
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.DatedBankrollGraphEntry
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.util.extensions.findById
import java.util.*
/**
* This class holds the results from the BankrollCalculator computations
* It has all the information required for the Bankroll various displays
*/
class BankrollReport(var setup: BankrollReportSetup) {
/**
* The java.util.Currency
*/
var currency: Currency? = null
/**
* The value of the bankroll
*/
var total: Double = 0.0
private set
/**
* The initial value of the bankroll, or of all bankrolls if virtual is computed
*/
var initial: Double = 0.0
set(value) {
field = value
this.computeBankrollTotal()
}
/**
* The net result from poker computables
*/
var netResult: Double = 0.0
set(value) {
field = value
this.computeBankrollTotal()
}
/**
* The net result from transactions
*/
var transactionsNet: Double = 0.0
set(value) {
field = value
this.computeBankrollTotal()
}
/**
* Computes the bankroll total
*/
private fun computeBankrollTotal() {
this.total = this.initial + this.netResult + this.transactionsNet
// Timber.d("init = $initial, net = $netResult, trans = $transactionsNet")
}
/**
* The sum of all deposits
*/
var depositTotal: Double = 0.0
set(value) {
field = value
this.computeNetBanked()
}
/**
* The sum of all withdrawals
*/
var withdrawalTotal: Double = 0.0
set(value) {
field = value
this.computeNetBanked()
}
/**
* Computes the net banked amount
*/
private fun computeNetBanked() {
this.netBanked = -this.withdrawalTotal - this.depositTotal
}
/**
* The difference between withdrawals and deposits
*/
var netBanked: Double = 0.0
private set
/**
* The risk of ruin
*/
var riskOfRuin: Double? = null
/**
* The list of transactions held by the bankroll
*/
var transactions: List<Transaction> = mutableListOf()
private set
/**
* A map containing TransactionBuckets by transaction types
*/
var transactionBuckets: HashMap<String, TransactionBucket> = HashMap()
private set
/**
* The list of bankroll graph points
*/
private var evolutionPoints: MutableList<BRGraphPoint> = mutableListOf()
/**
* The list of dated items used for the graph
*/
private var evolutionItems: MutableList<DatedBankrollGraphEntry> = mutableListOf()
/**
* Adds a list of dated items to the evolution items used to get the bankroll graph
*/
fun addDatedItems(items: Collection<DatedBankrollGraphEntry>) {
this.evolutionItems.addAll(items)
}
/**
* Adds a transaction to its type bucket
*/
fun addTransaction(transaction: Transaction) {
transaction.type?.let { type ->
var bucket = this.transactionBuckets[type.id]
if (bucket == null) {
val b = TransactionBucket(type.name, this.setup.virtualBankroll)
this.transactionBuckets[type.id] = b
bucket = b
}
bucket.addTransaction(transaction)
} ?: run {
throw PAIllegalStateException("Transaction has no type")
}
}
/**
* Generates the graph points used for the virtual bankroll
*/
fun generateGraphPointsIfNecessary() {
if (!this.setup.virtualBankroll) {
return
}
this.evolutionItems.sortBy { it.date }
var total = this.initial
this.evolutionItems.forEach {
val rate = it.bankroll?.rate ?: 1.0
// Timber.d("rate = $rate, amount = ${it.amount}")
total += it.amount * rate
// Timber.d("total = $total")
val point = BRGraphPoint(total, it.date, it.objectIdentifier)
this.evolutionPoints.add(point)
}
}
/**
* Returns a data set used for the bankroll graph
*/
fun lineDataSet(context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this.evolutionPoints.forEachIndexed { index, point ->
val entry = Entry(index.toFloat(), point.value.toFloat(), point.data)
entries.add(entry)
}
return DataSetFactory.lineDataSetInstance(entries, "", context)
}
}
/**
* A class describing the parameters required to launch a bankroll report
*
*/
class BankrollReportSetup(val bankrollId: String? = null, val from: Date? = null, val to: Date? = null) {
/**
* Returns whether the setup concerns the virtual bankroll,
* i.e. the bankroll summing all concrete bankrolls
*/
val virtualBankroll: Boolean
get() {
return this.bankrollId == null
}
/**
* the query used to get bankroll transactions
*/
fun query(realm: Realm): Query {
val query = Query()
this.bankrollId?.let {
val bankroll = realm.findById<Bankroll>(it) ?: throw PAIllegalStateException("Bankroll not found with id $it")
val bankrollCondition = QueryCondition.AnyBankroll(bankroll)
query.add(bankrollCondition)
}
this.from?.let {
query.add(QueryCondition.StartedFromDate(it))
}
this.to?.let {
query.add(QueryCondition.StartedToDate(it))
}
return query
}
/**
* Returns whether or not the initial value should be added for the bankroll total
*/
val shouldAddInitialValue: Boolean
get() {
return this.from == null
}
}
/**
* A TransactionBucket holds a list of _transactions and computes its amount sum
*/
class TransactionBucket(var name: String, useRate: Boolean = false) {
/**
* Whether the bankroll rate should be used
*/
private var useRate: Boolean = useRate
/**
* A list of _transactions
*/
private var _transactions: MutableList<Transaction> = mutableListOf()
val transactions: List<Transaction>
get() {
return this._transactions
}
/**
* The sum of all _transactions
*/
var total: Double = 0.0
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,118 +0,0 @@
package net.pokeranalytics.android.calculus.bankroll
import io.realm.Realm
import io.realm.RealmResults
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Transaction
import timber.log.Timber
import java.util.*
import kotlin.coroutines.CoroutineContext
object BankrollReportManager {
private val coroutineContext: CoroutineContext
get() = Dispatchers.Main
private var reports: MutableMap<String?, BankrollReport> = mutableMapOf()
private var computableResults: RealmResults<ComputableResult>
private var bankrolls: RealmResults<Bankroll>
private var transactions: RealmResults<Transaction>
init {
val realm = Realm.getDefaultInstance()
computableResults = realm.where(ComputableResult::class.java).findAll()
bankrolls = realm.where(Bankroll::class.java).findAll()
transactions = realm.where(Transaction::class.java).findAll()
initializeListeners()
realm.close()
}
/**
* Listens to all objects that might have an impact on any bankroll report
*/
private fun initializeListeners() {
this.computableResults.addChangeListener { t, changeSet ->
val indexes = changeSet.changes.plus(changeSet.insertions).toList()
val bankrolls = indexes.mapNotNull { t[it]?.session?.bankroll }.toSet()
this.updateBankrolls(bankrolls)
}
this.bankrolls.addChangeListener { t, changeSet ->
val indexes = changeSet.changes.plus(changeSet.insertions).toList()
val bankrolls = indexes.mapNotNull { t[it] }.toSet()
this.updateBankrolls(bankrolls)
}
this.transactions.addChangeListener { t, changeSet ->
val indexes = changeSet.changes.plus(changeSet.insertions).toList()
val bankrolls = indexes.mapNotNull { t[it]?.bankroll }.toSet()
this.updateBankrolls(bankrolls)
}
}
fun reportForBankroll(bankrollId: String?, handler: (BankrollReport) -> Unit) {
Timber.d("Request bankroll report for bankrollId = $bankrollId")
// if the report exists, return it
val existingReport: BankrollReport? = this.reports[bankrollId]
if (existingReport != null) {
handler(existingReport)
return
}
// otherwise compute it
GlobalScope.launch(coroutineContext) {
var report: BankrollReport? = null
val coroutine = GlobalScope.async {
val s = Date()
Timber.d(">>>>> start computing bankroll...")
val realm = Realm.getDefaultInstance()
val setup = BankrollReportSetup(bankrollId)
report = BankrollCalculator.computeReport(realm, setup)
realm.close()
val e = Date()
val duration = (e.time - s.time) / 1000.0
Timber.d(">>>>> ended in $duration seconds")
}
coroutine.await()
report?.let {
handler(it)
}
}
}
/**
* Notifies the manager of cases not managed by RealmResults listener, such as deletions
*/
fun notifyBankrollReportImpact(bankrollId: String) {
this.reports.remove(bankrollId)
this.reports.remove(null)
}
private fun updateBankrolls(bankrolls: Set<Bankroll>) {
this.invalidateReport(bankrolls)
}
private fun invalidateReport(bankrolls: Set<Bankroll>) {
this.reports.remove(null)
bankrolls.forEach { br ->
this.reports.remove(br.id)
}
}
}

@ -1,12 +0,0 @@
package net.pokeranalytics.android.calculus.calcul
import net.pokeranalytics.android.calculus.AggregationType
import net.pokeranalytics.android.ui.graph.Graph
val AggregationType.axisFormatting: Graph.AxisFormatting
get() {
return when (this) {
AggregationType.DURATION -> Graph.AxisFormatting.X_DURATION
else -> Graph.AxisFormatting.DEFAULT
}
}

@ -1,84 +0,0 @@
package net.pokeranalytics.android.calculus.calcul
import android.content.Context
import com.github.mikephil.charting.data.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Point
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.graph.DataSetFactory
import kotlin.math.abs
// MPAndroidChart
fun ComputedResults.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 ComputedResults.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 DataSetFactory.lineDataSetInstance(entries, this.group.query.getName(context), context)
}
fun ComputedResults.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 DataSetFactory.lineDataSetInstance(entries, stat.name, context)
}
private fun ComputedResults.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))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context)
}
private fun ComputedResults.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))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context, colors)
}

@ -1,64 +0,0 @@
package net.pokeranalytics.android.calculus.calcul
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.ComparisonReportActivity
import net.pokeranalytics.android.ui.activity.ProgressReportActivity
import net.pokeranalytics.android.ui.activity.TableReportActivity
import net.pokeranalytics.android.ui.view.RowRepresentable
/**
* The way the computed stats are going to be displayed
*/
enum class ReportDisplay : RowRepresentable {
TABLE,
PROGRESS,
COMPARISON,
MAP;
override val resId: Int?
get() {
return when (this) {
TABLE -> R.string.table
PROGRESS -> R.string.progress
COMPARISON -> R.string.comparison
MAP -> R.string.map
}
}
val activityClass: Class<*>
get() {
return when (this) {
TABLE -> TableReportActivity::class.java
PROGRESS -> ProgressReportActivity::class.java
COMPARISON -> ComparisonReportActivity::class.java
else -> throw PAIllegalStateException("undefined activity for report display")
}
}
val progressValues: Calculator.Options.ProgressValues
get() {
return when (this) {
PROGRESS, COMPARISON -> Calculator.Options.ProgressValues.STANDARD
else -> Calculator.Options.ProgressValues.NONE
}
}
// val requireProgressValues: Boolean
// get() {
// return when (this) {
// PROGRESS, COMPARISON -> true
// else -> false
// }
// }
val multipleStatSelection: Boolean
get() {
return when (this) {
PROGRESS -> false
else -> true
}
}
}

@ -1,64 +0,0 @@
package net.pokeranalytics.android.calculus.calcul
import android.content.Context
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.util.ColorUtils
/**
* Returns the list of entries corresponding to the provided [stat]
* One value will be returned by result
*/
fun Report.lineEntries(stat: Stat? = null, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
val statToUse = stat ?: this.options.stats.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 DataSetFactory.lineDataSetInstance(entries, statName, context)
}
fun Report.barEntries(stat: Stat? = null, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
val statToUse = stat ?: options.stats.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 label = statToUse?.name ?: ""
return DataSetFactory.barDataSetInstance(entries, label, context)
}
fun Report.multiLineEntries(context: Context): List<LineDataSet> {
val dataSets = mutableListOf<LineDataSet>()
options.stats.forEach { stat ->
this.results.forEachIndexed { index, result ->
val ds = result.singleLineEntries(stat, context)
ds.color = ColorUtils.almostRandomColor(index, context)
dataSets.add(ds)
}
}
return dataSets
}

@ -1,81 +0,0 @@
package net.pokeranalytics.android.calculus.calcul
import android.content.Context
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT
class StatRepresentable(var stat: Stat) : RowRepresentable {
companion object {
fun resId(stat: Stat): Int {
return when (stat) {
Stat.NET_RESULT -> R.string.net_result
Stat.BB_NET_RESULT -> R.string.total_net_result_bb_
Stat.HOURLY_RATE -> R.string.average_hour_rate
Stat.AVERAGE -> R.string.average
Stat.NUMBER_OF_SETS -> R.string.number_of_sessions
Stat.NUMBER_OF_GAMES -> R.string.number_of_records
Stat.HOURLY_DURATION -> R.string.duration
Stat.AVERAGE_HOURLY_DURATION -> R.string.average_hours_played
Stat.NET_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
Stat.HOURLY_RATE_BB -> R.string.average_hour_rate_bb_
Stat.AVERAGE_NET_BB -> R.string.average_net_result_bb_
Stat.WIN_RATIO -> R.string.win_ratio
Stat.AVERAGE_BUYIN -> R.string.average_buyin
Stat.ROI -> R.string.tournament_roi
Stat.STANDARD_DEVIATION -> R.string.standard_deviation
Stat.STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_per_hour
Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.standard_deviation_bb_per_100_hands
Stat.STANDARD_DEVIATION_BB -> R.string.bb_standard_deviation
Stat.HANDS_PLAYED -> R.string.number_of_hands
Stat.LOCATIONS_PLAYED -> R.string.locations_played
Stat.LONGEST_STREAKS -> R.string.longest_streaks
Stat.MAXIMUM_NET_RESULT -> R.string.max_net_result
Stat.MINIMUM_NET_RESULT -> R.string.min_net_result
Stat.MAXIMUM_DURATION -> R.string.longest_session
Stat.DAYS_PLAYED -> R.string.days_played
Stat.TOTAL_BUYIN -> R.string.total_buyin
else -> throw PAIllegalStateException("Stat ${stat.name} name required but undefined")
}
}
fun localizedTitle(stat: Stat, context: Context): String {
return context.getString(resId(stat))
}
}
override val resId: Int
get() {
return resId(this.stat)
}
/**
* Returns a label used to display the legend right value, typically a total or an average
*/
fun cumulativeLabelResId(context: Context): String {
val resId = when (this.stat) {
Stat.AVERAGE, Stat.AVERAGE_HOURLY_DURATION, Stat.NET_BB_PER_100_HANDS,
Stat.HOURLY_RATE_BB, Stat.AVERAGE_NET_BB, Stat.ROI, Stat.HOURLY_RATE -> R.string.average
Stat.NUMBER_OF_SETS -> R.string.number_of_sessions
Stat.NUMBER_OF_GAMES -> R.string.number_of_records
Stat.NET_RESULT -> R.string.total
Stat.STANDARD_DEVIATION -> R.string.net_result
Stat.STANDARD_DEVIATION_BB -> R.string.average_net_result_bb_
Stat.STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses
Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
Stat.WIN_RATIO, Stat.HOURLY_DURATION -> return this.localizedTitle(context)
else -> null
}
resId?.let {
return context.getString(it)
} ?: run {
return NULL_TEXT
}
}
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
}

@ -1,183 +0,0 @@
package net.pokeranalytics.android.calculus.optimalduration
import io.realm.Realm
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Session
import org.apache.commons.math3.fitting.PolynomialCurveFitter
import org.apache.commons.math3.fitting.WeightedObservedPoints
import timber.log.Timber
import java.util.*
import kotlin.math.pow
import kotlin.math.round
/***
* This class attempts to find the optimal game duration,
* meaning the duration where the player will maximize its results, based on his history.
* The results stands for cash game, and are separated between live and online.
* Various reasons can prevent the algorithm to find a duration, see below.
*/
class CashGameOptimalDurationCalculator {
companion object {
private const val bucket = 60 * 60 * 1000L // the duration of bucket
private const val bucketInterval =
4 // number of duration tests inside the bucket to find the best duration
private const val minimumValidityCount =
10 // the number of sessions inside a bucket to start having a reasonable average
private const val intervalValidity =
3 // the minimum number of unit between the shortest & longest valid buckets
private const val polynomialDegree = 7 // the degree of the computed polynomial
/***
* Starts the calculation
* [isLive] is a boolean to indicate if we're looking at live or online games
* return a duration or null if it could not be computed
*/
fun start(isLive: Boolean): Double? {
val realm = Realm.getDefaultInstance()
val query = Query().add(QueryCondition.IsCash) // cash game
query.add(
if (isLive) {
QueryCondition.IsLive
} else {
QueryCondition.IsOnline
}
) // live / online
query.add(QueryCondition.EndDateNotNull) // ended
query.add(QueryCondition.BiggestBetNotNull) // has BB value
val sessions = query.queryWith(realm.where(Session::class.java)).findAll()
val sessionsByDuration = sessions.groupBy {
val dur = round((it.netDuration / bucket).toDouble()) * bucket
// Timber.d("Stop notif > key: $dur")
dur
}
// define validity interval
var start: Double? = null
var end: Double? = null
var validBuckets = 0
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted()
// Timber.d("Stop notif > keys: $hkeys ")
for (key in sessionsByDuration.keys.sorted()) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (start == null && sessionCount >= minimumValidityCount) {
start = key
}
if (sessionCount >= minimumValidityCount) {
end = key
validBuckets++
}
}
// Timber.d("Stop notif > validBuckets: $validBuckets ")
if (!(start != null && end != null && (end - start) >= intervalValidity)) {
// Timber.d("Stop notif > invalid setup: $start / $end ")
return null
}
// define if we have enough sessions
if (sessions.size < 50) {
// Timber.d("Stop notif > not enough sessions: ${sessions.size} ")
return null
}
val options = Calculator.Options()
options.query = query
val report = Calculator.computeStats(realm, options)
val stdBB =
report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value
val p = polynomialRegression(sessions, stdBB)
var bestAverage = 0.0
var bestHourlyRate = 0.0
var bestDuration = 0.0
var maxDuration = 0.0
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted()
for (key in keys) {
val sessionCount = sessionsByDuration[key]?.size ?: 0
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid
for (i in 0 until bucketInterval) {
val duration = key + i * bucket / bucketInterval
val averageResult = getBB(duration, p)
val hourly = averageResult / duration
if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) {
bestAverage = averageResult
bestDuration = duration
}
if (duration > 0 && hourly > bestHourlyRate) {
bestHourlyRate = hourly
}
if (duration > maxDuration) {
maxDuration = duration
}
}
}
if (bestDuration > 0.0) {
return bestDuration
}
// Timber.d("Stop notif > not found, best duration: $bestDuration")
realm.close()
return null
}
private fun getBB(netDuration: Double, polynomial: DoubleArray): Double {
var y = 0.0
for (i in polynomial.indices) {
y += polynomial[i] * netDuration.pow(i)
}
return y
}
private fun polynomialRegression(
sessions: List<Session>,
bbStandardDeviation: Double?
): DoubleArray {
val stdBB = bbStandardDeviation ?: Double.MAX_VALUE
val points = WeightedObservedPoints()
val now = Date().time
sessions.forEach {
var weight = 5.0
val endTime = it.endDate?.time ?: 0L
val age = now - endTime
if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point
weight -= 1.0
}
if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points
weight -= 3.0
}
points.add(weight, it.netDuration.toDouble(), it.bbNet)
}
// polynomial of 7 degree, same as iOS
return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList())
}
}
}

@ -1,32 +0,0 @@
package net.pokeranalytics.android.exceptions
import net.pokeranalytics.android.model.Criteria
class ModelException(message: String) : Exception(message)
class FormattingException(message: String) : Exception(message)
class RowRepresentableEditDescriptorException(message: String) : Exception(message)
class ConfigurationException(message: String) : Exception(message)
class EnumIdentifierNotFoundException(message: String) : Exception(message)
class MisconfiguredSavableEnumException(message: String) : Exception(message)
class PAIllegalStateException(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 queryWith has no entity initialized")
// object FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable")
class QueryValueMapUnknown(message: String = "fieldName is missing"): PokerAnalyticsException(message)
class QueryTypeUnhandled(clazz: String) :
PokerAnalyticsException(message = "queryWith type not handled: $clazz")
class ComparisonCriteriaUnhandled(criteria: Criteria) :
PokerAnalyticsException(message = "Criteria type not handled: ${criteria.uniqueIdentifier}")
object QueryValueMapUnexpectedValue: PokerAnalyticsException(message = "valueMap null not expected")
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 queryWith type for $filterElementRow")
// data class MissingFieldNameForQueryCondition(val name: String) : PokerAnalyticsException(message = "Missing fieldname for QueryCondition ${name}")
// object InputFragmentException : PokerAnalyticsException(message = "RowEditableDelegate must be a Fragment")
}

@ -0,0 +1,5 @@
package net.pokeranalytics.android.exceptions
class ModelException(message: String) : Exception(message) {
}

@ -1,356 +0,0 @@
package net.pokeranalytics.android.model
import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.Criteria.Bankrolls.comparison
import net.pokeranalytics.android.model.Criteria.Games.comparison
import net.pokeranalytics.android.model.Criteria.Limits.comparison
import net.pokeranalytics.android.model.Criteria.Locations.comparison
import net.pokeranalytics.android.model.Criteria.Stakes.comparison
import net.pokeranalytics.android.model.Criteria.TableSizes.comparison
import net.pokeranalytics.android.model.Criteria.TournamentFeatures.comparison
import net.pokeranalytics.android.model.Criteria.TournamentFees.comparison
import net.pokeranalytics.android.model.Criteria.TournamentNames.comparison
import net.pokeranalytics.android.model.Criteria.TournamentTypes.comparison
import net.pokeranalytics.android.model.Criteria.TransactionTypes.comparison
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
import net.pokeranalytics.android.util.extensions.findById
fun List<Criteria>.combined(): List<Query> {
val comparatorList = ArrayList<List<Query>>()
this.forEach { criteria ->
comparatorList.add(criteria.queries)
}
return getCombinations(comparatorList)
}
fun getCombinations(queries: List<List<Query>>): List<Query> {
if (queries.isEmpty()) {
return listOf()
}
val mutableQueries = queries.toMutableList()
var combinations = mutableQueries.removeAt(0)
for (queryList in mutableQueries) {
val newCombinations = mutableListOf<Query>()
combinations.forEach { combinedQuery ->
queryList.forEach { queryToAdd ->
val nq = Query().merge(combinedQuery).merge(queryToAdd)
newCombinations.add(nq)
}
}
combinations = newCombinations
}
return combinations
}
sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable {
abstract class RealmCriteria(uniqueIdentifier: Int) : Criteria(uniqueIdentifier) {
inline fun <reified T : NameManageable> comparison(): List<Query> {
if (this is ListCustomFields) {
val objects = mutableListOf<QueryCondition.CustomFieldListQuery>()
val realm = Realm.getDefaultInstance()
realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach {
objects.add(QueryCondition.CustomFieldListQuery(it))
}
objects.sort()
realm.close()
return objects.map { Query(it) }
}
return compare<QueryCondition.QueryDataCondition<NameManageable>, T>()
}
}
abstract class SimpleCriteria(private val conditions: List<QueryCondition>, uniqueIdentifier: Int) : Criteria(uniqueIdentifier) {
fun comparison(): List<Query> {
return conditions.map { Query(it) }
}
}
abstract class ListCriteria(uniqueIdentifier: Int) : Criteria(uniqueIdentifier) {
inline fun <reified T : QueryCondition.ListOfValues<S>, reified S : Comparable<S>> comparison(): List<Query> {
if (this is ValueCustomFields) {
val realm = Realm.getDefaultInstance()
val distincts = realm.where<CustomFieldEntry>().equalTo("customFields.id", this.customFieldId).findAll().sort("numericValue", Sort.ASCENDING)
realm.close()
val objects = mutableListOf<QueryCondition.CustomFieldNumberQuery>()
distincts.mapNotNull {
it.numericValue
}.distinct().forEach {value ->
val condition: QueryCondition.CustomFieldNumberQuery = when (this.customFieldType(realm)) {
CustomField.Type.AMOUNT.uniqueIdentifier -> QueryCondition.CustomFieldAmountQuery()
CustomField.Type.NUMBER.uniqueIdentifier -> QueryCondition.CustomFieldNumberQuery()
else -> throw PokerAnalyticsException.QueryValueMapUnexpectedValue
}.apply {
this.customFieldId = this@ListCriteria.customFieldId
listOfValues = arrayListOf(value)
}
objects.add(condition)
}
objects.sort()
return objects.map { Query(it) }
}
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 Stakes -> if (session.cgStakes is S) {
session.cgStakes as S
} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
else -> null
}
}.distinct()
return compareList<T, S>(values = values)
}
return listOf()
}
}
object Bankrolls : RealmCriteria(1)
object Games : RealmCriteria(2)
object TournamentNames : RealmCriteria(3)
object Locations : RealmCriteria(4)
object TournamentFeatures : RealmCriteria(5)
object TransactionTypes : RealmCriteria(6)
object Limits : ListCriteria(7)
object TableSizes : ListCriteria(8)
object TournamentTypes : ListCriteria(9)
object MonthsOfYear : SimpleCriteria(List(12) { index ->
QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(index) }
}, 10)
object DaysOfWeek : SimpleCriteria(List(7) { index ->
QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(index + 1) }
}, 11)
object SessionTypes : SimpleCriteria(listOf(QueryCondition.IsCash, QueryCondition.IsTournament), 12)
object BankrollTypes : SimpleCriteria(listOf(QueryCondition.IsLive, QueryCondition.IsOnline), 13)
object DayPeriods : SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd), 14)
object Years : ListCriteria(15)
object AllMonthsUpToNow : ListCriteria(16)
object Stakes : ListCriteria(17)
object TournamentFees : ListCriteria(18)
object Cash : SimpleCriteria(listOf(QueryCondition.IsCash), 19)
object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament), 20)
data class ListCustomFields(override var customFieldId: String) : RealmCriteria(21), CustomFieldCriteria
data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria
object Duration : ListCriteria(23)
val queries: List<Query>
get() {
return when (this) {
is AllMonthsUpToNow -> {
val realm = Realm.getDefaultInstance()
val firstSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.ASCENDING).findFirst()
val lastSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.DESCENDING).findFirst()
realm.close()
val years: ArrayList<Query> = arrayListOf()
val firstYear = firstSession?.year ?: return years
val firstMonth = firstSession.month ?: return years
val lastYear = lastSession?.year ?: return years
val lastMonth = lastSession.month ?: return years
for (year in firstYear..lastYear) {
val currentYear = QueryCondition.AnyYear(year)
for (month in 0..11) {
if (year == firstYear && month < firstMonth) {
continue
}
if (year == lastYear && month > lastMonth) {
continue
}
val currentMonth = QueryCondition.AnyMonthOfYear(month)
val query = Query(currentMonth, currentYear)
years.add(query)
}
}
years
}
else -> {
return this.queryConditions
}
}
}
val queryConditions: List<Query>
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 TransactionTypes -> comparison<TransactionType>()
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<Query>()
val realm = Realm.getDefaultInstance()
val lastSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.DESCENDING).findFirst()
val yearNow = lastSession?.year ?: return years
realm.where<Session>().isNotNull("startDate").sort("year", Sort.ASCENDING).findFirst()?.year?.let {
for (index in 0..(yearNow - it)) {
val yearCondition = QueryCondition.AnyYear().apply {
listOfValues = arrayListOf(it + index)
}
years.add(Query(yearCondition))
}
}
realm.close()
years
}
is Stakes -> comparison<QueryCondition.AnyStake, String>()
is ListCustomFields -> comparison<CustomFieldEntry>()
is ValueCustomFields -> {
val realm = Realm.getDefaultInstance()
val queries = when (this.customFieldType(realm)) {
CustomField.Type.AMOUNT.uniqueIdentifier -> comparison<QueryCondition.CustomFieldAmountQuery, Double >()
CustomField.Type.NUMBER.uniqueIdentifier -> comparison<QueryCondition.CustomFieldNumberQuery, Double >()
else -> throw PokerAnalyticsException.ComparisonCriteriaUnhandled(this)
}
realm.close()
queries
}
is Duration -> {
val hourlyQueries = (0..12).map { i ->
val more = QueryCondition.Duration(i * 60)
more.operator = QueryCondition.Operator.MORE_OR_EQUAL
val less = QueryCondition.Duration((i + 1) * 60)
less.operator = QueryCondition.Operator.LESS
Query(more, less)
}.toMutableList()
val moreThan12Hours = QueryCondition.Duration(12 * 60)
moreThan12Hours.operator = QueryCondition.Operator.MORE_OR_EQUAL
hourlyQueries.add(Query(moreThan12Hours))
hourlyQueries
}
else -> throw PokerAnalyticsException.ComparisonCriteriaUnhandled(this)
}
}
override val resId: Int?
get() {
return when (this) {
Bankrolls -> R.string.bankroll
Games -> R.string.game
TournamentNames -> R.string.tournament_name
Locations -> R.string.location
TournamentFeatures -> R.string.tournament_feature
Limits -> R.string.limit
TableSizes -> R.string.table_size
TournamentTypes -> R.string.tournament_type
MonthsOfYear -> R.string.month_of_the_year
DaysOfWeek -> R.string.day_of_the_week
SessionTypes -> R.string.cash_or_tournament
BankrollTypes -> R.string.live_or_online
DayPeriods -> R.string.weekdays_or_weekend
Years -> R.string.year
AllMonthsUpToNow -> R.string.month
Stakes -> R.string.blind
TournamentFees -> R.string.entry_fees
// is ListCustomFields -> this.customField.resId
// is ValueCustomFields -> this.customField.resId
else -> null
}
}
companion object : IntSearchable<Criteria> {
inline fun <reified S : QueryCondition.QueryDataCondition<NameManageable>, reified T : NameManageable> compare(): List<Query> {
val objects = mutableListOf<S>()
val realm = Realm.getDefaultInstance()
realm.where<T>().sort("name").findAll().forEach {
val condition = (QueryCondition.getInstance<T>() as S).apply {
setObject(it)
}
objects.add(condition)
}
// objects.sort()
realm.close()
return objects.map { Query(it) }
}
inline fun <reified S : QueryCondition.ListOfValues<T>, T : Any> compareList(values: List<T>): List<Query> {
val objects = mutableListOf<S>()
values.forEach {
val condition = (S::class.java.newInstance()).apply {
listOfValues = arrayListOf(it)
}
objects.add(condition)
}
objects.sort()
return objects.map { Query(it) }
}
// SavableEnum
override fun valuesInternal(): Array<Criteria> {
return all.toTypedArray()
}
val all: List<Criteria>
get() {
return listOf(
Bankrolls, Games, TournamentNames, Locations,
TournamentFeatures, Limits, TableSizes, TournamentTypes,
MonthsOfYear, DaysOfWeek, SessionTypes,
BankrollTypes, DayPeriods, Years,
AllMonthsUpToNow,
Stakes, TournamentFees
)
}
}
}
interface CustomFieldCriteria {
var customFieldId: String
fun customField(realm: Realm) : CustomField {
return realm.findById(this.customFieldId) ?: throw PAIllegalStateException("Custom field not found")
}
fun customFieldType(realm: Realm): Int {
return this.customField(realm).type
}
}

@ -1,54 +0,0 @@
package net.pokeranalytics.android.model
import android.content.Context
import net.pokeranalytics.android.ui.view.RowRepresentable
enum class Limit : RowRepresentable {
NO,
POT,
FIXED,
SPREAD,
MIXED;
companion object {
fun getInstance(value: String) : Limit? {
return when (value) {
"NL", "No Limit" -> NO
"PL", "Pot Limit" -> POT
"FL", "Fixed Limit", "Limit" -> FIXED
"ML", "Mixed Limit" -> MIXED
"SL", "Spread Limit" -> SPREAD
else -> null
}
}
}
val shortName: String
get() {
return when (this) {
NO -> "NL"
POT -> "PL"
FIXED -> "FL"
MIXED -> "ML"
SPREAD -> "SL"
}
}
val longName: String
get() {
return when (this) {
NO -> "No Limit"
POT -> "Pot Limit"
FIXED -> "Limit"
MIXED -> "Mixed Limit"
SPREAD -> "Spread Limit"
}
}
override fun getDisplayName(context: Context): String {
return this.longName
}
}

@ -1,17 +1,23 @@
package net.pokeranalytics.android.model
import android.content.Context
import androidx.fragment.app.Fragment
import io.realm.Realm
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.Sort
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity
import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity
import net.pokeranalytics.android.ui.adapter.components.LiveDataDataSource
import net.pokeranalytics.android.ui.view.Localizable
import net.pokeranalytics.android.util.extensions.findById
/**
* An interface to easily handle the validity of any object we want to save
*/
interface ObjectSavable {
fun uniqueIdentifier(): String
fun isValidForSave(): Boolean {
return true
}
}
/**
* An enum managing the business objects related to a realm results
@ -20,53 +26,72 @@ enum class LiveData : Localizable {
BANKROLL,
GAME,
LOCATION,
TOURNAMENT_NAME,
TOURNAMENT_FEATURE,
TRANSACTION,
TRANSACTION_TYPE,
FILTER,
CUSTOM_FIELD,
REPORT_SETUP,
PLAYER,
HAND_HISTORY;
TRANSACTION_TYPE;
fun items(realm: Realm, fieldName: String? = null, sortOrder: Sort? = null): RealmResults<*> {
return realm.where(this.relatedEntity).findAll()
.sort(fieldName ?: this.sortingFieldName, sortOrder ?: this.sorting)
}
var subType:Int? = null
/**
* Return a copy of a RealmResults
*/
fun itemsArray(realm: Realm, fieldName: String? = null, sortOrder: Sort? = null): ArrayList<*> {
val results: ArrayList<Any> = ArrayList()
results.addAll(
realm.copyFromRealm(
realm.where(this.relatedEntity).findAll().sort(
fieldName ?: this.sortingFieldName, sortOrder ?: this.sorting
)
)
)
return results
}
private var sortingFieldName: String = "name"
private var sorting: Sort = Sort.ASCENDING
val relatedEntity: Class<out Deletable>
private val relatedEntity: Class<out RealmObject>
get() {
return when (this) {
BANKROLL -> Bankroll::class.java
GAME -> Game::class.java
LOCATION -> Location::class.java
TOURNAMENT_NAME -> TournamentName::class.java
TOURNAMENT_FEATURE -> TournamentFeature::class.java
TRANSACTION -> Transaction::class.java
TRANSACTION_TYPE -> TransactionType::class.java
FILTER -> Filter::class.java
CUSTOM_FIELD -> CustomField::class.java
REPORT_SETUP -> ReportSetup::class.java
PLAYER -> Player::class.java
HAND_HISTORY -> HandHistory::class.java
}
}
fun updateOrCreate(realm: Realm, primaryKey: String?): Deletable {
val proxyItem: Deletable? = this.getData(realm, primaryKey)
fun deleteData(realm:Realm, data:ObjectSavable) {
realm.where(this.relatedEntity).equalTo("id", data.uniqueIdentifier()).findAll().deleteAllFromRealm()
}
fun updateOrCreate(realm:Realm, primaryKey:String?): RealmObject {
val proxyItem: RealmObject? = this.getData(realm, primaryKey)
proxyItem?.let {
return realm.copyFromRealm(it)
return proxyItem
} ?: run {
return this.newEntity()
}
}
private fun newEntity(): Deletable {
return this.relatedEntity.newInstance()
fun newEntity(): RealmObject {
return when (this) {
BANKROLL -> Bankroll()
GAME -> Game()
LOCATION -> Location()
TOURNAMENT_FEATURE -> TournamentFeature()
TRANSACTION_TYPE -> TransactionType()
}
}
fun getData(realm: Realm, primaryKey: String?): Deletable? {
var proxyItem: Deletable? = null
fun getData(realm: Realm, primaryKey: String?): RealmObject? {
var proxyItem: RealmObject? = null
primaryKey?.let {
val t = realm.findById(this.relatedEntity, it)
val t = realm.where(this.relatedEntity).equalTo("id", it).findFirst()
t?.let {
proxyItem = t
}
@ -80,110 +105,17 @@ enum class LiveData : Localizable {
BANKROLL -> R.string.bankroll
GAME -> R.string.game
LOCATION -> R.string.location
TOURNAMENT_NAME -> R.string.tournament_name
TOURNAMENT_FEATURE -> R.string.tournament_feature
TRANSACTION -> R.string.operation
TRANSACTION_TYPE -> R.string.operation_type
FILTER -> R.string.filter
CUSTOM_FIELD -> R.string.custom_field
REPORT_SETUP -> R.string.custom
PLAYER -> R.string.player
HAND_HISTORY -> R.string.hand_history
}
}
val pluralResId: Int
get() {
return when (this) {
BANKROLL -> R.string.bankrolls
GAME -> R.string.games
LOCATION -> R.string.locations
TOURNAMENT_NAME -> R.string.tournament_names
TOURNAMENT_FEATURE -> R.string.tournament_features
TRANSACTION -> R.string.operations
TOURNAMENT_FEATURE -> R.string.tournament_type
TRANSACTION_TYPE -> R.string.operation_types
FILTER -> R.string.filters
CUSTOM_FIELD -> R.string.custom_fields
REPORT_SETUP -> R.string.custom
PLAYER -> R.string.players
HAND_HISTORY -> R.string.hands_history
}
}
private val newResId: Int
get() {
return when (this) {
BANKROLL -> R.string.new_bankroll
GAME -> R.string.new_variant
LOCATION -> R.string.new_location
TOURNAMENT_NAME -> R.string.new_tournament_name
TOURNAMENT_FEATURE -> R.string.new_tournament_feature
TRANSACTION -> R.string.new_operation
TRANSACTION_TYPE -> R.string.new_operation_type
FILTER -> R.string.new_filter
CUSTOM_FIELD -> R.string.new_custom_field
REPORT_SETUP -> R.string.new_report
PLAYER -> R.string.new_friend
HAND_HISTORY -> R.string.new_hand
}
}
val isSearchable: Boolean
get() {
return when (this) {
PLAYER, LOCATION -> true
else -> false
}
}
/**
* Return the new entity titleResId
*/
fun newEntityLocalizedTitle(context: Context): String {
return context.getString(this.newResId)
}
/**
* Return the update entity titleResId
*/
fun updateEntityLocalizedTitle(context: Context): String {
return context.getString(R.string.update_entity, this.localizedTitle(context).toLowerCase())
}
/**
* Return the update entity titleResId
*/
fun pluralLocalizedTitle(context: Context): String {
return context.getString(this.pluralResId, context)
}
fun openEditActivity(fragment: Fragment, primaryKey: String? = null, requestCode: Int) {
when (this) {
HAND_HISTORY -> {
HandHistoryActivity.newInstance(fragment, primaryKey)
}
else -> {
EditableDataActivity.newInstanceForResult(fragment, this, primaryKey, requestCode)
}
}
}
val sortFields: Array<String>
get() {
return when (this) {
TRANSACTION, HAND_HISTORY -> arrayOf("date")
FILTER -> arrayOf("useCount", "name")
else -> arrayOf("name")
}
}
val sortOrders: Array<Sort>
get() {
return when (this) {
TRANSACTION, HAND_HISTORY -> arrayOf(Sort.DESCENDING)
FILTER -> arrayOf(Sort.DESCENDING, Sort.ASCENDING)
else -> arrayOf(Sort.ASCENDING)
}
}
}
/*
interface ListableDataSource {
fun items(realm: Realm, fieldName: String? = null, sortOrder: Sort? = null): RealmResults<*>
var sortingFieldName: String
var sorting: Sort
var relatedEntity: Class < out RealmObject >
}
*/

@ -1,38 +0,0 @@
package net.pokeranalytics.android.model
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.PerformanceKey
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
enum class LiveOnline(override var uniqueIdentifier: Int) : PerformanceKey, IntIdentifiable {
LIVE(0),
ONLINE(1);
companion object : IntSearchable<LiveOnline> {
override fun valuesInternal(): Array<LiveOnline> {
return values()
}
}
override val resId: Int?
get() {
return when (this) {
LIVE -> R.string.live
ONLINE -> R.string.online
}
}
override val value: Int
get() {
return this.uniqueIdentifier
}
val isLive: Boolean
get() {
return (this == LIVE)
}
}

@ -1,13 +0,0 @@
package net.pokeranalytics.android.model
data class Stakes(var blinds: String?, var ante: Double?) {
companion object {
}
}

@ -1,77 +0,0 @@
package net.pokeranalytics.android.model
import android.content.Context
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.Parser
class TableSize(
var numberOfPlayer: Int,
var rowViewType: Int = RowViewType.TITLE_GRID.ordinal,
var alternativeLabels: Boolean = false
) : RowRepresentable {
companion object {
fun all(alternativeLabels: Boolean): List<TableSize> {
return Array(9, init = { index ->
TableSize(index + 2, alternativeLabels = alternativeLabels)
}).toList()
}
fun valueForLabel(label: String) : Int? {
Parser.parseNumber(label)?.let {
return it.toInt()
}
return when (label) {
"Full Ring", "Full-Ring" -> 10
"Short-Handed", "Short Handed" -> 6
"Heads-Up", "Heads Up" -> 2
else -> null
}
}
}
override fun getDisplayName(context: Context): String {
if (this.alternativeLabels) {
return this.numberOfPlayer.toString()
}
return if (this.numberOfPlayer == 2) {
return "HU"
} else {
"${this.numberOfPlayer}-max"
}
}
override val resId: Int?
get() {
return if (this.numberOfPlayer == 2) {
R.string.heads_up
} else {
R.string.max
}
}
override fun localizedTitle(context: Context): String {
if (this.alternativeLabels) {
return this.numberOfPlayer.toString()
}
this.resId?.let {
return if (this.numberOfPlayer == 2) {
context.getString(it)
} else {
"${this.numberOfPlayer}-${context.getString(it)}"
}
}
return super.localizedTitle(context)
}
override val viewType: Int
get() = rowViewType
}

@ -1,43 +0,0 @@
package net.pokeranalytics.android.model
import android.content.Context
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
enum class TournamentType(val label: String) : RowRepresentable {
MTT("MTT"),
SNG("SNG");
companion object {
val all : List<TournamentType>
get() {
return values().toList()
}
fun getValueForLabel(label: String) : TournamentType? {
return when (label) {
SNG.label, "Single-Table" -> SNG
MTT.label, "Multi-Table" -> MTT
else -> null
}
}
}
override val resId: Int?
get() {
return when (this) {
MTT -> R.string.mtt
SNG -> R.string.sng
}
}
override fun getDisplayName(context: Context): String {
return when (this) {
MTT -> "MTT"
SNG -> "SNG"
}
}
override val viewType: Int = RowViewType.TITLE.ordinal
}

@ -1,13 +0,0 @@
package net.pokeranalytics.android.model.blogpost
import com.google.gson.annotations.SerializedName
class BlogPost {
@SerializedName("id")
var id: Int = 0
@SerializedName("level")
var content: String = ""
}

@ -1,32 +1,14 @@
package net.pokeranalytics.android.model.extensions
import android.content.Context
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.util.NotificationSchedule
import net.pokeranalytics.android.util.extensions.toCurrency
import java.util.*
import java.util.concurrent.TimeUnit
enum class SessionState {
PENDING,
PLANNED,
STARTED,
PAUSED,
FINISHED;
val hasStarted: Boolean
get() {
return when (this) {
STARTED, PAUSED, FINISHED -> true
else -> false
}
}
FINISHED,
INVALID;
}
/**
@ -35,146 +17,24 @@ enum class SessionState {
*/
fun Session.getState(): SessionState {
val start = this.startDate
return if (start == null) {
SessionState.PENDING
} else {
if (start > Date()) {
SessionState.PLANNED
} else if (this.endDate != null) {
SessionState.FINISHED
} else if (this.pauseDate != null) {
SessionState.PAUSED
} else {
SessionState.STARTED
}
}
}
/**
* Format the session game type
*/
fun Session.getFormattedGameType(context: Context): String {
val parameters = mutableListOf<String>()
if (isTournament()) {
tournamentEntryFee?.let {
parameters.add(it.toCurrency(currency))
}
tournamentName?.let {
parameters.add(it.name)
} ?: run {
parameters.add(getFormattedGame())
tournamentType?.let { type ->
parameters.add(TournamentType.values()[type].localizedTitle(context))
}
}
if (parameters.size == 0) {
parameters.add(context.getString(R.string.tournament).capitalize())
}
} else {
if (this.cgAnte != null || this.cgBlinds != null) {
parameters.add(getFormattedStakes())
}
game?.let {
parameters.add(getFormattedGame())
}
if (parameters.size == 0) {
parameters.add(context.getString(R.string.cash_game).capitalize())
}
}
return parameters.joinToString(separator = " ")
}
fun Session.currentDuration(): Long {
return this.startDate?.let {
this.endDate().time - it.time
} ?: 0L
}
fun Session.cancelStopNotification(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(this.id)
}
fun Session.scheduleStopNotification(context: Context, optimalDuration: Long) {
val timeDelay = optimalDuration - this.currentDuration()
if (timeDelay <= 0) {
return
}
val title = context.getString(R.string.stop_notification_title)
val body = context.getString(R.string.stop_notification_body)
val data = Data.Builder()
.putString(NotificationSchedule.ParamKeys.TITLE.value, title)
.putString(NotificationSchedule.ParamKeys.BODY.value, body)
val work = OneTimeWorkRequestBuilder<NotificationSchedule>()
.setInitialDelay(timeDelay, TimeUnit.MILLISECONDS)
.setInputData(data.build())
.addTag(this.id)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(this.id, ExistingWorkPolicy.REPLACE, work)
}
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.sumOf { 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 (timeFrame == null) {
return SessionState.PENDING
}
if (overlapped.isEmpty()) { // 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
val endDate = timeFrame?.endDate
timeFrame?.let {sessionTimeFrame ->
timeFrame?.startDate?.let {startDate ->
if (startDate > Date()) {
return SessionState.PENDING
} else if (endDate != null) {
return SessionState.FINISHED
} else if (sessionTimeFrame.paused) {
return SessionState.PAUSED
} else {
return SessionState.STARTED
}
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
return SessionState.INVALID
}

@ -1,103 +0,0 @@
package net.pokeranalytics.android.model.filter
import io.realm.RealmModel
import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.util.CrashLogging
/**
* We want to be able to store filters in the database:
* - filters can be a combination of sub filters
* - 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 queryWith is described by the following:
* - a data type: Session, Hands...
* - a field: table size of a Session
* - an operator: equal, >=, <...
* - a value: an id, a number, a date...
*
* We can decide to have a collection of [operator, value] for a field
*
* Combination:
* - multiple datatype filters should be handled as 'OR'
* - multiple field filters should be handled as 'AND'
* - multiple '=' filters as 'OR'
* - multiple 'Greater than', 'less than' as 'AND'
* - multiple numericValues as 'OR'
*
* Also:
* A queryWith should be able to be converted into a Realm query
*
*/
class UnmanagedFilterField(message: String) : Exception(message) {
}
/**
* Companion-level Interface to indicate an RealmObject class can be filtered and to provide all the fieldNames (eg: parameter's path) needed to be query on.
*/
interface Filterable : RealmModel {
/**
* return the path of the parameter used in the [QueryCondition] related to this entity
*/
// fun fieldNameForQueryType(queryCondition: QueryCondition) : String?
}
inline fun <reified T : Filterable> RealmResults<T>.filter(query: Query) : RealmResults<T> {
return query.queryWith(this.where()).findAll()
}
class FilterHelper {
companion object {
inline fun <reified T : Filterable > fieldNameForQueryType(queryCondition: Class< out QueryCondition>): String? {
return 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 -> {
CrashLogging.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}"))
null
// 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>.queryWith(queryWith: FilterCondition) : List<Filterable> {
//
// return this.queryWith { f ->
// return@queryWith true
// }
//}
// doesnt compile: Class "FilterableRealmObject" must contain at least 1 persistable field.
//class FilterableRealmObject : RealmObject(), Filterable {
//
//}

@ -1,128 +0,0 @@
package net.pokeranalytics.android.model.filter
import android.content.Context
import io.realm.RealmQuery
import net.pokeranalytics.android.R
import net.pokeranalytics.android.util.NULL_TEXT
fun List<Query>.mapFirstCondition() : List<QueryCondition> {
return this.map { it.conditions.first() }
}
class Query {
constructor(query: Query) {
this._conditions.addAll(query.conditions)
}
constructor(vararg elements: QueryCondition?) {
if (elements.isNotEmpty()) {
this.add(elements.filterNotNull())
}
}
private val _conditions: MutableList<QueryCondition> = mutableListOf()
val conditions: List<QueryCondition>
get() {
return this._conditions
}
fun add(vararg elements: QueryCondition): Query {
if (elements.isNotEmpty()) {
this.add(elements.asList())
}
return this
}
fun add(queryCondition: QueryCondition): Query {
this._conditions.add(queryCondition)
return this
}
fun add(queryConditions: List<QueryCondition>): Query {
this._conditions.addAll(queryConditions)
return this
}
fun remove(queryCondition: QueryCondition) {
this._conditions.remove(queryCondition)
}
val defaultName: String
get() {
return when (this._conditions.size) {
0 -> NULL_TEXT
else -> this._conditions.joinToString("") { it.id.joinToString("") }
}
}
fun getName(context: Context, separator: String = " + "): String {
return when (this._conditions.size) {
0 -> context.getString(R.string.all_sessions) // @todo should be dependant of the underlying type, ie. Session, Transaction...
else -> this._conditions.joinToString(separator) { it.getDisplayNameWithValues(context) }
}
}
inline fun <reified T : Filterable> queryWith(query: RealmQuery<T>): RealmQuery<T> {
var realmQuery = query
val queryFromTime = this.conditions.firstOrNull {
it is QueryCondition.StartedFromTime
}
val queryToTime = this.conditions.firstOrNull {
it is QueryCondition.EndedToTime
}
this.conditions.forEach {
realmQuery = when (it) {
is QueryCondition.StartedFromTime -> {
it.queryWith(realmQuery, queryToTime)
}
is QueryCondition.EndedToTime -> {
it.queryWith(realmQuery, queryFromTime)
}
else -> {
it.queryWith(realmQuery)
}
}
}
// println("<<<<<< ${realmQuery.description}")
// val queryLast = this.conditions.firstOrNull {
// it is QueryCondition.Last
// }
// queryLast?.let {qc ->
// (qc as QueryCondition.Last).singleValue?.let {
// return realmQuery.limit(it.toLong())
// }
// }
return realmQuery
}
fun merge(query: Query) : Query {
this.add(query.conditions)
return this
}
fun copy(): Query {
return Query(this)
}
/*
Returns the first object Id of any QueryCondition
*/
val objectId: String?
get() {
for (c in this._conditions) {
when (c) {
is QueryCondition.QueryDataCondition<*> -> {
c.objectId?.let { return it }
}
else -> {}
}
}
return null
}
}

@ -1,108 +0,0 @@
package net.pokeranalytics.android.model.handhistory
import io.realm.Realm
import net.pokeranalytics.android.model.realm.Game
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.util.extensions.findById
import timber.log.Timber
class HandSetup {
companion object {
/***
* Returns a HandSetup instance using a [configurationId], the id of a Realm object,
* Session or HandHistory, used to later configure the HandHistory
*/
fun from(configurationId: String?, attached: Boolean, realm: Realm): HandSetup {
return if (configurationId != null) {
val handSetup = HandSetup()
val hh = realm.findById(HandHistory::class.java, configurationId)
if (hh != null) {
handSetup.configure(hh)
}
val session = realm.findById(Session::class.java, configurationId)
if (session != null) {
handSetup.configure(session, attached)
}
handSetup
} else {
HandSetup()
}
}
}
var type: Session.Type? = null
var blinds: String? = null
// var bigBlind: Double? = null
var ante: Double? = null
var tableSize: Int? = null
var bigBlindAnte: Boolean = false
var game: Game? = null
var session: Session? = null
var straddlePositions: MutableList<Position> = mutableListOf()
private set
fun clearStraddles() {
this.straddlePositions.clear()
}
private fun configure(handHistory: HandHistory) {
this.blinds = handHistory.blinds
this.bigBlindAnte = handHistory.bigBlindAnte
this.ante = handHistory.ante
this.tableSize = handHistory.numberOfPlayers
}
/***
* Configures the Hand Setup with a [session]
* [attached] denotes if the HandHistory must be directly linked to the session
*/
private fun configure(session: Session, attached: Boolean) {
if (attached) {
this.session = session
}
if (session.endDate == null) {
this.game = session.game // we don't want to force the max number of cards if unsure
}
this.type = session.sessionType
this.blinds = session.cgBlinds
this.ante = session.cgAnte
this.tableSize = session.tableSize
val blindValues = session.blindValues
if (blindValues.size > 2) {
this.straddlePositions = Position.positionsPerPlayers(10).drop(2).take(blindValues.size - 2).toMutableList()
}
}
/***
* This method sorts the straddle positions in their natural order
* If the straddle position contains the button, we're usually in a Mississipi straddle,
* meaning the BUT straddles, then CO, then HJ...
* Except if it goes to UTG, in which case we don't know if we're in standard straddle, or Mississipi
* We use the first straddled position to sort out this case
*/
fun setStraddlePositions(firstStraddlePosition: Position, positions: LinkedHashSet<Position>) {
var sortedPosition = positions.sortedBy { it.ordinal }
if (positions.contains(Position.BUT) && firstStraddlePosition != Position.UTG) {
sortedPosition = sortedPosition.reversed()
}
Timber.d("sortedPosition = $sortedPosition")
this.straddlePositions = sortedPosition.toMutableList()
Timber.d("this.straddlePositions = ${this.straddlePositions}")
}
}

@ -1,53 +0,0 @@
package net.pokeranalytics.android.model.handhistory
import android.content.Context
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.view.RowRepresentable
import java.util.*
enum class Position(var value: String) : RowRepresentable {
SB("SB"),
BB("BB"),
UTG("UTG"),
UTG1("UTG+1"),
UTG2("UTG+2"),
UTG3("UTG+3"),
MP("MP"),
HJ("HJ"),
CO("CO"),
BUT("BUT");
companion object {
fun positionsPerPlayers(playerCount: Int) : LinkedHashSet<Position> {
return when(playerCount) {
2 -> linkedSetOf(SB, BB)
3 -> linkedSetOf(SB, BB, BUT)
4 -> linkedSetOf(SB, BB, UTG, BUT)
5 -> linkedSetOf(SB, BB, UTG, CO, BUT)
6 -> linkedSetOf(SB, BB, UTG, HJ, CO, BUT)
7 -> linkedSetOf(SB, BB, UTG, MP, HJ, CO, BUT)
8 -> linkedSetOf(SB, BB, UTG, UTG1, MP, HJ, CO, BUT)
9 -> linkedSetOf(SB, BB, UTG, UTG1, UTG2, MP, HJ, CO, BUT)
10 -> linkedSetOf(SB, BB, UTG, UTG1, UTG2, UTG3, MP, HJ, CO, BUT)
else -> throw PAIllegalStateException("Unmanaged number of players")
}
}
}
val shortValue: String
get() {
return when (this) {
UTG1 -> "+1"
UTG2 -> "+2"
UTG3 -> "+3"
else -> this.value
}
}
override fun getDisplayName(context: Context): String {
return this.value
}
}

@ -1,50 +0,0 @@
package net.pokeranalytics.android.model.handhistory
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.modules.handhistory.replayer.HandStep
enum class Street : HandStep {
PREFLOP,
FLOP,
TURN,
RIVER,
SUMMARY;
override val street: Street = this
val totalBoardCards: Int
get() {
return when (this) {
PREFLOP -> 0
FLOP -> 3
TURN -> 4
RIVER, SUMMARY -> 5
}
}
val resId: Int
get() {
return when (this) {
PREFLOP -> R.string.street_preflop
FLOP -> R.string.street_flop
TURN -> R.string.street_turn
RIVER -> R.string.street_river
SUMMARY -> R.string.summary
}
}
val next: Street
get() {
return values()[this.ordinal + 1]
}
val previous: Street?
get() {
return when (this) {
PREFLOP -> null
else -> values()[this.ordinal - 1]
}
}
}

@ -1,22 +0,0 @@
package net.pokeranalytics.android.model.interfaces
import net.pokeranalytics.android.model.realm.Bankroll
import java.util.*
interface Dated {
var date: Date
}
interface DatedValue : Dated {
var amount: Double
}
interface DatedBankrollGraphEntry : DatedValue, GraphIdentifiableEntry {
var bankroll: Bankroll?
}

@ -1,129 +0,0 @@
package net.pokeranalytics.android.model.interfaces
import android.content.Context
import io.realm.Realm
import io.realm.RealmModel
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.ModelException
enum class SaveValidityStatus {
VALID,
ALREADY_EXISTS,
DATA_INVALID;
}
enum class DeleteValidityStatus {
VALID,
INVALID,
SESSIONS_LINKED,
TRANSACTIONS_LINKED;
}
/**
* An interface to group object which are managed by the database
*/
interface Manageable : Savable, Deletable
interface NameManageable : Manageable {
var name: String
override fun isValidForSave(): Boolean {
return this.name.isNotEmpty()
}
override fun alreadyExists(realm: Realm): Boolean {
return realm.where(this::class.java).equalTo("name", this.name).and().notEqualTo("id", this.id).findAll().isNotEmpty()
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
throw ModelException("${this::class.java} getFailedSaveMessage for $status not handled")
}
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
return R.string.relationship_error
}
}
class ObjectIdentifier(var id: String, var clazz: Class<out Identifiable>)
/**
* An interface associate a unique uniqueIdentifier to an object
*/
interface Identifiable : RealmModel {
/**
* A unique uniqueIdentifier getter
*/
var id: String
/**
* required because "this.class" returns the proxy class, making where<class> queries crash
*/
val realmObjectClass: Class<out Identifiable>
val objectIdentifier: ObjectIdentifier
get() {
return ObjectIdentifier(this.id, this.realmObjectClass)
}
}
/**
* An interface to easily handle the validity of any object we want to save
*/
interface Savable : Identifiable {
fun isValidForSave(): Boolean
fun alreadyExists(realm: Realm): Boolean
/**
* A method to define if an object is safe for saving in database
*/
fun getSaveValidityStatus(realm: Realm): SaveValidityStatus {
if (!isValidForSave()) {
return SaveValidityStatus.DATA_INVALID
}
if (alreadyExists(realm)) {
return SaveValidityStatus.ALREADY_EXISTS
}
return SaveValidityStatus.VALID
}
/**
* A method to get the reason why the object can't be saved
*/
fun getFailedSaveMessage(status: SaveValidityStatus): Int
}
/**
* An interface to easily handle the validity of any object we want to delete
*/
interface Deletable : Identifiable {
/**
* A method to define if an object is safe for deletion in database
*/
fun isValidForDelete(realm: Realm): Boolean
fun getDeleteStatus(context: Context, realm: Realm): DeleteValidityStatus {
if (!isValidForDelete(realm)) {
return DeleteValidityStatus.INVALID
}
return DeleteValidityStatus.VALID
}
/**
* A method to get the reason why the object can't be deleted
*/
fun getFailedDeleteMessage(status: DeleteValidityStatus): Int
/**
* A method to override if we need to delete linked objects or other stuff
*/
fun deleteDependencies(realm: Realm) {}
}

@ -1,189 +0,0 @@
package net.pokeranalytics.android.model.interfaces
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.util.BLIND_SEPARATOR
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.formatted
import net.pokeranalytics.android.util.extensions.toCurrency
import java.lang.Integer.min
import java.text.NumberFormat
import java.text.ParseException
import java.util.*
data class CodedStake(var stakes: String) : Comparable<CodedStake> {
var ante: Double? = null
var blinds: String? = null
var currency: Currency
init {
var currencyCode: String? = null
val parameters = this.stakes.split(StakesHolder.cbSeparator)
parameters.forEach { param ->
when {
param.contains(StakesHolder.cbAnte) -> ante = param.removePrefix(StakesHolder.cbAnte).let { NumberFormat.getInstance().parse(it)?.toDouble() }
param.contains(StakesHolder.cbBlinds) -> blinds = param.removePrefix(StakesHolder.cbBlinds)
param.contains(StakesHolder.cbCode) -> currencyCode = param.removePrefix(
StakesHolder.cbCode
)
}
}
this.currency = currencyCode?.let { Currency.getInstance(it) }
?: run { UserDefaults.currency }
}
override fun compareTo(other: CodedStake): Int {
if (this.currency == other.currency) {
this.blinds?.let { b1 ->
other.blinds?.let { b2 ->
if (b1 == b2) {
return this.compareAnte(other)
} else {
val bv1 = this.reversedBlindsArray(b1)
val bv2 = this.reversedBlindsArray(b2)
for (i in 0 until min(bv1.size, bv2.size)) {
if (bv1[i] != bv2[i]) {
return bv1[i].compareTo(bv2[i])
} else {
continue
}
}
return bv1.size.compareTo(bv2.size)
}
} ?: run {
return 1
}
} ?: run {
return this.compareAnte(other)
}
} else {
return this.currency.currencyCode.compareTo(other.currency.currencyCode)
}
}
private fun compareAnte(other: CodedStake): Int {
this.ante?.let { a1 ->
other.ante?.let { a2 ->
return a1.compareTo(a2)
} ?: run {
return 1
}
} ?: run {
return -1
}
}
private fun reversedBlindsArray(blinds: String): List<Double> {
return blinds.split(BLIND_SEPARATOR).mapNotNull { NumberFormat.getInstance().parse(it)?.toDouble() }.reversed()
}
fun formattedStakes(): String {
val components = arrayListOf<String>()
this.formattedBlinds()?.let { components.add(it) }
if ((this.ante ?: -1.0) > 0.0) {
this.formattedAnte()?.let { components.add("($it)") }
}
return if (components.isNotEmpty()) {
components.joinToString(" ")
} else {
NULL_TEXT
}
}
private fun formattedBlinds(): String? {
this.blinds?.let {
val placeholder = 1.0
val regex = Regex("-?\\d+(\\.\\d+)?")
return placeholder.toCurrency(currency).replace(regex, it)
}
return null
}
private fun formattedAnte(): String? {
this.ante?.let {
return it.toCurrency(this.currency)
}
return null
}
}
interface StakesHolder {
companion object {
const val cbSeparator = ";"
const val cbAnte = "A="
const val cbBlinds = "B="
const val cbCode = "C="
fun readableStakes(value: String): String {
return CodedStake(value).formattedStakes()
}
}
val ante: Double?
val blinds: String?
val biggestBet: Double?
val stakes: String?
val bankroll: Bankroll?
fun setHolderStakes(stakes: String?)
fun setHolderBiggestBet(biggestBet: Double?)
val blindValues: List<Double>
get() {
this.blinds?.let { blinds ->
val blindsSplit = blinds.split(BLIND_SEPARATOR)
return blindsSplit.mapNotNull {
try {
NumberFormat.getInstance().parse(it)?.toDouble()
} catch (e: ParseException) {
null
}
}
}
return listOf()
}
fun generateStakes() {
if (this.ante == null && this.blinds == null) {
setHolderStakes(null)
return
}
val components = arrayListOf<String>()
this.blinds?.let { components.add("${cbBlinds}${it}") }
this.ante?.let { components.add("${cbAnte}${it.formatted}") }
val code = this.bankroll?.currency?.code ?: UserDefaults.currency.currencyCode
components.add("${cbCode}${code}")
setHolderStakes(components.joinToString(cbSeparator))
}
fun defineHighestBet() {
val bets = arrayListOf<Double>()
this.ante?.let { bets.add(it) }
bets.addAll(this.blindValues)
setHolderBiggestBet(bets.maxOrNull())
}
}

@ -1,28 +0,0 @@
package net.pokeranalytics.android.model.interfaces
import java.util.*
/**
* Interface to let an object be filtered through specific time parameters
*/
interface TimeFilterable {
var dayOfWeek : Int?
var dayOfMonth : Int?
var month : Int?
var year : Int?
/**
* Call this method whenever the date of the object is modified
*/
fun updateTimeParameter(startDate: Date?) {
startDate?.let {
val cal = Calendar.getInstance()
cal.time = it
this.dayOfWeek = cal.get(Calendar.DAY_OF_WEEK)
this.dayOfMonth = cal.get(Calendar.DAY_OF_MONTH)
this.month = cal.get(Calendar.MONTH)
this.year = cal.get(Calendar.YEAR)
}
}
}

@ -1,34 +0,0 @@
package net.pokeranalytics.android.model.interfaces
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import java.util.*
interface GraphIdentifiableEntry : GraphUnderlyingEntry, Identifiable
interface Timed : GraphIdentifiableEntry {
fun startDate() : Date?
fun endDate() : Date
var breakDuration: Long
var pauseDate: Date?
var netDuration: Long
/**
* Computes the net netDuration of the session
*/
fun computeNetDuration() {
this.startDate()?.let { start ->
this.netDuration = this.endDate().time - start.time - this.breakDuration
} ?: run {
this.netDuration = 0L
}
}
val hourlyDuration: Double
get() = this.netDuration / 3600000.0
}

@ -1,14 +0,0 @@
package net.pokeranalytics.android.model.interfaces
import io.realm.RealmModel
/**
* An interface to be able to track the usage of an object
*/
interface UsageCountable : Identifiable {
var useCount: Int
get() { return 0 }
set(_) {}
val ownerClass: Class<out RealmModel>
}

@ -1,234 +0,0 @@
package net.pokeranalytics.android.model.migrations
import android.content.Context
import io.realm.Realm
import io.realm.kotlin.where
import net.pokeranalytics.android.PokerAnalyticsApplication
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.model.utils.Seed
import net.pokeranalytics.android.model.utils.SessionSetManager
import net.pokeranalytics.android.util.BLIND_SEPARATOR
import net.pokeranalytics.android.util.Preferences
import java.text.NumberFormat
class Patcher {
companion object {
fun patchAll(application: PokerAnalyticsApplication) {
val context = application.applicationContext
// NOTE: it's more than possible that at one point many patches become redundant
// with each other
patchMissingTransactionTypes(context)
Preferences.executeOnce(Preferences.Keys.PATCH_COMPUTABLE_RESULTS, context) {
patchComputableResults()
}
Preferences.executeOnce(Preferences.Keys.PATCH_SESSION_SETS, context) {
patchSessionSet()
}
Preferences.executeOnce(Preferences.Keys.PATCH_BREAK, context) {
patchBreaks()
}
Preferences.executeOnce(Preferences.Keys.PATCH_TRANSACTION_TYPES_NAMES, context) {
patchDefaultTransactionTypes(context)
}
Preferences.executeOnce(Preferences.Keys.PATCH_STAKES, context) {
patchStakes()
}
Preferences.executeOnce(Preferences.Keys.PATCH_NEGATIVE_LIMITS, context) {
patchNegativeLimits()
}
Preferences.executeOnce(Preferences.Keys.CLEAN_BLINDS_FILTERS, context) {
cleanBlindsFilters()
}
Preferences.executeOnce(Preferences.Keys.ADD_NEW_TRANSACTION_TYPES, context) {
patchMissingTransactionTypes(context)
}
Preferences.executeOnce(Preferences.Keys.PATCH_ZERO_TABLE, context) {
patchZeroTable()
}
Preferences.executeOnce(Preferences.Keys.PATCH_RATED_AMOUNT, context) {
patchRatedAmounts()
}
patchPerformances(application)
}
private fun patchMissingTransactionTypes(context: Context) {
val realm = Realm.getDefaultInstance()
val transactionTypes = TransactionType.Value.values()
realm.executeTransaction {
Seed.createDefaultTransactionTypes(transactionTypes, context, realm)
}
realm.close()
}
private fun patchBreaks() {
val realm = Realm.getDefaultInstance()
val sets = realm.where(SessionSet::class.java).findAll()
val sessions = Filter.queryOn<Session>(realm, Query(QueryCondition.IsCash))
val results = realm.where(Result::class.java).findAll()
realm.executeTransaction {
sets.forEach {
it.computeStats()
}
sessions.forEach {
it.generateStakes()
it.defineHighestBet()
}
results.forEach {
it.computeNumberOfRebuy()
}
}
realm.close()
}
private fun patchDefaultTransactionTypes(context: Context) {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val tts = realm.where(TransactionType::class.java).findAll()
tts.forEach { tt ->
tt.kind?.let { kind ->
val value = TransactionType.Value.values()[kind]
tt.name = value.localizedTitle(context)
}
}
}
realm.close()
}
private fun patchStakes() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val sessions = realm.where(Session::class.java).findAll()
sessions.forEach { session ->
val blinds = arrayListOf(session.cgOldSmallBlind, session.cgOldBigBlind).filterNotNull()
val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) }
session.cgAnte = null
if (blindsFormatted.isNotEmpty()) {
session.cgBlinds = blindsFormatted.joinToString(BLIND_SEPARATOR)
}
}
val handHistories = realm.where(HandHistory::class.java).findAll()
handHistories.forEach { hh ->
val blinds = arrayListOf(hh.oldSmallBlind, hh.oldBigBlind).filterNotNull()
val blindsFormatted = blinds.map { NumberFormat.getInstance().format(it) }
if (blindsFormatted.isNotEmpty()) {
hh.blinds = blindsFormatted.joinToString(BLIND_SEPARATOR)
}
}
}
realm.close()
}
private fun patchNegativeLimits() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val sessions = realm.where(Session::class.java).lessThan("limit", 0).findAll()
sessions.forEach { session ->
session.limit = null
}
}
realm.close()
}
private fun cleanBlindsFilters() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val blindFilterConditions = realm.where(FilterCondition::class.java).equalTo("filterName", "AnyBlind").findAll()
val filterIds = blindFilterConditions.mapNotNull { it.filters?.firstOrNull() }.map { it.id }
val filters = realm.where(Filter::class.java).`in`("id", filterIds.toTypedArray()).findAll()
filters.deleteAllFromRealm()
}
realm.close()
}
/*
02/09/19: A bug with the session set management made them kept instead of deleted,
thus making duration calculation wrong
*/
private fun patchSessionSet() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
realm.where(SessionSet::class.java).findAll().deleteAllFromRealm()
val sessions = realm.where(Session::class.java).isNotNull("startDate").isNotNull("endDate").findAll()
sessions.forEach { session ->
SessionSetManager.updateTimeline(session)
}
}
realm.close()
}
/*
15/04/21: Two needs:
- To get better performance for ITM Ratio, a positive session is a net >= 0 for cash game, or a cashedOut > 0 for tournaments
- The field "ratedTips" is added
*/
private fun patchComputableResults() {
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
val crs = realm.where(ComputableResult::class.java).findAll()
crs.forEach { cr ->
cr.session?.let { cr.updateWith(it) }
}
}
realm.close()
}
private fun patchPerformances(application: PokerAnalyticsApplication) {
val realm = Realm.getDefaultInstance()
val sessionCount = realm.where<Session>().findAll().size
val performanceCount = realm.where<Performance>().findAll().size
if (sessionCount > 1 && performanceCount == 0) {
application.reportWhistleBlower?.requestReportLaunch()
}
realm.close()
}
private fun patchZeroTable() {
val realm = Realm.getDefaultInstance()
val zero = 0
val sessions = realm.where<Session>().equalTo("numberOfTables", zero).findAll()
realm.executeTransaction {
sessions.forEach { s ->
s.numberOfTables = 1
}
}
realm.close()
}
private fun patchRatedAmounts() {
val realm = Realm.getDefaultInstance()
val transactions = realm.where<Transaction>().findAll()
realm.executeTransaction {
transactions.forEach { t ->
t.computeRatedAmount()
}
}
realm.close()
}
}
}

@ -1,348 +0,0 @@
package net.pokeranalytics.android.model.migrations
import io.realm.DynamicRealm
import io.realm.RealmMigration
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import timber.log.Timber
import java.util.*
class PokerAnalyticsMigration : RealmMigration {
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
// DynamicRealm exposes an editable schema
val schema = realm.schema
var currentVersion = oldVersion.toInt()
Timber.d("*** migrate from $oldVersion to $newVersion")
// Migrate to version 1
if (currentVersion == 0) {
Timber.d("*** Running migration 1")
schema.get("Filter")?.addField("entityType", Int::class.java)
?.setNullable("entityType", true)
schema.get("FilterElement")?.let {
it.setNullable("filterName", true)
it.setNullable("sectionName", true)
}
schema.get("FilterElementBlind")?.renameField("code", "currencyCode")
currentVersion++
}
// Migrate to version 2
if (currentVersion == 1) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.rename("FilterElement", "FilterCondition")
schema.get("Filter")?.renameField("filterElements", "filterConditions")
schema.get("SessionSet")?.let {
it.addField("id", String::class.java).setRequired("id", true)
it.addPrimaryKey("id")
}
currentVersion++
}
// Migrate to version 3
if (currentVersion == 2) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.rename("Report", "ReportSetup")
schema.get("Filter")?.removeField("entityType")
schema.get("Session")?.let {
it.addField("blinds", String::class.java).transform {
}
}
schema.get("FilterCondition")?.let {
it.removeField("blindValues")
it.removeField("numericValues")
it.addField("operator", Integer::class.java)
it.addField("intValue", Integer::class.java)
it.addRealmListField("intValues", Integer::class.java)
it.addField("doubleValue", Double::class.java).setNullable("doubleValue", true)
it.addRealmListField("doubleValues", Double::class.java)
if (it.isRequired("doubleValues")) {
it.setRequired("doubleValues", false)
}
it.addField("stringValue", String::class.java)
}
schema.get("ComputableResult")?.removeField("sessionSet")
schema.get("Bankroll")?.addField("initialValue", Double::class.java)
currentVersion++
}
// Migrate to version 4
if (currentVersion == 3) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Result")?.addField("numberOfRebuy", Double::class.java)
?.setNullable("numberOfRebuy", true)
currentVersion++
}
// Migrate to version 5
if (currentVersion == 4) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Bankroll")?.removeField("transactions")
currentVersion++
}
// Migrate to version 6
if (currentVersion == 5) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Transaction")?.let {
it.addField("dayOfWeek", Integer::class.java)
it.addField("month", Integer::class.java)
it.addField("year", Integer::class.java)
it.addField("dayOfMonth", Integer::class.java)
}
val cfEntry = schema.create("CustomFieldEntry")?.let {
it.addField("id", String::class.java).setRequired("id", true)
it.addPrimaryKey("id")
it.addField("value", String::class.java).setNullable("value", false)
it.addField("order", Integer::class.java).setNullable("order", false)
// it.addRealmObjectField("customField", it).setNullable("customField", false)
it.addField("numericValue", Double::class.java).setNullable("numericValue", true)
}
cfEntry?.let { customFieldEntrySchema ->
schema.get("CustomField")?.let {
it.addField("type", Integer::class.java).setNullable("type", false)
it.addField("duplicateValue", Boolean::class.java)
it.addField("sortCondition", Integer::class.java)
.setRequired("sortCondition", true)
it.addRealmListField("entries", customFieldEntrySchema)
}
schema.get("Session")?.let {
it.addField("startDateHourMinuteComponent", Double::class.java)
.setNullable("startDateHourMinuteComponent", true)
it.addField("endDateHourMinuteComponent", Double::class.java)
.setNullable("endDateHourMinuteComponent", true)
it.addRealmListField("customFieldEntries", customFieldEntrySchema)
}
}
schema.get("ReportSetup")?.let {
it.addRealmListField("statIds", Int::class.java).setNullable("statIds", true)
it.addRealmListField("criteriaCustomFieldIds", String::class.java)
it.addRealmListField("criteriaIds", Int::class.java)
.setNullable("criteriaIds", true)
it.removeField("filters")
schema.get("Filter")?.let { filterSchema ->
it.addRealmObjectField("filter", filterSchema)
}
}
schema.get("Filter")?.addField("filterableTypeUniqueIdentifier", Integer::class.java)
schema.get("Filter")?.addField("useCount", Int::class.java)
schema.get("Filter")?.removeField("usageCount")
currentVersion++
}
// Migrate to version 7
if (currentVersion == 6) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("TransactionType")?.addField("useCount", Int::class.java)
currentVersion++
}
// Migrate to version 8
if (currentVersion == 7) {
schema.create("Comment")?.let { commentSchema ->
commentSchema.addField("id", String::class.java).setRequired("id", true)
commentSchema.addPrimaryKey("id")
commentSchema.addField("content", String::class.java).setRequired("content", true)
commentSchema.addField("date", Date::class.java).setRequired("date", true)
schema.get("Player")?.let {
it.addField("summary", String::class.java).setRequired("summary", true)
it.addField("color", Int::class.java).setNullable("color", true)
it.addField("picture", String::class.java)
it.addRealmListField("comments", commentSchema)
}
}
currentVersion++
}
// Migrate to version 9
if (currentVersion == 8) {
schema.get("HandHistory")?.let { hhSchema ->
schema.get("Session")?.let { sessionSchema ->
sessionSchema.removeField("hands")
hhSchema.addRealmObjectField("session", sessionSchema)
} ?: throw PAIllegalStateException("Session schema not found")
hhSchema.addField("smallBlind", Double::class.java).setRequired("smallBlind", false)
hhSchema.addField("bigBlind", Double::class.java).setRequired("bigBlind", false)
hhSchema.addField("ante", Double::class.java)
hhSchema.addField("bigBlindAnte", Boolean::class.java)
hhSchema.addField("numberOfPlayers", Int::class.java)
hhSchema.addField("comment", String::class.java)
hhSchema.addField("heroIndex", Int::class.java).setRequired("heroIndex", false)
hhSchema.addField("dayOfWeek", Integer::class.java)
hhSchema.addField("month", Integer::class.java)
hhSchema.addField("year", Integer::class.java)
hhSchema.addField("dayOfMonth", Integer::class.java)
val cardSchema = schema.create("Card")
cardSchema.addField("value", Int::class.java).setRequired("value", false)
cardSchema.addField("suitIdentifier", Int::class.java)
.setRequired("suitIdentifier", false)
cardSchema.addField("index", Int::class.java)
hhSchema.addRealmListField("board", cardSchema)
val actionSchema = schema.create("Action")
actionSchema.addField("streetIdentifier", Int::class.java)
actionSchema.addField("index", Int::class.java)
actionSchema.addField("position", Int::class.java)
actionSchema.addField("typeIdentifier", Int::class.java)
.setRequired("typeIdentifier", false)
actionSchema.addField("amount", Double::class.java).setRequired("amount", false)
actionSchema.addField("effectiveAmount", Double::class.java)
actionSchema.addField("positionRemainingStack", Double::class.java)
.setRequired("positionRemainingStack", false)
hhSchema.addRealmListField("actions", actionSchema)
val playerSetupSchema = schema.create("PlayerSetup")
schema.get("Player")?.let {
playerSetupSchema.addRealmObjectField("player", it)
} ?: throw PAIllegalStateException("Session schema not found")
playerSetupSchema.addField("position", Int::class.java)
playerSetupSchema.addField("stack", Double::class.java).setRequired("stack", false)
playerSetupSchema.addRealmListField("cards", cardSchema)
hhSchema.addRealmListField("playerSetups", playerSetupSchema)
val wonPotSchema = schema.create("WonPot")
wonPotSchema.addField("position", Int::class.java)
wonPotSchema.addField("amount", Double::class.java)
hhSchema.addRealmListField("winnerPots", wonPotSchema)
}
currentVersion++
}
// Migrate to version 10
if (currentVersion == 9) {
schema.get("Session")?.addField("handsCount", Int::class.java)
?.setRequired("handsCount", false)
schema.get("Session")?.transform { obj -> // otherwise we get a 0 default value
obj.setNull("handsCount")
}
val configSchema = schema.create("UserConfig")
configSchema.addField("id", String::class.java).setRequired("id", true)
configSchema.addPrimaryKey("id")
configSchema.addField("liveDealtHandsPerHour", Int::class.java)
configSchema.addField("onlineDealtHandsPerHour", Int::class.java)
currentVersion++
}
// Migrate to version 11
if (currentVersion == 10) {
schema.get("ComputableResult")?.addField("ratedTips", Double::class.java)
currentVersion++
}
// Migrate to version 12
if (currentVersion == 11) {
schema.get("Session")?.let { ss ->
ss.addField("cgAnte", Double::class.java)
?.setNullable("cgAnte", true)
ss.addField("cgBiggestBet", Double::class.java)
?.setNullable("cgBiggestBet", true)
ss.addField("cgStakes", String::class.java)
ss.addField("cgBlinds", String::class.java)
ss.removeField("blinds")
ss.renameField("cgSmallBlind", "cgOldSmallBlind")
ss.renameField("cgBigBlind", "cgOldBigBlind")
}
schema.get("HandHistory")?.let { hs ->
hs.setNullable("ante", true)
hs.addField("stakes", String::class.java)
hs.addField("blinds", String::class.java)
hs.addField("biggestBet", Double::class.java)
?.setNullable("biggestBet", true)
hs.renameField("smallBlind", "oldSmallBlind")
hs.renameField("bigBlind", "oldBigBlind")
}
currentVersion++
}
// Migrate to version 13
if (currentVersion == 12) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Transaction")?.let { ts ->
ts.addField("transferRate", Double::class.java)
.setNullable("transferRate", true)
schema.get("Bankroll")?.let { bs ->
ts.addRealmObjectField("destination", bs)
} ?: throw PAIllegalStateException("Bankroll schema not found")
}
schema.create("Performance")?.let { ps ->
ps.addField("id", String::class.java).setRequired("id", true)
ps.addPrimaryKey("id")
ps.addField("reportId", Int::class.java)
ps.addField("key", Int::class.java)
ps.addField("name", String::class.java)
ps.addField("objectId", String::class.java)//.setNullable("objectId", true)
ps.addField("customFieldId", String::class.java)//.setNullable("customFieldId", true)
ps.addField("value", Double::class.java).setRequired("value", false) //.setNullable("value", true)
}
currentVersion++
}
// Migrate to version 14
if (currentVersion == 13) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Transaction")?.let { ts ->
ts.addField("ratedAmount", Double::class.java)
} ?: throw PAIllegalStateException("Transaction schema not found")
//transactionTypeIds
schema.get("UserConfig")?.let { ucs ->
ucs.addField("transactionTypeIds", String::class.java).setRequired("transactionTypeIds", true)
} ?: throw PAIllegalStateException("UserConfig schema not found")
schema.get("Performance")?.let { ps ->
if (!ps.isPrimaryKey("id")) {
ps.addPrimaryKey("id")
}
}
currentVersion++
}
}
override fun equals(other: Any?): Boolean {
return other is RealmMigration
}
override fun hashCode(): Int {
return RealmMigration::javaClass.hashCode()
}
}

@ -1,176 +1,96 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import android.text.InputType
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.model.ObjectSavable
import net.pokeranalytics.android.ui.adapter.components.LiveDataDataSource
import net.pokeranalytics.android.ui.adapter.components.RowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetData
import net.pokeranalytics.android.ui.view.BankrollRow
import net.pokeranalytics.android.ui.view.RowEditable
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.BankrollPropertiesRow
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.ui.view.SimpleRow
import java.util.*
enum class ResultCaptureType {
BUYIN_CASHED_OUT,
NET_RESULT;
companion object {
val buyinCashedOutFields = listOf(
SessionPropertiesRow.CASHED_OUT,
SessionPropertiesRow.BUY_IN,
SessionPropertiesRow.TIPS)
val netResultFields = listOf(SessionPropertiesRow.NET_RESULT)
}
val rowRepresentables: List<RowRepresentable>
get() {
return when (this) {
BUYIN_CASHED_OUT -> buyinCashedOutFields
NET_RESULT -> netResultFields
}
}
}
open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresentable {
@PrimaryKey
override var id = UUID.randomUUID().toString()
override var name: String = ""
// Indicates whether the bankroll is live or online
var live: Boolean = true
/**
* The list of transactions of the bankroll
*/
@LinkingObjects("bankroll")
val transactions: RealmResults<Transaction>? = null
/**
* The list of transactions where the bankroll is the destination
*/
@LinkingObjects("destination")
val destinationTransactions: RealmResults<Transaction>? = null
// The currency of the bankroll
var currency: Currency? = null
// The initial value of the bankroll
var initialValue: Double = 0.0
val rate: Double
get() {
return this.currency?.rate ?: Currency.DEFAULT_RATE
}
override fun getDisplayName(context: Context): String {
return this.name
}
override fun isValidForDelete(realm: Realm): Boolean {
return realm.where<Session>().equalTo("bankroll.id", id).findAll().isEmpty()
&& realm.where<Transaction>().equalTo("bankroll.id", id).findAll().isEmpty()
}
override fun getDeleteStatus(context: Context, realm: Realm): DeleteValidityStatus {
return if (!realm.where<Session>().equalTo("bankroll.id", id).findAll().isEmpty()) {
DeleteValidityStatus.SESSIONS_LINKED
} else if (!realm.where<Transaction>().equalTo("bankroll.id", id).findAll().isEmpty()) {
DeleteValidityStatus.TRANSACTIONS_LINKED
} else {
DeleteValidityStatus.VALID
}
}
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
return when (status) {
DeleteValidityStatus.SESSIONS_LINKED -> R.string.bankroll_relationship_error
DeleteValidityStatus.TRANSACTIONS_LINKED -> R.string.bankroll_relationship_error_transactions
else -> super.getFailedDeleteMessage(status)
}
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
return when (status) {
SaveValidityStatus.DATA_INVALID -> R.string.empty_name_for_br_error
SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_bankroll_name_error
else -> super.getFailedSaveMessage(status)
}
}
fun resultCaptureType(context: Context): ResultCaptureType {
return Preferences.getResultCaptureType(this, context)
?: run {
when (this.live) {
true -> ResultCaptureType.BUYIN_CASHED_OUT
else -> ResultCaptureType.NET_RESULT
}
}
}
companion object {
fun getOrCreate(realm: Realm, name: String, live: Boolean = true, currencyCode: String? = null, currencyRate: Double? = null) : Bankroll {
val br = realm.where<Bankroll>().equalTo("name", name).findFirst()
return if (br != null) {
br
} else {
val bankroll = Bankroll()
bankroll.name = name
bankroll.live = live
val currency = Currency()
currency.code = currencyCode
currency.rate = currencyRate
bankroll.currency = currency
realm.copyToRealm(bankroll)
}
}
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
BankrollPropertiesRow.ONLINE -> {
this.live = if (value is Boolean) !value else false
}
BankrollPropertiesRow.INITIAL_VALUE -> {
this.initialValue = value as Double? ?: 0.0
}
BankrollPropertiesRow.CURRENCY -> {
//TODO handle a use default currency option
this.currency?.code = value as String?
}
BankrollPropertiesRow.RATE -> {
this.currency?.rate = value as Double?
}
}
}
val utilCurrency: java.util.Currency
get() {
this.currency?.code?.let {
return java.util.Currency.getInstance(it)
}
return UserDefaults.currency
}
@Ignore
override val realmObjectClass: Class<out Identifiable> = Bankroll::class.java
import kotlin.collections.ArrayList
open class Bankroll(name: String = "") : RealmObject(), RowRepresentableDataSource,
RowEditable, ObjectSavable {
companion object {
fun newInstance() : Bankroll {
var bankroll: Bankroll = Bankroll()
return bankroll
}
}
@PrimaryKey
var id = UUID.randomUUID().toString()
// the name of the bankroll
var name: String = name
// Indicates whether the bankroll is live or online
var live: Boolean = true
// The list of transactions of the bankroll
var transactions: RealmList<Transaction> = RealmList()
// The currency of the bankroll
var currency: Currency? = null
// @todo rate management
override fun uniqueIdentifier(): String {
return this.id
}
override val adapterRows: ArrayList<RowRepresentable>
get() {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.addAll(BankrollRow.values())
return rows
}
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
SimpleRow.NAME -> this.name
BankrollRow.LIVE -> if (this.live) "live" else "online"
BankrollRow.CURRENCY -> this.currency?.code?: ""
else -> return super.stringForRow(row)
}
}
override fun boolForRow(row: RowRepresentable): Boolean {
when (row) {
BankrollRow.LIVE -> return true
else -> return super.boolForRow(row)
}
}
override fun getBottomSheetData(row: RowRepresentable): ArrayList<BottomSheetData> {
val data = java.util.ArrayList<BottomSheetData>()
when (row) {
SimpleRow.NAME -> data.add(BottomSheetData(this.name, SimpleRow.NAME.resId, InputType.TYPE_CLASS_TEXT))
}
return data
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
}
}
override fun isValidForSave(): Boolean {
val realm = Realm.getDefaultInstance()
return (realm.where(Bankroll::class.java)
.notEqualTo("id", this.id)
.equalTo("name", this.name)
.findAll().size == 0)
return this.name.isNotEmpty()
}
}

@ -1,79 +0,0 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.Realm
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.Manageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT
import java.util.*
open class Comment : RealmObject(), Manageable, RowRepresentable, RowUpdatable {
@PrimaryKey
override var id = UUID.randomUUID().toString()
var content: String = ""
var date: Date = Date()
@Ignore
override val realmObjectClass: Class<out Identifiable> = Comment::class.java
@Ignore
override val viewType: Int = RowViewType.CONTENT.ordinal
@Ignore
override val bottomSheetType: BottomSheetType = BottomSheetType.EDIT_TEXT_MULTI_LINES
// @Ignore
// override val valueCanBeClearedWhenEditing: Boolean = false
override fun localizedTitle(context: Context): String {
return context.getString(R.string.comment)
}
override fun getDisplayName(context: Context): String {
return if (content.isNotEmpty()) content else NULL_TEXT
}
// override fun startEditing(dataSource: Any?, parent: Fragment?) {
// if (parent == null) return
// if (parent !is RowRepresentableDelegate) return
// val data = RowEditableDataSource()
// data.append(this.content, R.string.value)
// InputFragment.buildAndShow(this, parent, data, isDeletable = true)
// }
override fun updateValue(value: Any?, row: RowRepresentable) {
this.content = value as String? ?: ""
}
override fun isValidForSave(): Boolean {
return true
}
override fun alreadyExists(realm: Realm): Boolean {
return realm.where(this::class.java).notEqualTo("id", this.id).findAll().isNotEmpty()
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
throw ModelException("${this::class.java} getFailedSaveMessage for $status not handled")
}
override fun isValidForDelete(realm: Realm): Boolean {
return true
}
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
return R.string.cf_entry_delete_popup_message
}
}

@ -1,68 +0,0 @@
package net.pokeranalytics.android.model.realm
import io.realm.RealmObject
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
open class ComputableResult : RealmObject(), Filterable {
var ratedNet: Double = 0.0
var bbNet: BB = 0.0
var hasBigBlind: Int = 0
var isPositive: Int = 0
var ratedBuyin: Double = 0.0
var estimatedHands: Double = 0.0
var bbPer100Hands: BB = 0.0
var session: Session? = null
var ratedTips: Double = 0.0
fun updateWith(session: Session) {
val rate = session.bankroll?.currency?.rate ?: 1.0
session.result?.let { result ->
this.ratedNet = result.net * rate
this.isPositive = result.isPositive
this.ratedBuyin = (result.buyin ?: 0.0) * rate
this.ratedTips = (result.tips ?: 0.0) * rate
}
this.bbNet = session.bbNet
this.hasBigBlind = if (session.cgBiggestBet != null) 1 else 0
this.estimatedHands = session.estimatedHands
this.bbPer100Hands =
session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100
}
enum class Field(val identifier: String) {
RATED_NET("ratedNet"),
BB_NET("bbNet"),
HAS_BIG_BLIND("hasBigBlind"),
IS_POSITIVE("isPositive"),
RATED_BUYIN("ratedBuyin"),
ESTIMATED_HANDS("estimatedHands"),
RATED_TIPS("ratedTips"),
// BB_PER100HANDS("bbPer100Hands")
}
companion object {
fun fieldNameForQueryType(queryCondition: Class<out QueryCondition>): String? {
Session.fieldNameForQueryType(queryCondition)?.let {
return "session.$it"
}
return null
}
}
}

@ -1,68 +1,19 @@
package net.pokeranalytics.android.model.realm
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.util.UserDefaults
import java.util.*
open class Currency : RealmObject() {
companion object {
@Ignore
val DEFAULT_RATE: Double = 1.0
}
open class Currency : RealmObject() {
@PrimaryKey
var id = UUID.randomUUID().toString()
/**
* The currency code of the currency, i.e. USD, EUR...
*/
// The currency code of the currency, i.e. USD, EUR...
var code: String? = null
set(value) {
try {
if (value != null) {
java.util.Currency.getInstance(value) // test validity of code
}
field = value
} catch (e: Exception) {
// make app crash earlier than later, possibly to show an error message to the user in the future
throw PAIllegalStateException(e.localizedMessage ?: e.toString())
}
}
/**
* The rate of the currency with the main currency
*/
var rate: Double? = DEFAULT_RATE
fun refreshRelatedRatedValues() {
val rate = this.rate ?: DEFAULT_RATE
val query = this.realm.where(ComputableResult::class.java)
query.`in`("session.bankroll.currency.id", arrayOf(this.id))
val cResults = query.findAll()
cResults.forEach { computable ->
computable.session?.result?.net?.let {
computable.ratedNet = it * rate
}
computable.session?.result?.buyin?.let {
computable.ratedBuyin = it * rate
}
computable.session?.bankrollHasBeenUpdated()
}
}
fun hasMainCurrencyCode() : Boolean {
this.code?.let { return it == UserDefaults.currency.currencyCode }
return false
}
// The rate of the currency with the main currency
var rate: Double? = null
}

@ -1,347 +1,19 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import android.text.InputType
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus
import net.pokeranalytics.android.model.interfaces.Identifiable
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.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.CustomFieldPropertiesRow
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import timber.log.Timber
import java.util.*
import kotlin.collections.ArrayList
open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource {
open class CustomField : RealmObject() {
companion object {
@PrimaryKey
var id = UUID.randomUUID().toString()
fun getOrCreate(realm: Realm, name: String, type: Int): CustomField {
val cf = realm.where(CustomField::class.java).equalTo("name", name).findFirst()
return if (cf != null) {
cf
} else {
val customField = CustomField()
customField.name = name
customField.type = type
realm.copyToRealm(customField)
}
}
// The name of the currency field
var name: String = ""
}
// @todo
@Ignore
override val realmObjectClass: Class<out Identifiable> = CustomField::class.java
/**
* The custom field type: a list of items, a number or an amont
*/
enum class Type(override var uniqueIdentifier: Int, var resId: Int, var isEnabled: Boolean = true) :
IntIdentifiable {
LIST(0, R.string.enum_custom_field_type),
NUMBER(1, R.string.number),
AMOUNT(2, R.string.amount)
}
/**
* The sorting used for the list, either custom, or alphabetically asc/desc
*/
enum class Sort(override var uniqueIdentifier: Int) : IntIdentifiable {
DEFAULT(0),
ASCENDING(1),
DESCENDING(2)
}
@PrimaryKey
override var id = UUID.randomUUID().toString()
/**
* The name of the custom field
*/
override var name: String = ""
// The type of the custom fields, mapped with the CustomField.Type enum
var type: Int = Type.LIST.uniqueIdentifier
set(value) {
if (field == Type.LIST.uniqueIdentifier && value != Type.LIST.uniqueIdentifier) {
this.entriesToDelete.addAll(this.entries)
this.entries.clear()
}
field = value
this.updateRowRepresentation()
}
/**
* Indicates whether the custom field value should be copied when a session is duplicated
*/
var duplicateValue: Boolean = false
/**
* The list of entries for the LIST type
*/
var entries: RealmList<CustomFieldEntry> = RealmList()
/**
* The sorting of the entries, mapped with the CustomField.Sort enum
*/
var sortCondition: Int = Sort.DEFAULT.uniqueIdentifier
set(value) {
field = value
sortEntries()
updateRowRepresentation()
}
@Ignore
private var entriesToDelete: ArrayList<CustomFieldEntry> = ArrayList()
@Ignore
private var rowRepresentation: List<RowRepresentable> = mutableListOf()
//helper
val isListType: Boolean
get() {
return this.type == Type.LIST.uniqueIdentifier
}
val isAmountType: Boolean
get() {
return this.type == Type.AMOUNT.uniqueIdentifier
}
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
CustomFieldPropertiesRow.TYPE -> this.type = (value as Type?)?.uniqueIdentifier ?: Type.LIST.uniqueIdentifier
CustomFieldPropertiesRow.COPY_ON_DUPLICATE -> this.duplicateValue = value as Boolean? ?: false
}
}
override fun isValidForSave(): Boolean {
return super.isValidForSave()
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
return when (status) {
SaveValidityStatus.DATA_INVALID -> R.string.cf_empty_field_error
SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_cf_error
else -> super.getFailedSaveMessage(status)
}
}
override fun alreadyExists(realm: Realm): Boolean {
return realm.where(this::class.java).equalTo("name", this.name).and().notEqualTo("id", this.id).findAll()
.isNotEmpty()
}
override fun isValidForDelete(realm: Realm): Boolean {
return true
// val sessions = realm.where<Session>().contains("customFieldEntries.customFields.id", id).findAll()
// return sessions.isEmpty()
}
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
//TODO:
return R.string.cf_entry_delete_popup_message
}
override fun deleteDependencies(realm: Realm) {
if (isValid) {
val entries = realm.where<CustomFieldEntry>().equalTo("customFields.id", id).findAll()
entries.deleteAllFromRealm()
}
}
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
return when (row) {
is CustomFieldEntry -> row.editingDescriptors(
mapOf(
"defaultValue" to row.value
)
)
else -> null
}
}
/**
* Update the row representation
*/
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.add(CustomFieldPropertiesRow.TYPE)
if (type == Type.LIST.uniqueIdentifier && entries.size >= 0) {
if (entries.isNotEmpty()) {
rows.add(CustomizableRowRepresentable(RowViewType.HEADER_TITLE, R.string.items_list))
sortEntries()
entries.forEach { customFieldEntry ->
customFieldEntry.isMovable = sortCondition == Sort.DEFAULT.uniqueIdentifier
}
rows.addAll(entries)
}
}
return rows
}
/**
* Sort the entries element
*/
private fun sortEntries() {
when (sortCondition) {
Sort.ASCENDING.uniqueIdentifier -> entries.sortBy { it.value }
Sort.DESCENDING.uniqueIdentifier -> entries.sortByDescending { it.value }
}
entries.forEachIndexed { index, customFieldEntry ->
customFieldEntry.order = index
}
}
fun updateRowRepresentation() {
this.rowRepresentation = this.updatedRowRepresentationForCurrentState()
}
/**
* Add an entry
*/
fun addEntry(): CustomFieldEntry {
val entry = CustomFieldEntry()
this.entries.add(entry)
sortEntries()
updateRowRepresentation()
return entry
}
/**
* Delete an entry
*/
fun deleteEntry(entry: CustomFieldEntry) {
entriesToDelete.add(entry)
entries.remove(entry)
sortEntries()
updateRowRepresentation()
}
fun cleanupEntries() { // called when saving the custom field
val realm = Realm.getDefaultInstance()
realm.executeTransaction {
this.entriesToDelete.forEach { // entries are out of realm
realm.where<CustomFieldEntry>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
}
}
realm.close()
this.entriesToDelete.clear()
}
fun getOrCreateEntry(realm: Realm, value: String): CustomFieldEntry {
this.entries.find { it.value == value }?.let {
Timber.d("L>> get")
return it
} ?: run {
Timber.d("L>> create")
val entry = realm.copyToRealm(CustomFieldEntry())
entry.value = value
this.entries.add(entry)
return entry
}
}
/**
* Clean the entries if the type is not a list & remove the deleted entries from realm
*/
// fun cleanEntries(realm: Realm) {
// realm.executeTransaction {
//
// if (!isListType) {
// entriesToDelete.addAll(entries)
// entries.clear()
// }
//
// // @TODO
// entriesToDelete.forEach {
// Timber.d("Delete entry: V=${it.value} N=${it.numericValue} / ID=${it.id}")
// realm.where<CustomFieldEntry>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
// }
// entriesToDelete.clear()
// }
// }
/**
* Returns a comparison criteria based on this custom field
*/
val criteria: Criteria
get() {
return when (this.type) {
Type.LIST.uniqueIdentifier -> Criteria.ListCustomFields(this.id)
else -> Criteria.ValueCustomFields(this.id)
}
}
@Ignore
override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal
override fun localizedTitle(context: Context): String {
return this.name
}
override fun getDisplayName(context: Context): String {
return this.name
}
override val bottomSheetType: BottomSheetType
get() {
return when (this.type) {
Type.LIST.uniqueIdentifier -> BottomSheetType.LIST_STATIC
else -> BottomSheetType.NUMERIC_TEXT
}
}
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor> {
return when (this.type) {
Type.LIST.uniqueIdentifier -> {
val defaultValue: Any? by map
val data: RealmList<CustomFieldEntry>? by map
arrayListOf(
RowRepresentableEditDescriptor(defaultValue, staticData = data)
)
}
else -> {
val defaultValue: Double? by map
arrayListOf(
RowRepresentableEditDescriptor(
defaultValue, inputType = InputType.TYPE_CLASS_NUMBER
or InputType.TYPE_NUMBER_FLAG_DECIMAL
or InputType.TYPE_NUMBER_FLAG_SIGNED
)
)
}
}
}
}

@ -1,153 +0,0 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import android.text.InputType
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 io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.toCurrency
import java.text.NumberFormat
import java.util.*
import java.util.Currency
open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable, RowUpdatable {
@Ignore
override val realmObjectClass: Class<out Identifiable> = CustomFieldEntry::class.java
@PrimaryKey
override var id = UUID.randomUUID().toString()
/**
* The order in the list
*/
var order: Int = 0
/**
* The inverse relationship with CustomField
*/
@LinkingObjects("entries")
val customFields: RealmResults<CustomField>? = null
val customField: CustomField?
get() {
return this.customFields?.first()
}
/**
* The string value of the entry
*/
var value: String = ""
/**
* The numeric value of the entry
*/
var numericValue: Double? = null
@Ignore
override var name: String = value
get() { return value }
@Ignore
var isMovable: Boolean = false
@Ignore
override val viewType: Int = RowViewType.TITLE_VALUE_ACTION.ordinal
override val imageRes: Int?
get() {
return if (isMovable) R.drawable.ic_reorder else null
}
override val imageTint: Int?
get() {
return R.color.kaki
}
@Ignore
override val bottomSheetType: BottomSheetType = BottomSheetType.EDIT_TEXT
override fun localizedTitle(context: Context): String {
return context.getString(R.string.value)
}
override fun getDisplayName(context: Context): String {
return if (value.isNotEmpty()) value else NULL_TEXT
}
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor>? {
val defaultValue: Any? by map
return arrayListOf(
RowRepresentableEditDescriptor(defaultValue, R.string.value, InputType.TYPE_CLASS_TEXT)
)
}
override fun isValidForSave(): Boolean {
return true
}
override fun alreadyExists(realm: Realm): Boolean {
return realm.where(this::class.java).notEqualTo("id", this.id).findAll().isNotEmpty()
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
throw ModelException("${this::class.java} getFailedSaveMessage for $status not handled")
}
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
return R.string.cf_entry_delete_popup_message
}
override fun deleteDependencies(realm: Realm) {
if (isValid) {
val entries = realm.where<Session>().contains("customFieldEntries.id", id).findAll()
entries.deleteAllFromRealm()
}
}
override fun updateValue(value: Any?, row: RowRepresentable) {
this.value = value as String? ?: ""
}
override fun isValidForDelete(realm: Realm): Boolean {
if (realm.where<Session>().contains("customFieldEntries.id", id).findAll().isNotEmpty()) {
return false
}
return true
}
/**
* Return the amount
*/
fun getFormattedValue(currency: Currency? = null): String {
return when (customField?.type) {
CustomField.Type.AMOUNT.uniqueIdentifier -> {
numericValue?.toCurrency(currency) ?: run { NULL_TEXT }
}
CustomField.Type.NUMBER.uniqueIdentifier -> {
NumberFormat.getInstance().format(this.numericValue)
}
else -> {
value
}
}
}
}

@ -1,202 +1,25 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.*
import io.realm.annotations.Ignore
import io.realm.MutableRealmInteger
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.modules.filter.FilterableType
import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rows.FilterItemRow
import timber.log.Timber
import java.util.*
/**
* A [Filter] is the top level representation of the filtering system
* It contains a list of [FilterCondition] describing the complete query to launch
* The [Filter] is working closely with a [Filterable] interface providing the entity we want the query being launched on
*/
open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, UsageCountable, ImageDecorator {
//import net.pokeranalytics.android.FilterComponent
@Ignore
override val realmObjectClass: Class<out Identifiable> = Filter::class.java
companion object {
open class Filter : RealmObject() {
// Create a new instance
fun newInstance(filterableTypeUniqueIdentifier: Int): Filter {
val filter = Filter()
filter.filterableTypeUniqueIdentifier = filterableTypeUniqueIdentifier
return filter
}
@PrimaryKey
var id = UUID.randomUUID().toString()
inline fun <reified T : Filterable> queryOn(realm: Realm, query: Query, sortField: String? = null): RealmResults<T> {
val rootQuery = realm.where<T>()
var realmQuery = query.queryWith(rootQuery)
sortField?.let {
realmQuery = realmQuery.sort(it)
}
return realmQuery.findAll()
}
}
// the filter name
var name: String = ""
override val viewType: Int
get() = RowViewType.TITLE_VALUE_ACTION.ordinal
override val imageRes: Int?
get() = R.drawable.ic_outline_settings
override val imageTint: Int?
get() = R.color.green
override val imageClickable: Boolean?
get() = true
// the number of use of the filter,
// for MutableRealmInteger, see https://realm.io/docs/java/latest/#counters
val usageCount: MutableRealmInteger = MutableRealmInteger.valueOf(0)
// var components: List<FilterComponent> = listOf()
@PrimaryKey
override var id = UUID.randomUUID().toString()
// the queryWith name
var name: String = ""
get() {
if (field.isEmpty()) {
return this.query.defaultName
}
return field
}
override var useCount: Int = 0
@Ignore
override val ownerClass: Class<out RealmModel> = Session::class.java
var filterConditions: RealmList<FilterCondition> = RealmList()
private set
private var filterableTypeUniqueIdentifier: Int? = null
val filterableType: FilterableType
get() {
this.filterableTypeUniqueIdentifier?.let {
return FilterableType.valueByIdentifier(it)
}
return FilterableType.ALL
}
fun createOrUpdateFilterConditions(filterConditionRows: List<FilterItemRow>) {
Timber.d("list of querys saving: ${filterConditionRows.map { it.queryCondition?.id }}")
Timber.d("list of querys previous: ${this.filterConditions.map { it.queryCondition.id }}")
filterConditionRows
.mapNotNull {
it.queryCondition?.groupId
}
.distinct()
.forEach { groupId ->
filterConditionRows
.filter {
it.queryCondition?.groupId == groupId
}
.apply {
val conditions = this.mapNotNull { it.queryCondition }
Timber.d("list of querys: ${conditions.map { it.id }}")
val newFilterCondition = FilterCondition(conditions, this.first().filterSectionRow)
val previousCondition = filterConditions.filter {
it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator
}
filterConditions.removeAll(previousCondition)
filterConditions.add(newFilterCondition)
}
}
}
fun remove(filterCategoryRow: FilterCategoryRow) {
val sections = filterCategoryRow.filterSectionRows.map { it.name }
val savedSections = filterConditions.filter { sections.contains(it.sectionName) }
this.filterConditions.removeAll(savedSections)
}
fun countBy(filterCategoryRow: FilterCategoryRow): Int {
val sections = filterCategoryRow.filterSectionRows.map { it.name }
Timber.d("list of sections $sections")
val savedSections = filterConditions.filter { sections.contains(it.sectionName) }.flatMap { it.queryCondition.id }
Timber.d("list of savedSections $savedSections")
return savedSections.size
}
fun contains(filterElementRow: QueryCondition): Boolean {
Timber.d("list of saved queries ${filterConditions.map { it.queryCondition.id }}")
Timber.d("list of contains ${filterElementRow.id}")
val contained = filterConditions.flatMap { it.queryCondition.id }.contains(filterElementRow.id.first())
Timber.d("is contained: $contained")
return contained
}
fun filterCondition(filterElementRow: QueryCondition): FilterCondition? {
return filterConditions.firstOrNull {
it.queryCondition.id.contains(filterElementRow.id.first())
}
}
inline fun <reified T : Filterable> query(firstField: String? = null, vararg remainingFields: String): RealmQuery<T> {
val realmQuery = realm.where<T>()
if (firstField != null) {
return this.query.queryWith(realmQuery).distinct(firstField, *remainingFields)
}
return this.query.queryWith(realmQuery)
}
inline fun <reified T : Filterable> results(firstField: String? = null, vararg remainingFields: String): RealmResults<T> {
return this.query<T>(firstField, *remainingFields).findAll()
}
val query: Query
get() {
val query = Query()
this.filterConditions.forEach {
query.add(it.queryCondition)
}
return query
}
override fun getDisplayName(context: Context): String {
if (name.isNotEmpty()) return name
return this.query.getName(context)
}
override fun isValidForDelete(realm: Realm): Boolean {
return true
}
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
return R.string.relationship_error
}
override val bottomSheetType: BottomSheetType
get() {
return BottomSheetType.EDIT_TEXT
}
override fun localizedTitle(context: Context): String {
return context.getString(R.string.name)
}
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor>? {
val defaultValue: String? by map
return arrayListOf(RowRepresentableEditDescriptor(defaultValue, R.string.name))
}
override fun updateValue(value: Any?, row: RowRepresentable) {
val newName = value as String? ?: ""
if (newName.isNotEmpty()) {
name = newName
}
}
}

@ -1,111 +0,0 @@
package net.pokeranalytics.android.model.realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import java.util.*
open class FilterCondition() : RealmObject() {
private constructor(filterName: String, sectionName: String) : this() {
this.filterName = filterName
this.sectionName = sectionName
}
constructor(filterElementRows: List<QueryCondition>, section: FilterSectionRow) : this(filterElementRows.first().baseId, section.name) {
val row = filterElementRows.first()
this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName
this.operator = row.operator.ordinal
if (row is QueryCondition.CustomFieldRelated) {
this.stringValue = row.customFieldId
}
when (row) {
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 })
else -> {}
}
}
var filterName: String? = null
var sectionName: String? = null
val queryCondition : QueryCondition
get() = QueryCondition.valueOf<QueryCondition>(this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName)
.apply {
this.updateValueBy(this@FilterCondition)
}
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
@LinkingObjects("filterConditions")
val filters: RealmResults<Filter>? = null
inline fun <reified T> getValues(): ArrayList <T> {
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
}
}
fun <T> getv(clazz: Class<T>) : T {
return when (clazz) {
Int::class -> intValue ?: 0
Double::class -> doubleValue?: 0.0
Date::class -> dateValue ?: Date()
String::class -> stringValue ?: ""
else -> throw PokerAnalyticsException.QueryValueMapUnexpectedValue
} as T
}
inline fun <reified T> 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
}
}
fun setValue(value:Double) {
doubleValue = value
}
fun setValue(value:Date) {
dateValue = value
}
fun setValue(value:Int) {
intValue= value
}
fun setValue(value:String) {
stringValue = value
}
}

@ -1,136 +1,66 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.Realm
import io.realm.RealmModel
import android.text.InputType
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.model.interfaces.Identifiable
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.model.ObjectSavable
import net.pokeranalytics.android.ui.adapter.components.LiveDataDataSource
import net.pokeranalytics.android.ui.adapter.components.RowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetData
import net.pokeranalytics.android.ui.view.GameRow
import net.pokeranalytics.android.ui.view.RowEditable
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.GamePropertiesRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.ui.view.SimpleRow
import java.util.*
import kotlin.collections.ArrayList
open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable, RowUpdatable, UsageCountable {
open class Game : RealmObject(), RowRepresentableDataSource, RowEditable, ObjectSavable {
@Ignore
override val realmObjectClass: Class<out Identifiable> = Game::class.java
@PrimaryKey
var id = UUID.randomUUID().toString()
companion object {
val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows
}
}
// The name of the game
var name: String = ""
@PrimaryKey
override var id = UUID.randomUUID().toString()
// A shorter name for the game
var shortName: String? = null
// The name of the game
override var name: String = ""
// A shorter name for the game
var shortName: String? = null
// CountableUsage
override var useCount: Int = 0
@Ignore
override val ownerClass: Class<out RealmModel> = Session::class.java
fun getNotNullShortName() : String {
this.shortName?.let {
return it
}
return this.name
}
override fun getDisplayName(context: Context): String {
return this.name
}
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override fun charSequenceForRow(
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
GamePropertiesRow.SHORT_NAME -> this.shortName ?: NULL_TEXT
else -> return super.charSequenceForRow(row, context, 0)
}
}
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
return when (row) {
SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.name))
GamePropertiesRow.SHORT_NAME -> row.editingDescriptors(mapOf("defaultValue" to this.shortName))
else -> null
}
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
GamePropertiesRow.SHORT_NAME -> this.shortName = value as String? ?: ""
}
}
override fun uniqueIdentifier(): String {
return this.id
}
override fun isValidForSave(): Boolean {
return name.isNotEmpty()
}
override val adapterRows: ArrayList<RowRepresentable>
get() {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.addAll(GameRow.values())
return rows
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
return when (status) {
SaveValidityStatus.DATA_INVALID -> R.string.variant_empty_name_error
SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_variant_error
else -> super.getFailedSaveMessage(status)
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
SimpleRow.NAME -> this.name
GameRow.SHORT_NAME -> this.shortName?:""
else -> return super.stringForRow(row)
}
}
override fun isValidForDelete(realm: Realm): Boolean {
return realm.where<Session>().equalTo("game.id", id).findAll().isEmpty()
}
val playerHandMaxCards: Int?
get() {
return when {
isHoldem -> 2
isOmaha5 -> 5
isOmaha4 -> 4
else -> null
}
}
private val isHoldem: Boolean
get() {
return name.contains("texas", ignoreCase = true) || name.contains("holdem", ignoreCase = true) || name.contains("hold'em", ignoreCase = true) || name.contains("HE")
}
private val isOmaha4: Boolean
get() {
return name.contains("omaha", ignoreCase = true) || name.contains("PLO", ignoreCase = true)
}
override fun getBottomSheetData(row: RowRepresentable): ArrayList<BottomSheetData> {
val data = java.util.ArrayList<BottomSheetData>()
when (row) {
SimpleRow.NAME -> data.add(BottomSheetData(this.name, SimpleRow.NAME.resId, InputType.TYPE_CLASS_TEXT))
GameRow.SHORT_NAME -> data.add(BottomSheetData(this.shortName, GameRow.SHORT_NAME.resId, InputType.TYPE_CLASS_TEXT))
}
return data
}
private val isOmaha5: Boolean
get() {
return name.contains("5")
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
GameRow.SHORT_NAME -> this.shortName = value as String
}
}
override fun isValidForSave(): Boolean {
return this.name.isNotEmpty()
}
}

@ -0,0 +1,16 @@
package net.pokeranalytics.android.model.realm
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import java.util.*
open class HandHistory : RealmObject() {
@PrimaryKey
var id = UUID.randomUUID().toString()
// the date of the hand history
var date: Date = Date()
}

@ -1,71 +1,66 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import com.google.android.libraries.places.api.model.Place
import io.realm.Realm
import android.text.InputType
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.model.ObjectSavable
import net.pokeranalytics.android.ui.adapter.components.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetData
import net.pokeranalytics.android.ui.view.RowEditable
import net.pokeranalytics.android.ui.view.LocationRow
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.SimpleRow
import java.util.*
open class Location : RealmObject(), NameManageable, RowRepresentable, RowUpdatable {
open class Location : RealmObject(), RowRepresentableDataSource, RowEditable, ObjectSavable {
@Ignore
override val realmObjectClass: Class<out Identifiable> = Location::class.java
@PrimaryKey
var id = UUID.randomUUID().toString()
@PrimaryKey
override var id = UUID.randomUUID().toString()
// The name of the location
var name: String = ""
// The name of the location
override var name: String = ""
// the longitude of the location
var longitude: Double? = null
// The readable address of the location
private var address: String? = null
// the latitude of the location
var latitude: Double? = null
// the longitude of the location
var longitude: Double? = null
override fun uniqueIdentifier(): String {
return this.id
}
// the latitude of the location
var latitude: Double? = null
override fun getDisplayName(context: Context): String {
return this.name
}
override val adapterRows: ArrayList<RowRepresentable>
get() {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.addAll(LocationRow.values())
return rows
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
}
}
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
SimpleRow.NAME -> this.name
else -> return super.stringForRow(row)
}
}
/**
* Fill the location attributes with a place object
*/
fun setPlace(place: Place) {
this.name = place.name ?: ""
this.address = place.address ?: ""
this.latitude = place.latLng?.latitude
this.longitude = place.latLng?.longitude
}
override fun getBottomSheetData(row: RowRepresentable): ArrayList<BottomSheetData> {
val data = java.util.ArrayList<BottomSheetData>()
when (row) {
SimpleRow.NAME -> data.add(BottomSheetData(this.name, SimpleRow.NAME.resId, InputType.TYPE_CLASS_TEXT))
}
return data
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
return when (status) {
SaveValidityStatus.DATA_INVALID -> R.string.location_empty_field_error
SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_location_error
else -> super.getFailedSaveMessage(status)
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
}
}
}
override fun isValidForDelete(realm: Realm): Boolean {
return realm.where<Session>().equalTo("location.id", id).findAll().isEmpty()
}
override fun isValidForSave(): Boolean {
return this.name.isNotEmpty()
}
}

@ -1,72 +0,0 @@
package net.pokeranalytics.android.model.realm
import io.realm.Realm
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.LiveOnline
import net.pokeranalytics.android.ui.view.rows.StaticReport
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.extensions.lookupForNameInAllTablesById
import java.util.*
interface PerformanceKey {
val resId: Int?
val value: Int
}
open class Performance() : RealmObject() {
@PrimaryKey
var id: String = UUID.randomUUID().toString()
constructor(
report: StaticReport,
key: PerformanceKey,
name: String? = null,
objectId: String? = null,
customFieldId: String? = null,
value: Double? = null
) : this() {
this.reportId = report.uniqueIdentifier
this.key = key.value
this.name = name
this.objectId = objectId
this.customFieldId = customFieldId
this.value = value
}
var reportId: Int = 0
var key: Int = 0
var name: String? = null
var objectId: String? = null
var customFieldId: String? = null
var value: Double? = null
fun toStaticReport(realm: Realm): StaticReport {
return StaticReport.newInstance(realm, this.reportId, this.customFieldId)
}
fun displayValue(realm: Realm): CharSequence {
this.name?.let { return it }
this.objectId?.let { realm.lookupForNameInAllTablesById(it) }
return NULL_TEXT
}
val stat: Stat
get() {
return Stat.valueByIdentifier(this.key.toInt())
}
val resId: Int?
get() {
return when (this.reportId) {
StaticReport.OptimalDuration.uniqueIdentifier -> LiveOnline.valueByIdentifier(this.key).resId
else -> stat.resId
}
}
}

@ -1,101 +1,15 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow
import net.pokeranalytics.android.util.RANDOM_PLAYER
import java.util.*
open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepresentable, RowUpdatable {
open class Player : RealmObject() {
@PrimaryKey
override var id = UUID.randomUUID().toString()
var id = UUID.randomUUID().toString()
// The name of the player
override var name: String = ""
var name: String = ""
// New fields
var summary: String = ""
var color: Int? = null
var picture: String? = null
var comments: RealmList<Comment> = RealmList()
@Ignore
override val realmObjectClass: Class<out Identifiable> = Player::class.java
@Ignore
override val viewType: Int = RowViewType.ROW_PLAYER.ordinal
override fun isValidForDelete(realm: Realm): Boolean {
//TODO
return true
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
return when(status) {
SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_user_error
SaveValidityStatus.DATA_INVALID -> R.string.user_empty_field_error
else -> super.getFailedSaveMessage(status)
}
}
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
//TODO
return R.string.relationship_error
}
override fun getDisplayName(context: Context): String {
return this.name
}
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
PlayerPropertiesRow.NAME -> this.name = value as String? ?: ""
PlayerPropertiesRow.SUMMARY -> this.summary = value as String? ?: ""
PlayerPropertiesRow.IMAGE -> this.picture = value as? String
}
}
/**
* Return if the player has a picture
*/
fun hasPicture(): Boolean {
return picture != null && picture?.isNotEmpty() == true
}
val initials: String
get() {
return if (this.name.isNotEmpty()) {
val playerData = this.name.split(" ")
when {
playerData.size > 1 -> {
playerData[0].first().toString() + playerData[1].first().toString()
}
this.name.length > 1 -> {
this.name.substring(0, 2)
}
else -> {
this.name.substring(0, this.name.length)
}
}
} else {
RANDOM_PLAYER
}
}
fun hands(realm: Realm): RealmResults<HandHistory> {
return realm.where(HandHistory::class.java).equalTo("playerSetups.player.id", this.id).findAll()
}
}
}

@ -0,0 +1,33 @@
package net.pokeranalytics.android.model.realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import java.util.*
enum class ReportDisplay {
TABLE,
GRAPH,
MAP
}
open class Report : RealmObject() {
@PrimaryKey
var id = UUID.randomUUID().toString()
// The name of the report
var name: String = ""
// The type of display of the report
var display: Int = ReportDisplay.TABLE.ordinal
// @todo define the configuration options
// var comparators: List<Int> = listOf()
// var stats: List<Int> = listOf()
// The filters associated with the report
var filters: RealmList<Filter> = RealmList()
}

@ -1,103 +0,0 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.findById
import java.util.*
open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
@Ignore
override val realmObjectClass: Class<out Identifiable> = ReportSetup::class.java
@PrimaryKey
override var id = UUID.randomUUID().toString()
// The name of the report
var name: String = ""
// The type of display of the report
var display: Int = ReportDisplay.TABLE.ordinal
/**
* A list of statIds to compute
* Must contain at least 1
*/
var statIds: RealmList<Int> = RealmList()
/**
* An optional list of criteriaIds to compare statIds
*/
var criteriaIds: RealmList<Int> = RealmList()
/**
* An optional list of custom fields ids to be compared
*/
var criteriaCustomFieldIds: RealmList<String> = RealmList()
/**
* An optional filter to narrow the results
*/
var filter: Filter? = null
// RowRepresentable
override fun getDisplayName(context: Context): String {
return this.name
}
@Ignore
override val viewType: Int = RowViewType.TITLE_ARROW.ordinal
/**
* Returns the Options based on the ReportSetup parameters
*/
fun options(realm: Realm, reportDisplay: ReportDisplay): Calculator.Options {
val stats = this.statIds.map { Stat.valueByIdentifier(it) }
// Comparison criteria
val criteria = this.criteriaIds.map { Criteria.valueByIdentifier(it) }
val customFields = this.criteriaCustomFieldIds.mapNotNull { realm.findById<CustomField>(it) }
val cfCriteria = customFields.map { it.criteria }
val allCriteria = mutableListOf<Criteria>()
allCriteria.addAll(criteria)
allCriteria.addAll(cfCriteria)
return Calculator.Options(
stats = stats,
progressValues = reportDisplay.progressValues,
criterias = allCriteria,
filter = this.filter,
userGenerated = true,
reportSetupId = this.id
)
}
// Deletable
override fun isValidForDelete(realm: Realm): Boolean {
return true
}
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

@ -2,85 +2,40 @@ package net.pokeranalytics.android.model.realm
import io.realm.RealmList
import io.realm.RealmObject
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(), Filterable {
open class Result : RealmObject() {
companion object {
// the user associated to this session result
var player: Player? = null
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition>): String? {
Session.fieldNameForQueryType(queryCondition)?.let {
return "sessions.$it"
}
return null
}
}
/**
* The buyin amount
*/
// The buyin amount
var buyin: Double? = null
set(value) {
field = value
this.computeNumberOfRebuy()
this.computeNet(true)
this.computeNet()
}
/**
* The cashed out amount
*/
// the cashed out amount
var cashout: Double? = null
set(value) {
field = value
this.computeNet(true)
if (value != null) {
this.session?.end()
}
this.computeNet()
}
/**
* The net result
*/
var netResult: Double? = null
set(value) {
// this.session?.bankroll?.let { bankroll ->
// if (bankroll.live) {
// throw PAIllegalStateException("Can't set net result on a live bankroll")
// }
// } ?: run {
// throw PAIllegalStateException("Session doesn't have any bankroll")
// }
field = value
this.computeNet(false)
if (value != null) {
this.session?.end()
}
}
/**
* The pre-computed net (readonly)
*/
var net: Double = 0.0
private set
// The net result
var netResult: Double? = null
set(value) {
field = value
this.computeNet()
}
/**
* Tips
*/
var tips: Double? = null
set(value) {
field = value
this.session?.computeStats()
}
// The net (readonly)
var net: Double = 0.0
private set
// The transactions associated with the Result, impacting the result
// The transactions associated with the Result, impacting the result
var transactions: RealmList<Transaction> = RealmList()
set(value) {
field = value
@ -90,85 +45,19 @@ open class Result : RealmObject(), Filterable {
// 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
@Ignore
val session: Session? = this.sessions?.firstOrNull()
/**
* Returns 1 if the session is positive
*/
val isPositive: Int
get() {
return if (session?.isTournament() == true) {
if ((this.cashout ?: -1.0) > 0.0) 1 else 0 // if cashout is null we want to count a negative session
} else {
if (this.net >= 0.0) 1 else 0
}
}
// Computes the Net
private fun computeNet(withBuyin: Boolean? = null) {
val transactionsSum = transactions.sumOf { it.amount }
// choose the method to compute the net
var useBuyin = withBuyin ?: true
if (withBuyin == null) {
if (netResult != null) {
useBuyin = false
} else if (buyin != null || cashout != null) {
useBuyin = true
} else {
this.session?.let { session ->
if (session.isCashGame() && !session.isLive) {
useBuyin = false
}
}
}
}
if (useBuyin) {
val buyin = this.buyin ?: 0.0
val cashOut = this.cashout ?: 0.0
this.net = cashOut - buyin + transactionsSum
} else {
val netResult = this.netResult ?: 0.0
this.net = netResult + transactionsSum
}
// Precompute results
this.session?.computeStats()
this.session?.sessionSet?.computeStats()
}
// Computes the number of rebuy
fun computeNumberOfRebuy() {
this.session?.let {
if (it.isCashGame()) {
it.cgBiggestBet?.let { bb ->
if (bb > 0.0) {
this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0)
} else {
this.numberOfRebuy = null
}
}
} else {
it.tournamentEntryFee?.let { entryFee ->
if (entryFee > 0.0) {
this.numberOfRebuy = (this.buyin ?: 0.0) / entryFee
} else {
this.numberOfRebuy = null
}
}
}
fun computeNet() {
val transactionsSum = transactions.sumByDouble { it.amount }
this.netResult?.let {
this.net = it + transactionsSum
} ?: run {
val buyin = this.buyin ?: 0.0
val cashOut = this.cashout ?: 0.0
this.net = cashOut - buyin + transactionsSum
}
}
// @todo tips?
}
// @todo tips?
}

@ -1,146 +1,63 @@
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.Stat
import net.pokeranalytics.android.calculus.StatFormattingException
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.Timed
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.TextFormat
import java.text.DateFormat
import java.util.*
open class SessionSet() : RealmObject(), Timed, Filterable {
@PrimaryKey
override var id = UUID.randomUUID().toString()
var startDate: Date = Date()
set(value) {
field = value
this.computeNetDuration()
open class SessionSet() : RealmObject() {
/**
* The timeframe of the set, i.e. its start & end date
*/
var timeFrame: TimeFrame? = null
/**
* The list of sessions associated with this set
*/
@LinkingObjects("sessionSet")
val sessions: RealmResults<Session>? = null
@Ignore // a duration shortcut
var duration: Long = 0L
get() {
return this.timeFrame?.duration ?: 0L
}
var endDate: Date = Date()
set(value) {
field = value
this.computeNetDuration()
@Ignore // a duration in hour
var hourlyDuration: Double = 0.0
get() {
return this.timeFrame?.hourlyDuration ?: 0.0
}
override fun startDate(): Date? {
return this.startDate
}
@Ignore // a netResult shortcut
var netResult: Double = 0.0
get () {
return this.sessions?.sumByDouble { it.value } ?: 0.0
}
override fun endDate(): Date {
return this.endDate
}
@Ignore // a duration shortcut
var hourlyRate: Double = 0.0
override var breakDuration: Long = 0L
set(value) {
field = value
this.computeNetDuration()
}
@Ignore
var estimatedHands: Double = 25.0 * (this.timeFrame?.hourlyDuration?.toDouble() ?: 0.0)
/**
* The start date of the break
*/
override var pauseDate: Date? = null
@Ignore
var bbNetResult: Double = 0.0
/**
* the net duration of the set (READONLY)
*/
override var netDuration: Long = 0L
companion object {
fun computeStats() {
this.ratedNet = this.sessions?.sumOf { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0
this.estimatedHands = this.sessions?.sumOf { it.estimatedHands } ?: 0.0
this.bbNet = this.sessions?.sumOf { it.bbNet } ?: 0.0
this.breakDuration = this.sessions?.max("breakDuration")?.toLong() ?: 0L
}
fun newInstance(realm: Realm) : SessionSet {
val sessionSet: SessionSet = realm.createObject(SessionSet::class.java)
sessionSet.timeFrame = realm.createObject(TimeFrame::class.java)
return sessionSet
}
/**
* The list of endedSessions associated with this set
*/
@LinkingObjects("sessionSet")
val sessions: RealmResults<Session>? = null
}
var ratedNet: Double = 0.0
val hourlyRate: Double
get() {
return this.ratedNet / this.hourlyDuration
}
var estimatedHands: Double = 0.0
var bbNet: BB = 0.0
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"),
ESTIMATED_HANDS("estimatedHands"),
NET_DURATION("netDuration")
}
companion object {
fun newInstance(realm: Realm) : SessionSet {
val sessionSet = SessionSet()
return realm.copyToRealm(sessionSet)
}
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? {
Session.fieldNameForQueryType(queryCondition)?.let {
return "sessions.$it"
}
return null
}
}
// Stat Base
override fun entryTitle(context: Context): String {
return DateFormat.getDateInstance(DateFormat.SHORT).format(this.startDate)
}
override fun formattedValue(stat: Stat) : TextFormat {
return when (stat) {
Stat.NET_RESULT, Stat.AVERAGE -> stat.textFormat(this.ratedNet, currency = null)
Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> stat.textFormat(this.hourlyDuration, currency = null)
Stat.HOURLY_RATE -> stat.textFormat(this.hourlyRate, currency = null)
Stat.HANDS_PLAYED -> stat.textFormat(this.estimatedHands, currency = null)
Stat.HOURLY_RATE_BB -> stat.textFormat(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.textFormat(this.estimatedHands, currency = null)
} else {
return TextFormat(NULL_TEXT)
}
}
else -> throw StatFormattingException("format undefined for stat ${stat.name}")
}
}
@Ignore
override val realmObjectClass: Class<out Identifiable> = SessionSet::class.java
}

@ -1,288 +1,230 @@
//package net.pokeranalytics.android.model.realm
//
//import io.realm.RealmObject
//import io.realm.RealmQuery
//import io.realm.RealmResults
//import io.realm.annotations.Ignore
//import io.realm.annotations.LinkingObjects
//import net.pokeranalytics.android.exceptions.ModelException
//import timber.log.Timber
//import java.util.*
//
//open class TimeFrame : RealmObject() {
//
// // A start date
// var startDate: Date = Date()
// private set(value) {
// field = value
// this.computeNetDuration()
// }
//
// // An end date
// var endDate: Date? = null
// private set(value) {
// field = value
// this.computeNetDuration()
// }
//
// // The latest pause date
// var pauseDate: Date? = null
// set(value) {
// field?.let {
// if (value == null && field != null) {
// breakDuration += Date().time - it.time
// }
// }
// field = value
// this.computeNetDuration()
// }
//
// // The break netDuration
// var breakDuration: Long = 0L
// set(value) {
// field = value
// this.computeNetDuration()
// }
//
// // the total netDuration
// var netDuration: Long = 0L
// private set
//
// var hourlyDuration: Double = 0.0
// get() {
// return this.netDuration / 3600000.0 // 3.6 millions of milliseconds
// }
//
// // Session
// @LinkingObjects("timeFrame")
// private val endedSessions: RealmResults<Session>? = null // we should have only one session
//
// @Ignore
// var session: Session? = null
// get() = if (this.endedSessions != null && this.endedSessions.isEmpty()) null else this.endedSessions?.first()
//
// // Group
// @LinkingObjects("timeFrame")
// private val sets: RealmResults<SessionSet>? = null // we should have only one sessionGroup
//
// @Ignore
// var set: SessionSet? = null
// get() = this.sets?.first()
//
// fun setStart(startDate: Date) {
// this.startDate = startDate
// this.session?.let {
// this.notifySessionDateChange(it)
// }
// }
//
// fun setEnd(endDate: Date?) {
// this.endDate = endDate
// this.session?.let {
// this.notifySessionDateChange(it)
// }
// }
//
// fun setDate(startDate: Date, endDate: Date?) {
// this.startDate = startDate
// this.endDate = endDate
//
// this.session?.let {
// this.notifySessionDateChange(it)
// }
// }
//
// /**
// * Computes the net netDuration of the session
// */
// private fun computeNetDuration() {
// var endDate: Date = this.endDate ?: Date()
// this.netDuration = endDate.time - this.startDate.time - this.breakDuration
// }
//
// /**
// * Queries all time frames that might be impacted by the date change
// * Makes all necessary changes to keep sequential time frames
// */
// fun notifySessionDateChange(owner: Session) {
//
// var query: RealmQuery<SessionSet> = this.realm.where(SessionSet::class.java)
// query.isNotNull("timeFrame")
//
//// Timber.d("this> sd = : ${this.startDate}, ed = ${this.endDate}")
//
// val sets = realm.where(SessionSet::class.java).findAll()
//// Timber.d("set count = ${sets.size}")
//
// if (this.endDate == null) {
// query.greaterThanOrEqualTo("timeFrame.startDate", this.startDate)
// .or()
// .greaterThanOrEqualTo("timeFrame.endDate", this.startDate)
// .or()
// .isNull("timeFrame.endDate")
// } else {
// val endDate = this.endDate!!
// query
// .lessThanOrEqualTo("timeFrame.startDate", this.startDate)
// .greaterThanOrEqualTo("timeFrame.endDate", this.startDate)
// .or()
// .lessThanOrEqualTo("timeFrame.startDate", endDate)
// .greaterThanOrEqualTo("timeFrame.endDate", endDate)
// .or()
// .greaterThanOrEqualTo("timeFrame.startDate", this.startDate)
// .lessThanOrEqualTo("timeFrame.endDate", endDate)
// .or()
// .isNull("timeFrame.endDate")
// .lessThanOrEqualTo("timeFrame.startDate", endDate)
// }
//
// val sessionGroups = query.findAll()
//
// this.updateTimeFrames(sessionGroups, owner)
//
// }
//
// /**
// * Update Time frames from sets
// */
// private fun updateTimeFrames(sessionSets: RealmResults<SessionSet>, owner: Session) {
//
// when (sessionSets.size) {
// 0 -> this.createOrUpdateSessionSet(owner)
// 1 -> this.updateSessionGroup(owner, sessionSets.first()!!)
// else -> this.mergeSessionGroups(owner, sessionSets)
// }
//
// }
//
// /**
// * Creates the session sessionGroup when the session has none
// */
// private fun createOrUpdateSessionSet(owner: Session) {
//
// val set = owner.sessionSet
// if (set != null) {
// set.timeFrame?.startDate = this.startDate
// set.timeFrame?.endDate = this.endDate
// } else {
// this.createSessionSet(owner)
// }
//
//// Timber.d("sd = : ${set.timeFrame?.startDate}, ed = ${set.timeFrame?.endDate}")
// Timber.d("netDuration 1 = : ${set?.timeFrame?.netDuration}")
//
// }
//
// fun createSessionSet(owner: Session) {
// val set: SessionSet = SessionSet.newInstanceForResult(this.realm)
// set.timeFrame?.let {
// it.startDate = this.startDate
// it.endDate = this.endDate
// } ?: run {
// throw ModelException("TimeFrame should never be null here")
// }
//
// owner.sessionSet = set
// }
//
//
// /**
// * Single SessionSet update, the session might be the owner
// * Changes the sessionGroup timeframe using the current timeframe dates
// */
// private fun updateSessionGroup(owner: Session, sessionSet: SessionSet) {
//
// var timeFrame: TimeFrame = sessionSet.timeFrame!! // tested in the query
//// timeFrame.setDate(this.startDate, this.endDate)
//
// val sisterSessions = sessionSet.endedSessions!! // shouldn't crash ever
//
// // if we have only one session in the set and that it corresponds to the set
// if (sessionSet.endedSessions?.size == 1 && sessionSet.endedSessions?.first() == owner) {
// timeFrame.setDate(this.startDate, this.endDate)
// } else { // there are 2+ endedSessions to manage and possible splits
//
// val endDate = this.endDate
//
// // case where all endedSessions are over but the set is not, we might have a split, so we delete the set and save everything again
// if (endDate != null && sisterSessions.all { it.timeFrame?.endDate != null } && timeFrame.endDate == null) {
// var endedSessions = mutableListOf<Session>(owner)
// sessionSet.endedSessions?.forEach { endedSessions.add(it) }
// sessionSet.deleteFromRealm()
// endedSessions.forEach { it.timeFrame?.notifySessionDateChange(it) }
// } else {
//
// if (this.startDate.before(timeFrame.startDate)) {
// timeFrame.startDate = this.startDate
// }
// if (endDate != null && timeFrame.endDate != null && endDate.after(timeFrame.endDate)) {
// timeFrame.endDate = endDate
// } else if (endDate == null) {
// timeFrame.endDate = null
// }
//
// owner.sessionSet = sessionSet
//
//// Timber.d("sd = : ${sessionSet.timeFrame?.startDate}, ed = ${sessionSet.timeFrame?.endDate}")
// Timber.d("netDuration 2 = : ${sessionSet.timeFrame?.netDuration}")
// }
//
// }
//
// }
//
// /**
// * Multiple session sets update:
// * Merges all sets into one (delete all then create a new one)
// */
// private fun mergeSessionGroups(owner: Session, sessionSets: RealmResults<SessionSet>) {
//
// var startDate: Date = this.startDate
// var endDate: Date? = this.endDate
//
// // find earlier and later dates from all sets
// val timeFrames = sessionSets.mapNotNull { it.timeFrame }
// timeFrames.forEach { tf ->
// if (tf.startDate.before(startDate)) {
// startDate = tf.startDate
// }
//
// endDate?.let { ed ->
// tf.endDate?.let { tfed ->
// if (tfed.after(ed)) {
// endDate = tfed
// }
// }
// } ?: run {
// endDate = tf.endDate
// }
//
// }
//
// // get all endedSessions from sets
// var endedSessions = mutableSetOf<Session>()
// sessionSets.forEach { set ->
// set.endedSessions?.asIterable()?.let { endedSessions.addAll(it) }
// }
//
// // delete all sets
// sessionSets.deleteAllFromRealm()
//
// // Create a new sets
// val set: SessionSet = SessionSet.newInstanceForResult(this.realm)
// set.timeFrame?.let {
// it.setDate(startDate, endDate)
// } ?: run {
// throw ModelException("TimeFrame should never be null here")
// }
//
// // Add the session linked to this timeframe to the new sessionGroup
// owner.sessionSet = set
//
// // Add all orphan endedSessions
// endedSessions.forEach { it.sessionSet = set }
// Timber.d("netDuration 3 = : ${set.timeFrame?.netDuration}")
//
// }
//
//}
package net.pokeranalytics.android.model.realm
import io.realm.Realm
import io.realm.RealmObject
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects
import net.pokeranalytics.android.exceptions.ModelException
import timber.log.Timber
import java.util.*
open class TimeFrame : RealmObject() {
// A start date
var startDate: Date = Date()
private set(value) {
field = value
this.computeDuration()
}
// An end date
var endDate: Date? = null
private set(value) {
field = value
this.computeDuration()
}
// The break duration
var breakDuration: Long = 0L
set(value) {
field = value
this.computeDuration()
}
// the total duration
var duration: Long = 0L
private set
var hourlyDuration: Double = 0.0
get() {
return this.duration / 3600000.0 // 3.6 millions of milliseconds
}
// indicates a state of pause
var paused: Boolean = false
// Session
@LinkingObjects("timeFrame")
private val sessions: RealmResults<Session>? = null // we should have only one session
@Ignore
var session: Session? = null
get() = if (this.sessions != null && this.sessions.isEmpty()) null else this.sessions?.first()
// Group
@LinkingObjects("timeFrame")
private val sets: RealmResults<SessionSet>? = null // we should have only one sessionGroup
@Ignore
var set: SessionSet? = null
get() = this.sets?.first()
fun setDate(startDate: Date?, endDate: Date?) {
startDate?.let {
this.startDate = startDate
}
this.endDate = endDate
this.computeDuration()
if (this.session != null) {
this.notifySessionDateChange()
}
}
private fun computeDuration() {
var endDate: Date = this.endDate ?: Date()
val netDuration = endDate.time - this.startDate.time - this.breakDuration
this.duration = netDuration
}
fun notifySessionDateChange() {
val realm = Realm.getDefaultInstance()
var query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java)
query.isNotNull("timeFrame")
// Timber.d("this> sd = : ${this.startDate}, ed = ${this.endDate}")
if (this.endDate == null) {
query.greaterThan("timeFrame.startDate", this.startDate.time).or().greaterThan("timeFrame.endDate", this.startDate.time)
} else {
val endDate = this.endDate!!
query
.lessThan("timeFrame.startDate", this.startDate)
.greaterThan("timeFrame.endDate", this.startDate)
.or()
.lessThan("timeFrame.startDate", endDate)
.greaterThan("timeFrame.endDate", endDate)
.or()
.greaterThan("timeFrame.startDate", this.startDate)
.lessThan("timeFrame.endDate", endDate)
}
val sessionGroups = query.findAll()
this.updateTimeFrames(sessionGroups)
// realm.close()
}
/**
* Update Time frames from sets
*/
private fun updateTimeFrames(sessionSets: RealmResults<SessionSet>) {
when (sessionSets.size) {
0 -> this.createSessionGroup()
1 -> this.updateSingleSessionGroup(sessionSets.first()!!)
else -> this.mergeSessionGroups(sessionSets)
}
}
/**
* Creates the session sessionGroup when the session has none
*/
private fun createSessionGroup() {
val realm = Realm.getDefaultInstance()
val set: SessionSet = SessionSet.newInstance(realm)
set.timeFrame?.let {
it.startDate = this.startDate
it.endDate = this.endDate
} ?: run {
throw ModelException("TimeFrame should never be null here")
}
this.session?.let {
it.sessionSet = set
} ?: run {
throw ModelException("Session should never be null here")
}
Timber.d("sd = : ${set.timeFrame?.startDate}, ed = ${set.timeFrame?.endDate}")
}
/**
* Single session sessionGroup update
* Changes the sessionGroup timeframe using the current timeframe dates
*/
private fun updateSingleSessionGroup(sessionSet: SessionSet) {
var groupTimeFrame: TimeFrame = sessionSet.timeFrame!! // tested in the query
if (this.startDate.before(groupTimeFrame.startDate)) {
groupTimeFrame.startDate = this.startDate
}
val endDate = this.endDate
if (endDate != null && groupTimeFrame.endDate != null && endDate.after(groupTimeFrame.endDate)) {
groupTimeFrame.endDate = endDate
} else if (endDate == null) {
groupTimeFrame.endDate = null
}
this.session?.sessionSet = sessionSet
}
/**
* Multiple session sets update:
* Merges all sets into one (delete all then create a new one)
*/
private fun mergeSessionGroups(sessionSets: RealmResults<SessionSet>) {
var startDate: Date = this.startDate
var endDate: Date? = this.endDate
// find earlier and later dates from all sets
val timeFrames = sessionSets.mapNotNull { it.timeFrame }
timeFrames.forEach { tf ->
if (tf.startDate.before(startDate)) {
startDate = tf.startDate
}
endDate?.let { ed ->
tf.endDate?.let { tfed ->
if (tfed.after(ed)) {
endDate = tfed
}
}
} ?: run {
endDate = tf.endDate
}
}
// get all sessions from sets
var sessions = mutableSetOf<Session>()
sessionSets.forEach { it.sessions?.asIterable()?.let { it1 -> sessions.addAll(it1) } }
// delete all sets
sessionSets.deleteAllFromRealm()
// Create a new sets
val set: SessionSet = SessionSet.newInstance(realm)
set.timeFrame?.let {
it.startDate = startDate
it.endDate = endDate
} ?: run {
throw ModelException("TimeFrame should never be null here")
}
// Add the session linked to this timeframe to the new sessionGroup
this.sessions?.first()?.let {
it.sessionSet = set
} ?: run {
throw ModelException("TimeFrame should never be null here")
}
// Add all orphan sessions
sessions.forEach { it.sessionSet = set }
}
}

@ -1,92 +1,60 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.Realm
import io.realm.RealmModel
import android.text.InputType
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.UsageCountable
import net.pokeranalytics.android.model.interfaces.Identifiable
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.model.ObjectSavable
import net.pokeranalytics.android.ui.adapter.components.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetData
import net.pokeranalytics.android.ui.view.RowEditable
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.ui.view.rows.TournamentFeatureRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.ui.view.SimpleRow
import net.pokeranalytics.android.ui.view.TournamentFeatureRow
import java.util.*
import kotlin.collections.ArrayList
open class TournamentFeature : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource, UsageCountable {
companion object {
val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.addAll(TournamentFeatureRow.values())
rows
}
}
@Ignore
override val realmObjectClass: Class<out Identifiable> = TournamentFeature::class.java
open class TournamentFeature : RealmObject(), RowRepresentableDataSource, RowEditable, ObjectSavable {
@PrimaryKey
override var id = UUID.randomUUID().toString()
var id = UUID.randomUUID().toString()
// The name of the feature
override var name: String = ""
// CountableUsage
override var useCount: Int = 0
var name: String = ""
@Ignore
override val ownerClass: Class<out RealmModel> = Session::class.java
override fun getDisplayName(context: Context): String {
return this.name
override fun uniqueIdentifier(): String {
return this.id
}
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override val adapterRows: ArrayList<RowRepresentable>
get() {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.addAll(TournamentFeatureRow.values())
return rows
}
override fun charSequenceForRow(
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
else -> return super.charSequenceForRow(row, context, 0)
SimpleRow.NAME -> this.name
else -> return super.stringForRow(row)
}
}
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? {
return row.editingDescriptors(mapOf(
"defaultValue" to this.name))
}
override fun updateValue(value: Any?, row: RowRepresentable) {
override fun getBottomSheetData(row: RowRepresentable): ArrayList<BottomSheetData> {
val data = java.util.ArrayList<BottomSheetData>()
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
SimpleRow.NAME -> data.add(BottomSheetData(this.name, SimpleRow.NAME.resId, InputType.TYPE_CLASS_TEXT))
}
return data
}
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
return when (status) {
SaveValidityStatus.DATA_INVALID -> R.string.tournament_feature_empty_field_error
SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_tournament_feature_error
else -> super.getFailedSaveMessage(status)
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
}
}
override fun isValidForDelete(realm: Realm): Boolean {
return realm.where<Session>().equalTo("tournamentFeatures.id", id).findAll().isEmpty()
override fun isValidForSave(): Boolean {
return this.name.isNotEmpty()
}
}

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

Loading…
Cancel
Save