Compare commits

..

1 Commits

Author SHA1 Message Date
Razmig Sarkissian 757da5623a merge interface wip 7 years ago
  1. 5
      .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. 246
      app/src/main/AndroidManifest.xml
  31. BIN
      app/src/main/ic_launcher-playstore.png
  32. 112
      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. 670
      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. 337
      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. 170
      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. 182
      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. 338
      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. 197
      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. 132
      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. 75
      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. 92
      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. 141
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  97. 1134
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  98. 133
      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. 92
      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

5
.gitignore vendored

@ -6,11 +6,8 @@
# Built application files # Built application files
*.apk *.apk
*.ap_ *.ap_
*.aab
/app/release /app/release
app/*2020/
# Files for the Dalvik VM # Files for the Dalvik VM
*.dex *.dex
@ -18,7 +15,7 @@ app/*2020/
*.class *.class
# Generated files # Generated files
bin bin/
gen/ gen/
# Gradle files # Gradle files

@ -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: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
//apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android' apply plugin: 'realm-android'
// Crashlytics //apply plugin: 'io.fabric'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
// Serialization
apply plugin: "kotlinx-serialization"
repositories { repositories {
maven { url 'https://jitpack.io' } // required for MPAndroidChart maven { url 'https://maven.fabric.io/public' }
jcenter() // for kotlin serialization
} }
android { android {
compileSdkVersion 35 compileSdkVersion 28
buildToolsVersion "30.0.3"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
applicationId "net.pokeranalytics.android" applicationId "net.pokeranalytics.android"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 35 targetSdkVersion 28
versionCode 180 versionCode 1
versionName "6.0.38" versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { 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 { release {
minifyEnabled true minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 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 { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// 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"
// Android // Android
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.core:core-ktx:1.1.0-alpha04'
implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.0.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'
// Places // Firebase
implementation 'com.google.android.libraries.places:places:2.3.0' implementation 'com.google.firebase:firebase-core:16.0.7'
// Billing / Subscriptions // Crashlytics
implementation 'com.android.billingclient:billing:7.0.0' //implementation 'com.crashlytics.sdk.android:crashlytics:2.9.9'
// Import the BoM for the Firebase platform // Kotlin
implementation platform('com.google.firebase:firebase-bom:26.1.0') implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
// Declare the dependencies for the Crashlytics and Analytics libraries implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
// 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'
// Logs // Logs
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
// MPAndroidChart // Test
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' androidTestImplementation 'androidx.test:core:1.0.0'
androidTestImplementation 'androidx.test:runner:1.1.0'
// CSV Parser: https://mvnrepository.com/artifact/org.apache.commons/commons-csv androidTestImplementation 'androidx.test:rules:1.1.0'
implementation 'org.apache.commons:commons-csv:1.7' androidTestImplementation 'androidx.test.ext:junit:1.1.0'
// 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"
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Volley
implementation 'com.android.volley:volley:1.2.1'
// Instrumented Tests // Required -- JUnit 4 framework
androidTestImplementation 'androidx.test:core:1.6.1' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.6.2' // Optional -- Robolectric environment
androidTestImplementation 'androidx.test:rules:1.6.1' // testImplementation 'androidx.test:core:1.1.0'
androidTestImplementation 'androidx.test.ext:junit:1.2.1' // Optional -- Mockito framework
testImplementation 'com.android.support.test:runner:1.0.1'
testImplementation 'com.android.support.test:rules:1.0.1'
// Test // testImplementation 'androidx.test.espresso:espresso-core:3.1.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'com.android.support.test:runner:1.0.2'
testImplementation 'com.android.support.test:rules:1.0.2'
// 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": [
{ {
"client_info": { "client_info": {
"mobilesdk_app_id": "1:245968016816:android:e5597a41d79df0a31d7275", "mobilesdk_app_id": "1:245968016816:android:47f8b4f74b1296b4",
"android_client_info": { "android_client_info": {
"package_name": "net.pokeranalytics.android" "package_name": "net.pokeranalytics.android"
} }
}, },
"oauth_client": [ "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_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com",
"client_type": 3 "client_type": 3
@ -25,20 +33,20 @@
} }
], ],
"services": { "services": {
"analytics_service": {
"status": 1
},
"appinvite_service": { "appinvite_service": {
"status": 2,
"other_platform_oauth_client": [ "other_platform_oauth_client": [
{ {
"client_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com", "client_id": "245968016816-756j040n0luup2nlfu9e49qm9jv0oih2.apps.googleusercontent.com",
"client_type": 3 "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 # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-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"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> package="net.pokeranalytics.android">
<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 <application
android:name=".PokerAnalyticsApplication"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:largeHeap="true" android:name=".PokerAnalyticsApplication"
android:theme="@style/PokerAnalyticsTheme"> android:theme="@style/PokerAnalyticsTheme">
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.HomeActivity" android:name=".ui.activity.HomeActivity"
android:label="@string/app_name" android:label="@string/app_name">
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".ui.activity.DataListActivity" />
<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 <activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity" android:name=".ui.activity.SessionActivity"
android:launchMode="singleTop" android:launchMode="singleTop"/>
android:screenOrientation="portrait"
android:exported="true" />
<activity <activity android:name=".ui.activity.EditableDataActivity"/>
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 <meta-data
android:name="preloaded_fonts" android:name="preloaded_fonts"
android:resource="@array/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> </application>
</manifest> </manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

@ -1,114 +1,58 @@
package net.pokeranalytics.android package net.pokeranalytics.android
import android.app.Application import android.app.Application
import android.content.Context
import android.os.Build
import com.google.firebase.FirebaseApp
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.kotlin.where import io.realm.RealmResults
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 net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.utils.Seed import net.pokeranalytics.android.util.PokerAnalyticsLogs
import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.billing.AppGuard
import timber.log.Timber import timber.log.Timber
import java.util.*
class PokerAnalyticsApplication : Application() { class PokerAnalyticsApplication: Application() {
var reportWhistleBlower: ReportWhistleBlower? = null var sessions: RealmResults<Session>? = null
var backupOperator: BackupOperator? = null
companion object { // private val listener: OrderedRealmCollectionChangeListener<RealmResults<Session>> =
// OrderedRealmCollectionChangeListener() { realmResults: RealmResults<Session>, changeSet: OrderedCollectionChangeSet ->
fun timeSinceInstall(context: Context): Long { //
val installTime: Long = context.packageManager.getPackageInfo(context.packageName, 0).firstInstallTime // if (changeSet == null) {
return System.currentTimeMillis() - installTime // 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() { override fun onCreate() {
super.onCreate() super.onCreate()
if (!BuildConfig.DEBUG) {
FirebaseApp.initializeApp(this)
}
UserDefaults.init(this)
// AppGuard / Billing services
AppGuard.load(this.applicationContext)
// Realm // Realm
Realm.init(this) Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder() val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME) .name(Realm.DEFAULT_REALM_NAME)
.schemaVersion(14) .deleteRealmIfMigrationNeeded()
.allowWritesOnUiThread(true)
.migration(PokerAnalyticsMigration())
.initialData(Seed(this))
.build() .build()
Realm.setDefaultConfiguration(realmConfiguration) Realm.setDefaultConfiguration(realmConfiguration)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val realm: Realm = Realm.getDefaultInstance()
val locales = resources.configuration.locales // Add observer on session time frame changes
CrashLogging.log("App onCreate. Locales = $locales") 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) { if (BuildConfig.DEBUG) {
// Logs // Logs
Timber.plant(PokerAnalyticsLogs()) Timber.plant(PokerAnalyticsLogs())
} } else {
Timber.d("SDK version = ${Build.VERSION.SDK_INT}") //Fabric.with(this, Crashlytics())
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)
}
} }
} }

@ -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 package net.pokeranalytics.android.calculus
import android.content.Context
import io.realm.Realm
import net.pokeranalytics.android.calculus.Stat.* import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.realm.SessionSet
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
/** /**
* The class performing statIds computation * The class performing stats computation
*/ */
class Calculator { class Calculator {
/** /**
* The options used for calculations and display * The options used for calculations or display
*/ */
class Options( 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 * The way the stats are going to be displayed
*/ */
// var progressValues: ProgressValues = progressValues enum class Display {
// get() { TABLE,
// if (field == ProgressValues.NONE && this.display.requireProgressValues) { EVOLUTION,
// return ProgressValues.STANDARD COMPARISON,
// } MAP,
// return field POLYNOMIAL
// }
init {
this.aggregationType?.let {
this.criterias = it.criterias
} }
}
/** /**
* The type of evolution numericValues * The type of evolution values
*/ */
enum class ProgressValues { enum class EvolutionValues {
NONE, NONE,
STANDARD, STANDARD,
TIMED 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 * This function determines whether the standard deviation should be computed
*/ */
val computeStandardDeviation: Boolean fun shouldComputeStandardDeviation() : Boolean {
get() { this.displayedStats.forEach { stat ->
this.stats.forEach { return when (stat) {
if (it.isStandardDeviation) { STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> true
return true else -> false
} }
} }
return false return false
} }
/** // var aggregation: Aggregation? = null
* 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 { companion object {
fun computeStatsWithEvolutionByAggregationType( fun computePreAggregation(sets: List<SessionSet>, options: Options): List<ComputedResults> {
realm: Realm, return listOf()
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 * Computes all stats for list of Session sessionGroup
*/ */
fun computeGroups(realm: Realm, groups: List<ComputableGroup>, options: Options = Options()): Report { fun computeGroups(groups: List<SessionGroup>, options: Options): List<ComputedResults> {
val report = Report(options) var computedResults: MutableList<ComputedResults> = mutableListOf()
groups.forEach { group -> groups.forEach { group ->
// Computes actual sessionGroup stats
// Clean existing computables / sessionSets if group is reused val results: ComputedResults = Calculator.compute(group, options = options)
group.cleanup()
// Computes actual sessionGroup statIds
val results: ComputedResults = this.compute(realm, group, options)
// Computes the compared sessionGroup if existing // Computes the compared sessionGroup if existing
val comparedGroup = group.comparedGroup val comparedGroup = group.comparedSessions
if (comparedGroup != null) { if (comparedGroup != null) {
val comparedResults = this.compute(realm, comparedGroup, options) val comparedResults = Calculator.compute(comparedGroup, options = options)
group.comparedComputedResults = comparedResults group.comparedComputedResults = comparedResults
results.computeStatVariations(comparedResults) results.computeStatVariations(comparedResults)
} }
if (options.shouldManageMultiGroupProgressValues) { results.finalize(options) // later treatment, such as evolution values sorting
group.comparedComputedResults = report.results.lastOrNull() computedResults.add(results)
} }
results.finalize() // later treatment, such as evolution numericValues sorting return computedResults
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 * Computes stats for a SessionSet
*/ */
fun compute(realm: Realm, computableGroup: ComputableGroup, options: Options = Options()): ComputedResults { fun compute(sessionGroup: SessionGroup, 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() val sessions: List<SessionInterface> = sessionGroup.sessions
results.addStat(TOTAL_TIPS, totalTips) var sessionSets = sessionGroup.sessions.mapNotNull { it.sessionSet }.toHashSet()
// Timber.d("########## totalBuyin = ${totalBuyin} ### sum = ${sum}") var results: ComputedResults = ComputedResults()
val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble() var sum: Double = 0.0
maxNetResult?.let { var totalHands: Double = 0.0
results.addStat(MAXIMUM_NET_RESULT, it) var bbSum: Double = 0.0
} var bbSessionCount: Int = 0
var winningSessionCount: Int = 0
val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble() var totalBuyin: Double = 0.0
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 = // Compute for each session
computable.session ?: throw PAIllegalStateException("Computing lone ComputableResult") var index: Int = 0
results.addEvolutionValue(tSum, stat = NET_RESULT, data = session) sessions.forEach { s ->
results.addEvolutionValue(tSum / index, stat = AVERAGE, data = session) index++;
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 -> sum += s.value
results.addEvolutionValue(roi, stat = ROI, data = session) bbSum += s.bbNetResult
bbSessionCount += s.bigBlindSessionCount
if (s.value >= 0) {
winningSessionCount++
} }
totalBuyin += s.buyin
totalHands += s.estimatedHands
} if (options.evolutionValues == Options.EvolutionValues.STANDARD) {
if (currentStreak >= 0) { results.addEvolutionValue(sum, NETRESULT)
longestWinStreak = max(longestWinStreak, currentStreak) results.addEvolutionValue(sum / index, AVERAGE)
} else { results.addEvolutionValue(index.toDouble(), NUMBER_OF_GAMES)
longestLoseStreak = min(longestLoseStreak, currentStreak) 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)
} }
// loseStreak is negative and we want it positive
results.addStat(LONGEST_STREAKS, longestWinStreak.toDouble(), -longestLoseStreak.toDouble())
} }
val sessionSets = computableGroup.sessionSets(realm, options.shouldSortValues) // Compute for each serie
results.addStat(NUMBER_OF_SETS, sessionSets.size.toDouble()) var duration: Double = 0.0
var hourlyRate: Double = 0.0; var hourlyRateBB: Double = 0.0
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
var gIndex = 0; var gSum = 0.0; var gTotalHands = 0.0; var gBBSum = 0.0;
sessionSets.forEach { sessionSet -> sessionSets.forEach { sessionSet ->
tIndex++ gIndex++
duration += sessionSet.hourlyDuration
val setStats = SSStats(sessionSet, computableGroup.query) gSum += sessionSet.netResult
gTotalHands += sessionSet.estimatedHands
tRatedNetSum += setStats.ratedNet gBBSum += sessionSet.bbNetResult
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)
} hourlyRate = gSum / duration
Options.ProgressValues.TIMED -> { hourlyRateBB = gBBSum / duration
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())
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)
} }
gHourlyDuration = tHourlyDuration
gBBSum = tBBSum
maxDuration = tMaxDuration
} }
var hourlyRate = 0.0 val average: Double = sum / sessions.size.toDouble()
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 // 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 // Standard Deviation
if (options.computeStandardDeviation) { if (options.shouldComputeStandardDeviation()) {
// 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)
}
var stdSum: Double = 0.0
sessions.forEach { s ->
stdSum += Math.pow(s.value - average, 2.0)
} }
val standardDeviation: Double = Math.sqrt(stdSum / sessions.size)
return results 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
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
}
} }

@ -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 package net.pokeranalytics.android.calculus
import android.content.Context
import net.pokeranalytics.android.R 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.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType 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 * An enum representing all the types of Session statistics
*/ */
enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable, PerformanceKey { enum class Stat : RowRepresentable {
NET_RESULT(1), NETRESULT,
BB_NET_RESULT(2), HOURLY_RATE,
HOURLY_RATE(3), AVERAGE,
AVERAGE(4), NUMBER_OF_SETS,
NUMBER_OF_SETS(5), NUMBER_OF_GAMES,
NUMBER_OF_GAMES(6), DURATION,
HOURLY_DURATION(7), AVERAGE_DURATION,
AVERAGE_HOURLY_DURATION(8), NET_BB_PER_100_HANDS,
NET_BB_PER_100_HANDS(9), HOURLY_RATE_BB,
HOURLY_RATE_BB(10), AVERAGE_NET_BB,
AVERAGE_NET_BB(11), WIN_RATIO,
WIN_RATIO(12), AVERAGE_BUYIN,
AVERAGE_BUYIN(13), ROI,
ROI(14), STANDARD_DEVIATION,
STANDARD_DEVIATION(15), STANDARD_DEVIATION_HOURLY,
STANDARD_DEVIATION_HOURLY(16), STANDARD_DEVIATION_BB_PER_100_HANDS,
STANDARD_DEVIATION_BB_PER_100_HANDS(17), HANDS_PLAYED;
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() * 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
} }
val userSelectableList: List<Stat>
get() {
return values().filter { it.canBeUserSelected }
} }
val evolutionValuesList: List<Stat> companion object {
get() {
return values().filter { it.hasProgressValues }
}
fun returnOnInvestment(netResult: Double, buyin: Double): Double? { fun returnOnInvestment(netResult: Double, buyin: Double) : Double {
if (buyin == 0.0) {
return null
}
return netResult / buyin return netResult / buyin
} }
fun netBBPer100Hands(netBB: Double, numberOfHands: Double): Double? { fun netBBPer100Hands(netBB: Double, numberOfHands: Double) : Double {
if (numberOfHands == 0.0) {
return null
}
return netBB / numberOfHands * 100 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? override val resId: Int?
get() { get() {
return when (this) { return when (this) {
NET_RESULT -> R.string.net_result NETRESULT -> R.string.net_result
BB_NET_RESULT -> R.string.total_net_result_bb_
HOURLY_RATE -> R.string.average_hour_rate HOURLY_RATE -> R.string.average_hour_rate
AVERAGE -> R.string.average AVERAGE -> R.string.average
NUMBER_OF_SETS -> R.string.number_of_sessions NUMBER_OF_SETS -> R.string.number_of_groups
NUMBER_OF_GAMES -> R.string.number_of_records NUMBER_OF_GAMES -> R.string.number_of_games
HOURLY_DURATION -> R.string.duration DURATION -> R.string.duration
AVERAGE_HOURLY_DURATION -> R.string.average_hours_played AVERAGE_DURATION -> R.string.average_duration
NET_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands NET_BB_PER_100_HANDS -> R.string.net_bb_per_100_hands
HOURLY_RATE_BB -> R.string.average_hour_rate_bb_ HOURLY_RATE_BB -> R.string.hourly_rate_bb
AVERAGE_NET_BB -> R.string.average_net_result_bb_ AVERAGE_NET_BB -> R.string.average_net_bb
WIN_RATIO -> R.string.win_ratio WIN_RATIO -> R.string.win_ratio
AVERAGE_BUYIN -> R.string.average_buyin AVERAGE_BUYIN -> R.string.average_buyin
ROI -> R.string.tournament_roi ROI -> R.string.roi
STANDARD_DEVIATION -> R.string.standard_deviation STANDARD_DEVIATION -> R.string.standard_deviation
STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_per_hour STANDARD_DEVIATION_HOURLY -> R.string.standard_deviation_hourly
STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.standard_deviation_bb_per_100_hands 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.hands_played
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")
} }
} }
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
}
/** /**
* Formats the value of the stat to be suitable for display * ComputedStat contains a [stat] and their associated [value]
*/ */
fun textFormat(value: Double, secondValue: Double? = null, currency: Currency? = null): TextFormat { class ComputedStat(stat: Stat, value: Double) {
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 constructor(stat: Stat, value: Double, previousValue: Double?) : this(stat, value) {
get() { if (previousValue != null) {
return when (this) { this.variation = (value - previousValue) / previousValue
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 * The statistic type
*/ */
fun cumulativeLabelResId(context: Context): String { var stat: Stat = stat
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 * The stat value
*/ */
val aggregationTypes: List<AggregationType> var value: Double = value
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 * The variation of the stat
*/ */
val hasProgressGraph: Boolean var variation: Double? = null
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 * Formats the value of the stat to be suitable for display
*/ */
val graphSignificantIndividualValue: Boolean fun format() : StatFormat {
get() { return StatFormat()
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 * Returns a StatFormat instance for an evolution value located at the specified [index]
*/ */
val graphShouldShowNumberOfSessions: Boolean fun evolutionValueFormat(index: Int) : StatFormat {
get() { return StatFormat()
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
}
}
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
}
}
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 package net.pokeranalytics.android.model
import android.content.Context
import androidx.fragment.app.Fragment
import io.realm.Realm import io.realm.Realm
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.Sort import io.realm.Sort
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory import net.pokeranalytics.android.ui.adapter.components.LiveDataDataSource
import net.pokeranalytics.android.ui.modules.data.EditableDataActivity
import net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity
import net.pokeranalytics.android.ui.view.Localizable 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 * An enum managing the business objects related to a realm results
@ -20,53 +26,72 @@ enum class LiveData : Localizable {
BANKROLL, BANKROLL,
GAME, GAME,
LOCATION, LOCATION,
TOURNAMENT_NAME,
TOURNAMENT_FEATURE, TOURNAMENT_FEATURE,
TRANSACTION, TRANSACTION_TYPE;
TRANSACTION_TYPE,
FILTER, fun items(realm: Realm, fieldName: String? = null, sortOrder: Sort? = null): RealmResults<*> {
CUSTOM_FIELD, return realm.where(this.relatedEntity).findAll()
REPORT_SETUP, .sort(fieldName ?: this.sortingFieldName, sortOrder ?: this.sorting)
PLAYER, }
HAND_HISTORY;
/**
* 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
}
var subType:Int? = null private var sortingFieldName: String = "name"
private var sorting: Sort = Sort.ASCENDING
val relatedEntity: Class<out Deletable>
private val relatedEntity: Class<out RealmObject>
get() { get() {
return when (this) { return when (this) {
BANKROLL -> Bankroll::class.java BANKROLL -> Bankroll::class.java
GAME -> Game::class.java GAME -> Game::class.java
LOCATION -> Location::class.java LOCATION -> Location::class.java
TOURNAMENT_NAME -> TournamentName::class.java
TOURNAMENT_FEATURE -> TournamentFeature::class.java TOURNAMENT_FEATURE -> TournamentFeature::class.java
TRANSACTION -> Transaction::class.java
TRANSACTION_TYPE -> TransactionType::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 { fun deleteData(realm:Realm, data:ObjectSavable) {
val proxyItem: Deletable? = this.getData(realm, primaryKey) 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 { proxyItem?.let {
return realm.copyFromRealm(it) return proxyItem
} ?: run { } ?: run {
return this.newEntity() return this.newEntity()
} }
} }
private fun newEntity(): Deletable { fun newEntity(): RealmObject {
return this.relatedEntity.newInstance() return when (this) {
BANKROLL -> Bankroll()
GAME -> Game()
LOCATION -> Location()
TOURNAMENT_FEATURE -> TournamentFeature()
TRANSACTION_TYPE -> TransactionType()
}
} }
fun getData(realm: Realm, primaryKey: String?): Deletable? { fun getData(realm: Realm, primaryKey: String?): RealmObject? {
var proxyItem: Deletable? = null var proxyItem: RealmObject? = null
primaryKey?.let { primaryKey?.let {
val t = realm.findById(this.relatedEntity, it) val t = realm.where(this.relatedEntity).equalTo("id", it).findFirst()
t?.let { t?.let {
proxyItem = t proxyItem = t
} }
@ -80,110 +105,17 @@ enum class LiveData : Localizable {
BANKROLL -> R.string.bankroll BANKROLL -> R.string.bankroll
GAME -> R.string.game GAME -> R.string.game
LOCATION -> R.string.location LOCATION -> R.string.location
TOURNAMENT_NAME -> R.string.tournament_name TOURNAMENT_FEATURE -> R.string.tournament_type
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
TRANSACTION_TYPE -> R.string.operation_types 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 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.model.realm.Session
import net.pokeranalytics.android.util.NotificationSchedule
import net.pokeranalytics.android.util.extensions.toCurrency
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
enum class SessionState { enum class SessionState {
PENDING, PENDING,
PLANNED,
STARTED, STARTED,
PAUSED, PAUSED,
FINISHED; FINISHED,
INVALID;
val hasStarted: Boolean
get() {
return when (this) {
STARTED, PAUSED, FINISHED -> true
else -> false
}
}
} }
/** /**
@ -35,146 +17,24 @@ enum class SessionState {
*/ */
fun Session.getState(): SessionState { fun Session.getState(): SessionState {
val start = this.startDate if (timeFrame == null) {
return if (start == null) { return SessionState.PENDING
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 { val endDate = timeFrame?.endDate
parameters.add(it.name) timeFrame?.let {sessionTimeFrame ->
} ?: run { timeFrame?.startDate?.let {startDate ->
parameters.add(getFormattedGame()) if (startDate > Date()) {
tournamentType?.let { type -> return SessionState.PENDING
parameters.add(TournamentType.values()[type].localizedTitle(context)) } else if (endDate != null) {
} return SessionState.FINISHED
} } else if (sessionTimeFrame.paused) {
return SessionState.PAUSED
if (parameters.size == 0) {
parameters.add(context.getString(R.string.tournament).capitalize())
}
} else { } else {
if (this.cgAnte != null || this.cgBlinds != null) { return SessionState.STARTED
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 (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
}
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 package net.pokeranalytics.android.model.realm
import android.content.Context import android.text.InputType
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmObject 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.annotations.PrimaryKey
import io.realm.kotlin.where import net.pokeranalytics.android.model.ObjectSavable
import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.adapter.components.LiveDataDataSource
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus import net.pokeranalytics.android.ui.adapter.components.RowRepresentableDataSource
import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetData
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.ui.view.BankrollRow
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.ui.view.RowEditable
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable import net.pokeranalytics.android.ui.view.SimpleRow
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 java.util.* import java.util.*
import kotlin.collections.ArrayList
enum class ResultCaptureType { open class Bankroll(name: String = "") : RealmObject(), RowRepresentableDataSource,
RowEditable, ObjectSavable {
BUYIN_CASHED_OUT,
NET_RESULT;
companion object { companion object {
val buyinCashedOutFields = listOf( fun newInstance() : Bankroll {
SessionPropertiesRow.CASHED_OUT, var bankroll: Bankroll = Bankroll()
SessionPropertiesRow.BUY_IN, return bankroll
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 @PrimaryKey
override var id = UUID.randomUUID().toString() var id = UUID.randomUUID().toString()
override var name: String = "" // the name of the bankroll
var name: String = name
// Indicates whether the bankroll is live or online // Indicates whether the bankroll is live or online
var live: Boolean = true var live: Boolean = true
/** // The list of transactions of the bankroll
* The list of transactions of the bankroll var transactions: RealmList<Transaction> = RealmList()
*/
@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 // The currency of the bankroll
var currency: Currency? = null var currency: Currency? = null
// The initial value of the bankroll // @todo rate management
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 { override fun uniqueIdentifier(): String {
return realm.where<Session>().equalTo("bankroll.id", id).findAll().isEmpty() return this.id
&& realm.where<Transaction>().equalTo("bankroll.id", id).findAll().isEmpty()
} }
override fun getDeleteStatus(context: Context, realm: Realm): DeleteValidityStatus { override val adapterRows: ArrayList<RowRepresentable>
return if (!realm.where<Session>().equalTo("bankroll.id", id).findAll().isEmpty()) { get() {
DeleteValidityStatus.SESSIONS_LINKED val rows = ArrayList<RowRepresentable>()
} else if (!realm.where<Transaction>().equalTo("bankroll.id", id).findAll().isEmpty()) { rows.add(SimpleRow.NAME)
DeleteValidityStatus.TRANSACTIONS_LINKED rows.addAll(BankrollRow.values())
} else { return rows
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 { override fun stringForRow(row: RowRepresentable): String {
return when (status) { return when (row) {
SaveValidityStatus.DATA_INVALID -> R.string.empty_name_for_br_error SimpleRow.NAME -> this.name
SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_bankroll_name_error BankrollRow.LIVE -> if (this.live) "live" else "online"
else -> super.getFailedSaveMessage(status) BankrollRow.CURRENCY -> this.currency?.code?: ""
else -> return super.stringForRow(row)
} }
} }
fun resultCaptureType(context: Context): ResultCaptureType { override fun boolForRow(row: RowRepresentable): Boolean {
return Preferences.getResultCaptureType(this, context) when (row) {
?: run { BankrollRow.LIVE -> return true
when (this.live) { else -> return super.boolForRow(row)
true -> ResultCaptureType.BUYIN_CASHED_OUT
else -> ResultCaptureType.NET_RESULT
}
} }
} }
companion object { override fun getBottomSheetData(row: RowRepresentable): ArrayList<BottomSheetData> {
val data = java.util.ArrayList<BottomSheetData>()
fun getOrCreate(realm: Realm, name: String, live: Boolean = true, currencyCode: String? = null, currencyRate: Double? = null) : Bankroll { when (row) {
SimpleRow.NAME -> data.add(BottomSheetData(this.name, SimpleRow.NAME.resId, InputType.TYPE_CLASS_TEXT))
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)
}
} }
return data
} }
override fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
SimpleRow.NAME -> this.name = value as String? ?: "" 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 override fun isValidForSave(): Boolean {
get() { val realm = Realm.getDefaultInstance()
this.currency?.code?.let { return (realm.where(Bankroll::class.java)
return java.util.Currency.getInstance(it) .notEqualTo("id", this.id)
} .equalTo("name", this.name)
return UserDefaults.currency .findAll().size == 0)
return this.name.isNotEmpty()
} }
@Ignore
override val realmObjectClass: Class<out Identifiable> = Bankroll::class.java
} }

@ -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 package net.pokeranalytics.android.model.realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.util.UserDefaults
import java.util.* import java.util.*
open class Currency : RealmObject() {
companion object {
@Ignore
val DEFAULT_RATE: Double = 1.0
} open class Currency : RealmObject() {
@PrimaryKey @PrimaryKey
var id = UUID.randomUUID().toString() 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 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 { // The rate of the currency with the main currency
this.code?.let { return it == UserDefaults.currency.currencyCode } var rate: Double? = null
return false
}
} }

@ -1,347 +1,19 @@
package net.pokeranalytics.android.model.realm 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.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey 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 java.util.*
import kotlin.collections.ArrayList
open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource { open class CustomField : RealmObject() {
companion object {
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)
}
}
}
@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 @PrimaryKey
override var id = UUID.randomUUID().toString() 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 { // The name of the currency field
return this.name var name: String = ""
}
override val bottomSheetType: BottomSheetType // @todo
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 package net.pokeranalytics.android.model.realm
import android.content.Context import io.realm.MutableRealmInteger
import io.realm.* import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey 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.* import java.util.*
/** //import net.pokeranalytics.android.FilterComponent
* 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 {
@Ignore
override val realmObjectClass: Class<out Identifiable> = Filter::class.java
companion object {
// Create a new instance
fun newInstance(filterableTypeUniqueIdentifier: Int): Filter {
val filter = Filter()
filter.filterableTypeUniqueIdentifier = filterableTypeUniqueIdentifier
return filter
}
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()
}
}
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
open class Filter : RealmObject() {
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() var id = UUID.randomUUID().toString()
// the queryWith name // the filter name
var name: String = "" 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 { // the number of use of the filter,
return context.getString(R.string.name) // for MutableRealmInteger, see https://realm.io/docs/java/latest/#counters
} val usageCount: MutableRealmInteger = MutableRealmInteger.valueOf(0)
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor>? { // var components: List<FilterComponent> = listOf()
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 package net.pokeranalytics.android.model.realm
import android.content.Context import android.text.InputType
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where import net.pokeranalytics.android.model.ObjectSavable
import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.adapter.components.LiveDataDataSource
import net.pokeranalytics.android.model.interfaces.UsageCountable import net.pokeranalytics.android.ui.adapter.components.RowRepresentableDataSource
import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetData
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.ui.view.GameRow
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.ui.view.RowEditable
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.ui.view.SimpleRow
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 java.util.* 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
companion object {
val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows
}
}
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() var id = UUID.randomUUID().toString()
// The name of the game // The name of the game
override var name: String = "" var name: String = ""
// A shorter name for the game // A shorter name for the game
var shortName: String? = null var shortName: String? = null
// CountableUsage override fun uniqueIdentifier(): String {
override var useCount: Int = 0 return this.id
@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>? { override val adapterRows: ArrayList<RowRepresentable>
return rowRepresentation get() {
val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.addAll(GameRow.values())
return rows
} }
override fun charSequenceForRow( override fun stringForRow(row: RowRepresentable): String {
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
return when (row) { return when (row) {
SimpleRow.NAME -> if (this.name.isNotEmpty()) this.name else NULL_TEXT SimpleRow.NAME -> this.name
GamePropertiesRow.SHORT_NAME -> this.shortName ?: NULL_TEXT GameRow.SHORT_NAME -> this.shortName?:""
else -> return super.charSequenceForRow(row, context, 0) else -> return super.stringForRow(row)
} }
} }
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? { override fun getBottomSheetData(row: RowRepresentable): ArrayList<BottomSheetData> {
return when (row) { val data = java.util.ArrayList<BottomSheetData>()
SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.name)) when (row) {
GamePropertiesRow.SHORT_NAME -> row.editingDescriptors(mapOf("defaultValue" to this.shortName)) SimpleRow.NAME -> data.add(BottomSheetData(this.name, SimpleRow.NAME.resId, InputType.TYPE_CLASS_TEXT))
else -> null GameRow.SHORT_NAME -> data.add(BottomSheetData(this.shortName, GameRow.SHORT_NAME.resId, InputType.TYPE_CLASS_TEXT))
} }
return data
} }
override fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
SimpleRow.NAME -> this.name = value as String? ?: "" SimpleRow.NAME -> this.name = value as String? ?: ""
GamePropertiesRow.SHORT_NAME -> this.shortName = value as String? ?: "" GameRow.SHORT_NAME -> this.shortName = value as String
} }
} }
override fun isValidForSave(): Boolean { override fun isValidForSave(): Boolean {
return name.isNotEmpty() return this.name.isNotEmpty()
}
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 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)
}
private val isOmaha5: Boolean
get() {
return name.contains("5")
}
} }

@ -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,35 +1,25 @@
package net.pokeranalytics.android.model.realm package net.pokeranalytics.android.model.realm
import android.content.Context import android.text.InputType
import com.google.android.libraries.places.api.model.Place
import io.realm.Realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where import net.pokeranalytics.android.model.ObjectSavable
import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.adapter.components.*
import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetData
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.ui.view.RowEditable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.ui.view.LocationRow
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowUpdatable import net.pokeranalytics.android.ui.view.SimpleRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import java.util.* 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 @PrimaryKey
override var id = UUID.randomUUID().toString() var id = UUID.randomUUID().toString()
// The name of the location // The name of the location
override var name: String = "" var name: String = ""
// The readable address of the location
private var address: String? = null
// the longitude of the location // the longitude of the location
var longitude: Double? = null var longitude: Double? = null
@ -37,35 +27,40 @@ open class Location : RealmObject(), NameManageable, RowRepresentable, RowUpdata
// the latitude of the location // the latitude of the location
var latitude: Double? = null var latitude: Double? = null
override fun getDisplayName(context: Context): String { override fun uniqueIdentifier(): String {
return this.name return this.id
} }
override fun updateValue(value: Any?, row: RowRepresentable) { override val adapterRows: ArrayList<RowRepresentable>
when (row) { get() {
SimpleRow.NAME -> this.name = value as String? ?: "" val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME)
rows.addAll(LocationRow.values())
return rows
}
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
SimpleRow.NAME -> this.name
else -> return super.stringForRow(row)
} }
} }
/** override fun getBottomSheetData(row: RowRepresentable): ArrayList<BottomSheetData> {
* Fill the location attributes with a place object val data = java.util.ArrayList<BottomSheetData>()
*/ when (row) {
fun setPlace(place: Place) { SimpleRow.NAME -> data.add(BottomSheetData(this.name, SimpleRow.NAME.resId, InputType.TYPE_CLASS_TEXT))
this.name = place.name ?: "" }
this.address = place.address ?: "" return data
this.latitude = place.latLng?.latitude
this.longitude = place.latLng?.longitude
} }
override fun getFailedSaveMessage(status: SaveValidityStatus): Int { override fun updateValue(value: Any?, row: RowRepresentable) {
return when (status) { when (row) {
SaveValidityStatus.DATA_INVALID -> R.string.location_empty_field_error SimpleRow.NAME -> this.name = value as String? ?: ""
SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_location_error
else -> super.getFailedSaveMessage(status)
} }
} }
override fun isValidForDelete(realm: Realm): Boolean { override fun isValidForSave(): Boolean {
return realm.where<Session>().equalTo("location.id", id).findAll().isEmpty() 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 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.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey 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.* import java.util.*
open class Player : RealmObject(), NameManageable, Savable, Deletable, RowRepresentable, RowUpdatable { open class Player : RealmObject() {
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() var id = UUID.randomUUID().toString()
// The name of the player // 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,84 +2,39 @@ package net.pokeranalytics.android.model.realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects
import io.realm.annotations.RealmClass import io.realm.annotations.RealmClass
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
@RealmClass @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? { // The buyin amount
Session.fieldNameForQueryType(queryCondition)?.let {
return "sessions.$it"
}
return null
}
}
/**
* The buyin amount
*/
var buyin: Double? = null var buyin: Double? = null
set(value) { set(value) {
field = value field = value
this.computeNumberOfRebuy() this.computeNet()
this.computeNet(true)
} }
/** // the cashed out amount
* The cashed out amount
*/
var cashout: Double? = null var cashout: Double? = null
set(value) { set(value) {
field = value field = value
this.computeNet(true) this.computeNet()
if (value != null) {
this.session?.end()
}
} }
/** // The net result
* The net result
*/
var netResult: Double? = null var netResult: Double? = null
set(value) { 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 field = value
this.computeNet(false) this.computeNet()
if (value != null) {
this.session?.end()
}
} }
/** // The net (readonly)
* The pre-computed net (readonly)
*/
var net: Double = 0.0 var net: Double = 0.0
private set private set
/**
* Tips
*/
var tips: Double? = null
set(value) {
field = value
this.session?.computeStats()
}
// The transactions associated with the Result, impacting the result // The transactions associated with the Result, impacting the result
var transactions: RealmList<Transaction> = RealmList() var transactions: RealmList<Transaction> = RealmList()
set(value) { set(value) {
@ -90,85 +45,19 @@ open class Result : RealmObject(), Filterable {
// The tournament final position, if applicable // The tournament final position, if applicable
var tournamentFinalPosition: Int? = null 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 // Computes the Net
private fun computeNet(withBuyin: Boolean? = null) { fun computeNet() {
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 transactionsSum = transactions.sumByDouble { it.amount }
this.netResult?.let {
this.net = it + transactionsSum
} ?: run {
val buyin = this.buyin ?: 0.0 val buyin = this.buyin ?: 0.0
val cashOut = this.cashout ?: 0.0 val cashOut = this.cashout ?: 0.0
this.net = cashOut - buyin + transactionsSum 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
}
}
}
}
} }
// @todo tips? // @todo tips?
} }

@ -1,146 +1,63 @@
package net.pokeranalytics.android.model.realm package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.Realm import io.realm.Realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects 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()
}
var endDate: Date = Date()
set(value) {
field = value
this.computeNetDuration()
}
override fun startDate(): Date? {
return this.startDate
}
override fun endDate(): Date {
return this.endDate
}
override var breakDuration: Long = 0L open class SessionSet() : RealmObject() {
set(value) {
field = value
this.computeNetDuration()
}
/** /**
* The start date of the break * The timeframe of the set, i.e. its start & end date
*/ */
override var pauseDate: Date? = null
/** var timeFrame: TimeFrame? = null
* the net duration of the set (READONLY)
*/
override var netDuration: Long = 0L
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
}
/** /**
* The list of endedSessions associated with this set * The list of sessions associated with this set
*/ */
@LinkingObjects("sessionSet") @LinkingObjects("sessionSet")
val sessions: RealmResults<Session>? = null val sessions: RealmResults<Session>? = null
var ratedNet: Double = 0.0 @Ignore // a duration shortcut
var duration: Long = 0L
val hourlyRate: Double
get() { get() {
return this.ratedNet / this.hourlyDuration return this.timeFrame?.duration ?: 0L
} }
var estimatedHands: Double = 0.0 @Ignore // a duration in hour
var hourlyDuration: Double = 0.0
var bbNet: BB = 0.0
val bbHourlyRate: BB
get() { get() {
return this.bbNet / this.hourlyDuration return this.timeFrame?.hourlyDuration ?: 0.0
} }
enum class Field(val identifier: String) { @Ignore // a netResult shortcut
START_DATE("startDate"), var netResult: Double = 0.0
RATED_NET("ratedNet"), get () {
HOURLY_RATE("hourlyRate"), return this.sessions?.sumByDouble { it.value } ?: 0.0
BB_NET("bbNet"),
ESTIMATED_HANDS("estimatedHands"),
NET_DURATION("netDuration")
} }
companion object { @Ignore // a duration shortcut
var hourlyRate: Double = 0.0
fun newInstance(realm: Realm) : SessionSet { @Ignore
val sessionSet = SessionSet() var estimatedHands: Double = 25.0 * (this.timeFrame?.hourlyDuration?.toDouble() ?: 0.0)
return realm.copyToRealm(sessionSet)
}
fun fieldNameForQueryType(queryCondition: Class < out QueryCondition >): String? {
Session.fieldNameForQueryType(queryCondition)?.let {
return "sessions.$it"
}
return null
}
} @Ignore
var bbNetResult: Double = 0.0
// Stat Base companion object {
override fun entryTitle(context: Context): String { fun newInstance(realm: Realm) : SessionSet {
return DateFormat.getDateInstance(DateFormat.SHORT).format(this.startDate) val sessionSet: SessionSet = realm.createObject(SessionSet::class.java)
sessionSet.timeFrame = realm.createObject(TimeFrame::class.java)
return sessionSet
} }
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 package net.pokeranalytics.android.model.realm
//
//import io.realm.RealmObject import io.realm.Realm
//import io.realm.RealmQuery import io.realm.RealmObject
//import io.realm.RealmResults import io.realm.RealmQuery
//import io.realm.annotations.Ignore import io.realm.RealmResults
//import io.realm.annotations.LinkingObjects import io.realm.annotations.Ignore
//import net.pokeranalytics.android.exceptions.ModelException import io.realm.annotations.LinkingObjects
//import timber.log.Timber import net.pokeranalytics.android.exceptions.ModelException
//import java.util.* import timber.log.Timber
// import java.util.*
//open class TimeFrame : RealmObject() {
// open class TimeFrame : RealmObject() {
// // A start date
// var startDate: Date = Date() // A start date
// private set(value) { var startDate: Date = Date()
// field = value private set(value) {
// this.computeNetDuration() field = value
// } this.computeDuration()
// }
// // An end date
// var endDate: Date? = null // An end date
// private set(value) { var endDate: Date? = null
// field = value private set(value) {
// this.computeNetDuration() field = value
// } this.computeDuration()
// }
// // The latest pause date
// var pauseDate: Date? = null // The break duration
// set(value) { var breakDuration: Long = 0L
// field?.let { set(value) {
// if (value == null && field != null) { field = value
// breakDuration += Date().time - it.time this.computeDuration()
// } }
// }
// field = value // the total duration
// this.computeNetDuration() var duration: Long = 0L
// } private set
//
// // The break netDuration var hourlyDuration: Double = 0.0
// var breakDuration: Long = 0L get() {
// set(value) { return this.duration / 3600000.0 // 3.6 millions of milliseconds
// field = value }
// this.computeNetDuration()
// } // indicates a state of pause
// var paused: Boolean = false
// // the total netDuration
// var netDuration: Long = 0L // Session
// private set @LinkingObjects("timeFrame")
// private val sessions: RealmResults<Session>? = null // we should have only one session
// var hourlyDuration: Double = 0.0
// get() { @Ignore
// return this.netDuration / 3600000.0 // 3.6 millions of milliseconds var session: Session? = null
// } get() = if (this.sessions != null && this.sessions.isEmpty()) null else this.sessions?.first()
//
// // Session // Group
// @LinkingObjects("timeFrame") @LinkingObjects("timeFrame")
// private val endedSessions: RealmResults<Session>? = null // we should have only one session private val sets: RealmResults<SessionSet>? = null // we should have only one sessionGroup
//
// @Ignore @Ignore
// var session: Session? = null var set: SessionSet? = null
// get() = if (this.endedSessions != null && this.endedSessions.isEmpty()) null else this.endedSessions?.first() get() = this.sets?.first()
//
// // Group fun setDate(startDate: Date?, endDate: Date?) {
// @LinkingObjects("timeFrame")
// private val sets: RealmResults<SessionSet>? = null // we should have only one sessionGroup startDate?.let {
// this.startDate = startDate
// @Ignore }
// var set: SessionSet? = null
// get() = this.sets?.first() this.endDate = endDate
//
// fun setStart(startDate: Date) { this.computeDuration()
// this.startDate = startDate
// this.session?.let { if (this.session != null) {
// this.notifySessionDateChange(it) this.notifySessionDateChange()
// } }
// } }
//
// fun setEnd(endDate: Date?) { private fun computeDuration() {
// this.endDate = endDate var endDate: Date = this.endDate ?: Date()
// this.session?.let { val netDuration = endDate.time - this.startDate.time - this.breakDuration
// this.notifySessionDateChange(it) this.duration = netDuration
// } }
// }
// fun notifySessionDateChange() {
// fun setDate(startDate: Date, endDate: Date?) { val realm = Realm.getDefaultInstance()
// this.startDate = startDate var query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java)
// this.endDate = endDate query.isNotNull("timeFrame")
//
// this.session?.let { // Timber.d("this> sd = : ${this.startDate}, ed = ${this.endDate}")
// this.notifySessionDateChange(it)
// } if (this.endDate == null) {
// } query.greaterThan("timeFrame.startDate", this.startDate.time).or().greaterThan("timeFrame.endDate", this.startDate.time)
// } else {
// /** val endDate = this.endDate!!
// * Computes the net netDuration of the session query
// */ .lessThan("timeFrame.startDate", this.startDate)
// private fun computeNetDuration() { .greaterThan("timeFrame.endDate", this.startDate)
// var endDate: Date = this.endDate ?: Date() .or()
// this.netDuration = endDate.time - this.startDate.time - this.breakDuration .lessThan("timeFrame.startDate", endDate)
// } .greaterThan("timeFrame.endDate", endDate)
// .or()
// /** .greaterThan("timeFrame.startDate", this.startDate)
// * Queries all time frames that might be impacted by the date change .lessThan("timeFrame.endDate", endDate)
// * Makes all necessary changes to keep sequential time frames }
// */
// fun notifySessionDateChange(owner: Session) { val sessionGroups = query.findAll()
//
// var query: RealmQuery<SessionSet> = this.realm.where(SessionSet::class.java) this.updateTimeFrames(sessionGroups)
// query.isNotNull("timeFrame")
// // realm.close()
//// Timber.d("this> sd = : ${this.startDate}, ed = ${this.endDate}") }
//
// val sets = realm.where(SessionSet::class.java).findAll() /**
//// Timber.d("set count = ${sets.size}") * Update Time frames from sets
// */
// if (this.endDate == null) { private fun updateTimeFrames(sessionSets: RealmResults<SessionSet>) {
// query.greaterThanOrEqualTo("timeFrame.startDate", this.startDate)
// .or() when (sessionSets.size) {
// .greaterThanOrEqualTo("timeFrame.endDate", this.startDate) 0 -> this.createSessionGroup()
// .or() 1 -> this.updateSingleSessionGroup(sessionSets.first()!!)
// .isNull("timeFrame.endDate") else -> this.mergeSessionGroups(sessionSets)
// } else { }
// val endDate = this.endDate!!
// query }
// .lessThanOrEqualTo("timeFrame.startDate", this.startDate)
// .greaterThanOrEqualTo("timeFrame.endDate", this.startDate) /**
// .or() * Creates the session sessionGroup when the session has none
// .lessThanOrEqualTo("timeFrame.startDate", endDate) */
// .greaterThanOrEqualTo("timeFrame.endDate", endDate) private fun createSessionGroup() {
// .or()
// .greaterThanOrEqualTo("timeFrame.startDate", this.startDate) val realm = Realm.getDefaultInstance()
// .lessThanOrEqualTo("timeFrame.endDate", endDate)
// .or() val set: SessionSet = SessionSet.newInstance(realm)
// .isNull("timeFrame.endDate") set.timeFrame?.let {
// .lessThanOrEqualTo("timeFrame.startDate", endDate) it.startDate = this.startDate
// } it.endDate = this.endDate
// } ?: run {
// val sessionGroups = query.findAll() throw ModelException("TimeFrame should never be null here")
// }
// this.updateTimeFrames(sessionGroups, owner)
// this.session?.let {
// } it.sessionSet = set
// } ?: run {
// /** throw ModelException("Session should never be null here")
// * Update Time frames from sets }
// */
// private fun updateTimeFrames(sessionSets: RealmResults<SessionSet>, owner: Session) { Timber.d("sd = : ${set.timeFrame?.startDate}, ed = ${set.timeFrame?.endDate}")
//
// when (sessionSets.size) { }
// 0 -> this.createOrUpdateSessionSet(owner)
// 1 -> this.updateSessionGroup(owner, sessionSets.first()!!) /**
// else -> this.mergeSessionGroups(owner, sessionSets) * 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
// * Creates the session sessionGroup when the session has none
// */ if (this.startDate.before(groupTimeFrame.startDate)) {
// private fun createOrUpdateSessionSet(owner: Session) { groupTimeFrame.startDate = this.startDate
// }
// val set = owner.sessionSet val endDate = this.endDate
// if (set != null) { if (endDate != null && groupTimeFrame.endDate != null && endDate.after(groupTimeFrame.endDate)) {
// set.timeFrame?.startDate = this.startDate groupTimeFrame.endDate = endDate
// set.timeFrame?.endDate = this.endDate } else if (endDate == null) {
// } else { groupTimeFrame.endDate = null
// this.createSessionSet(owner) }
// }
// this.session?.sessionSet = sessionSet
//// Timber.d("sd = : ${set.timeFrame?.startDate}, ed = ${set.timeFrame?.endDate}")
// Timber.d("netDuration 1 = : ${set?.timeFrame?.netDuration}") }
//
// } /**
// * Multiple session sets update:
// fun createSessionSet(owner: Session) { * Merges all sets into one (delete all then create a new one)
// val set: SessionSet = SessionSet.newInstanceForResult(this.realm) */
// set.timeFrame?.let { private fun mergeSessionGroups(sessionSets: RealmResults<SessionSet>) {
// it.startDate = this.startDate
// it.endDate = this.endDate var startDate: Date = this.startDate
// } ?: run { var endDate: Date? = this.endDate
// throw ModelException("TimeFrame should never be null here")
// } // find earlier and later dates from all sets
// val timeFrames = sessionSets.mapNotNull { it.timeFrame }
// owner.sessionSet = set timeFrames.forEach { tf ->
// } if (tf.startDate.before(startDate)) {
// startDate = tf.startDate
// }
// /**
// * Single SessionSet update, the session might be the owner endDate?.let { ed ->
// * Changes the sessionGroup timeframe using the current timeframe dates tf.endDate?.let { tfed ->
// */ if (tfed.after(ed)) {
// private fun updateSessionGroup(owner: Session, sessionSet: SessionSet) { endDate = tfed
// }
// var timeFrame: TimeFrame = sessionSet.timeFrame!! // tested in the query }
//// timeFrame.setDate(this.startDate, this.endDate) } ?: run {
// endDate = tf.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) // get all sessions from sets
// } else { // there are 2+ endedSessions to manage and possible splits var sessions = mutableSetOf<Session>()
// sessionSets.forEach { it.sessions?.asIterable()?.let { it1 -> sessions.addAll(it1) } }
// val endDate = this.endDate
// // delete all sets
// // 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 sessionSets.deleteAllFromRealm()
// if (endDate != null && sisterSessions.all { it.timeFrame?.endDate != null } && timeFrame.endDate == null) {
// var endedSessions = mutableListOf<Session>(owner) // Create a new sets
// sessionSet.endedSessions?.forEach { endedSessions.add(it) } val set: SessionSet = SessionSet.newInstance(realm)
// sessionSet.deleteFromRealm() set.timeFrame?.let {
// endedSessions.forEach { it.timeFrame?.notifySessionDateChange(it) } it.startDate = startDate
// } else { it.endDate = endDate
// } ?: run {
// if (this.startDate.before(timeFrame.startDate)) { throw ModelException("TimeFrame should never be null here")
// timeFrame.startDate = this.startDate }
// }
// if (endDate != null && timeFrame.endDate != null && endDate.after(timeFrame.endDate)) { // Add the session linked to this timeframe to the new sessionGroup
// timeFrame.endDate = endDate this.sessions?.first()?.let {
// } else if (endDate == null) { it.sessionSet = set
// timeFrame.endDate = null } ?: run {
// } throw ModelException("TimeFrame should never be null here")
// }
// owner.sessionSet = sessionSet
// // Add all orphan sessions
//// Timber.d("sd = : ${sessionSet.timeFrame?.startDate}, ed = ${sessionSet.timeFrame?.endDate}") sessions.forEach { it.sessionSet = set }
// 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}")
//
// }
//
//}

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

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

Loading…
Cancel
Save