Compare commits

..

No commits in common. 'master' and 'feature/top10' have entirely different histories.

  1. 5
      .gitignore
  2. 8
      .gitlab-ci.yml
  3. 139
      CLAUDE.md
  4. 135
      app/build.gradle
  5. 24
      app/google-services.json
  6. 26
      app/proguard-rules.pro
  7. 8
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/BankrollInstrumentedUnitTest.kt
  8. 19
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/FavoriteSessionUnitTest.kt
  9. 57
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  10. 82
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/BlindFilterInstrumentedTest.kt
  11. 39
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/DateFilterInstrumentedUnitTest.kt
  12. 10
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/RealmFilterInstrumentedUnitTest.kt
  13. 80
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/SessionFilterInstrumentedUnitTest.kt
  14. 12
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/filter/TransactionFilterInstrumentedUnitTest.kt
  15. 3
      app/src/debug/AndroidManifest.xml
  16. 176
      app/src/main/AndroidManifest.xml
  17. BIN
      app/src/main/ic_launcher-playstore.png
  18. 82
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  19. 75
      app/src/main/java/net/pokeranalytics/android/api/BackupApi.kt
  20. 49
      app/src/main/java/net/pokeranalytics/android/api/BlogPostApi.kt
  21. 94
      app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt
  22. 242
      app/src/main/java/net/pokeranalytics/android/api/MultipartRequest.kt
  23. 10
      app/src/main/java/net/pokeranalytics/android/calculus/AggregationType.kt
  24. 174
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  25. 111
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  26. 35
      app/src/main/java/net/pokeranalytics/android/calculus/ComputedStat.kt
  27. 273
      app/src/main/java/net/pokeranalytics/android/calculus/Report.kt
  28. 338
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  29. 115
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  30. 48
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollCalculator.kt
  31. 97
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReport.kt
  32. 118
      app/src/main/java/net/pokeranalytics/android/calculus/bankroll/BankrollReportManager.kt
  33. 12
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/AggregationTypeExtensions.kt
  34. 84
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ComputedResultsExtensions.kt
  35. 64
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ReportDisplay.kt
  36. 64
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/ReportExtensions.kt
  37. 81
      app/src/main/java/net/pokeranalytics/android/calculus/calcul/StatRepresentable.kt
  38. 183
      app/src/main/java/net/pokeranalytics/android/calculus/optimalduration/CashGameOptimalDurationCalculator.kt
  39. 25
      app/src/main/java/net/pokeranalytics/android/exceptions/Exceptions.kt
  40. 57
      app/src/main/java/net/pokeranalytics/android/model/Criteria.kt
  41. 21
      app/src/main/java/net/pokeranalytics/android/model/Limit.kt
  42. 54
      app/src/main/java/net/pokeranalytics/android/model/LiveData.kt
  43. 38
      app/src/main/java/net/pokeranalytics/android/model/LiveOnline.kt
  44. 13
      app/src/main/java/net/pokeranalytics/android/model/Stakes.kt
  45. 28
      app/src/main/java/net/pokeranalytics/android/model/TableSize.kt
  46. 12
      app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt
  47. 13
      app/src/main/java/net/pokeranalytics/android/model/blogpost/BlogPost.kt
  48. 66
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  49. 8
      app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt
  50. 92
      app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt
  51. 524
      app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt
  52. 108
      app/src/main/java/net/pokeranalytics/android/model/handhistory/HandSetup.kt
  53. 53
      app/src/main/java/net/pokeranalytics/android/model/handhistory/Position.kt
  54. 50
      app/src/main/java/net/pokeranalytics/android/model/handhistory/Street.kt
  55. 6
      app/src/main/java/net/pokeranalytics/android/model/interfaces/CountableUsage.kt
  56. 7
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Dated.kt
  57. 19
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Manageable.kt
  58. 189
      app/src/main/java/net/pokeranalytics/android/model/interfaces/StakesHolder.kt
  59. 8
      app/src/main/java/net/pokeranalytics/android/model/interfaces/TimeFilterable.kt
  60. 7
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Timed.kt
  61. 174
      app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt
  62. 200
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  63. 101
      app/src/main/java/net/pokeranalytics/android/model/realm/Bankroll.kt
  64. 79
      app/src/main/java/net/pokeranalytics/android/model/realm/Comment.kt
  65. 17
      app/src/main/java/net/pokeranalytics/android/model/realm/ComputableResult.kt
  66. 22
      app/src/main/java/net/pokeranalytics/android/model/realm/Currency.kt
  67. 136
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt
  68. 7
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomFieldEntry.kt
  69. 85
      app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt
  70. 29
      app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt
  71. 63
      app/src/main/java/net/pokeranalytics/android/model/realm/Game.kt
  72. 16
      app/src/main/java/net/pokeranalytics/android/model/realm/HandHistory.kt
  73. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/Location.kt
  74. 72
      app/src/main/java/net/pokeranalytics/android/model/realm/Performance.kt
  75. 92
      app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt
  76. 15
      app/src/main/java/net/pokeranalytics/android/model/realm/ReportSetup.kt
  77. 61
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  78. 725
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  79. 29
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  80. 31
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt
  81. 25
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentName.kt
  82. 117
      app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt
  83. 94
      app/src/main/java/net/pokeranalytics/android/model/realm/TransactionType.kt
  84. 41
      app/src/main/java/net/pokeranalytics/android/model/realm/UserConfig.kt
  85. 252
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Action.kt
  86. 292
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/Card.kt
  87. 775
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/HandHistory.kt
  88. 31
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/PlayerSetup.kt
  89. 17
      app/src/main/java/net/pokeranalytics/android/model/realm/handhistory/WonPot.kt
  90. 14
      app/src/main/java/net/pokeranalytics/android/model/realm/rows/BankrollRow.kt
  91. 57
      app/src/main/java/net/pokeranalytics/android/model/realm/rows/CustomFieldRow.kt
  92. 11
      app/src/main/java/net/pokeranalytics/android/model/retrofit/ConvertResult.kt
  93. 42
      app/src/main/java/net/pokeranalytics/android/model/utils/DataUtils.kt
  94. 49
      app/src/main/java/net/pokeranalytics/android/model/utils/FavoriteSessionFinder.kt
  95. 35
      app/src/main/java/net/pokeranalytics/android/model/utils/Seed.kt
  96. 44
      app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt
  97. 21
      app/src/main/java/net/pokeranalytics/android/model/utils/SessionUtils.kt
  98. 90
      app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollActivity.kt
  99. 9
      app/src/main/java/net/pokeranalytics/android/ui/activity/BankrollDetailsActivity.kt
  100. 39
      app/src/main/java/net/pokeranalytics/android/ui/activity/BillingActivity.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

@ -27,10 +27,10 @@ stages:
- build - build
- test - test
#lintDebug: lintDebug:
# stage: build stage: build
# script: script:
# - ./gradlew -Pci --console=plain :app:lintDebug -PbuildDir=lint - ./gradlew -Pci --console=plain :app:lintDebug -PbuildDir=lint
assembleDebug: assembleDebug:
stage: build stage: build

@ -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,61 +1,52 @@
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.gms.google-services' // Crashlytics
apply plugin: 'com.google.firebase.crashlytics'
// Serialization
apply plugin: "kotlinx-serialization"
repositories { repositories {
maven { url 'https://maven.fabric.io/public' }
maven { url 'https://jitpack.io' } // required for MPAndroidChart maven { url 'https://jitpack.io' } // required for MPAndroidChart
jcenter() // for kotlin serialization
} }
android { android {
compileSdkVersion 35 compileSdkVersion 28
buildToolsVersion "30.0.3" buildToolsVersion "28.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 { lintOptions {
jvmTarget = JavaVersion.VERSION_1_8 disable 'MissingTranslation'
} }
defaultConfig { defaultConfig {
applicationId "net.pokeranalytics.android" applicationId "net.pokeranalytics.android"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 35 targetSdkVersion 28
versionCode 180 versionCode 30
versionName "6.0.38" versionName "2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
debug { debug {
ext.enableCrashlytics = false 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 true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationVariants.all { variant -> applicationVariants.all { variant ->
variant.outputs.all { output -> variant.outputs.all { output ->
def date = new Date() def date = new Date()
def formattedDate = date.format('yyMMdd_HHmm') def formattedDate = date.format('yyMMdd_HHmm')
def appName = "PokerAnalytics" def appName = "PokerAnalytics"
def buildType = variant.buildType.name def buildType = variant.variantData.variantConfiguration.buildType.name
def newName def newName
if (buildType == 'debug'){ if (buildType == 'debug'){
newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_debug.apk" newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_debug.apk"
@ -67,17 +58,6 @@ android {
} }
} }
} }
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 { configurations {
release { release {
@ -85,50 +65,44 @@ android {
} }
} }
buildFeatures {
viewBinding true
}
namespace 'net.pokeranalytics.android'
lint {
disable 'MissingTranslation'
}
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
// Kotlin // Kotlin
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6' implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 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.2.0-alpha01'
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 'androidx.browser:browser:1.0.0'
// implementation 'com.google.android.play:core-ktx:1.8.1' // In-app Reviews
implementation 'com.google.android.play:review:2.0.1' // Retrofit
implementation 'com.google.android.play:review-ktx:2.0.1' implementation 'com.squareup.retrofit2:retrofit:2.5.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
implementation 'com.google.code.gson:gson:2.8.5'
// Places // Places
implementation 'com.google.android.libraries.places:places:2.3.0' implementation 'com.google.android.libraries.places:places:1.1.0'
// Billing / Subscriptions // Billing / Subscriptions
implementation 'com.android.billingclient:billing:7.0.0' // WARNING FOR 2.0: https://developer.android.com/google/play/billing/billing_library_releases_notes
// Purchases MUST BE ACKNOWLEDGED
implementation 'com.android.billingclient:billing:1.2.2'
// Firebase
implementation 'com.google.firebase:firebase-core:16.0.9'
// Import the BoM for the Firebase platform // Crashlytics
implementation platform('com.google.firebase:firebase-bom:26.1.0') implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
// Declare the dependencies for the Crashlytics and Analytics libraries
// When using the BoM, you don't specify versions in Firebase library dependencies
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
// Logs // Logs
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
@ -137,47 +111,18 @@ dependencies {
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
// CSV Parser: https://mvnrepository.com/artifact/org.apache.commons/commons-csv // CSV Parser: https://mvnrepository.com/artifact/org.apache.commons/commons-csv
implementation 'org.apache.commons:commons-csv:1.7' implementation 'org.apache.commons:commons-csv:1.6'
// 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 // Instrumented Tests
androidTestImplementation 'androidx.test:core:1.6.1' androidTestImplementation 'androidx.test:core:1.1.0'
androidTestImplementation 'androidx.test:runner:1.6.2' androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.6.1' androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.ext:junit:1.1.0'
// Test // Test
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.12'
testImplementation 'com.android.support.test:runner:1.0.2' testImplementation 'com.android.support.test:runner:1.0.2'
testImplementation 'com.android.support.test:rules: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'
} }

@ -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
} }
} }
} }

@ -29,10 +29,6 @@
-dontwarn javax.** -dontwarn javax.**
-dontwarn io.realm.** -dontwarn io.realm.**
-keep class net.pokeranalytics.android.model.** { *; } -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
@ -65,25 +61,3 @@
# Enum # Enum
-optimizations !class/unboxing/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(...);
}

@ -19,7 +19,7 @@ class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() {
TransactionType.Value.values().forEachIndexed { index, value -> TransactionType.Value.values().forEachIndexed { index, value ->
val type = TransactionType() val type = TransactionType()
type.additive = value.additive type.additive = value.additive
type.kind = value.uniqueIdentifier type.kind = index
type.lock = true type.lock = true
realm.insertOrUpdate(type) realm.insertOrUpdate(type)
} }
@ -72,12 +72,12 @@ class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() {
} }
val br1 = realm.where(Bankroll::class.java).equalTo("name", "br1").findFirst() val br1 = realm.where(Bankroll::class.java).equalTo("name", "br1").findFirst()
val brSetup1 = BankrollReportSetup(br1?.id) val brSetup1 = BankrollReportSetup(br1)
val report1 = BankrollCalculator.computeReport(realm, brSetup1) val report1 = BankrollCalculator.computeReport(realm, brSetup1)
Assert.assertEquals(400.0, report1.total, EPSILON) Assert.assertEquals(400.0, report1.total, EPSILON)
val br2 = realm.where(Bankroll::class.java).equalTo("name", "br2").findFirst() val br2 = realm.where(Bankroll::class.java).equalTo("name", "br2").findFirst()
val brSetup2 = BankrollReportSetup(br2?.id) val brSetup2 = BankrollReportSetup(br2)
val report2 = BankrollCalculator.computeReport(realm, brSetup2) val report2 = BankrollCalculator.computeReport(realm, brSetup2)
Assert.assertEquals(2000.0, report2.total, EPSILON) Assert.assertEquals(2000.0, report2.total, EPSILON)
@ -116,7 +116,7 @@ class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() {
} }
val brSetup1 = BankrollReportSetup(br1?.id) val brSetup1 = BankrollReportSetup(br1)
val report1 = BankrollCalculator.computeReport(realm, brSetup1) val report1 = BankrollCalculator.computeReport(realm, brSetup1)
Assert.assertEquals(400.0, report1.total, EPSILON) Assert.assertEquals(400.0, report1.total, EPSILON)

@ -30,9 +30,12 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
s2.endDate = Date() s2.endDate = Date()
s3.endDate = Date() s3.endDate = Date()
s1.cgBlinds = "2/4" s1.cgBigBlind = 4.0
s2.cgBlinds = "2/4" s1.cgSmallBlind = 2.0
s3.cgBlinds = "1/2" s2.cgBigBlind = 4.0
s2.cgSmallBlind = 2.0
s3.cgBigBlind = 1.0
s3.cgSmallBlind = 1.0
realm.insert(s1) realm.insert(s1)
realm.insert(s2) realm.insert(s2)
@ -42,7 +45,7 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, null, realm, InstrumentationRegistry.getInstrumentation().targetContext) val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, null, realm, InstrumentationRegistry.getInstrumentation().targetContext)
if (favSession != null) { if (favSession != null) {
Assert.assertEquals(4.0, favSession.cgBiggestBet) Assert.assertEquals(4.0, favSession.cgBigBlind)
} else { } else {
Assert.fail("session shouldn't be null") Assert.fail("session shouldn't be null")
} }
@ -62,9 +65,9 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
val loc1 = realm.createObject(Location::class.java, "1") val loc1 = realm.createObject(Location::class.java, "1")
val loc2 = realm.createObject(Location::class.java, "2") val loc2 = realm.createObject(Location::class.java, "2")
s1.cgBiggestBet = 4.0 s1.cgBigBlind = 4.0
s2.cgBiggestBet = 4.0 s2.cgBigBlind = 4.0
s3.cgBiggestBet = 1.0 s3.cgBigBlind = 1.0
s1.location = loc1 s1.location = loc1
s2.location = loc1 s2.location = loc1
@ -78,7 +81,7 @@ class FavoriteSessionUnitTest : RealmInstrumentedUnitTest() {
val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, loc2, realm, InstrumentationRegistry.getInstrumentation().targetContext) val favSession = FavoriteSessionFinder.favoriteSession(Session.Type.CASH_GAME.ordinal, loc2, realm, InstrumentationRegistry.getInstrumentation().targetContext)
if (favSession != null) { if (favSession != null) {
Assert.assertEquals(1.0, favSession.cgBiggestBet) Assert.assertEquals(1.0, favSession.cgBigBlind)
} else { } else {
Assert.fail("session shouldn't be null") Assert.fail("session shouldn't be null")
} }

@ -44,8 +44,8 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
s2.result?.buyin = 200.0 s2.result?.buyin = 200.0
s2.result?.cashout = 500.0 // net result = 300 s2.result?.cashout = 500.0 // net result = 300
s1.cgBlinds = "0.5" // bb net result = -200bb s1.cgBigBlind = 0.5 // bb net result = -200bb
s2.cgBlinds = "2.0" // bb net result = 150bb s2.cgBigBlind = 2.0 // bb net result = 150bb
s2.tableSize = 5 s2.tableSize = 5
@ -186,13 +186,13 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
Assert.fail("No std100 stat") Assert.fail("No std100 stat")
} }
results.computedStat(Stat.MAXIMUM_NET_RESULT)?.let { results.computedStat(Stat.MAXIMUM_NETRESULT)?.let {
assertEquals(300.0, it.value, delta) assertEquals(300.0, it.value, delta)
} ?: run { } ?: run {
Assert.fail("No MAXIMUM_NETRESULT") Assert.fail("No MAXIMUM_NETRESULT")
} }
results.computedStat(Stat.MINIMUM_NET_RESULT)?.let { results.computedStat(Stat.MINIMUM_NETRESULT)?.let {
assertEquals(-100.0, it.value, delta) assertEquals(-100.0, it.value, delta)
} ?: run { } ?: run {
Assert.fail("No MINIMUM_NETRESULT") Assert.fail("No MINIMUM_NETRESULT")
@ -506,55 +506,6 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
} }
@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 @Test
fun testSessionRestartInOverlappingSessions() { fun testSessionRestartInOverlappingSessions() {

@ -7,7 +7,7 @@ import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.FilterCondition import net.pokeranalytics.android.model.realm.FilterCondition
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import java.util.* import java.util.*
@ -28,25 +28,28 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBlinds = "0.5/1" s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1" s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b1) val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBlinds = "1/2" s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyStake() val filter = QueryCondition.AnyBlind()
val blind = QueryCondition.AnyStake().apply { val blind = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!) listOfValues = arrayListOf(s1.blinds!!)
} }
// blind.filterSectionRow = FilterSectionRow.Blind blind.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Stakes) val filterElement = FilterCondition(filterElementRows = arrayListOf(blind))
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -71,27 +74,33 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBlinds = "0.5/1" s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1" s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b1) val s3 = Session.testInstance(100.0, false, Date(), 1, b1)
s3.cgBlinds = "1/2" s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyStake() val filter = QueryCondition.AnyBlind()
val blind1 = QueryCondition.AnyStake().apply { val blind1 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!) listOfValues = arrayListOf(s1.blinds!!)
} }
val blind2 = QueryCondition.AnyStake().apply { val blind2 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s2.blinds!!) listOfValues = arrayListOf(s2.blinds!!)
} }
val filterElements = FilterCondition(arrayListOf(blind1, blind2), FilterSectionRow.Stakes) blind1.filterSectionRow = FilterSectionRow.Blind
blind2.filterSectionRow = FilterSectionRow.Blind
val filterElements = FilterCondition(filterElementRows = arrayListOf(blind1, blind2))
filter.updateValueBy(filterElements) filter.updateValueBy(filterElements)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -116,24 +125,30 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBlinds = "0.5/1"
s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1" s2.cgBigBlind = 1.0
s2.cgSmallBlind = 0.5
val s3 = Session.testInstance(100.0, false, Date(), 1, b2) val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBlinds = "1/2" s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyStake() val filter = QueryCondition.AnyBlind()
val blind = QueryCondition.AnyStake().apply { val blind = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s3.blinds!!) listOfValues = arrayListOf(s3.blinds!!)
} }
val filterElement = FilterCondition(arrayListOf(blind), FilterSectionRow.Stakes) blind.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind))
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -157,27 +172,34 @@ class BlindFilterInstrumentedTest : BaseFilterInstrumentedUnitTest() {
b2.currency = currency b2.currency = currency
val s1 = Session.testInstance(100.0, false, Date(), 1, b1) val s1 = Session.testInstance(100.0, false, Date(), 1, b1)
s1.cgBlinds = "0.5/1" s1.cgBigBlind = 1.0
s1.cgSmallBlind = 0.5
val s2 = Session.testInstance(100.0, false, Date(), 1, b1) val s2 = Session.testInstance(100.0, false, Date(), 1, b1)
s2.cgBlinds = "0.5/1" s2.cgBigBlind = 2.0
s2.cgSmallBlind = 1.0
val s3 = Session.testInstance(100.0, false, Date(), 1, b2) val s3 = Session.testInstance(100.0, false, Date(), 1, b2)
s3.cgBlinds = "1/2" s3.cgBigBlind = 2.0
s3.cgSmallBlind = 1.0
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.AnyStake()
val stake1 = QueryCondition.AnyStake().apply { val filter = QueryCondition.AnyBlind()
listOfValues = arrayListOf(s1.cgStakes!!)
val blind1 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s1.blinds!!)
} }
val stake2 = QueryCondition.AnyStake().apply { val blind2 = QueryCondition.AnyBlind().apply {
listOfValues = arrayListOf(s2.cgStakes!!) listOfValues = arrayListOf(s2.blinds!!)
} }
val filterElement = FilterCondition(arrayListOf(stake1, stake2), FilterSectionRow.Stakes) blind1.filterSectionRow = FilterSectionRow.Blind
blind2.filterSectionRow = FilterSectionRow.Blind
val filterElement = FilterCondition(filterElementRows = arrayListOf(blind1, blind2))
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))

@ -7,7 +7,7 @@ import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.FilterCondition import net.pokeranalytics.android.model.realm.FilterCondition
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.util.extensions.hourMinute import net.pokeranalytics.android.util.extensions.hourMinute
import net.pokeranalytics.android.util.extensions.startOfDay import net.pokeranalytics.android.util.extensions.startOfDay
import org.junit.Assert import org.junit.Assert
@ -35,7 +35,8 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
cal.time = s1.startDate cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(cal.get(Calendar.DAY_OF_WEEK)) } val filterElementRow = QueryCondition.AnyDayOfWeek().apply { listOfValues = arrayListOf(cal.get(Calendar.DAY_OF_WEEK)) }
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate) filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -63,7 +64,8 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
cal.time = s1.startDate cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(cal.get(Calendar.MONTH)) } val filterElementRow = QueryCondition.AnyMonthOfYear().apply { listOfValues = arrayListOf(cal.get(Calendar.MONTH)) }
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate) filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -90,7 +92,8 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyYear() val filter = QueryCondition.AnyYear()
cal.time = s1.startDate cal.time = s1.startDate
val filterElementRow = QueryCondition.AnyYear().apply { listOfValues = arrayListOf(cal.get(Calendar.YEAR)) } val filterElementRow = QueryCondition.AnyYear().apply { listOfValues = arrayListOf(cal.get(Calendar.YEAR)) }
val filterElement = FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.DynamicDate) filterElementRow.filterSectionRow = FilterSectionRow.DynamicDate
val filterElement = FilterCondition(arrayListOf(filterElementRow))
filter.updateValueBy(filterElement) filter.updateValueBy(filterElement)
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -381,9 +384,10 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val s2 = Session.testInstance(100.0, true, cal.time, 1) val s2 = Session.testInstance(100.0, true, cal.time, 1)
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.StartedFromDate(Date()) val filter = QueryCondition.StartedFromDate()
val filterElementRow = QueryCondition.StartedFromDate(Date()).apply { singleValue = s2.startDate!!} val filterElementRow = QueryCondition.StartedFromDate().apply { singleValue = s2.startDate!!}
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate)) filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -408,9 +412,10 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.StartedToDate(Date()) val filter = QueryCondition.StartedToDate()
val filterElementRow = QueryCondition.StartedToDate(Date()).apply { singleValue = s1.startDate!! } val filterElementRow = QueryCondition.StartedToDate().apply { singleValue = s1.startDate!! }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate)) filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -436,9 +441,10 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.EndedFromDate(Date()) val filter = QueryCondition.EndedFromDate()
val filterElementRow = QueryCondition.EndedFromDate(Date()).apply { singleValue = s2.endDate() } val filterElementRow = QueryCondition.EndedFromDate().apply { singleValue = s2.endDate() }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate)) filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -464,9 +470,10 @@ class DateFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.EndedToDate(Date()) val filter = QueryCondition.EndedToDate()
val filterElementRow = QueryCondition.EndedToDate(Date()).apply { singleValue = s1.endDate() } val filterElementRow = QueryCondition.EndedToDate().apply { singleValue = s1.endDate() }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.FixedDate)) filterElementRow.filterSectionRow = FilterSectionRow.FixedDate
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))

@ -5,9 +5,8 @@ import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rows.FixedValueFilterItemRow import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow
import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -25,9 +24,8 @@ class RealmFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
filter.name = "testSaveLoadCashFilter" filter.name = "testSaveLoadCashFilter"
val filterElement = QueryCondition.IsCash val filterElement = QueryCondition.IsCash
val filterItemRow = FixedValueFilterItemRow(filterElement, FilterSectionRow.CashOrTournament) filterElement.filterSectionRow = FilterSectionRow.CashOrTournament
filter.createOrUpdateFilterConditions(arrayListOf(filterElement))
filter.createOrUpdateFilterConditions(arrayListOf(filterItemRow))
val useCount = filter.countBy(FilterCategoryRow.GENERAL) val useCount = filter.countBy(FilterCategoryRow.GENERAL)
Assert.assertEquals(1, useCount) Assert.assertEquals(1, useCount)

@ -6,7 +6,7 @@ import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -110,7 +110,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyBankroll() val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) } val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Bankroll)) filterElementRow.filterSectionRow = FilterSectionRow.Bankroll
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -140,9 +141,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyBankroll() val filter = QueryCondition.AnyBankroll()
val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) } val filterElementRow = QueryCondition.AnyBankroll().apply { setObject(b1) }
val filterElementRow2 = QueryCondition.AnyBankroll().apply { setObject(b2) } filterElementRow.filterSectionRow = FilterSectionRow.Bankroll
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Bankroll)) val filterElementRow2 = QueryCondition.AnyBankroll().apply { setObject(b2) }
filterElementRow2.filterSectionRow = FilterSectionRow.Bankroll
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -166,7 +169,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val anyGame = QueryCondition.AnyGame(g2) val anyGame = QueryCondition.AnyGame(g2)
val fc = FilterCondition(arrayListOf(anyGame), FilterSectionRow.Game) val fc = FilterCondition(filterElementRows = arrayListOf(anyGame))
val sessions = Filter.queryOn<Session>(realm, Query(fc.queryCondition)) val sessions = Filter.queryOn<Session>(realm, Query(fc.queryCondition))
Assert.assertEquals(1, sessions.size) Assert.assertEquals(1, sessions.size)
@ -194,10 +197,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filterElementRow = QueryCondition.AnyGame().apply { setObject(g2) } val filterElementRow = QueryCondition.AnyGame().apply { setObject(g2) }
filterElementRow.filterSectionRow = FilterSectionRow.Game
val filterElementRow2 = QueryCondition.AnyGame().apply { setObject(g3) } val filterElementRow2 = QueryCondition.AnyGame().apply { setObject(g3) }
filterElementRow2.filterSectionRow = FilterSectionRow.Game
val filterCondition = FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Game) val filterCondition = FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2))
val queryCondition = filterCondition.queryCondition val queryCondition = filterCondition.queryCondition
val sessions = Filter.queryOn<Session>(realm, Query(queryCondition)) val sessions = Filter.queryOn<Session>(realm, Query(queryCondition))
@ -222,7 +225,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyLocation() val filter = QueryCondition.AnyLocation()
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) } val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Location)) filterElementRow.filterSectionRow = FilterSectionRow.Location
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -253,9 +257,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyLocation() val filter = QueryCondition.AnyLocation()
val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) } val filterElementRow = QueryCondition.AnyLocation().apply { setObject(l1) }
filterElementRow.filterSectionRow = FilterSectionRow.Location
val filterElementRow2 = QueryCondition.AnyLocation().apply { setObject(l3) } val filterElementRow2 = QueryCondition.AnyLocation().apply { setObject(l3) }
filterElementRow2.filterSectionRow = FilterSectionRow.Location
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.Location)) filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -281,7 +287,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentName() val filter = QueryCondition.AnyTournamentName()
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) } val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.TournamentName)) filterElementRow.filterSectionRow = FilterSectionRow.TournamentName
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -311,8 +318,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentName() val filter = QueryCondition.AnyTournamentName()
val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) } val filterElementRow = QueryCondition.AnyTournamentName().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentName
val filterElementRow2 = QueryCondition.AnyTournamentName().apply { setObject(t2) } val filterElementRow2 = QueryCondition.AnyTournamentName().apply { setObject(t2) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.TournamentName)) filterElementRow.filterSectionRow = FilterSectionRow.TournamentName
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -345,10 +354,12 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AllTournamentFeature() val filter = QueryCondition.AllTournamentFeature()
val filterElementRow = QueryCondition.AllTournamentFeature().apply { setObject(t1) } val filterElementRow = QueryCondition.AllTournamentFeature().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow2 = QueryCondition.AllTournamentFeature().apply { setObject(t2) } val filterElementRow2 = QueryCondition.AllTournamentFeature().apply { setObject(t2) }
filterElementRow2.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow3 = QueryCondition.AllTournamentFeature().apply { setObject(t4) } val filterElementRow3 = QueryCondition.AllTournamentFeature().apply { setObject(t4) }
filterElementRow3.filterSectionRow = FilterSectionRow.TournamentFeature
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2, filterElementRow3), FilterSectionRow.TournamentFeature)) filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2, filterElementRow3)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -378,11 +389,14 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentFeature() val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t1) } val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t1) }
filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow2 = QueryCondition.AnyTournamentFeature().apply { setObject(t2) } val filterElementRow2 = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
filterElementRow2.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow3 = QueryCondition.AnyTournamentFeature().apply { setObject(t3) } val filterElementRow3 = QueryCondition.AnyTournamentFeature().apply { setObject(t3) }
filterElementRow3.filterSectionRow = FilterSectionRow.TournamentFeature
val filterElementRow4 = QueryCondition.AnyTournamentFeature().apply { setObject(t4) } val filterElementRow4 = QueryCondition.AnyTournamentFeature().apply { setObject(t4) }
filterElementRow4.filterSectionRow = FilterSectionRow.TournamentFeature
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2, filterElementRow3, filterElementRow4), FilterSectionRow.TournamentFeature)) filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2, filterElementRow3, filterElementRow4)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -409,7 +423,8 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTournamentFeature() val filter = QueryCondition.AnyTournamentFeature()
val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t2) } val filterElementRow = QueryCondition.AnyTournamentFeature().apply { setObject(t2) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.TournamentFeature)) filterElementRow.filterSectionRow = FilterSectionRow.TournamentFeature
filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -433,9 +448,10 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filter = QueryCondition.AnyTableSize() val filter = QueryCondition.AnyTableSize()
val filterElementRow = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(2) } val filterElementRow = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(2) }
filterElementRow.filterSectionRow = FilterSectionRow.TableSize
val filterElementRow2 = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(4) } val filterElementRow2 = QueryCondition.AnyTableSize().apply { listOfValues = arrayListOf(4) }
filterElementRow.filterSectionRow = FilterSectionRow.TableSize
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow, filterElementRow2), FilterSectionRow.TableSize)) filter.updateValueBy(FilterCondition(filterElementRows = arrayListOf(filterElementRow, filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -458,8 +474,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.NetAmountWon() val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) } val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(204.0) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value)) filterElementRow.filterSectionRow = FilterSectionRow.Value
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filterElementRow)) val sessions = Filter.queryOn<Session>(realm, Query(filterElementRow))
@ -482,8 +499,9 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filter = QueryCondition.NetAmountWon() val filter = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) } val filterElementRow = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(540.0) }
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value)) filterElementRow.filterSectionRow = FilterSectionRow.Value
filter.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val sessions = Filter.queryOn<Session>(realm, Query(filter)) val sessions = Filter.queryOn<Session>(realm, Query(filter))
@ -506,12 +524,14 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val filterMore = QueryCondition.NetAmountWon() val filterMore = QueryCondition.NetAmountWon()
val filterElementRow = QueryCondition.moreOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(199.0) } val filterElementRow = QueryCondition.more<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(199.0) }
filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow), FilterSectionRow.Value)) filterElementRow.filterSectionRow = FilterSectionRow.Value
filterMore.updateValueBy(FilterCondition(arrayListOf(filterElementRow)))
val filterLess = QueryCondition.NetAmountWon() val filterLess = QueryCondition.NetAmountWon()
val filterElementRow2 = QueryCondition.lessOrEqual<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) } val filterElementRow2 = QueryCondition.less<QueryCondition.NetAmountWon>().apply { listOfValues = arrayListOf(400.0) }
filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2), FilterSectionRow.Value)) filterElementRow2.filterSectionRow = FilterSectionRow.Value
filterLess.updateValueBy(FilterCondition(arrayListOf(filterElementRow2)))
val sessions = Filter.queryOn<Session>(realm, Query(filterMore, filterLess)) val sessions = Filter.queryOn<Session>(realm, Query(filterMore, filterLess))
@ -536,11 +556,11 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val s3 = Session.testInstance(netResult = 500.0) val s3 = Session.testInstance(netResult = 500.0)
s3.cgBlinds = "2.0" s3.cgBigBlind = 2.0
s3.result!!.buyin = 1000.0 s3.result!!.buyin = 1000.0
val s4 = Session.testInstance(netResult = 570.0) val s4 = Session.testInstance(netResult = 570.0)
s4.cgBlinds = "5.0" s4.cgBigBlind = 5.0
s4.result!!.buyin = 200.0 s4.result!!.buyin = 200.0
realm.commitTransaction() realm.commitTransaction()
@ -584,7 +604,7 @@ class SessionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val filterMore = QueryCondition.TournamentFinalPosition(QueryCondition.Operator.MORE, finalPosition = 10) val filterMore = QueryCondition.TournamentFinalPosition(QueryCondition.Operator.MORE, finalPosition = 10)
sessions = Filter.queryOn(realm, Query(filterMore)) sessions = Filter.queryOn<Session>(realm, Query(filterMore))
Assert.assertEquals(1, sessions.size) Assert.assertEquals(1, sessions.size)

@ -2,16 +2,18 @@ package net.pokeranalytics.android.unitTests.filter
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import io.realm.RealmList
import io.realm.RealmResults
import net.pokeranalytics.android.R
import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest import net.pokeranalytics.android.components.BaseFilterInstrumentedUnitTest
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.TransactionType
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.util.*
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class TransactionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() { class TransactionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
@ -27,7 +29,7 @@ class TransactionFilterInstrumentedUnitTest : BaseFilterInstrumentedUnitTest() {
val name = "test" val name = "test"
type.name = name type.name = name
type.additive = value.additive type.additive = value.additive
type.kind = value.uniqueIdentifier type.kind = index
type.lock = true type.lock = true
realm.insertOrUpdate(type) realm.insertOrUpdate(type)
} }

@ -1,5 +1,6 @@
<?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"
package="net.pokeranalytics.android">
<!-- Add WRITE_EXTERNAL_STORAGE only for debug / test --> <!-- Add WRITE_EXTERNAL_STORAGE only for debug / test -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

@ -1,18 +1,11 @@
<?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"> 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.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.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="com.android.vending.BILLING" />
<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:name=".PokerAnalyticsApplication"
@ -21,19 +14,17 @@
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:theme="@style/PokerAnalyticsTheme"> android:theme="@style/PokerAnalyticsTheme">
<meta-data <meta-data
android:name="firebase_crashlytics_collection_enabled" android:name="firebase_crashlytics_collection_enabled"
android:value="true" /> android:value="false" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.HomeActivity" android:name="net.pokeranalytics.android.ui.activity.HomeActivity"
android:label="@string/app_name"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:label="@string/app_name"
android:exported="true"> android:screenOrientation="portrait">
<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" /> <action android:name="android.intent.action.VIEW" />
@ -41,14 +32,6 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity>
<activity
android:name="net.pokeranalytics.android.ui.activity.ImportActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter tools:ignore="AppLinkUrlError"> <intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -62,186 +45,119 @@
</activity> </activity>
<!-- DatabaseCopyActivity is only used in development for now --> <activity
android:name="net.pokeranalytics.android.ui.activity.ImportActivity"
<!-- <activity android:name=".ui.activity.DatabaseCopyActivity"--> android:screenOrientation="portrait"
<!-- android:launchMode="singleTop"--> 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>
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.session.SessionActivity" android:name="net.pokeranalytics.android.ui.activity.SessionActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:exported="true" /> android:windowSoftInputMode="adjustNothing" />
<!-- No screenOrientation="portrait" to fix Oreo crash --> <!-- No screenOrientation="portrait" to fix Oreo crash -->
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.feed.NewDataMenuActivity" android:name="net.pokeranalytics.android.ui.activity.NewDataMenuActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/PokerAnalyticsTheme.MenuDialog" android:theme="@style/PokerAnalyticsTheme.MenuDialog" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity" android:name="net.pokeranalytics.android.ui.activity.BankrollActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.handhistory.HandHistoryActivity" android:name="net.pokeranalytics.android.ui.activity.BankrollDetailsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" 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 <activity
android:name="net.pokeranalytics.android.ui.activity.Top10Activity" android:name="net.pokeranalytics.android.ui.activity.Top10Activity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.SettingsActivity" android:name="net.pokeranalytics.android.ui.activity.SettingsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.GraphActivity" android:name="net.pokeranalytics.android.ui.activity.GraphActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity" android:name="net.pokeranalytics.android.ui.activity.ProgressReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity" android:name="net.pokeranalytics.android.ui.activity.ComparisonReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.calendar.CalendarDetailsActivity" android:name="net.pokeranalytics.android.ui.activity.CalendarDetailsActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity" android:name="net.pokeranalytics.android.ui.activity.ComparisonChartActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.datalist.DataListActivity" android:name="net.pokeranalytics.android.ui.activity.DataListActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersListActivity" android:name="net.pokeranalytics.android.ui.activity.FiltersListActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.data.EditableDataActivity" android:name="net.pokeranalytics.android.ui.activity.EditableDataActivity"
android:launchMode="standard" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity" android:name="net.pokeranalytics.android.ui.activity.CurrenciesActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.modules.filter.FiltersActivity" android:name="net.pokeranalytics.android.ui.activity.FiltersActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity
android:name="net.pokeranalytics.android.ui.activity.FilterDetailsActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.GDPRActivity" android:name="net.pokeranalytics.android.ui.activity.GDPRActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.BillingActivity" android:name="net.pokeranalytics.android.ui.activity.BillingActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity" android:name="net.pokeranalytics.android.ui.activity.ReportCreationActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait" />
android:exported="true" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.TableReportActivity" android:name="net.pokeranalytics.android.ui.activity.TableReportActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" 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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

@ -1,47 +1,29 @@
package net.pokeranalytics.android package net.pokeranalytics.android
import android.app.Application import android.app.Application
import android.content.Context import com.crashlytics.android.Crashlytics
import android.os.Build import com.crashlytics.android.core.CrashlyticsCore
import com.google.firebase.FirebaseApp import io.fabric.sdk.android.Fabric
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.kotlin.where import io.realm.kotlin.where
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.calculus.ReportWhistleBlower
import net.pokeranalytics.android.model.migrations.Patcher import net.pokeranalytics.android.model.migrations.Patcher
import net.pokeranalytics.android.model.migrations.PokerAnalyticsMigration 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.model.utils.Seed
import net.pokeranalytics.android.util.* import net.pokeranalytics.android.util.FakeDataManager
import net.pokeranalytics.android.util.PokerAnalyticsLogs
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.billing.AppGuard 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 backupOperator: BackupOperator? = null
companion object {
fun timeSinceInstall(context: Context): Long {
val installTime: Long = context.packageManager.getPackageInfo(context.packageName, 0).firstInstallTime
return System.currentTimeMillis() - installTime
}
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (!BuildConfig.DEBUG) {
FirebaseApp.initializeApp(this)
}
UserDefaults.init(this) UserDefaults.init(this)
// AppGuard / Billing services // AppGuard / Billing services
@ -51,49 +33,37 @@ class PokerAnalyticsApplication : Application() {
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) .schemaVersion(6)
.allowWritesOnUiThread(true)
.migration(PokerAnalyticsMigration()) .migration(PokerAnalyticsMigration())
.initialData(Seed(this)) .initialData(Seed(this))
.build() .build()
Realm.setDefaultConfiguration(realmConfiguration) Realm.setDefaultConfiguration(realmConfiguration)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // val realm = Realm.getDefaultInstance()
val locales = resources.configuration.locales // realm.executeTransaction {
CrashLogging.log("App onCreate. Locales = $locales") // realm.where(Session::class.java).findAll().deleteAllFromRealm()
} // }
// realm.close()
// Set up Crashlytics, disabled for debug builds
val crashlyticsKit = Crashlytics.Builder()
.core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
.build()
// Initialize Fabric with the debug-disabled crashlytics.
Fabric.with(this, crashlyticsKit)
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
// Logs // Logs
Timber.plant(PokerAnalyticsLogs()) Timber.plant(PokerAnalyticsLogs())
} }
Timber.d("SDK version = ${Build.VERSION.SDK_INT}")
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}") Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
Timber.d("Realm path = ${Realm.getDefaultInstance().path}") this.createFakeSessions()
// this.createFakeSessions()
} }
// Patch Patcher.patchAll(this.applicationContext)
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)
// }
} }
/** /**
@ -102,12 +72,12 @@ class PokerAnalyticsApplication : Application() {
private fun createFakeSessions() { private fun createFakeSessions() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val sessionsCount = realm.where<Session>().count() val sessionsCount = realm.where<Session>().findAll().size
realm.close() realm.close()
if (sessionsCount < 10) { if (sessionsCount < 10) {
CoroutineScope(context = Dispatchers.IO).launch { GlobalScope.launch {
FakeDataManager.createFakeSessions(500) FakeDataManager.createFakeSessions(200)
} }
} }

@ -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 +1,80 @@
package net.pokeranalytics.android.api package net.pokeranalytics.android.api
import android.content.Context import android.content.Context
import androidx.annotation.Keep import net.pokeranalytics.android.BuildConfig
import com.android.volley.VolleyError import net.pokeranalytics.android.R
import com.android.volley.toolbox.StringRequest import net.pokeranalytics.android.model.retrofit.CurrencyConverterValue
import com.android.volley.toolbox.Volley import net.pokeranalytics.android.util.URL
import kotlinx.serialization.Serializable import okhttp3.Interceptor
import kotlinx.serialization.decodeFromString import okhttp3.OkHttpClient
import kotlinx.serialization.json.Json import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
import java.util.concurrent.TimeUnit
/**
* CurrencyCode Converter API
*/
interface CurrencyConverterApi {
@Keep companion object {
@Serializable
data class RateResponse(var info: RateInfo)
@Keep private var currencyConverterApi: CurrencyConverterApi? = null
@Serializable
data class RateInfo(var rate: Double)
class CurrencyConverterApi { fun getApi(context: Context): CurrencyConverterApi? {
companion object { if (currencyConverterApi == null) {
val json = Json { ignoreUnknownKeys = true } var serviceEndpoint = URL.API_CURRENCY_CONVERTER
fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) { val httpClient = OkHttpClient.Builder()
val queue = Volley.newRequestQueue(context) // Logging interceptor
val url = "https://api.apilayer.com/exchangerates_data/convert?to=$toCurrency&from=$fromCurrency&amount=1" if (BuildConfig.DEBUG) {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BASIC
httpClient.addInterceptor(interceptor)
}
Timber.d("Api call = $url") // Add headers
val interceptor = Interceptor { chain ->
val original = chain.request()
val originalHttpUrl = original.url()
val stringRequest = object : StringRequest( val url = originalHttpUrl.newBuilder()
Method.GET, url, .addQueryParameter("apiKey", context.getString(R.string.currency_converter_api))
{ response -> .build()
val o = json.decodeFromString<RateResponse>(response) val requestBuilder = original.newBuilder()
Timber.d("rate = ${o.info.rate}") .url(url)
callback(o.info.rate, null)
},
{
Timber.d("Api call failed: ${it.message}")
callback(null, it)
}) {
override fun getHeaders(): MutableMap<String, String> { chain.proceed(requestBuilder.build())
val headers = HashMap<String, String>()
headers["apikey"] = "XnfeyID3PMKd3k4zTPW0XmZAbcZlZgqH"
return headers
} }
httpClient.addInterceptor(interceptor)
val client = httpClient
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(serviceEndpoint.value)
.client(client)
.build()
currencyConverterApi = retrofit.create(CurrencyConverterApi::class.java)
} }
queue.add(stringRequest)
return currencyConverterApi
} }
} }
@GET("convert")
fun convert(@Query("q") currencies: String, @Query("compact") compact: String = "y"): Call<Map<String, CurrencyConverterValue>>
} }

@ -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
}
}
}

@ -2,7 +2,7 @@ package net.pokeranalytics.android.calculus
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.ui.graph.Graph import net.pokeranalytics.android.ui.graph.AxisFormatting
enum class AggregationType { enum class AggregationType {
SESSION, SESSION,
@ -20,6 +20,14 @@ enum class AggregationType {
} }
} }
val axisFormatting: AxisFormatting
get() {
return when (this) {
DURATION -> AxisFormatting.X_DURATION
else -> AxisFormatting.DEFAULT
}
}
val criterias: List<Criteria> val criterias: List<Criteria>
get() { get() {
return when (this) { return when (this) {

@ -2,20 +2,24 @@ package net.pokeranalytics.android.calculus
import android.content.Context import android.content.Context
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat.* import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.combined import net.pokeranalytics.android.model.combined
import net.pokeranalytics.android.model.extensions.hourlyDuration import net.pokeranalytics.android.model.extensions.hourlyDuration
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.filter import net.pokeranalytics.android.model.filter.filter
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.ui.activity.ComparisonReportActivity
import net.pokeranalytics.android.ui.activity.ProgressReportActivity
import net.pokeranalytics.android.ui.activity.TableReportActivity
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.util.extensions.startOfDay import net.pokeranalytics.android.util.extensions.startOfDay
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
/** /**
* The class performing statIds computation * The class performing statIds computation
@ -26,18 +30,18 @@ class Calculator {
* The options used for calculations and display * The options used for calculations and display
*/ */
class Options( class Options(
var progressValues: ProgressValues = ProgressValues.NONE, var display: Display = Display.TABLE,
progressValues: ProgressValues = ProgressValues.NONE,
var stats: List<Stat> = listOf(), var stats: List<Stat> = listOf(),
var criterias: List<Criteria> = listOf(), var criterias: List<Criteria> = listOf(),
var query: Query = Query(), var query: Query = Query(),
var filterId: String? = null, var filterId: String? = null,
private var aggregationType: AggregationType? = null, private var aggregationType: AggregationType? = null,
var userGenerated: Boolean = false, var userGenerated: Boolean = false,
var reportSetupId: String? = null, var reportSetupId: String? = null
var includedTransactions: List<TransactionType> = listOf()
) { ) {
constructor( constructor(display: Display = Display.TABLE,
progressValues: ProgressValues = ProgressValues.NONE, progressValues: ProgressValues = ProgressValues.NONE,
stats: List<Stat> = listOf(), stats: List<Stat> = listOf(),
criterias: List<Criteria> = listOf(), criterias: List<Criteria> = listOf(),
@ -45,18 +49,18 @@ class Calculator {
aggregationType: AggregationType? = null, aggregationType: AggregationType? = null,
userGenerated: Boolean = false, userGenerated: Boolean = false,
reportSetupId: String? = null) : reportSetupId: String? = null) :
this(progressValues, stats, criterias, filter?.query ?: Query(), filter?.id, aggregationType, userGenerated, reportSetupId) this(display, progressValues, stats, criterias, filter?.query ?: Query(), filter?.id, aggregationType, userGenerated, reportSetupId)
/** /**
* Specifies whether progress values should be added and their kind * Specifies whether progress values should be added and their kind
*/ */
// var progressValues: ProgressValues = progressValues var progressValues: ProgressValues = progressValues
// get() { get() {
// if (field == ProgressValues.NONE && this.display.requireProgressValues) { if (field == ProgressValues.NONE && this.display == Display.PROGRESS) {
// return ProgressValues.STANDARD return ProgressValues.STANDARD
// } }
// return field return field
// } }
init { init {
this.aggregationType?.let { this.aggregationType?.let {
@ -64,6 +68,40 @@ class Calculator {
} }
} }
/**
* The way the computed stats are going to be displayed
*/
enum class Display : RowRepresentable {
TABLE,
PROGRESS,
COMPARISON,
MAP,
POLYNOMIAL;
override val resId: Int?
get() {
return when (this) {
TABLE -> R.string.table
PROGRESS -> R.string.progress
COMPARISON -> R.string.comparison
MAP -> R.string.map
POLYNOMIAL -> null
}
}
val activityClass: Class<*>
get() {
return when (this) {
TABLE -> TableReportActivity::class.java
PROGRESS -> ProgressReportActivity::class.java
COMPARISON -> ComparisonReportActivity::class.java
else -> throw IllegalStateException("undefined activity for report display")
// MAP -> R.string.map
// POLYNOMIAL -> null
}
}
}
/** /**
* The type of evolution numericValues * The type of evolution numericValues
@ -80,7 +118,7 @@ class Calculator {
val computeStandardDeviation: Boolean val computeStandardDeviation: Boolean
get() { get() {
this.stats.forEach { this.stats.forEach {
if (it.isStandardDeviation) { if (it == STANDARD_DEVIATION_BB_PER_100_HANDS || it == STANDARD_DEVIATION || it == STANDARD_DEVIATION_HOURLY) {
return true return true
} }
} }
@ -94,7 +132,6 @@ class Calculator {
get() { get() {
return this.stats.contains(LONGEST_STREAKS) return this.stats.contains(LONGEST_STREAKS)
} }
/** /**
* Whether the values should be sorted * Whether the values should be sorted
*/ */
@ -102,7 +139,6 @@ class Calculator {
get() { get() {
return this.progressValues != ProgressValues.NONE || this.computeLongestStreak return this.progressValues != ProgressValues.NONE || this.computeLongestStreak
} }
/** /**
* Whether the number of locations played should be computed * Whether the number of locations played should be computed
*/ */
@ -152,6 +188,7 @@ class Calculator {
): Report { ): Report {
val options = Options( val options = Options(
display = Options.Display.PROGRESS,
progressValues = Options.ProgressValues.STANDARD, progressValues = Options.ProgressValues.STANDARD,
stats = listOf(stat), stats = listOf(stat),
aggregationType = aggregationType aggregationType = aggregationType
@ -177,13 +214,13 @@ class Calculator {
val computableGroups: MutableList<ComputableGroup> = mutableListOf() val computableGroups: MutableList<ComputableGroup> = mutableListOf()
val combinations = options.criterias.combined() options.criterias.combined().forEach { comparatorQuery ->
// Timber.d("Combinations: ${ combinations.map { it.defaultName }}")
for (comparatorQuery in combinations) {
comparatorQuery.merge(options.query) comparatorQuery.merge(options.query)
val group = ComputableGroup(comparatorQuery) val group = ComputableGroup(comparatorQuery)
computableGroups.add(group) computableGroups.add(group)
} }
if (computableGroups.size == 0) { if (computableGroups.size == 0) {
@ -201,6 +238,7 @@ class Calculator {
val report = Report(options) val report = Report(options)
groups.forEach { group -> groups.forEach { group ->
// val s = Date()
// Clean existing computables / sessionSets if group is reused // Clean existing computables / sessionSets if group is reused
group.cleanup() group.cleanup()
@ -240,27 +278,11 @@ class Calculator {
val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues) val results = ComputedResults(computableGroup, options.shouldManageMultiGroupProgressValues)
val computables = computableGroup.computables(realm, options.shouldSortValues) val computables = computableGroup.computables(realm, options.shouldSortValues)
// Timber.d(">>>> Start computing group ${computableGroup.name}, ${computables.size} computables")
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()) 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 sum = computables.sum(ComputableResult.Field.RATED_NET.identifier).toDouble()
results.addStat(NET_RESULT, sum)
val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble() val totalHands = computables.sum(ComputableResult.Field.ESTIMATED_HANDS.identifier).toDouble()
results.addStat(HANDS_PLAYED, totalHands) results.addStat(HANDS_PLAYED, totalHands)
@ -277,60 +299,41 @@ class Calculator {
val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble() val totalBuyin = computables.sum(ComputableResult.Field.RATED_BUYIN.identifier).toDouble()
results.addStat(TOTAL_BUYIN, totalBuyin) results.addStat(TOTAL_BUYIN, totalBuyin)
val totalTips = computables.sum(ComputableResult.Field.RATED_TIPS.identifier).toDouble()
results.addStat(TOTAL_TIPS, totalTips)
// Timber.d("########## totalBuyin = ${totalBuyin} ### sum = ${sum}")
val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble() val maxNetResult = computables.max(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
maxNetResult?.let { maxNetResult?.let {
results.addStat(MAXIMUM_NET_RESULT, it) results.addStat(MAXIMUM_NETRESULT, it)
} }
val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble() val minNetResult = computables.min(ComputableResult.Field.RATED_NET.identifier)?.toDouble()
minNetResult?.let { minNetResult?.let {
results.addStat(MINIMUM_NET_RESULT, it) results.addStat(MINIMUM_NETRESULT, it)
} }
Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 -> Stat.netBBPer100Hands(bbSum, totalHands)?.let { netBB100 ->
results.addStat(NET_BB_PER_100_HANDS, netBB100) results.addStat(NET_BB_PER_100_HANDS, netBB100)
} }
Stat.returnOnInvestment(ratedNet, totalBuyin)?.let { roi -> Stat.returnOnInvestment(sum, totalBuyin)?.let { roi ->
results.addStat(ROI, 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) { if (options.computeLocationsPlayed) {
results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble()) results.addStat(LOCATIONS_PLAYED, computables.distinctBy { it.session?.location?.id }.size.toDouble())
} }
var average = 0.0 // also used for standard deviation later var average = 0.0 // also used for standard deviation later
if (computables.size > 0) { if (computables.size > 0) {
average = ratedNet / computables.size.toDouble() average = sum / computables.size.toDouble()
val winRatio = winningSessionCount.toDouble() / computables.size.toDouble() val winRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val itmRatio = winningSessionCount.toDouble() / computables.size.toDouble()
val avgBuyin = totalBuyin / computables.size.toDouble() val avgBuyin = totalBuyin / computables.size.toDouble()
results.addStats( results.addStats(
setOf( setOf(
ComputedStat(AVERAGE, average), ComputedStat(AVERAGE, average),
ComputedStat(WIN_RATIO, winRatio), ComputedStat(WIN_RATIO, winRatio),
ComputedStat(TOURNAMENT_ITM_RATIO, itmRatio),
ComputedStat(AVERAGE_BUYIN, avgBuyin) ComputedStat(AVERAGE_BUYIN, avgBuyin)
) )
) )
} }
var averageBB = 0.0
if (bbSessionCount > 0) {
averageBB = bbSum / bbSessionCount
results.addStat(AVERAGE_NET_BB, averageBB)
}
val shouldIterateOverComputables = val shouldIterateOverComputables =
(options.progressValues == Options.ProgressValues.STANDARD || options.computeLongestStreak) (options.progressValues == Options.ProgressValues.STANDARD || options.computeLongestStreak)
@ -348,7 +351,6 @@ class Calculator {
var longestWinStreak = 0 var longestWinStreak = 0
var longestLoseStreak = 0 var longestLoseStreak = 0
var currentStreak = 0 var currentStreak = 0
var tITMCount = 0
computables.forEach { computable -> computables.forEach { computable ->
index++ index++
@ -356,7 +358,6 @@ class Calculator {
tBBSum += computable.bbNet tBBSum += computable.bbNet
tBBSessionCount += computable.hasBigBlind tBBSessionCount += computable.hasBigBlind
tWinningSessionCount += computable.isPositive tWinningSessionCount += computable.isPositive
tITMCount += computable.isPositive
tBuyinSum += computable.ratedBuyin tBuyinSum += computable.ratedBuyin
tHands += computable.estimatedHands tHands += computable.estimatedHands
@ -377,21 +378,16 @@ class Calculator {
} }
val session = val session =
computable.session ?: throw PAIllegalStateException("Computing lone ComputableResult") computable.session ?: throw IllegalStateException("Computing lone ComputableResult")
results.addEvolutionValue(tSum, stat = NET_RESULT, data = session) results.addEvolutionValue(tSum, stat = NET_RESULT, data = session)
results.addEvolutionValue(tSum / index, stat = AVERAGE, data = session) results.addEvolutionValue(tSum / index, stat = AVERAGE, data = session)
results.addEvolutionValue(index.toDouble(), stat = NUMBER_OF_GAMES, data = session) results.addEvolutionValue(index.toDouble(), stat = NUMBER_OF_GAMES, data = session)
results.addEvolutionValue(tBBSum / tBBSessionCount, stat = AVERAGE_NET_BB, data = session) results.addEvolutionValue(tBBSum / tBBSessionCount, stat = AVERAGE_NET_BB, data = session)
results.addEvolutionValue(tBBSum, stat = BB_NET_RESULT, data = session)
results.addEvolutionValue( results.addEvolutionValue(
(tWinningSessionCount.toDouble() / index.toDouble()), (tWinningSessionCount.toDouble() / index.toDouble()),
stat = WIN_RATIO, stat = WIN_RATIO,
data = session 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(tBuyinSum / index, stat = AVERAGE_BUYIN, data = session)
results.addEvolutionValue(computable.ratedNet, stat = STANDARD_DEVIATION, data = session) results.addEvolutionValue(computable.ratedNet, stat = STANDARD_DEVIATION, data = session)
@ -433,9 +429,9 @@ class Calculator {
} }
} }
val shouldIterateOverSets = computableGroup.conditions.isNotEmpty() val shouldIterateOverSets = computableGroup.conditions.isNotEmpty() ||
|| options.progressValues != Options.ProgressValues.NONE options.progressValues != Options.ProgressValues.NONE ||
|| options.computeDaysPlayed options.computeDaysPlayed
// Session Set // Session Set
if (shouldIterateOverSets) { if (shouldIterateOverSets) {
@ -482,6 +478,10 @@ class Calculator {
) )
results.addEvolutionValue(tHourlyRateBB, stat = HOURLY_RATE_BB, data = sessionSet) results.addEvolutionValue(tHourlyRateBB, stat = HOURLY_RATE_BB, data = sessionSet)
Stat.netBBPer100Hands(tBBSum, tTotalHands)?.let { netBB100 ->
results.addEvolutionValue(netBB100, stat = NET_BB_PER_100_HANDS, data = sessionSet)
}
} }
Options.ProgressValues.TIMED -> { Options.ProgressValues.TIMED -> {
results.addEvolutionValue(tRatedNetSum, tHourlyDuration, NET_RESULT, sessionSet) results.addEvolutionValue(tRatedNetSum, tHourlyDuration, NET_RESULT, sessionSet)
@ -533,7 +533,7 @@ class Calculator {
var hourlyRate = 0.0 var hourlyRate = 0.0
if (gHourlyDuration != null) { if (gHourlyDuration != null) {
hourlyRate = ratedNet / gHourlyDuration hourlyRate = sum / gHourlyDuration
if (sessionSets.size > 0) { if (sessionSets.size > 0) {
val avgDuration = gHourlyDuration / sessionSets.size val avgDuration = gHourlyDuration / sessionSets.size
results.addStat(HOURLY_RATE, hourlyRate) results.addStat(HOURLY_RATE, hourlyRate)
@ -560,19 +560,15 @@ class Calculator {
// Session // Session
var stdSum = 0.0 var stdSum = 0.0
var stdBBSum = 0.0
var stdBBper100HandsSum = 0.0 var stdBBper100HandsSum = 0.0
computables.forEach { session -> computables.forEach { session ->
stdSum += (session.ratedNet - average).pow(2.0) stdSum += Math.pow(session.ratedNet - average, 2.0)
stdBBSum += (session.bbNet - averageBB).pow(2.0) stdBBper100HandsSum += Math.pow(session.bbPer100Hands - bbPer100Hands, 2.0)
stdBBper100HandsSum += (session.bbPer100Hands - bbPer100Hands).pow(2.0)
} }
val standardDeviation = sqrt(stdSum / computables.size) val standardDeviation = Math.sqrt(stdSum / computables.size)
val standardDeviationBB = sqrt(stdBBSum / computables.size) val standardDeviationBBper100Hands = Math.sqrt(stdBBper100HandsSum / computables.size)
val standardDeviationBBper100Hands = sqrt(stdBBper100HandsSum / computables.size)
results.addStat(STANDARD_DEVIATION, standardDeviation) results.addStat(STANDARD_DEVIATION, standardDeviation)
results.addStat(STANDARD_DEVIATION_BB, standardDeviationBB)
results.addStat(STANDARD_DEVIATION_BB_PER_100_HANDS, standardDeviationBBper100Hands) results.addStat(STANDARD_DEVIATION_BB_PER_100_HANDS, standardDeviationBBper100Hands)
// Session Set // Session Set
@ -581,9 +577,9 @@ class Calculator {
sessionSets.forEach { set -> sessionSets.forEach { set ->
val ssStats = SSStats(set, computableGroup.query) val ssStats = SSStats(set, computableGroup.query)
val sHourlyRate = ssStats.hourlyRate val sHourlyRate = ssStats.hourlyRate
hourlyStdSum += (sHourlyRate - hourlyRate).pow(2.0) hourlyStdSum += Math.pow(sHourlyRate - hourlyRate, 2.0)
} }
val hourlyStandardDeviation = sqrt(hourlyStdSum / sessionSets.size) val hourlyStandardDeviation = Math.sqrt(hourlyStdSum / sessionSets.size)
results.addStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation) results.addStat(STANDARD_DEVIATION_HOURLY, hourlyStandardDeviation)
} }
@ -619,10 +615,10 @@ class SSStats(sessionSet: SessionSet, query: Query) { // Session Set Stats
if (setSessions.size == filteredSessions.size) { if (setSessions.size == filteredSessions.size) {
this.initStatsWithSet(sessionSet) this.initStatsWithSet(sessionSet)
} else { } else {
ratedNet = filteredSessions.sumOf { it.computableResult?.ratedNet ?: 0.0 } ratedNet = filteredSessions.sumByDouble { it.computableResult?.ratedNet ?: 0.0 }
bbSum = filteredSessions.sumOf { it.bbNet } bbSum = filteredSessions.sumByDouble { it.bbNet }
hourlyDuration = filteredSessions.hourlyDuration hourlyDuration = filteredSessions.hourlyDuration
estimatedHands = filteredSessions.sumOf { it.estimatedHands } estimatedHands = filteredSessions.sumByDouble { it.estimatedHands }
} }
} }
} }

@ -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)
}
}

@ -1,13 +1,24 @@
package net.pokeranalytics.android.calculus package net.pokeranalytics.android.calculus
import android.content.Context import android.content.Context
import net.pokeranalytics.android.exceptions.PAIllegalStateException import com.github.mikephil.charting.data.*
import net.pokeranalytics.android.model.interfaces.GraphIdentifiableEntry import io.realm.Realm
import net.pokeranalytics.android.ui.graph.Graph import io.realm.RealmResults
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.Timed
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.view.DefaultLegendValues import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent import net.pokeranalytics.android.ui.view.LegendContent
import net.pokeranalytics.android.util.ColorUtils
import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.util.TextFormat
import kotlin.math.abs
/** /**
* The class returned after performing calculation in the Calculator object * The class returned after performing calculation in the Calculator object
@ -19,10 +30,7 @@ class Report(var options: Calculator.Options) {
*/ */
private var _results: MutableList<ComputedResults> = mutableListOf() private var _results: MutableList<ComputedResults> = mutableListOf()
val results: List<ComputedResults> val results: List<ComputedResults> = this._results
get() {
return this._results
}
/** /**
* Adds a new result to the list of ComputedResults * Adds a new result to the list of ComputedResults
@ -31,32 +39,142 @@ class Report(var options: Calculator.Options) {
this._results.add(result) this._results.add(result)
} }
fun max(stat: Stat): ComputedResults? { /**
* Returns the list of entries corresponding to the provided [stat]
* One value will be returned by result
*/
fun lineEntries(stat: Stat? = null, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
val statToUse = stat ?: options.stats.firstOrNull()
val statName = statToUse?.name ?: ""
var computedResults: ComputedResults? = null statToUse?.let {
var count = 0 this._results.forEachIndexed { index, results ->
var max = Double.MIN_VALUE results.computedStat(it)?.progressValue?.let { progressValue ->
for (cr in this._results) { entries.add(Entry(index.toFloat(), progressValue.toFloat(), results))
cr.computedStat(stat)?.value?.let { value ->
count += 1
if (value > max) {
computedResults = cr
max = value
} }
} }
} }
return if (count >= 2) { computedResults } else { null }
return DataSetFactory.lineDataSetInstance(entries, statName, context)
} }
fun 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)
}
class Point(val x: Double, val y: Double, val data: Any) { fun multiLineEntries(context: Context): List<LineDataSet> {
constructor(y: Double, data: Any) : this(0.0, y, data) 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
}
}
/**
* A sessionGroup of computable items identified by a name
*/
class ComputableGroup(var query: Query, var stats: 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
} }
class ComputedResults(group: ComputableGroup, /**
shouldManageMultiGroupProgressValues: Boolean = false) : GraphUnderlyingEntry { * 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._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
}
fun cleanup() {
this._computables = null
this._sessionSets = null
}
val isEmpty: Boolean
get() {
return this._computables?.isEmpty() ?: true
}
}
class ComputedResults(group: ComputableGroup, shouldManageMultiGroupProgressValues: Boolean = false) :
GraphUnderlyingEntry {
/** /**
* The session group used to computed the statIds * The session group used to computed the statIds
@ -71,18 +189,14 @@ class ComputedResults(group: ComputableGroup,
private var shouldManageMultiGroupProgressValues = shouldManageMultiGroupProgressValues private var shouldManageMultiGroupProgressValues = shouldManageMultiGroupProgressValues
private val allStats: Collection<ComputedStat> fun allStats(): Collection<ComputedStat> {
get() { return this._computedStats.values } return this._computedStats.values
val evolutionValues: Map<Stat, MutableList<Point>>
get() {
return this._evolutionValues
} }
/** /**
* Adds a value to the evolution values * Adds a value to the evolution values
*/ */
fun addEvolutionValue(value: Double, duration: Double? = null, stat: Stat, data: GraphIdentifiableEntry) { fun addEvolutionValue(value: Double, duration: Double? = null, stat: Stat, data: Timed) {
val point = if (duration != null) { val point = if (duration != null) {
Point(duration, y = value, data = data.objectIdentifier) Point(duration, y = value, data = data.objectIdentifier)
@ -127,7 +241,7 @@ class ComputedResults(group: ComputableGroup,
fun computeProgressValues(computedResults: ComputedResults) { fun computeProgressValues(computedResults: ComputedResults) {
this.allStats.forEach { computedStat -> this.allStats().forEach { computedStat ->
val stat = computedStat.stat val stat = computedStat.stat
computedResults.computedStat(stat)?.let { previousComputedStat -> computedResults.computedStat(stat)?.let { previousComputedStat ->
when (stat) { when (stat) {
@ -162,7 +276,7 @@ class ComputedResults(group: ComputableGroup,
val bbSessionCount = this.computedStat(Stat.BB_SESSION_COUNT)?.progressValue val bbSessionCount = this.computedStat(Stat.BB_SESSION_COUNT)?.progressValue
val totalBuyin = this.computedStat(Stat.TOTAL_BUYIN)?.progressValue val totalBuyin = this.computedStat(Stat.TOTAL_BUYIN)?.progressValue
this.allStats.forEach { computedStat -> this.allStats().forEach { computedStat ->
when (computedStat.stat) { when (computedStat.stat) {
Stat.HOURLY_RATE -> { Stat.HOURLY_RATE -> {
if (netResult != null && duration != null) { if (netResult != null && duration != null) {
@ -237,6 +351,79 @@ class ComputedResults(group: ComputableGroup,
this.consolidateProgressStats() this.consolidateProgressStats()
} }
// MPAndroidChart
fun defaultStatEntries(stat: Stat, context: Context): DataSet<out Entry> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HOURLY_DURATION -> this.barEntries(stat, context = context)
Stat.STANDARD_DEVIATION -> this.distributionEntries(stat, context)
else -> this.singleLineEntries(stat, context)
}
}
fun singleLineEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEachIndexed { index, p ->
entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, this.group.query.getName(context), context)
}
fun durationEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, stat.name, context)
}
private fun barEntries(stat: Stat, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context)
}
private fun distributionEntries(stat: Stat, context: Context): BarDataSet {
val colors = mutableListOf<Int>()
val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points ->
val negative = mutableListOf<Point>()
val positive = mutableListOf<Point>()
points.sortByDescending { it.y }
points.forEach {
if (it.y < 0) {
negative.add(it)
} else {
positive.add(it)
}
}
negative.forEachIndexed { index, p ->
entries.add(BarEntry(index.toFloat(), abs(p.y.toFloat()), p.data))
colors.add(context.getColor(R.color.red))
}
positive.forEachIndexed { index, p ->
val x = negative.size + index.toFloat()
entries.add(BarEntry(x, p.y.toFloat(), p.data))
colors.add(context.getColor(R.color.green))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context, colors)
}
val isEmpty: Boolean val isEmpty: Boolean
get() { get() {
return this.group.isEmpty return this.group.isEmpty
@ -250,31 +437,31 @@ class ComputedResults(group: ComputableGroup,
override fun formattedValue(stat: Stat): TextFormat { override fun formattedValue(stat: Stat): TextFormat {
this.computedStat(stat)?.let { this.computedStat(stat)?.let {
return it.textFormat return it.format()
} ?: run { } ?: run {
throw PAIllegalStateException("Missing stat in results") throw IllegalStateException("Missing stat in results")
} }
} }
override fun legendValues( override fun legendValues(
stat: Stat, stat: Stat,
total: Double, entry: Entry,
style: Graph.Style, style: GraphFragment.Style,
groupName: String, groupName: String,
context: Context context: Context
): LegendContent { ): LegendContent {
when (style) { when (style) {
Graph.Style.BAR -> { GraphFragment.Style.BAR -> {
return when (stat) { return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> { Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.textFormat(total, currency = null) val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
DefaultLegendValues(this.entryTitle(context), totalStatValue) DefaultLegendValues(this.entryTitle(context), totalStatValue)
} }
else -> { else -> {
val entryValue = this.formattedValue(stat) val entryValue = this.formattedValue(stat)
val countValue = this.computedStat(Stat.NUMBER_OF_GAMES)?.textFormat val countValue = this.computedStat(Stat.NUMBER_OF_GAMES)?.format()
DefaultLegendValues(this.entryTitle(context), entryValue, countValue) DefaultLegendValues(this.entryTitle(context), entryValue, countValue)
} }
} }
@ -283,12 +470,12 @@ class ComputedResults(group: ComputableGroup,
return when (stat) { return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> { Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.WIN_RATIO -> {
val totalStatValue = stat.textFormat(total, currency = null) val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
DefaultLegendValues(this.entryTitle(context), totalStatValue) DefaultLegendValues(this.entryTitle(context), totalStatValue)
} }
else -> { else -> {
val entryValue = this.formattedValue(stat) val entryValue = this.formattedValue(stat)
val totalStatValue = stat.textFormat(total, currency = null) val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue) DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue)
} }
} }
@ -298,3 +485,9 @@ class ComputedResults(group: ComputableGroup,
} }
} }
class Point(val x: Double, val y: Double, val data: Any) {
constructor(y: Double, data: Any) : this(0.0, y, data)
}

@ -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)
}
}

@ -3,8 +3,6 @@ package net.pokeranalytics.android.calculus
import android.content.Context 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.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.NULL_TEXT
@ -16,14 +14,15 @@ import net.pokeranalytics.android.util.extensions.formattedHourlyDuration
import net.pokeranalytics.android.util.extensions.toCurrency import net.pokeranalytics.android.util.extensions.toCurrency
import java.util.* import java.util.*
import kotlin.math.exp import kotlin.math.exp
import kotlin.math.pow
class StatFormattingException(message: String) : Exception(message) 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(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepresentable {
NET_RESULT(1), NET_RESULT(1),
BB_NET_RESULT(2), BB_NET_RESULT(2),
@ -45,17 +44,14 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HANDS_PLAYED(18), HANDS_PLAYED(18),
LOCATIONS_PLAYED(19), LOCATIONS_PLAYED(19),
LONGEST_STREAKS(20), LONGEST_STREAKS(20),
MAXIMUM_NET_RESULT(21), MAXIMUM_NETRESULT(21),
MINIMUM_NET_RESULT(22), MINIMUM_NETRESULT(22),
MAXIMUM_DURATION(23), MAXIMUM_DURATION(23),
DAYS_PLAYED(24), DAYS_PLAYED(24),
WINNING_SESSION_COUNT(25), WINNING_SESSION_COUNT(25),
BB_SESSION_COUNT(26), BB_SESSION_COUNT(26),
TOTAL_BUYIN(27), TOTAL_BUYIN(27),
RISK_OF_RUIN(28), RISK_OF_RUIN(28),
STANDARD_DEVIATION_BB(29),
TOURNAMENT_ITM_RATIO(30),
TOTAL_TIPS(31)
; ;
companion object : IntSearchable<Stat> { companion object : IntSearchable<Stat> {
@ -95,7 +91,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
} }
val numerator = -2 * hourlyRate * bankrollValue val numerator = -2 * hourlyRate * bankrollValue
val denominator = hourlyStandardDeviation.pow(2.0) val denominator = Math.pow(hourlyStandardDeviation, 2.0)
val ratio = numerator / denominator val ratio = numerator / denominator
return exp(ratio) return exp(ratio)
@ -103,8 +99,6 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
} }
override val value: Int = this.uniqueIdentifier
override val resId: Int? override val resId: Int?
get() { get() {
return when (this) { return when (this) {
@ -125,18 +119,15 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
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_per_hour
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.number_of_hands HANDS_PLAYED -> R.string.number_of_hands
LOCATIONS_PLAYED -> R.string.locations_played LOCATIONS_PLAYED -> R.string.locations_played
LONGEST_STREAKS -> R.string.longest_streaks LONGEST_STREAKS -> R.string.longest_streaks
MAXIMUM_NET_RESULT -> R.string.max_net_result MAXIMUM_NETRESULT -> R.string.max_net_result
MINIMUM_NET_RESULT -> R.string.min_net_result MINIMUM_NETRESULT -> R.string.min_net_result
MAXIMUM_DURATION -> R.string.longest_session MAXIMUM_DURATION -> R.string.longest_session
DAYS_PLAYED -> R.string.days_played DAYS_PLAYED -> R.string.days_played
TOTAL_BUYIN -> R.string.total_buyin TOTAL_BUYIN -> R.string.total_buyin
TOURNAMENT_ITM_RATIO -> R.string.itm_ratio else -> throw IllegalStateException("Stat ${this.name} name required but undefined")
TOTAL_TIPS -> R.string.total_tips
else -> throw PAIllegalStateException("Stat ${this.name} name required but undefined")
} }
} }
@ -144,7 +135,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
/** /**
* Formats the value of the stat to be suitable for display * Formats the value of the stat to be suitable for display
*/ */
fun textFormat(value: Double, secondValue: Double? = null, currency: Currency? = null): TextFormat { fun format(value: Double, secondValue: Double? = null, currency: Currency? = null): TextFormat {
if (value.isNaN()) { if (value.isNaN()) {
return TextFormat(NULL_TEXT, R.color.white) return TextFormat(NULL_TEXT, R.color.white)
@ -152,14 +143,14 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
when (this) { when (this) {
// Amounts + red/green // Amounts + red/green
NET_RESULT, HOURLY_RATE, AVERAGE, MAXIMUM_NET_RESULT, MINIMUM_NET_RESULT -> { NET_RESULT, HOURLY_RATE, AVERAGE, MAXIMUM_NETRESULT, MINIMUM_NETRESULT -> {
val color = if (value >= this.threshold) R.color.green else R.color.red val color = if (value >= this.threshold) R.color.green else R.color.red
return TextFormat(value.toCurrency(currency), color) return TextFormat(value.toCurrency(currency), color)
} }
// Red/green numericValues // Red/green numericValues
HOURLY_RATE_BB, AVERAGE_NET_BB, NET_BB_PER_100_HANDS, BB_NET_RESULT -> { 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 val color = if (value >= this.threshold) R.color.green else R.color.red
return TextFormat(value.formatted, color) return TextFormat(value.formatted(), color)
} }
// white integers // white integers
NUMBER_OF_SETS, NUMBER_OF_GAMES, HANDS_PLAYED, LOCATIONS_PLAYED, DAYS_PLAYED -> { NUMBER_OF_SETS, NUMBER_OF_GAMES, HANDS_PLAYED, LOCATIONS_PLAYED, DAYS_PLAYED -> {
@ -168,17 +159,12 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> { HOURLY_DURATION, AVERAGE_HOURLY_DURATION, MAXIMUM_DURATION -> {
return TextFormat(value.formattedHourlyDuration()) return TextFormat(value.formattedHourlyDuration())
} // red/green percentages } // red/green percentages
WIN_RATIO, ROI, TOURNAMENT_ITM_RATIO -> { WIN_RATIO, ROI, RISK_OF_RUIN -> {
val color = if (value * 100 >= this.threshold) R.color.green else R.color.red val color = if (value * 100 >= this.threshold) R.color.green else R.color.red
return TextFormat("${(value * 100).formatted}%", color) return TextFormat("${(value * 100).formatted()}%", color)
} } // white amountsr
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, AVERAGE_BUYIN, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY,
STANDARD_DEVIATION_BB_PER_100_HANDS, STANDARD_DEVIATION_BB, TOTAL_BUYIN, TOTAL_TIPS -> { STANDARD_DEVIATION_BB_PER_100_HANDS -> {
return TextFormat(value.toCurrency(currency)) return TextFormat(value.toCurrency(currency))
} }
LONGEST_STREAKS -> { LONGEST_STREAKS -> {
@ -191,8 +177,6 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
private val threshold: Double private val threshold: Double
get() { get() {
return when (this) { return when (this) {
RISK_OF_RUIN -> 5.0
TOURNAMENT_ITM_RATIO -> 10.0
WIN_RATIO -> 50.0 WIN_RATIO -> 50.0
else -> 0.0 else -> 0.0
} }
@ -208,12 +192,11 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, HOURLY_RATE -> R.string.average HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, HOURLY_RATE -> R.string.average
NUMBER_OF_SETS -> R.string.number_of_sessions NUMBER_OF_SETS -> R.string.number_of_sessions
NUMBER_OF_GAMES -> R.string.number_of_records NUMBER_OF_GAMES -> R.string.number_of_records
NET_RESULT, BB_NET_RESULT -> R.string.total NET_RESULT -> R.string.total
STANDARD_DEVIATION -> R.string.net_result 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_HOURLY -> R.string.hour_rate_without_pauses
STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands 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) WIN_RATIO, HOURLY_DURATION -> return this.localizedTitle(context)
else -> null else -> null
} }
resId?.let { resId?.let {
@ -246,40 +229,19 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
val hasProgressGraph: Boolean val hasProgressGraph: Boolean
get() { get() {
return when (this) { return when (this) {
HOURLY_DURATION, AVERAGE_HOURLY_DURATION, STANDARD_DEVIATION_BB, HOURLY_DURATION, AVERAGE_HOURLY_DURATION,
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, HANDS_PLAYED, STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> false
STANDARD_DEVIATION_BB_PER_100_HANDS, TOTAL_TIPS -> false
else -> true 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 * Returns if the stat has a significant value to display in a progress graph
*/ */
val graphSignificantIndividualValue: Boolean val graphSignificantIndividualValue: Boolean
get() { get() {
return when (this) { return when (this) {
AVERAGE, WIN_RATIO, TOURNAMENT_ITM_RATIO, WIN_RATIO, NUMBER_OF_SETS, NUMBER_OF_GAMES, STANDARD_DEVIATION, HOURLY_DURATION -> false
NUMBER_OF_SETS, NUMBER_OF_GAMES,
STANDARD_DEVIATION, HOURLY_DURATION -> false
else -> true else -> true
} }
} }
@ -324,8 +286,7 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
return when (this) { return when (this) {
NET_RESULT, NET_BB_PER_100_HANDS, HOURLY_RATE_BB, NET_RESULT, NET_BB_PER_100_HANDS, HOURLY_RATE_BB,
AVERAGE_HOURLY_DURATION, HOURLY_DURATION, AVERAGE_HOURLY_DURATION, HOURLY_DURATION,
NUMBER_OF_SETS, ROI, AVERAGE_BUYIN, NUMBER_OF_SETS, ROI, AVERAGE_BUYIN, WIN_RATIO,
WIN_RATIO, TOURNAMENT_ITM_RATIO,
AVERAGE_NET_BB, NUMBER_OF_GAMES, AVERAGE -> true AVERAGE_NET_BB, NUMBER_OF_GAMES, AVERAGE -> true
else -> false else -> false
} }
@ -333,3 +294,33 @@ enum class Stat(override var uniqueIdentifier: Int) : IntIdentifiable, RowRepres
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
} }
/**
* ComputedStat contains a [stat] and their associated [value]
*/
class ComputedStat(var stat: Stat, var value: Double, var secondValue: Double? = null, var currency: Currency? = null) {
constructor(stat: Stat, value: Double, previousValue: Double?) : this(stat, value) {
if (previousValue != null) {
this.variation = (value - previousValue) / previousValue
}
}
/**
* The value used to get evolution dataset
*/
var progressValue: Double? = null
/**
* The variation of the stat
*/
var variation: Double? = null
/**
* Formats the value of the stat to be suitable for display
*/
fun format(): TextFormat {
return this.stat.format(this.value, this.secondValue, this.currency)
}
}

@ -5,11 +5,7 @@ import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.ComputableGroup import net.pokeranalytics.android.calculus.ComputableGroup
import net.pokeranalytics.android.calculus.ComputedResults import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Stat 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.model.realm.*
import net.pokeranalytics.android.util.extensions.findById
class BankrollCalculator { class BankrollCalculator {
@ -17,15 +13,12 @@ class BankrollCalculator {
fun computeReport(realm: Realm, setup: BankrollReportSetup) : BankrollReport { fun computeReport(realm: Realm, setup: BankrollReportSetup) : BankrollReport {
//val realm = Realm.getDefaultInstance()
val report = BankrollReport(setup) val report = BankrollReport(setup)
realm.refresh() // fixes an issue where a newly created bankroll is not found, throwing an exception
val bankrolls: List<Bankroll> = val bankrolls: List<Bankroll> =
if (setup.bankrollId != null) { if (setup.bankroll != null) listOf(setup.bankroll)
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() else realm.where(Bankroll::class.java).findAll()
var initialValue = 0.0 var initialValue = 0.0
@ -39,49 +32,30 @@ class BankrollCalculator {
initialValue += bankroll.initialValue * rate 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 -> bankroll.transactions?.let { transactions ->
val sum = transactions.sum("amount") val sum = transactions.sum("amount")
transactionNet += rate * sum.toDouble() 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.transactionsNet = transactionNet
report.initial = initialValue report.initial = initialValue
val baseQuery = setup.query(realm) val query = setup.query
val transactions = Filter.queryOn<Transaction>(realm, baseQuery) val transactions = Filter.queryOn<Transaction>(realm, query)
report.addDatedItems(transactions) report.addDatedItems(transactions)
transactions.forEach { transactions.forEach {
report.addTransaction(it) report.addTransaction(it)
} }
val sessionQuery = Query(QueryCondition.DateNotNull).merge(baseQuery) val sessions = Filter.queryOn<Session>(realm, query)
val sessions = Filter.queryOn<Session>(realm, sessionQuery)
report.addDatedItems(sessions) report.addDatedItems(sessions)
if (setup.virtualBankroll) { if (setup.virtualBankroll) {
val options = Calculator.Options(stats = listOf(Stat.NET_RESULT, Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY)) val options = Calculator.Options(stats = listOf(Stat.NET_RESULT, Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY))
val group = ComputableGroup(baseQuery) val group = ComputableGroup(query)
val result = Calculator.compute(realm, group, options) val result = Calculator.compute(realm, group, options)
result.computedStat(Stat.NET_RESULT)?.let { result.computedStat(Stat.NET_RESULT)?.let {
report.netResult = it.value report.netResult = it.value
@ -89,18 +63,20 @@ class BankrollCalculator {
this.computeRiskOfRuin(report, result) this.computeRiskOfRuin(report, result)
} else { } else {
val results = Filter.queryOn<Result>(realm, baseQuery)
val results = Filter.queryOn<Result>(realm, query)
report.netResult = results.sum("net").toDouble() report.netResult = results.sum("net").toDouble()
} }
val depositType = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm) val depositType = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm)
report.transactionBuckets[depositType.id]?.let { bucket -> report.transactionBuckets[depositType.id]?.let { bucket ->
report.depositTotal = bucket.transactions.sumOf { it.amount } report.depositTotal = bucket.transactions.sumByDouble { it.amount }
} }
val withdrawalType = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm) val withdrawalType = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm)
report.transactionBuckets[withdrawalType.id]?.let { bucket -> report.transactionBuckets[withdrawalType.id]?.let { bucket ->
report.withdrawalTotal = bucket.transactions.sumOf { it.amount } report.withdrawalTotal = bucket.transactions.sumByDouble { it.amount }
} }
report.generateGraphPointsIfNecessary() report.generateGraphPointsIfNecessary()

@ -3,27 +3,54 @@ package net.pokeranalytics.android.calculus.bankroll
import android.content.Context import android.content.Context
import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet 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.Query
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.DatedBankrollGraphEntry import net.pokeranalytics.android.model.interfaces.DatedValue
import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.ui.graph.DataSetFactory import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import java.util.* import java.util.*
import kotlin.collections.HashMap
//object BankrollReportManager {
//
// var mainReport: BankrollReport? = null
// var reports: MutableMap<String, BankrollReport> = mutableMapOf()
//
// fun udpateBankrolls(bankrolls: List<Bankroll>) {
// this.invalidateMainReport()
// bankrolls.forEach {
// this.reports.remove(it.id)
// }
// }
//
// fun deleteBankrolls(bankrolls: List<Bankroll>) {
// this.invalidateMainReport()
// bankrolls.forEach {
// this.reports.remove(it.id)
// }
// }
//
// private fun invalidateMainReport() {
// this.mainReport = null
// }
//
// private fun launchReports(bankrolls: List<Bankroll>) {
//
// this.mainReport = BankrollCalculator.computeReport()
//
//
// }
//
//}
/** /**
* This class holds the results from the BankrollCalculator computations * This class holds the results from the BankrollCalculator computations
* It has all the information required for the Bankroll various displays * It has all the information required for the Bankroll various displays
*/ */
class BankrollReport(var setup: BankrollReportSetup) { class BankrollReport(var setup: BankrollReportSetup) : RowRepresentable {
/**
* The java.util.Currency
*/
var currency: Currency? = null
/** /**
* The value of the bankroll * The value of the bankroll
@ -63,7 +90,6 @@ class BankrollReport(var setup: BankrollReportSetup) {
*/ */
private fun computeBankrollTotal() { private fun computeBankrollTotal() {
this.total = this.initial + this.netResult + this.transactionsNet this.total = this.initial + this.netResult + this.transactionsNet
// Timber.d("init = $initial, net = $netResult, trans = $transactionsNet")
} }
/** /**
@ -122,12 +148,21 @@ class BankrollReport(var setup: BankrollReportSetup) {
/** /**
* The list of dated items used for the graph * The list of dated items used for the graph
*/ */
private var evolutionItems: MutableList<DatedBankrollGraphEntry> = mutableListOf() private var evolutionItems: MutableList<DatedValue> = mutableListOf()
override val viewType: Int
get() {
return if (setup.bankroll == null) {
RowViewType.LEGEND_DEFAULT.ordinal
} else {
RowViewType.TITLE_VALUE_ARROW.ordinal
}
}
/** /**
* Adds a list of dated items to the evolution items used to get the bankroll graph * Adds a list of dated items to the evolution items used to get the bankroll graph
*/ */
fun addDatedItems(items: Collection<DatedBankrollGraphEntry>) { fun addDatedItems(items: Collection<DatedValue>) {
this.evolutionItems.addAll(items) this.evolutionItems.addAll(items)
} }
@ -141,7 +176,7 @@ class BankrollReport(var setup: BankrollReportSetup) {
var bucket = this.transactionBuckets[type.id] var bucket = this.transactionBuckets[type.id]
if (bucket == null) { if (bucket == null) {
val b = TransactionBucket(type.name, this.setup.virtualBankroll) val b = TransactionBucket(this.setup.virtualBankroll)
this.transactionBuckets[type.id] = b this.transactionBuckets[type.id] = b
bucket = b bucket = b
} }
@ -149,7 +184,7 @@ class BankrollReport(var setup: BankrollReportSetup) {
bucket.addTransaction(transaction) bucket.addTransaction(transaction)
} ?: run { } ?: run {
throw PAIllegalStateException("Transaction has no type") throw IllegalStateException("Transaction has no type")
} }
} }
@ -165,14 +200,10 @@ class BankrollReport(var setup: BankrollReportSetup) {
this.evolutionItems.sortBy { it.date } this.evolutionItems.sortBy { it.date }
var total = this.initial var total = 0.0
this.evolutionItems.forEach { this.evolutionItems.forEach {
val rate = it.bankroll?.rate ?: 1.0 total += it.amount
val point = BRGraphPoint(total, it.date, it)
// 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) this.evolutionPoints.add(point)
} }
@ -197,7 +228,7 @@ class BankrollReport(var setup: BankrollReportSetup) {
* A class describing the parameters required to launch a bankroll report * 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) { class BankrollReportSetup(val bankroll: Bankroll? = null, val from: Date? = null, val to: Date? = null) {
/** /**
* Returns whether the setup concerns the virtual bankroll, * Returns whether the setup concerns the virtual bankroll,
@ -205,25 +236,29 @@ class BankrollReportSetup(val bankrollId: String? = null, val from: Date? = null
*/ */
val virtualBankroll: Boolean val virtualBankroll: Boolean
get() { get() {
return this.bankrollId == null return this.bankroll == null
} }
/** /**
* the query used to get bankroll transactions * the query used to get bankroll transactions
*/ */
fun query(realm: Realm): Query { val query: Query
get() {
val query = Query() val query = Query()
this.bankrollId?.let { this.bankroll?.let {
val bankroll = realm.findById<Bankroll>(it) ?: throw PAIllegalStateException("Bankroll not found with id $it")
val bankrollCondition = QueryCondition.AnyBankroll(bankroll) val bankrollCondition = QueryCondition.AnyBankroll(bankroll)
query.add(bankrollCondition) query.add(bankrollCondition)
} }
this.from?.let { this.from?.let {
query.add(QueryCondition.StartedFromDate(it)) val fromCondition = QueryCondition.StartedFromDate()
fromCondition.singleValue = it
query.add(fromCondition)
} }
this.to?.let { this.to?.let {
query.add(QueryCondition.StartedToDate(it)) val toCondition = QueryCondition.StartedToDate()
toCondition.singleValue = it
query.add(toCondition)
} }
return query return query
} }
@ -241,7 +276,7 @@ class BankrollReportSetup(val bankrollId: String? = null, val from: Date? = null
/** /**
* A TransactionBucket holds a list of _transactions and computes its amount sum * A TransactionBucket holds a list of _transactions and computes its amount sum
*/ */
class TransactionBucket(var name: String, useRate: Boolean = false) { class TransactionBucket(useRate: Boolean = false) {
/** /**
* Whether the bankroll rate should be used * Whether the bankroll rate should be used
@ -282,6 +317,6 @@ class TransactionBucket(var name: String, useRate: Boolean = false) {
data class BRGraphPoint(var value: Double, var date: Date, var data: Any? = null) { data class BRGraphPoint(var value: Double, var date: Date, var data: Any? = null) {
// var variation: Double = 0.0 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,6 +1,6 @@
package net.pokeranalytics.android.exceptions package net.pokeranalytics.android.exceptions
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.ui.view.rowrepresentable.FilterElementRow
class ModelException(message: String) : Exception(message) class ModelException(message: String) : Exception(message)
class FormattingException(message: String) : Exception(message) class FormattingException(message: String) : Exception(message)
@ -10,23 +10,18 @@ class ConfigurationException(message: String) : Exception(message)
class EnumIdentifierNotFoundException(message: String) : Exception(message) class EnumIdentifierNotFoundException(message: String) : Exception(message)
class MisconfiguredSavableEnumException(message: String) : Exception(message) class MisconfiguredSavableEnumException(message: String) : Exception(message)
class PAIllegalStateException(message: String) : Exception(message)
sealed class PokerAnalyticsException(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 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 FilterElementUnknownSectionName: PokerAnalyticsException(message = "No filterElement section name was found to identify the queryCondition")
// object FilterMissingEntity: PokerAnalyticsException(message = "This queryWith has no entity initialized") object FilterMissingEntity: PokerAnalyticsException(message = "This queryWith has no entity initialized")
// object FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable") object FilterUnhandledEntity : PokerAnalyticsException(message = "This entity is not filterable")
class QueryValueMapUnknown(message: String = "fieldName is missing"): PokerAnalyticsException(message) object QueryValueMapUnknown: PokerAnalyticsException(message = "fieldName is missing")
class QueryTypeUnhandled(clazz: String) : object QueryTypeUnhandled: PokerAnalyticsException(message = "queryWith type not handled")
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 QueryValueMapUnexpectedValue: PokerAnalyticsException(message = "valueMap null not expected")
object FilterElementExpectedValueMissing : PokerAnalyticsException(message = "queryWith is empty or null") 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 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 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 UnknownQueryTypeForRow(val filterElementRow: FilterElementRow) : PokerAnalyticsException(message = "no queryWith type for $filterElementRow")
// data class MissingFieldNameForQueryCondition(val name: String) : PokerAnalyticsException(message = "Missing fieldname for QueryCondition ${name}") data class MissingFieldNameForQueryCondition(val name: String) : PokerAnalyticsException(message = "Missing fieldname for QueryCondition ${name}")
// object InputFragmentException : PokerAnalyticsException(message = "RowEditableDelegate must be a Fragment")
} }

@ -4,13 +4,12 @@ import io.realm.Realm
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.where import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.exceptions.PokerAnalyticsException import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.Criteria.Bankrolls.comparison import net.pokeranalytics.android.model.Criteria.Bankrolls.comparison
import net.pokeranalytics.android.model.Criteria.Blinds.comparison
import net.pokeranalytics.android.model.Criteria.Games.comparison import net.pokeranalytics.android.model.Criteria.Games.comparison
import net.pokeranalytics.android.model.Criteria.Limits.comparison import net.pokeranalytics.android.model.Criteria.Limits.comparison
import net.pokeranalytics.android.model.Criteria.Locations.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.TableSizes.comparison
import net.pokeranalytics.android.model.Criteria.TournamentFeatures.comparison import net.pokeranalytics.android.model.Criteria.TournamentFeatures.comparison
import net.pokeranalytics.android.model.Criteria.TournamentFees.comparison import net.pokeranalytics.android.model.Criteria.TournamentFees.comparison
@ -68,7 +67,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach { realm.findById(CustomField::class.java, this.customFieldId)?.entries?.forEach {
objects.add(QueryCondition.CustomFieldListQuery(it)) objects.add(QueryCondition.CustomFieldListQuery(it))
} }
objects.sort() objects.sorted()
realm.close() realm.close()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
@ -104,10 +103,11 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
} }
objects.add(condition) objects.add(condition)
} }
objects.sort() objects.sorted()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
QueryCondition.distinct<Session, T, S>()?.let { QueryCondition.distinct<Session, T, S>()?.let {
val values = it.mapNotNull { session -> val values = it.mapNotNull { session ->
when (this) { when (this) {
@ -123,8 +123,8 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
is TournamentFees -> if (session.tournamentEntryFee is S) { is TournamentFees -> if (session.tournamentEntryFee is S) {
session.tournamentEntryFee as S session.tournamentEntryFee as S
} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
is Stakes -> if (session.cgStakes is S) { is Blinds -> if (session.blinds is S) {
session.cgStakes as S session.blinds as S
} else throw PokerAnalyticsException.QueryValueMapUnexpectedValue } else throw PokerAnalyticsException.QueryValueMapUnexpectedValue
else -> null else -> null
} }
@ -158,21 +158,20 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
object DayPeriods : SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd), 14) object DayPeriods : SimpleCriteria(listOf(QueryCondition.IsWeekDay, QueryCondition.IsWeekEnd), 14)
object Years : ListCriteria(15) object Years : ListCriteria(15)
object AllMonthsUpToNow : ListCriteria(16) object AllMonthsUpToNow : ListCriteria(16)
object Stakes : ListCriteria(17) object Blinds : ListCriteria(17)
object TournamentFees : ListCriteria(18) object TournamentFees : ListCriteria(18)
object Cash : SimpleCriteria(listOf(QueryCondition.IsCash), 19) object Cash : SimpleCriteria(listOf(QueryCondition.IsCash), 19)
object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament), 20) object Tournament : SimpleCriteria(listOf(QueryCondition.IsTournament), 20)
data class ListCustomFields(override var customFieldId: String) : RealmCriteria(21), CustomFieldCriteria data class ListCustomFields(override var customFieldId: String) : RealmCriteria(21), CustomFieldCriteria
data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria data class ValueCustomFields(override var customFieldId: String) : ListCriteria(22), CustomFieldCriteria
object Duration : ListCriteria(23)
val queries: List<Query> val queries: List<Query>
get() { get() {
return when (this) { return when (this) {
is AllMonthsUpToNow -> { is AllMonthsUpToNow -> {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val firstSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.ASCENDING).findFirst() val firstSession = realm.where<Session>().sort("startDate", Sort.ASCENDING).findFirst()
val lastSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.DESCENDING).findFirst() val lastSession = realm.where<Session>().sort("startDate", Sort.DESCENDING).findFirst()
realm.close() realm.close()
val years: ArrayList<Query> = arrayListOf() val years: ArrayList<Query> = arrayListOf()
@ -223,10 +222,10 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
is Years -> { is Years -> {
val years = arrayListOf<Query>() val years = arrayListOf<Query>()
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val lastSession = realm.where<Session>().isNotNull("startDate").sort("startDate", Sort.DESCENDING).findFirst() val lastSession = realm.where<Session>().sort("startDate", Sort.DESCENDING).findFirst()
val yearNow = lastSession?.year ?: return years val yearNow = lastSession?.year ?: return years
realm.where<Session>().isNotNull("startDate").sort("year", Sort.ASCENDING).findFirst()?.year?.let { realm.where<Session>().sort("year", Sort.ASCENDING).findFirst()?.year?.let {
for (index in 0..(yearNow - it)) { for (index in 0..(yearNow - it)) {
val yearCondition = QueryCondition.AnyYear().apply { val yearCondition = QueryCondition.AnyYear().apply {
listOfValues = arrayListOf(it + index) listOfValues = arrayListOf(it + index)
@ -237,34 +236,19 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
realm.close() realm.close()
years years
} }
is Stakes -> comparison<QueryCondition.AnyStake, String>() is Blinds -> comparison<QueryCondition.AnyBlind, String>()
is ListCustomFields -> comparison<CustomFieldEntry>() is ListCustomFields -> comparison<CustomFieldEntry>()
is ValueCustomFields -> { is ValueCustomFields -> {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val queries = when (this.customFieldType(realm)) { val queries = when (this.customFieldType(realm)) {
CustomField.Type.AMOUNT.uniqueIdentifier -> comparison<QueryCondition.CustomFieldAmountQuery, Double >() CustomField.Type.AMOUNT.uniqueIdentifier -> comparison<QueryCondition.CustomFieldAmountQuery, Double >()
CustomField.Type.NUMBER.uniqueIdentifier -> comparison<QueryCondition.CustomFieldNumberQuery, Double >() CustomField.Type.NUMBER.uniqueIdentifier -> comparison<QueryCondition.CustomFieldNumberQuery, Double >()
else -> throw PokerAnalyticsException.ComparisonCriteriaUnhandled(this) else -> throw PokerAnalyticsException.QueryTypeUnhandled
} }
realm.close() realm.close()
queries queries
} }
is Duration -> { else -> throw PokerAnalyticsException.QueryTypeUnhandled
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)
} }
} }
@ -286,7 +270,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
DayPeriods -> R.string.weekdays_or_weekend DayPeriods -> R.string.weekdays_or_weekend
Years -> R.string.year Years -> R.string.year
AllMonthsUpToNow -> R.string.month AllMonthsUpToNow -> R.string.month
Stakes -> R.string.blind Blinds -> R.string.blind
TournamentFees -> R.string.entry_fees TournamentFees -> R.string.entry_fees
// is ListCustomFields -> this.customField.resId // is ListCustomFields -> this.customField.resId
// is ValueCustomFields -> this.customField.resId // is ValueCustomFields -> this.customField.resId
@ -299,13 +283,13 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
inline fun <reified S : QueryCondition.QueryDataCondition<NameManageable>, reified T : NameManageable> compare(): List<Query> { inline fun <reified S : QueryCondition.QueryDataCondition<NameManageable>, reified T : NameManageable> compare(): List<Query> {
val objects = mutableListOf<S>() val objects = mutableListOf<S>()
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.where<T>().sort("name").findAll().forEach { realm.where<T>().findAll().forEach {
val condition = (QueryCondition.getInstance<T>() as S).apply { val condition = (QueryCondition.getInstance<T>() as S).apply {
setObject(it) setObject(it)
} }
objects.add(condition) objects.add(condition)
} }
// objects.sort() objects.sorted()
realm.close() realm.close()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
@ -318,7 +302,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
} }
objects.add(condition) objects.add(condition)
} }
objects.sort() objects.sorted()
return objects.map { Query(it) } return objects.map { Query(it) }
} }
@ -334,8 +318,7 @@ sealed class Criteria(override var uniqueIdentifier: Int) : IntIdentifiable, Row
TournamentFeatures, Limits, TableSizes, TournamentTypes, TournamentFeatures, Limits, TableSizes, TournamentTypes,
MonthsOfYear, DaysOfWeek, SessionTypes, MonthsOfYear, DaysOfWeek, SessionTypes,
BankrollTypes, DayPeriods, Years, BankrollTypes, DayPeriods, Years,
AllMonthsUpToNow, AllMonthsUpToNow, Blinds, TournamentFees
Stakes, TournamentFees
) )
} }
} }
@ -346,7 +329,7 @@ interface CustomFieldCriteria {
var customFieldId: String var customFieldId: String
fun customField(realm: Realm) : CustomField { fun customField(realm: Realm) : CustomField {
return realm.findById(this.customFieldId) ?: throw PAIllegalStateException("Custom field not found") return realm.findById(this.customFieldId) ?: throw IllegalStateException("Custom field not found")
} }
fun customFieldType(realm: Realm): Int { fun customFieldType(realm: Realm): Int {

@ -14,11 +14,11 @@ enum class Limit : RowRepresentable {
fun getInstance(value: String) : Limit? { fun getInstance(value: String) : Limit? {
return when (value) { return when (value) {
"NL", "No Limit" -> NO "No Limit" -> NO
"PL", "Pot Limit" -> POT "Pot Limit" -> POT
"FL", "Fixed Limit", "Limit" -> FIXED "Fixed Limit", "Limit" -> FIXED
"ML", "Mixed Limit" -> MIXED "Mixed Limit" -> MIXED
"SL", "Spread Limit" -> SPREAD "Spread Limit" -> SPREAD
else -> null else -> null
} }
} }
@ -38,12 +38,13 @@ enum class Limit : RowRepresentable {
val longName: String val longName: String
get() { get() {
val limit = "Limit"
return when (this) { return when (this) {
NO -> "No Limit" NO -> "No $limit"
POT -> "Pot Limit" POT -> "Pot $limit"
FIXED -> "Limit" FIXED -> "Fixed $limit"
MIXED -> "Mixed Limit" MIXED -> "Mixed $limit"
SPREAD -> "Spread Limit" SPREAD -> "Spread $limit"
} }
} }

@ -1,15 +1,10 @@
package net.pokeranalytics.android.model package net.pokeranalytics.android.model
import android.content.Context import android.content.Context
import androidx.fragment.app.Fragment
import io.realm.Realm import io.realm.Realm
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.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.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 import net.pokeranalytics.android.util.extensions.findById
@ -26,9 +21,7 @@ enum class LiveData : Localizable {
TRANSACTION_TYPE, TRANSACTION_TYPE,
FILTER, FILTER,
CUSTOM_FIELD, CUSTOM_FIELD,
REPORT_SETUP, REPORT_SETUP;
PLAYER,
HAND_HISTORY;
var subType:Int? = null var subType:Int? = null
@ -45,8 +38,6 @@ enum class LiveData : Localizable {
FILTER -> Filter::class.java FILTER -> Filter::class.java
CUSTOM_FIELD -> CustomField::class.java CUSTOM_FIELD -> CustomField::class.java
REPORT_SETUP -> ReportSetup::class.java REPORT_SETUP -> ReportSetup::class.java
PLAYER -> Player::class.java
HAND_HISTORY -> HandHistory::class.java
} }
} }
@ -87,8 +78,6 @@ enum class LiveData : Localizable {
FILTER -> R.string.filter FILTER -> R.string.filter
CUSTOM_FIELD -> R.string.custom_field CUSTOM_FIELD -> R.string.custom_field
REPORT_SETUP -> R.string.custom REPORT_SETUP -> R.string.custom
PLAYER -> R.string.player
HAND_HISTORY -> R.string.hand_history
} }
} }
@ -105,8 +94,6 @@ enum class LiveData : Localizable {
FILTER -> R.string.filters FILTER -> R.string.filters
CUSTOM_FIELD -> R.string.custom_fields CUSTOM_FIELD -> R.string.custom_fields
REPORT_SETUP -> R.string.custom REPORT_SETUP -> R.string.custom
PLAYER -> R.string.players
HAND_HISTORY -> R.string.hands_history
} }
} }
@ -123,18 +110,10 @@ enum class LiveData : Localizable {
FILTER -> R.string.new_filter FILTER -> R.string.new_filter
CUSTOM_FIELD -> R.string.new_custom_field CUSTOM_FIELD -> R.string.new_custom_field
REPORT_SETUP -> R.string.new_report 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 * Return the new entity titleResId
@ -157,33 +136,4 @@ enum class LiveData : Localizable {
return context.getString(this.pluralResId, context) 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)
}
}
} }

@ -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 {
}
}

@ -4,28 +4,18 @@ import android.content.Context
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
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.Parser
class TableSize( class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITLE_GRID.ordinal) : RowRepresentable {
var numberOfPlayer: Int,
var rowViewType: Int = RowViewType.TITLE_GRID.ordinal,
var alternativeLabels: Boolean = false
) : RowRepresentable {
companion object { companion object {
fun all(alternativeLabels: Boolean): List<TableSize> { val all: List<TableSize>
return Array(9, init = { index -> get() {
TableSize(index + 2, alternativeLabels = alternativeLabels) return Array(9, init =
}).toList() { index -> TableSize(index + 2) }).toList()
} }
fun valueForLabel(label: String) : Int? { fun valueForLabel(label: String) : Int? {
Parser.parseNumber(label)?.let {
return it.toInt()
}
return when (label) { return when (label) {
"Full Ring", "Full-Ring" -> 10 "Full Ring", "Full-Ring" -> 10
"Short-Handed", "Short Handed" -> 6 "Short-Handed", "Short Handed" -> 6
@ -36,10 +26,6 @@ class TableSize(
} }
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
if (this.alternativeLabels) {
return this.numberOfPlayer.toString()
}
return if (this.numberOfPlayer == 2) { return if (this.numberOfPlayer == 2) {
return "HU" return "HU"
} else { } else {
@ -57,10 +43,6 @@ class TableSize(
} }
override fun localizedTitle(context: Context): String { override fun localizedTitle(context: Context): String {
if (this.alternativeLabels) {
return this.numberOfPlayer.toString()
}
this.resId?.let { this.resId?.let {
return if (this.numberOfPlayer == 2) { return if (this.numberOfPlayer == 2) {
context.getString(it) context.getString(it)

@ -5,20 +5,20 @@ import net.pokeranalytics.android.R
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
enum class TournamentType(val label: String) : RowRepresentable { enum class TournamentType : RowRepresentable {
MTT("MTT"), MTT,
SNG("SNG"); SNG;
companion object { companion object {
val all : List<TournamentType> val all : List<TournamentType>
get() { get() {
return values().toList() return TournamentType.values() as List<TournamentType>
} }
fun getValueForLabel(label: String) : TournamentType? { fun getValueForLabel(label: String) : TournamentType? {
return when (label) { return when (label) {
SNG.label, "Single-Table" -> SNG "Single-Table" -> SNG
MTT.label, "Multi-Table" -> MTT "Multi-Table" -> MTT
else -> null else -> null
} }
} }

@ -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,17 +1,11 @@
package net.pokeranalytics.android.model.extensions package net.pokeranalytics.android.model.extensions
import android.content.Context 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.R
import net.pokeranalytics.android.model.TournamentType 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 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,
@ -20,7 +14,7 @@ enum class SessionState {
PAUSED, PAUSED,
FINISHED; FINISHED;
val hasStarted: Boolean var hasStarted: Boolean = false
get() { get() {
return when (this) { return when (this) {
STARTED, PAUSED, FINISHED -> true STARTED, PAUSED, FINISHED -> true
@ -36,27 +30,27 @@ enum class SessionState {
fun Session.getState(): SessionState { fun Session.getState(): SessionState {
val start = this.startDate val start = this.startDate
return if (start == null) { if (start == null) {
SessionState.PENDING return SessionState.PENDING
} else { } else {
if (start > Date()) { if (start > Date()) {
SessionState.PLANNED return SessionState.PLANNED
} else if (this.endDate != null) { } else if (this.endDate != null) {
SessionState.FINISHED return SessionState.FINISHED
} else if (this.pauseDate != null) { } else if (this.pauseDate != null) {
SessionState.PAUSED return SessionState.PAUSED
} else { } else {
SessionState.STARTED return SessionState.STARTED
} }
} }
} }
/** /**
* Format the session game type * Formate the session game type
*/ */
fun Session.getFormattedGameType(context: Context): String { fun Session.getFormattedGameType(context: Context): String {
val parameters = mutableListOf<String>() var parameters = mutableListOf<String>()
if (isTournament()) { if (isTournament()) {
tournamentEntryFee?.let { tournamentEntryFee?.let {
@ -76,8 +70,8 @@ fun Session.getFormattedGameType(context: Context): String {
parameters.add(context.getString(R.string.tournament).capitalize()) parameters.add(context.getString(R.string.tournament).capitalize())
} }
} else { } else {
if (this.cgAnte != null || this.cgBlinds != null) { if (cgSmallBlind != null && cgBigBlind != null) {
parameters.add(getFormattedStakes()) parameters.add(getFormattedBlinds())
} }
game?.let { game?.let {
parameters.add(getFormattedGame()) parameters.add(getFormattedGame())
@ -90,40 +84,6 @@ fun Session.getFormattedGameType(context: Context): String {
return parameters.joinToString(separator = " ") 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 val AbstractList<Session>.hourlyDuration: Double
get() { get() {
val intervals = mutableListOf<TimeInterval>() val intervals = mutableListOf<TimeInterval>()
@ -131,7 +91,7 @@ val AbstractList<Session>.hourlyDuration: Double
val interval = TimeInterval(it.startDate!!, it.endDate!!, it.breakDuration) val interval = TimeInterval(it.startDate!!, it.endDate!!, it.breakDuration)
intervals.update(interval) intervals.update(interval)
} }
return intervals.sumOf { it.hourlyDuration } return intervals.sumByDouble { it.hourlyDuration }
} }
class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) { class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) {
@ -153,7 +113,7 @@ fun MutableList<TimeInterval>.update(timeInterval: TimeInterval): MutableList<Ti
} }
if (overlapped.isEmpty()) { // add if (overlapped.size == 0) { // add
this.add(timeInterval) this.add(timeInterval)
} else { // update } else { // update

@ -2,9 +2,8 @@ package net.pokeranalytics.android.model.filter
import io.realm.RealmModel import io.realm.RealmModel
import io.realm.RealmResults import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.util.CrashLogging
/** /**
* We want to be able to store filters in the database: * We want to be able to store filters in the database:
@ -58,19 +57,20 @@ class FilterHelper {
inline fun <reified T : Filterable > fieldNameForQueryType(queryCondition: Class< out QueryCondition>): String? { inline fun <reified T : Filterable > fieldNameForQueryType(queryCondition: Class< out QueryCondition>): String? {
return when (T::class.java) { val fieldName = when (T::class.java) {
Session::class.java -> Session.fieldNameForQueryType(queryCondition) Session::class.java -> Session.fieldNameForQueryType(queryCondition)
ComputableResult::class.java -> ComputableResult.fieldNameForQueryType(queryCondition) ComputableResult::class.java -> ComputableResult.fieldNameForQueryType(queryCondition)
SessionSet::class.java -> SessionSet.fieldNameForQueryType(queryCondition) SessionSet::class.java -> SessionSet.fieldNameForQueryType(queryCondition)
Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition) Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition)
Result::class.java -> Result.fieldNameForQueryType(queryCondition) Result::class.java -> Result.fieldNameForQueryType(queryCondition)
else -> { else -> {
CrashLogging.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}"))
null null
// throw UnmanagedFilterField("Filterable type fields are not defined for class ${T::class}") // throw UnmanagedFilterField("Filterable type fields are not defined for class ${T::class}")
} }
} }
return fieldName
/* /*
fieldName?.let { fieldName?.let {
return fieldName return fieldName

@ -11,13 +11,9 @@ fun List<Query>.mapFirstCondition() : List<QueryCondition> {
class Query { class Query {
constructor(query: Query) { constructor(vararg elements: QueryCondition) {
this._conditions.addAll(query.conditions) if (elements.size > 0) {
} this.add(elements.asList())
constructor(vararg elements: QueryCondition?) {
if (elements.isNotEmpty()) {
this.add(elements.filterNotNull())
} }
} }
@ -27,27 +23,24 @@ class Query {
return this._conditions return this._conditions
} }
fun add(vararg elements: QueryCondition): Query { fun add(vararg elements: QueryCondition) {
if (elements.isNotEmpty()) { if (elements.size > 0) {
this.add(elements.asList()) this.add(elements.asList())
} }
return this
} }
fun add(queryCondition: QueryCondition): Query { fun add(queryCondition: QueryCondition) {
this._conditions.add(queryCondition) this._conditions.add(queryCondition)
return this
}
fun add(queryConditions: List<QueryCondition>): Query {
this._conditions.addAll(queryConditions)
return this
} }
fun remove(queryCondition: QueryCondition) { fun remove(queryCondition: QueryCondition) {
this._conditions.remove(queryCondition) this._conditions.remove(queryCondition)
} }
fun add(queryConditions: List<QueryCondition>) {
this._conditions.addAll(queryConditions)
}
val defaultName: String val defaultName: String
get() { get() {
return when (this._conditions.size) { return when (this._conditions.size) {
@ -56,46 +49,42 @@ class Query {
} }
} }
fun getName(context: Context, separator: String = " + "): String { fun getName(context: Context): String {
return when (this._conditions.size) { return when (this._conditions.size) {
0 -> context.getString(R.string.all_sessions) // @todo should be dependant of the underlying type, ie. Session, Transaction... 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) } else -> this._conditions.joinToString(" : ") { it.getDisplayName(context) }
} }
} }
inline fun <reified T : Filterable> queryWith(query: RealmQuery<T>): RealmQuery<T> { inline fun <reified T : Filterable> queryWith(query: RealmQuery<T>): RealmQuery<T> {
var realmQuery = query var realmQuery = query
val queryFromTime = this.conditions.firstOrNull { val queryFromTime = this.conditions.filter {
it is QueryCondition.StartedFromTime it is QueryCondition.StartedFromTime
} }.firstOrNull()
val queryToTime = this.conditions.firstOrNull { val queryToTime = this.conditions.filter {
it is QueryCondition.EndedToTime it is QueryCondition.EndedToTime
} }.firstOrNull()
this.conditions.forEach { this.conditions.forEach {
realmQuery = when (it) { if (it is QueryCondition.StartedFromTime) {
is QueryCondition.StartedFromTime -> { realmQuery = it.queryWith(realmQuery, queryToTime)
it.queryWith(realmQuery, queryToTime) } else if (it is QueryCondition.EndedToTime) {
} realmQuery = it.queryWith(realmQuery, queryFromTime)
is QueryCondition.EndedToTime -> { } else {
it.queryWith(realmQuery, queryFromTime) realmQuery = it.queryWith(realmQuery)
}
else -> {
it.queryWith(realmQuery)
}
} }
} }
// println("<<<<<< ${realmQuery.description}") // println("<<<<<< ${realmQuery.description}")
// val queryLast = this.conditions.firstOrNull { val queryLast = this.conditions.filter {
// it is QueryCondition.Last it is QueryCondition.Last
// } }.firstOrNull()
// queryLast?.let {qc -> queryLast?.let {qc ->
// (qc as QueryCondition.Last).singleValue?.let { (qc as QueryCondition.Last).singleValue?.let {
// return realmQuery.limit(it.toLong()) return realmQuery.limit(it.toLong())
// } }
// } }
return realmQuery return realmQuery
} }
@ -104,25 +93,4 @@ class Query {
return this 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
}
} }

@ -12,73 +12,45 @@ import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.TableSize import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.interfaces.CodedStake
import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.StakesHolder
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
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.ui.view.rowrepresentable.FilterElementRow
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterSectionRow
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.UserDefaults import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.extensions.* import net.pokeranalytics.android.util.extensions.*
import timber.log.Timber
import java.text.DateFormatSymbols import java.text.DateFormatSymbols
import java.text.NumberFormat import java.text.NumberFormat
import java.util.* import java.util.*
import kotlin.reflect.KClass import kotlin.collections.ArrayList
/** /**
* Enum describing the way a query should be handled * Enum describing the way a query should be handled
* Some queries requires a value to be checked upon through equals, in, more, less, between * Some queries requires a value to be checked upon through equals, in, more, less, between
*/ */
sealed class QueryCondition : RowRepresentable {
companion object { sealed class QueryCondition : FilterElementRow {
fun <T: Any> newInstance(kClass: KClass<T>): T {
return try {
kClass.objectInstance ?: kClass.java.newInstance()
} catch (e: Exception) {
// some object instance can fail due to: java.lang.Class has no zero argument constructor
// We should have just one constructor with a single parameter
val primaryConstructor = kClass.java.declaredConstructors.first()
val paramClass = primaryConstructor.parameterTypes.first()
val param = when (paramClass) {
Int::class.java -> 0
Double::class.java -> 0.0
else -> paramClass.newInstance()
}
val constructor = kClass.java.getDeclaredConstructor(paramClass)
constructor.newInstance(param)
}
}
// inline fun <reified T : QueryCondition> more(): T { companion object {
// return newInstance(T::class).apply { this.operator = Operator.MORE } inline fun <reified T : QueryCondition> more(): T {
// } return T::class.java.newInstance().apply { this.operator = Operator.MORE }
//
// inline fun <reified T : QueryCondition> less(): T {
// return newInstance(T::class).apply { this.operator = Operator.LESS }
// }
inline fun <reified T : QueryCondition> moreOrEqual(): T {
return newInstance(T::class).apply { this.operator = Operator.MORE_OR_EQUAL }
} }
inline fun <reified T : QueryCondition> lessOrEqual(): T { inline fun <reified T : QueryCondition> less(): T {
return newInstance(T::class).apply { this.operator = Operator.LESS_OR_EQUAL } return T::class.java.newInstance().apply { this.operator = Operator.LESS }
} }
inline fun <reified T : QueryCondition> moreEqualOrLessEqual(): ArrayList<T> { inline fun <reified T : QueryCondition> moreOrLess(): ArrayList<T> {
return arrayListOf(moreOrEqual(), lessOrEqual()) return arrayListOf(more(), less())
} }
fun <T : QueryCondition> valueOf(name: String): T { fun <T : QueryCondition> valueOf(name: String): T {
val kClass = Class.forName("${QueryCondition::class.qualifiedName}$$name").kotlin val kClass = Class.forName("${QueryCondition::class.qualifiedName}$$name").kotlin
return newInstance(kClass) as T val instance = kClass.objectInstance ?: kClass.java.newInstance()
return instance as T
} }
inline fun <reified T : Identifiable> getInstance(): QueryCondition { inline fun <reified T : Identifiable> getInstance(): QueryCondition {
@ -89,14 +61,13 @@ sealed class QueryCondition : RowRepresentable {
TransactionType::class.java -> AnyTransactionType() TransactionType::class.java -> AnyTransactionType()
TournamentName::class.java -> AnyTournamentName() TournamentName::class.java -> AnyTournamentName()
TournamentFeature::class.java -> AllTournamentFeature() TournamentFeature::class.java -> AllTournamentFeature()
else -> throw PokerAnalyticsException.QueryTypeUnhandled((T::class.java).name) else -> throw PokerAnalyticsException.QueryTypeUnhandled
} }
} }
inline fun <reified T : Filterable, reified S : QueryCondition, reified U : Comparable<U>> distinct(): RealmResults<T>? { inline fun <reified T : Filterable, reified S : QueryCondition, reified U : Comparable<U>> distinct(): RealmResults<T>? {
FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let { FilterHelper.fieldNameForQueryType<T>(S::class.java)?.let {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.refresh()
val distincts = when (T::class) { val distincts = when (T::class) {
String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING) String::class, Int::class -> realm.where<T>().distinct(it).findAll().sort(it, Sort.ASCENDING)
@ -114,12 +85,9 @@ sealed class QueryCondition : RowRepresentable {
ANY, ANY,
ALL, ALL,
MORE, MORE,
MORE_OR_EQUAL,
LESS, LESS,
LESS_OR_EQUAL,
EQUALS, EQUALS,
TRUE, TRUE,
NOTNULL
; ;
} }
@ -127,17 +95,16 @@ sealed class QueryCondition : RowRepresentable {
val groupId: String val groupId: String
get() { get() {
return when (this.operator) { when (this.operator) {
Operator.MORE, Operator.LESS, Operator.MORE_OR_EQUAL, Operator.LESS_OR_EQUAL -> return "${this.operator.name.toLowerCase().capitalize()}$baseId" Operator.MORE, Operator.LESS -> return "${this.operator.name.toLowerCase().capitalize()}$baseId"
else -> this.baseId
} }
return baseId
} }
val id: List<String> val id: List<String>
get() { get() {
when (this.operator) { when (this.operator) {
Operator.MORE, Operator.LESS, Operator.MORE_OR_EQUAL, Operator.LESS_OR_EQUAL -> return listOf("$baseId+${this.operator.name}") Operator.MORE, Operator.LESS -> return listOf("$baseId+${this.operator.name}")
else -> {}
} }
return when (this) { return when (this) {
@ -154,43 +121,24 @@ sealed class QueryCondition : RowRepresentable {
abstract var operator: Operator abstract var operator: Operator
override fun getDisplayName(context: Context): String {
this.resId?.let {
return context.getString(it)
}
return baseId
}
open fun getDisplayNameWithValues(context: Context): String {
return getDisplayName(context)
}
abstract class ListOfValues<T> : QueryCondition(), Comparable<ListOfValues<T>> where T : Comparable<T> { abstract class ListOfValues<T> : QueryCondition(), Comparable<ListOfValues<T>> where T : Comparable<T> {
abstract var listOfValues: MutableList<T> abstract var listOfValues: ArrayList<T>
abstract fun labelForValue(value: T, context: Context): String abstract fun labelForValue(value: T, context: Context): String
open fun entityName(context: Context): String { open fun entityName(context: Context): String {
return getDisplayName(context) return getDisplayName(context)
} }
override fun getDisplayNameWithValues(context: Context): String {
return this.getDisplayName(context, this.listOfValues)
}
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return getDisplayName(context, this.listOfValues)
}
private fun getDisplayName(context: Context, values: List<T>): String {
val prefix = this.resId?.let { val prefix = this.resId?.let {
context.getString(it) + " " context.getString(it) + " "
} ?: "" } ?: ""
return when (values.size) { return when (listOfValues.size) {
0 -> return NULL_TEXT 0 -> return NULL_TEXT
else -> prefix + values.joinToString(", ") { labelForValue(it, context) } 1, 2 -> prefix + listOfValues.map { labelForValue(it, context) }.joinToString(", ")
// else -> "${values.size} $prefix ${entityName(context)}" else -> "${listOfValues.size} $prefix ${entityName(context)}"
} }
} }
@ -201,41 +149,17 @@ sealed class QueryCondition : RowRepresentable {
fun firstValue(context: Context): String? { fun firstValue(context: Context): String? {
return this.listOfValues.firstOrNull()?.let { this.labelForValue(it, context) } return this.listOfValues.firstOrNull()?.let { this.labelForValue(it, context) }
} }
}
abstract class SingleValue<T>(value: T) : QueryCondition() where T : Comparable<T> {
var singleValue: T = value
abstract fun labelForValue(value: T, context: Context): String
override fun getDisplayName(context: Context): String {
return this.resId?.let {
context.getString(it)
} ?: ""
// return getDisplayName(context, this.singleValue)
}
override fun getDisplayNameWithValues(context: Context): String {
return this.getDisplayName(context, this.singleValue)
}
open fun getDisplayName(context: Context, value: T): String {
val prefix = this.resId?.let {
context.getString(it)
} ?: ""
return prefix + " " + labelForValue(value, context)
} }
abstract class SingleValue<T> : ListOfValues<T>() where T : Comparable<T> {
override var listOfValues = ArrayList<T>()
abstract var singleValue: T?
} }
abstract class ListOfDouble : ListOfValues<Double>() { abstract class ListOfDouble : ListOfValues<Double>() {
open var sign: Int = 1 open var sign: Int = 1
override var operator: Operator = Operator.ANY override var operator: Operator = Operator.ANY
override var listOfValues = mutableListOf<Double>() override var listOfValues: ArrayList<Double> = arrayListOf()
override fun updateValueBy(filterCondition: FilterCondition) { override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition) super.updateValueBy(filterCondition)
listOfValues = filterCondition.getValues() listOfValues = filterCondition.getValues()
@ -248,7 +172,7 @@ sealed class QueryCondition : RowRepresentable {
abstract class ListOfInt : ListOfValues<Int>() { abstract class ListOfInt : ListOfValues<Int>() {
override var operator: Operator = Operator.ANY override var operator: Operator = Operator.ANY
override var listOfValues = mutableListOf<Int>() override var listOfValues: ArrayList<Int> = arrayListOf()
override fun updateValueBy(filterCondition: FilterCondition) { override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition) super.updateValueBy(filterCondition)
listOfValues = filterCondition.getValues() listOfValues = filterCondition.getValues()
@ -261,8 +185,7 @@ sealed class QueryCondition : RowRepresentable {
abstract class ListOfString : ListOfValues<String>() { abstract class ListOfString : ListOfValues<String>() {
override var operator: Operator = Operator.ANY override var operator: Operator = Operator.ANY
override var listOfValues = mutableListOf<String>() override var listOfValues = ArrayList<String>()
override fun labelForValue(value: String, context: Context): String { override fun labelForValue(value: String, context: Context): String {
return value return value
} }
@ -273,34 +196,56 @@ sealed class QueryCondition : RowRepresentable {
} }
} }
abstract class SingleDate(date: Date) : SingleValue<Date>(date) { abstract class SingleDate : SingleValue<Date>() {
override fun labelForValue(value: Date, context: Context): String { override fun labelForValue(value: Date, context: Context): String {
return value.shortDate() return value.shortDate()
} }
// override var listOfValues = mutableListOf<Date>() override var listOfValues = ArrayList<Date>()
override var singleValue: Date?
get() {
return listOfValues.firstOrNull()
}
set(value) {
listOfValues.removeAll(this.listOfValues)
value?.let { listOfValues.add(it) }
}
override fun updateValueBy(filterCondition: FilterCondition) { override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition) super.updateValueBy(filterCondition)
singleValue = filterCondition.getValue() singleValue = filterCondition.getValue()
} }
} }
abstract class SingleInt(value: Int) : SingleValue<Int>(value) { abstract class SingleInt : SingleValue<Int>() {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return value.toString() return value.toString()
} }
override var singleValue: Int?
get() {
return listOfValues.firstOrNull()
}
set(value) {
listOfValues.removeAll(this.listOfValues)
value?.let { listOfValues.add(it) }
}
override fun updateValueBy(filterCondition: FilterCondition) { override fun updateValueBy(filterCondition: FilterCondition) {
super.updateValueBy(filterCondition) super.updateValueBy(filterCondition)
singleValue = filterCondition.getValue() singleValue = filterCondition.getValue()
} }
}
override fun getDisplayName(context: Context): String {
this.resId?.let {
return context.getString(it)
}
return baseId
} }
// override var filterSectionRow: FilterSectionRow = FilterSectionRow.CashOrTournament override var filterSectionRow: FilterSectionRow = FilterSectionRow.CashOrTournament
abstract class QueryDataCondition<T : NameManageable> : ListOfString() { abstract class QueryDataCondition<T : NameManageable> : ListOfString() {
fun setObject(dataObject: T) { fun setObject(dataObject: T) {
@ -312,11 +257,11 @@ sealed class QueryCondition : RowRepresentable {
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
val entityName = entityName(realm, context) val entityName = entityName(realm)
val completeLabel = when (listOfValues.size) { val completeLabel = when (listOfValues.size) {
0 -> NULL_TEXT 0 -> NULL_TEXT
1, 2 -> { 1, 2 -> {
listOfValues.joinToString(", ") { labelForValue(realm, it) } listOfValues.map { labelForValue(realm, it) }.joinToString(", ")
} }
else -> "${listOfValues.size} $entityName" else -> "${listOfValues.size} $entityName"
} }
@ -324,52 +269,42 @@ sealed class QueryCondition : RowRepresentable {
return completeLabel return completeLabel
} }
override fun getDisplayNameWithValues(context: Context): String { open fun entityName(realm: Realm): String {
return this.getDisplayName(context) return baseId
}
open fun entityName(realm: Realm, context: Context): String {
return entityName(context)
} }
private fun labelForValue(realm: Realm, value: String): String { private fun labelForValue(realm: Realm, value: String): String {
val query = realm.where(entity) val query = realm.where(entity)
return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT return query.equalTo("id", value).findFirst()?.name ?: NULL_TEXT
} }
val objectId: String?
get() {
return this.listOfValues.firstOrNull()
} }
}
interface DateTime { interface DateTime {
val showTime: Boolean val showTime: Boolean
} }
abstract class DateQuery(date: Date) : SingleDate(date), DateTime { abstract class DateQuery : SingleDate(), DateTime {
override val showTime: Boolean = false override val showTime: Boolean = false
override fun labelForValue(value: Date, context: Context): String { override fun labelForValue(value: Date, context: Context): String {
return if (showTime) { return singleValue?.let {
singleValue.shortTime() if (showTime) {
it.shortTime()
} else { } else {
singleValue.shortDate() it.shortDate()
} }
} ?: NULL_TEXT
} }
} }
abstract class TimeQuery(date: Date) : DateQuery(date) { abstract class TimeQuery : DateQuery() {
override val showTime: Boolean = true override val showTime: Boolean = true
} }
abstract class TrueQueryCondition : QueryCondition() { abstract class TrueQueryCondition : QueryCondition() {
override var operator: Operator = Operator.TRUE override var operator: Operator = Operator.TRUE
} }
abstract class NotNullQueryCondition : QueryCondition() {
override var operator: Operator = Operator.NOTNULL
}
object IsLive : TrueQueryCondition() object IsLive : TrueQueryCondition()
@ -385,10 +320,6 @@ sealed class QueryCondition : RowRepresentable {
constructor(bankroll: Bankroll) : this() { constructor(bankroll: Bankroll) : this() {
this.setObject(bankroll) this.setObject(bankroll)
} }
override fun entityName(context: Context): String {
return context.getString(R.string.bankrolls)
}
} }
class AnyGame() : QueryDataCondition<Game>() { class AnyGame() : QueryDataCondition<Game>() {
@ -397,42 +328,38 @@ sealed class QueryCondition : RowRepresentable {
constructor(game: Game) : this() { constructor(game: Game) : this() {
this.setObject(game) this.setObject(game)
} }
override fun entityName(context: Context): String {
return context.getString(R.string.games)
}
} }
class AnyTournamentName : QueryDataCondition<TournamentName>() { class AnyTournamentName() : QueryDataCondition<TournamentName>() {
override val entity: Class<TournamentName> = TournamentName::class.java override val entity: Class<TournamentName> = TournamentName::class.java
override fun entityName(context: Context): String { constructor(tournamentName: TournamentName) : this() {
return context.getString(R.string.tournament_names) this.setObject(tournamentName)
} }
} }
class AnyTournamentFeature : QueryDataCondition<TournamentFeature>() { class AnyTournamentFeature() : QueryDataCondition<TournamentFeature>() {
override val entity: Class<TournamentFeature> = TournamentFeature::class.java override val entity: Class<TournamentFeature> = TournamentFeature::class.java
override fun entityName(context: Context): String { constructor(tournamentFeature: TournamentFeature) : this() {
return context.getString(R.string.tournament_features) this.setObject(tournamentFeature)
} }
} }
class AllTournamentFeature : QueryDataCondition<TournamentFeature>() { class AllTournamentFeature() : QueryDataCondition<TournamentFeature>() {
override var operator = Operator.ALL override var operator = Operator.ALL
override val entity: Class<TournamentFeature> = TournamentFeature::class.java override val entity: Class<TournamentFeature> = TournamentFeature::class.java
override fun entityName(context: Context): String { constructor(tournamentFeature: TournamentFeature) : this() {
return context.getString(R.string.tournament_features) this.setObject(tournamentFeature)
} }
} }
class AnyLocation : QueryDataCondition<Location>() { class AnyLocation() : QueryDataCondition<Location>() {
override val entity: Class<Location> = Location::class.java override val entity: Class<Location> = Location::class.java
override fun entityName(context: Context): String { constructor(location: Location) : this() {
return context.getString(R.string.locations) this.setObject(location)
} }
} }
@ -442,56 +369,34 @@ sealed class QueryCondition : RowRepresentable {
constructor(transactionType: TransactionType) : this() { constructor(transactionType: TransactionType) : this() {
this.setObject(transactionType) this.setObject(transactionType)
} }
override fun entityName(context: Context): String {
return context.getString(R.string.operation_types)
}
} }
class AnyLimit : ListOfInt() { class AnyLimit : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return Limit.values()[value].getDisplayName(context) return Limit.values()[value].getDisplayName(context)
} }
override fun entityName(context: Context): String {
return context.getString(R.string.limits)
}
} }
class AnyTableSize : ListOfInt() { class AnyTableSize : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return TableSize(value).getDisplayName(context) return TableSize(value).getDisplayName(context)
} }
override fun entityName(context: Context): String {
return context.getString(R.string.table_sizes)
}
} }
class AnyTournamentType : ListOfInt() { class AnyTournamentType : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return TournamentType.values()[value].getDisplayName(context) return TournamentType.values()[value].getDisplayName(context)
} }
override fun entityName(context: Context): String {
return context.getString(R.string.tournament_types)
}
} }
class AnyStake : ListOfString() { class AnyBlind : ListOfString()
override fun labelForValue(value: String, context: Context): String { object Last : SingleInt() {
return StakesHolder.readableStakes(value) override var operator = Operator.EQUALS
} override fun getDisplayName(context: Context): String {
//TODO update string "last %i"
override fun entityName(context: Context): String { return "${context.getString(R.string.last_i_records)} $singleValue"
return context.getString(R.string.stakes)
}
override fun compareTo(other: ListOfValues<String>): Int {
return CodedStake(this.listOfValues.first()).compareTo(CodedStake(other.listOfValues.first()))
} }
} }
class NumberOfTable : ListOfInt() { class NumberOfTable : ListOfInt() {
@ -545,7 +450,7 @@ sealed class QueryCondition : RowRepresentable {
class TournamentNumberOfPlayer : ListOfInt() { class TournamentNumberOfPlayer : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return value.toString() + " " + context.getString(R.string.players).toLowerCase() return value.toString() + " " + context.getString(R.string.number_of_players)
} }
override fun entityName(context: Context): String { override fun entityName(context: Context): String {
@ -553,20 +458,20 @@ sealed class QueryCondition : RowRepresentable {
} }
} }
class StartedFromDate(date: Date) : DateQuery(date) { class StartedFromDate : DateQuery() {
override var operator = Operator.MORE_OR_EQUAL override var operator = Operator.MORE
} }
class StartedToDate(date: Date) : DateQuery(date) { class StartedToDate : DateQuery() {
override var operator = Operator.LESS_OR_EQUAL override var operator = Operator.LESS
} }
class EndedFromDate(date: Date) : DateQuery(date) { class EndedFromDate : DateQuery() {
override var operator = Operator.MORE_OR_EQUAL override var operator = Operator.MORE
} }
class EndedToDate(date: Date) : DateQuery(date) { class EndedToDate : DateQuery() {
override var operator = Operator.LESS_OR_EQUAL override var operator = Operator.LESS
} }
class AnyDayOfWeek : ListOfInt() { class AnyDayOfWeek : ListOfInt() {
@ -576,23 +481,23 @@ sealed class QueryCondition : RowRepresentable {
} }
class AnyMonthOfYear() : ListOfInt() { class AnyMonthOfYear() : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String {
return DateFormatSymbols.getInstance(Locale.getDefault()).months[value].capitalize()
}
constructor(month: Int) : this() { constructor(month: Int) : this() {
listOfValues = arrayListOf(month) listOfValues = arrayListOf(month)
} }
}
class AnyYear() : ListOfInt() {
override fun labelForValue(value: Int, context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return DateFormatSymbols.getInstance(Locale.getDefault()).months[value].capitalize() return "$value"
}
} }
class AnyYear() : ListOfInt() {
constructor(year: Int) : this() { constructor(year: Int) : this() {
listOfValues = arrayListOf(year) listOfValues = arrayListOf(year)
} }
override fun labelForValue(value: Int, context: Context): String {
return "$value"
}
} }
object IsWeekDay : TrueQueryCondition() object IsWeekDay : TrueQueryCondition()
@ -610,19 +515,24 @@ sealed class QueryCondition : RowRepresentable {
} }
} }
class PastDay(value: Int) : SingleInt(value) { class PastDay : SingleInt() {
override var operator = Operator.EQUALS override var operator = Operator.EQUALS
override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
override fun getDisplayNameWithValues(context: Context): String { override fun labelForValue(value: Int, context: Context): String {
return context.getString(R.string.period_in_days_with_value, this.singleValue.toString()) return value.toString()
}
override fun entityName(context: Context): String {
return this.resId?.let {
" " + context.getString(it)
} ?: ""
} }
} }
class Duration(value: Int) : SingleInt(value) { class Duration : SingleInt() {
override var operator = Operator.EQUALS override var operator = Operator.EQUALS
var minutes: Int?
var minutes: Int
get() { get() {
return singleValue return singleValue
} }
@ -630,9 +540,12 @@ sealed class QueryCondition : RowRepresentable {
singleValue = value singleValue = value
} }
val netDuration: Long val netDuration: Long?
get() { get() {
return (singleValue * 60 * 1000).toLong() minutes?.let {
return (it * 60 * 1000).toLong()
}
return null
} }
override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal override val viewType: Int = RowViewType.TITLE_VALUE_CHECK.ordinal
@ -643,16 +556,23 @@ sealed class QueryCondition : RowRepresentable {
} }
} }
object DateNotNull : NotNullQueryCondition() class StartedFromTime() : TimeQuery() {
object EndDateNotNull : NotNullQueryCondition() override var operator = Operator.MORE
object BiggestBetNotNull : NotNullQueryCondition()
constructor(date: Date) : this() {
singleValue = date
}
}
class EndedToTime() : TimeQuery() {
override var operator = Operator.LESS
class StartedFromTime(date: Date) : TimeQuery(date) { constructor(date: Date) : this() {
override var operator = Operator.MORE_OR_EQUAL singleValue = date
} }
class EndedToTime(date: Date) : TimeQuery(date) {
override var operator = Operator.LESS_OR_EQUAL
} }
interface CustomFieldRelated { interface CustomFieldRelated {
@ -667,8 +587,12 @@ sealed class QueryCondition : RowRepresentable {
} }
} }
class CustomFieldQuery : QueryDataCondition<CustomField>() { class CustomFieldQuery() : QueryDataCondition<CustomField>() {
override var entity: Class<CustomField> = CustomField::class.java override var entity: Class<CustomField> = CustomField::class.java
constructor(customField: CustomField) : this() {
this.setObject(customField)
}
} }
open class CustomFieldNumberQuery() : ListOfDouble(), CustomFieldRelated { open class CustomFieldNumberQuery() : ListOfDouble(), CustomFieldRelated {
@ -690,9 +614,7 @@ sealed class QueryCondition : RowRepresentable {
val completeLabel = when (listOfValues.size) { val completeLabel = when (listOfValues.size) {
0 -> return NULL_TEXT 0 -> return NULL_TEXT
1, 2 -> { 1, 2 -> {
return name + prefix + listOfValues.joinToString(", ") { return name + prefix + listOfValues.map { labelForValue(it, context) }.joinToString(", ")
labelForValue(it, context)
}
} }
else -> "${listOfValues.size} $prefix $name" else -> "${listOfValues.size} $prefix $name"
} }
@ -727,7 +649,7 @@ sealed class QueryCondition : RowRepresentable {
?: throw PokerAnalyticsException.QueryValueMapUnexpectedValue ?: throw PokerAnalyticsException.QueryValueMapUnexpectedValue
} }
override fun entityName(realm: Realm, context: Context): String { override fun entityName(realm: Realm): String {
return customFieldName(realm) return customFieldName(realm)
} }
@ -748,16 +670,9 @@ sealed class QueryCondition : RowRepresentable {
): RealmQuery<T> { ): RealmQuery<T> {
val fieldName = FilterHelper.fieldNameForQueryType<T>(this::class.java) val fieldName = FilterHelper.fieldNameForQueryType<T>(this::class.java)
// if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
// val className = T::class.java fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown
// fieldName ?: throw PokerAnalyticsException.QueryValueMapUnknown("fieldName missing for $this, class = $className")
// }
if (fieldName == null) {
val className = T::class.java
Timber.w("Possible missing filter configuration for $this in class $className")
} }
fieldName ?: return realmQuery fieldName ?: return realmQuery
when (this) { when (this) {
@ -795,22 +710,23 @@ sealed class QueryCondition : RowRepresentable {
.lessThanOrEqualTo(fieldName, calendar.time.endOfDay()) .lessThanOrEqualTo(fieldName, calendar.time.endOfDay())
} }
is PastDay -> { is PastDay -> {
singleValue?.let {
val startDate = Date() val startDate = Date()
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
calendar.time = startDate calendar.time = startDate
calendar.add(Calendar.DAY_OF_YEAR, -singleValue) calendar.add(Calendar.DAY_OF_YEAR, -it)
return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and() return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and()
.lessThanOrEqualTo(fieldName, startDate.endOfDay()) .lessThanOrEqualTo(fieldName, startDate.endOfDay())
} }
return realmQuery
}
is DuringThisWeek -> { is DuringThisWeek -> {
val startDate = Date()
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
calendar.set(Calendar.HOUR_OF_DAY, 0) calendar.time = startDate
calendar.clear(Calendar.MINUTE) calendar.set(Calendar.DAY_OF_WEEK_IN_MONTH, Calendar.SUNDAY)
calendar.clear(Calendar.SECOND)
calendar.clear(Calendar.MILLISECOND)
calendar.set(Calendar.DAY_OF_WEEK, calendar.firstDayOfWeek)
return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and() return realmQuery.greaterThanOrEqualTo(fieldName, calendar.time.startOfDay()).and()
.lessThanOrEqualTo(fieldName, Date().endOfDay()) .lessThanOrEqualTo(fieldName, startDate.endOfDay())
} }
is DuringThisMonth -> { is DuringThisMonth -> {
val startDate = Date() val startDate = Date()
@ -830,25 +746,32 @@ sealed class QueryCondition : RowRepresentable {
} }
is StartedFromTime -> { is StartedFromTime -> {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
calendar.time = singleValue singleValue?.let {
calendar.time = it
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute()) realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
if (otherQueryCondition is EndedToTime) { if (otherQueryCondition is EndedToTime) {
calendar.time = otherQueryCondition.singleValue otherQueryCondition.singleValue?.let { endTime ->
calendar.time = endTime
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute()) realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
} }
}
}
return realmQuery return realmQuery
} }
is EndedToTime -> { is EndedToTime -> {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
singleValue?.let {
calendar.time = singleValue calendar.time = singleValue
realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute()) realmQuery.lessThanOrEqualTo(fieldName, calendar.hourMinute())
if (otherQueryCondition is StartedFromTime) { if (otherQueryCondition is StartedFromTime) {
calendar.time = otherQueryCondition.singleValue otherQueryCondition.singleValue?.let { startTime ->
calendar.time = startTime
realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute()) realmQuery.greaterThanOrEqualTo(fieldName, calendar.hourMinute())
} }
}
}
return realmQuery return realmQuery
} }
else -> {}
} }
if (this is CustomFieldRelated) { if (this is CustomFieldRelated) {
@ -869,62 +792,58 @@ sealed class QueryCondition : RowRepresentable {
return when (operator) { return when (operator) {
Operator.EQUALS -> { Operator.EQUALS -> {
when (this) { when (this) {
is SingleDate -> realmQuery.equalTo(fieldName, singleValue) is SingleDate -> realmQuery.equalTo(
is SingleInt -> realmQuery.equalTo(fieldName, singleValue) fieldName,
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is SingleInt -> realmQuery.equalTo(
fieldName,
singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is ListOfInt -> realmQuery.equalTo(fieldName, listOfValues.first()) is ListOfInt -> realmQuery.equalTo(fieldName, listOfValues.first())
is ListOfDouble -> realmQuery.equalTo(fieldName, listOfValues.first() * sign) is ListOfDouble -> realmQuery.equalTo(fieldName, listOfValues.first() * sign)
is ListOfString -> realmQuery.equalTo(fieldName, listOfValues.first()) is ListOfString -> realmQuery.equalTo(fieldName, listOfValues.first())
else -> realmQuery else -> realmQuery
} }
} }
Operator.MORE_OR_EQUAL -> { Operator.MORE -> {
when (this) { when (this) {
is SingleDate -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue.startOfDay()) is SingleDate -> realmQuery.greaterThanOrEqualTo(
fieldName,
singleValue?.startOfDay() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is Duration -> realmQuery.greaterThan(
fieldName,
netDuration ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is TournamentFinalPosition -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first()) is TournamentFinalPosition -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
is TournamentNumberOfPlayer -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first()) is TournamentNumberOfPlayer -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first())
is Duration -> realmQuery.greaterThanOrEqualTo(fieldName, netDuration) is SingleInt -> realmQuery.greaterThan(
is SingleInt -> realmQuery.greaterThanOrEqualTo(fieldName, singleValue) fieldName,
is ListOfInt -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first()) singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
is NetAmountLost -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * -1) )
is ListOfDouble -> realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * sign)
else -> realmQuery
}
}
Operator.MORE -> {
when (this) {
is Duration -> realmQuery.greaterThan(fieldName, netDuration)
is SingleInt -> realmQuery.greaterThan(fieldName, singleValue)
is ListOfInt -> realmQuery.greaterThan(fieldName, listOfValues.first()) is ListOfInt -> realmQuery.greaterThan(fieldName, listOfValues.first())
is NetAmountLost -> realmQuery.lessThan(fieldName, listOfValues.first() * -1) is NetAmountLost -> realmQuery.lessThan(fieldName, listOfValues.first() * -1)
is ListOfDouble -> realmQuery.greaterThan(fieldName, listOfValues.first() * sign) is ListOfDouble -> realmQuery.greaterThan(fieldName, listOfValues.first() * sign)
else -> realmQuery else -> realmQuery
} }
} }
Operator.LESS_OR_EQUAL -> { Operator.LESS -> {
when (this) { when (this) {
is SingleDate -> realmQuery.lessThanOrEqualTo(fieldName, singleValue.endOfDay()) is SingleDate -> realmQuery.lessThanOrEqualTo(
fieldName,
singleValue?.endOfDay() ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is Duration -> realmQuery.lessThan(
fieldName,
netDuration ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
)
is TournamentFinalPosition -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first()) is TournamentFinalPosition -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
is TournamentNumberOfPlayer -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first()) is TournamentNumberOfPlayer -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
is Duration -> realmQuery.lessThanOrEqualTo(fieldName, netDuration) is SingleInt -> realmQuery.lessThan(
is SingleInt -> realmQuery.lessThanOrEqualTo(fieldName, singleValue) fieldName,
is ListOfInt -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first()) singleValue ?: throw PokerAnalyticsException.FilterElementExpectedValueMissing
is NetAmountLost -> { )
realmQuery.greaterThanOrEqualTo(fieldName, listOfValues.first() * -1)
realmQuery.lessThan(fieldName, 0.0)
}
is NetAmountWon -> {
realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first())
realmQuery.greaterThan(fieldName, 0.0)
}
is ListOfDouble -> realmQuery.lessThanOrEqualTo(fieldName, listOfValues.first() * sign)
else -> realmQuery
}
}
Operator.LESS -> {
when (this) {
is Duration -> realmQuery.lessThan(fieldName, netDuration)
is SingleInt -> realmQuery.lessThan(fieldName, singleValue)
is ListOfInt -> realmQuery.lessThan(fieldName, listOfValues.first()) is ListOfInt -> realmQuery.lessThan(fieldName, listOfValues.first())
is NetAmountLost -> { is NetAmountLost -> {
realmQuery.greaterThan(fieldName, listOfValues.first() * -1) realmQuery.greaterThan(fieldName, listOfValues.first() * -1)
@ -963,7 +882,6 @@ sealed class QueryCondition : RowRepresentable {
else -> realmQuery else -> realmQuery
} }
} }
Operator.NOTNULL -> realmQuery.isNotNull(fieldName)
else -> realmQuery else -> realmQuery
} }
} }
@ -980,8 +898,8 @@ sealed class QueryCondition : RowRepresentable {
is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal is PastDay -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> { else -> {
when (this.operator) { when (this.operator) {
Operator.MORE, Operator.MORE_OR_EQUAL -> RowViewType.TITLE_VALUE_CHECK.ordinal Operator.MORE -> RowViewType.TITLE_VALUE_CHECK.ordinal
Operator.LESS, Operator.LESS_OR_EQUAL -> RowViewType.TITLE_VALUE_CHECK.ordinal Operator.LESS -> RowViewType.TITLE_VALUE_CHECK.ordinal
else -> RowViewType.TITLE_CHECK.ordinal else -> RowViewType.TITLE_CHECK.ordinal
} }
} }
@ -994,8 +912,8 @@ sealed class QueryCondition : RowRepresentable {
is PastDay -> BottomSheetType.EDIT_TEXT is PastDay -> BottomSheetType.EDIT_TEXT
else -> { else -> {
when (this.operator) { when (this.operator) {
Operator.MORE, Operator.MORE_OR_EQUAL -> BottomSheetType.EDIT_TEXT Operator.MORE -> BottomSheetType.EDIT_TEXT
Operator.LESS, Operator.LESS_OR_EQUAL -> BottomSheetType.EDIT_TEXT Operator.LESS -> BottomSheetType.EDIT_TEXT
else -> BottomSheetType.NONE else -> BottomSheetType.NONE
} }
} }
@ -1022,27 +940,27 @@ sealed class QueryCondition : RowRepresentable {
is IsWeekDay -> R.string.week_days is IsWeekDay -> R.string.week_days
is IsWeekEnd -> R.string.weekend is IsWeekEnd -> R.string.weekend
is PastDay -> R.string.period_in_days is PastDay -> R.string.period_in_days
// is TournamentNumberOfPlayer -> { is TournamentNumberOfPlayer -> {
// when (this.operator) { when (this.operator) {
// Operator.MORE -> R.string.minimum Operator.MORE -> R.string.minimum
// Operator.LESS -> R.string.maximum Operator.LESS -> R.string.maximum
// else -> null else -> null
// } }
// } }
// is NetAmountWon -> { is NetAmountWon -> {
// when (this.operator) { when (this.operator) {
// Operator.MORE -> R.string.won_amount_more_than Operator.MORE -> R.string.won_amount_more_than
// Operator.LESS -> R.string.won_amount_less_than Operator.LESS -> R.string.won_amount_less_than
// else -> null else -> null
// } }
// } }
// is NetAmountLost -> { is NetAmountLost -> {
// when (this.operator) { when (this.operator) {
// Operator.MORE -> R.string.lost_amount_more_than Operator.MORE -> R.string.lost_amount_more_than
// Operator.LESS -> R.string.lost_amount_less_than Operator.LESS -> R.string.lost_amount_less_than
// else -> null else -> null
// } }
// } }
is TournamentFinalPosition -> { is TournamentFinalPosition -> {
when (this.operator) { when (this.operator) {
Operator.MORE -> R.string.minimum Operator.MORE -> R.string.minimum
@ -1052,10 +970,8 @@ sealed class QueryCondition : RowRepresentable {
} }
else -> { else -> {
when (this.operator) { when (this.operator) {
Operator.MORE_OR_EQUAL -> R.string.more_or_equal_sign Operator.MORE -> R.string.more_than
Operator.MORE -> R.string.more_sign Operator.LESS -> R.string.less_than
Operator.LESS_OR_EQUAL -> R.string.less_or_equal_sign
Operator.LESS -> R.string.less_sign
else -> null else -> 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,14 +1,10 @@
package net.pokeranalytics.android.model.interfaces package net.pokeranalytics.android.model.interfaces
import io.realm.RealmModel
/** /**
* An interface to be able to track the usage of an object * An interface to be able to track the usage of an object
*/ */
interface UsageCountable : Identifiable { interface CountableUsage : Identifiable {
var useCount: Int var useCount: Int
get() { return 0 } get() { return 0 }
set(_) {} set(_) {}
val ownerClass: Class<out RealmModel>
} }

@ -1,6 +1,5 @@
package net.pokeranalytics.android.model.interfaces package net.pokeranalytics.android.model.interfaces
import net.pokeranalytics.android.model.realm.Bankroll
import java.util.* import java.util.*
interface Dated { interface Dated {
@ -14,9 +13,3 @@ interface DatedValue : Dated {
var amount: Double var amount: Double
} }
interface DatedBankrollGraphEntry : DatedValue, GraphIdentifiableEntry {
var bankroll: Bankroll?
}

@ -5,6 +5,7 @@ import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.ModelException import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.ui.view.RowRepresentable
enum class SaveValidityStatus { enum class SaveValidityStatus {
VALID, VALID,
@ -22,7 +23,7 @@ enum class DeleteValidityStatus {
/** /**
* An interface to group object which are managed by the database * An interface to group object which are managed by the database
*/ */
interface Manageable : Savable, Deletable interface Manageable : Savable, Deletable, Editable
interface NameManageable : Manageable { interface NameManageable : Manageable {
var name: String var name: String
@ -44,7 +45,6 @@ interface NameManageable : Manageable {
} }
} }
class ObjectIdentifier(var id: String, var clazz: Class<out Identifiable>)
/** /**
* An interface associate a unique uniqueIdentifier to an object * An interface associate a unique uniqueIdentifier to an object
@ -55,16 +55,17 @@ interface Identifiable : RealmModel {
* A unique uniqueIdentifier getter * A unique uniqueIdentifier getter
*/ */
var id: String var id: String
}
/** /**
* required because "this.class" returns the proxy class, making where<class> queries crash * An interface to update the fields of an object
*/ */
val realmObjectClass: Class<out Identifiable> interface Editable : Identifiable {
/**
val objectIdentifier: ObjectIdentifier * a method to handle the modification of the object.
get() { * Through [RowRepresentable] the object is able to update the right variable with the new value.
return ObjectIdentifier(this.id, this.realmObjectClass) */
} fun updateValue(value: Any?, row: RowRepresentable)
} }
/** /**

@ -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())
}
}

@ -19,10 +19,10 @@ interface TimeFilterable {
startDate?.let { startDate?.let {
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
cal.time = it cal.time = it
this.dayOfWeek = cal.get(Calendar.DAY_OF_WEEK) dayOfWeek = cal.get(Calendar.DAY_OF_WEEK)
this.dayOfMonth = cal.get(Calendar.DAY_OF_MONTH) dayOfMonth = cal.get(Calendar.DAY_OF_MONTH)
this.month = cal.get(Calendar.MONTH) month = cal.get(Calendar.MONTH)
this.year = cal.get(Calendar.YEAR) year = cal.get(Calendar.YEAR)
} }
} }
} }

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

@ -2,79 +2,24 @@ package net.pokeranalytics.android.model.migrations
import android.content.Context import android.content.Context
import io.realm.Realm 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.Query
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.* 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 net.pokeranalytics.android.util.Preferences
import java.text.NumberFormat
class Patcher { class Patcher {
companion object { companion object {
fun patchAll(application: PokerAnalyticsApplication) { fun patchAll(context: Context) {
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) { Preferences.executeOnce(Preferences.Keys.PATCH_BREAK, context) {
patchBreaks() patchBreaks()
} }
Preferences.executeOnce(Preferences.Keys.PATCH_TRANSACTION_TYPES_NAMES, context) { Preferences.executeOnce(Preferences.Keys.PATCH_TRANSACTION_TYPES_NAMES, context) {
patchDefaultTransactionTypes(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() { private fun patchBreaks() {
@ -89,8 +34,7 @@ class Patcher {
it.computeStats() it.computeStats()
} }
sessions.forEach { sessions.forEach {
it.generateStakes() it.formatBlinds()
it.defineHighestBet()
} }
results.forEach { results.forEach {
it.computeNumberOfRebuy() it.computeNumberOfRebuy()
@ -115,120 +59,6 @@ class Patcher {
realm.close() 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()
}
}
} }

@ -2,9 +2,7 @@ package net.pokeranalytics.android.model.migrations
import io.realm.DynamicRealm import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import timber.log.Timber import timber.log.Timber
import java.util.*
class PokerAnalyticsMigration : RealmMigration { class PokerAnalyticsMigration : RealmMigration {
@ -21,8 +19,7 @@ class PokerAnalyticsMigration : RealmMigration {
if (currentVersion == 0) { if (currentVersion == 0) {
Timber.d("*** Running migration 1") Timber.d("*** Running migration 1")
schema.get("Filter")?.addField("entityType", Int::class.java) schema.get("Filter")?.addField("entityType", Int::class.java)?.setNullable("entityType", true)
?.setNullable("entityType", true)
schema.get("FilterElement")?.let { schema.get("FilterElement")?.let {
it.setNullable("filterName", true) it.setNullable("filterName", true)
it.setNullable("sectionName", true) it.setNullable("sectionName", true)
@ -84,8 +81,7 @@ class PokerAnalyticsMigration : RealmMigration {
if (currentVersion == 3) { if (currentVersion == 3) {
Timber.d("*** Running migration ${currentVersion + 1}") Timber.d("*** Running migration ${currentVersion + 1}")
schema.get("Result")?.addField("numberOfRebuy", Double::class.java) schema.get("Result")?.addField("numberOfRebuy", Double::class.java)?.setNullable("numberOfRebuy", true)
?.setNullable("numberOfRebuy", true)
currentVersion++ currentVersion++
} }
@ -119,8 +115,7 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("CustomField")?.let { schema.get("CustomField")?.let {
it.addField("type", Integer::class.java).setNullable("type", false) it.addField("type", Integer::class.java).setNullable("type", false)
it.addField("duplicateValue", Boolean::class.java) it.addField("duplicateValue", Boolean::class.java)
it.addField("sortCondition", Integer::class.java) it.addField("sortCondition", Integer::class.java).setRequired("sortCondition", true)
.setRequired("sortCondition", true)
it.addRealmListField("entries", customFieldEntrySchema) it.addRealmListField("entries", customFieldEntrySchema)
} }
@ -137,8 +132,7 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("ReportSetup")?.let { schema.get("ReportSetup")?.let {
it.addRealmListField("statIds", Int::class.java).setNullable("statIds", true) it.addRealmListField("statIds", Int::class.java).setNullable("statIds", true)
it.addRealmListField("criteriaCustomFieldIds", String::class.java) it.addRealmListField("criteriaCustomFieldIds", String::class.java)
it.addRealmListField("criteriaIds", Int::class.java) it.addRealmListField("criteriaIds", Int::class.java).setNullable("criteriaIds", true)
.setNullable("criteriaIds", true)
it.removeField("filters") it.removeField("filters")
schema.get("Filter")?.let { filterSchema -> schema.get("Filter")?.let { filterSchema ->
it.addRealmObjectField("filter", filterSchema) it.addRealmObjectField("filter", filterSchema)
@ -150,191 +144,6 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("Filter")?.removeField("usageCount") schema.get("Filter")?.removeField("usageCount")
currentVersion++ 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 { override fun equals(other: Any?): Boolean {
@ -344,5 +153,4 @@ class PokerAnalyticsMigration : RealmMigration {
override fun hashCode(): Int { override fun hashCode(): Int {
return RealmMigration::javaClass.hashCode() return RealmMigration::javaClass.hashCode()
} }
} }

@ -4,47 +4,19 @@ 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.LinkingObjects import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus 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.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
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.rowrepresentable.BankrollRow
import net.pokeranalytics.android.ui.view.rows.BankrollPropertiesRow import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
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.*
enum class ResultCaptureType { open class Bankroll : RealmObject(), NameManageable, RowRepresentable {
BUYIN_CASHED_OUT,
NET_RESULT;
companion object {
val buyinCashedOutFields = listOf(
SessionPropertiesRow.CASHED_OUT,
SessionPropertiesRow.BUY_IN,
SessionPropertiesRow.TIPS)
val netResultFields = listOf(SessionPropertiesRow.NET_RESULT)
}
val rowRepresentables: List<RowRepresentable>
get() {
return when (this) {
BUYIN_CASHED_OUT -> buyinCashedOutFields
NET_RESULT -> netResultFields
}
}
}
open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresentable {
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -60,12 +32,6 @@ open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresenta
@LinkingObjects("bankroll") @LinkingObjects("bankroll")
val transactions: RealmResults<Transaction>? = null 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
@ -74,13 +40,32 @@ open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresenta
val rate: Double val rate: Double
get() { get() {
return this.currency?.rate ?: Currency.DEFAULT_RATE return this.currency?.rate ?: 1.0
} }
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return this.name return this.name
} }
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
BankrollRow.LIVE -> {
this.live = if (value is Boolean) !value else false
}
BankrollRow.INITIAL_VALUE -> {
this.initialValue = value as Double? ?: 0.0
}
BankrollRow.CURRENCY -> {
//TODO handle a use default currency option
this.currency?.code = value as String?
}
BankrollRow.RATE -> {
this.currency?.rate = value as Double?
}
}
}
override fun isValidForDelete(realm: Realm): Boolean { override fun isValidForDelete(realm: Realm): Boolean {
return realm.where<Session>().equalTo("bankroll.id", id).findAll().isEmpty() return realm.where<Session>().equalTo("bankroll.id", id).findAll().isEmpty()
&& realm.where<Transaction>().equalTo("bankroll.id", id).findAll().isEmpty() && realm.where<Transaction>().equalTo("bankroll.id", id).findAll().isEmpty()
@ -112,16 +97,6 @@ open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresenta
} }
} }
fun resultCaptureType(context: Context): ResultCaptureType {
return Preferences.getResultCaptureType(this, context)
?: run {
when (this.live) {
true -> ResultCaptureType.BUYIN_CASHED_OUT
else -> ResultCaptureType.NET_RESULT
}
}
}
companion object { companion object {
fun getOrCreate(realm: Realm, name: String, live: Boolean = true, currencyCode: String? = null, currencyRate: Double? = null) : Bankroll { fun getOrCreate(realm: Realm, name: String, live: Boolean = true, currencyCode: String? = null, currencyRate: Double? = null) : Bankroll {
@ -143,34 +118,4 @@ open class Bankroll : RealmObject(), NameManageable, RowUpdatable, RowRepresenta
} }
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
BankrollPropertiesRow.ONLINE -> {
this.live = if (value is Boolean) !value else false
}
BankrollPropertiesRow.INITIAL_VALUE -> {
this.initialValue = value as Double? ?: 0.0
}
BankrollPropertiesRow.CURRENCY -> {
//TODO handle a use default currency option
this.currency?.code = value as String?
}
BankrollPropertiesRow.RATE -> {
this.currency?.rate = value as Double?
}
}
}
val utilCurrency: java.util.Currency
get() {
this.currency?.code?.let {
return java.util.Currency.getInstance(it)
}
return UserDefaults.currency
}
@Ignore
override val realmObjectClass: Class<out Identifiable> = Bankroll::class.java
} }

@ -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
}
}

@ -4,7 +4,7 @@ import io.realm.RealmObject
import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
open class ComputableResult : RealmObject(), Filterable { open class ComputableResult() : RealmObject(), Filterable {
var ratedNet: Double = 0.0 var ratedNet: Double = 0.0
@ -20,26 +20,26 @@ open class ComputableResult : RealmObject(), Filterable {
var bbPer100Hands: BB = 0.0 var bbPer100Hands: BB = 0.0
var session: Session? = null // var sessionSet: SessionSet? = null
var ratedTips: Double = 0.0 var session: Session? = null
fun updateWith(session: Session) { fun updateWith(session: Session) {
// this.sessionSet = session.sessionSet
val rate = session.bankroll?.currency?.rate ?: 1.0 val rate = session.bankroll?.currency?.rate ?: 1.0
session.result?.let { result -> session.result?.let { result ->
this.ratedNet = result.net * rate this.ratedNet = result.net * rate
this.isPositive = result.isPositive this.isPositive = result.isPositive
this.ratedBuyin = (result.buyin ?: 0.0) * rate this.ratedBuyin = (result.buyin ?: 0.0) * rate
this.ratedTips = (result.tips ?: 0.0) * rate
} }
this.bbNet = session.bbNet this.bbNet = session.bbNet
this.hasBigBlind = if (session.cgBiggestBet != null) 1 else 0 this.hasBigBlind = if (session.cgBigBlind != null) 1 else 0
this.estimatedHands = session.estimatedHands this.estimatedHands = session.estimatedHands
this.bbPer100Hands = this.bbPer100Hands = session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100
session.bbNet / (session.numberOfHandsPerHour * session.hourlyDuration) * 100
} }
@ -50,8 +50,7 @@ open class ComputableResult : RealmObject(), Filterable {
IS_POSITIVE("isPositive"), IS_POSITIVE("isPositive"),
RATED_BUYIN("ratedBuyin"), RATED_BUYIN("ratedBuyin"),
ESTIMATED_HANDS("estimatedHands"), ESTIMATED_HANDS("estimatedHands"),
RATED_TIPS("ratedTips"), BB_PER100HANDS("bbPer100Hands")
// BB_PER100HANDS("bbPer100Hands")
} }
companion object { companion object {

@ -3,18 +3,13 @@ package net.pokeranalytics.android.model.realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore 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 net.pokeranalytics.android.util.UserDefaults
import java.util.* import java.util.*
open class Currency : RealmObject() { open class Currency : RealmObject() {
companion object {
@Ignore @Ignore
val DEFAULT_RATE: Double = 1.0 val DEFAULTRATE: Double = 1.0
}
@PrimaryKey @PrimaryKey
var id = UUID.randomUUID().toString() var id = UUID.randomUUID().toString()
@ -23,26 +18,15 @@ open class Currency : RealmObject() {
* 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 * The rate of the currency with the main currency
*/ */
var rate: Double? = DEFAULT_RATE var rate: Double? = DEFAULTRATE
fun refreshRelatedRatedValues() { fun refreshRelatedRatedValues() {
val rate = this.rate ?: DEFAULT_RATE val rate = this.rate ?: DEFAULTRATE
val query = this.realm.where(ComputableResult::class.java) val query = this.realm.where(ComputableResult::class.java)
query.`in`("session.bankroll.currency.id", arrayOf(this.id)) query.`in`("session.bankroll.currency.id", arrayOf(this.id))
val cResults = query.findAll() val cResults = query.findAll()

@ -11,44 +11,22 @@ import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus 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.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
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.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rows.CustomFieldPropertiesRow import net.pokeranalytics.android.ui.view.rowrepresentable.CustomFieldRow
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.SimpleRow import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.util.enumerations.IntIdentifiable import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import timber.log.Timber
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource { open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable {
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 * The custom field type: a list of items, a number or an amont
@ -112,9 +90,13 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
@Ignore @Ignore
private var entriesToDelete: ArrayList<CustomFieldEntry> = ArrayList() private var entriesToDelete: ArrayList<CustomFieldEntry> = ArrayList()
@Ignore
override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal
@Ignore @Ignore
private var rowRepresentation: List<RowRepresentable> = mutableListOf() private var rowRepresentation: List<RowRepresentable> = mutableListOf()
//helper //helper
val isListType: Boolean val isListType: Boolean
@ -127,6 +109,14 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
return this.type == Type.AMOUNT.uniqueIdentifier return this.type == Type.AMOUNT.uniqueIdentifier
} }
override fun localizedTitle(context: Context): String {
return this.name
}
override fun getDisplayName(context: Context): String {
return this.name
}
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation return rowRepresentation
} }
@ -134,8 +124,8 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
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? ?: ""
CustomFieldPropertiesRow.TYPE -> this.type = (value as Type?)?.uniqueIdentifier ?: Type.LIST.uniqueIdentifier CustomFieldRow.TYPE -> this.type = (value as Type?)?.uniqueIdentifier ?: Type.LIST.uniqueIdentifier
CustomFieldPropertiesRow.COPY_ON_DUPLICATE -> this.duplicateValue = value as Boolean? ?: false CustomFieldRow.COPY_ON_DUPLICATE -> this.duplicateValue = value as Boolean? ?: false
} }
} }
@ -167,6 +157,13 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
return R.string.cf_entry_delete_popup_message return R.string.cf_entry_delete_popup_message
} }
override val bottomSheetType: BottomSheetType
get() {
return when (type) {
Type.LIST.uniqueIdentifier -> BottomSheetType.LIST_STATIC
else -> BottomSheetType.NUMERIC_TEXT
}
}
override fun deleteDependencies(realm: Realm) { override fun deleteDependencies(realm: Realm) {
if (isValid) { if (isValid) {
@ -175,7 +172,7 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
} }
} }
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? { override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
return when (row) { return when (row) {
is CustomFieldEntry -> row.editingDescriptors( is CustomFieldEntry -> row.editingDescriptors(
mapOf( mapOf(
@ -186,13 +183,35 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
} }
} }
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor>? {
return when (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
)
)
}
}
}
/** /**
* Update the row representation * Update the row representation
*/ */
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> { private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> {
val rows = ArrayList<RowRepresentable>() val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME) rows.add(SimpleRow.NAME)
rows.add(CustomFieldPropertiesRow.TYPE) rows.add(CustomFieldRow.TYPE)
if (type == Type.LIST.uniqueIdentifier && entries.size >= 0) { if (type == Type.LIST.uniqueIdentifier && entries.size >= 0) {
if (entries.isNotEmpty()) { if (entries.isNotEmpty()) {
@ -250,6 +269,7 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
fun cleanupEntries() { // called when saving the custom field fun cleanupEntries() { // called when saving the custom field
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransaction {
this.entriesToDelete.forEach { // entries are out of realm this.entriesToDelete.forEach { // entries are out of realm
realm.where<CustomFieldEntry>().equalTo("id", it.id).findFirst()?.deleteFromRealm() realm.where<CustomFieldEntry>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
@ -259,19 +279,6 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
this.entriesToDelete.clear() 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 * Clean the entries if the type is not a list & remove the deleted entries from realm
*/ */
@ -298,50 +305,9 @@ open class CustomField : RealmObject(), RowRepresentable, RowUpdatable, NameMana
val criteria: Criteria val criteria: Criteria
get() { get() {
return when (this.type) { return when (this.type) {
Type.LIST.uniqueIdentifier -> Criteria.ListCustomFields(this.id) CustomField.Type.LIST.uniqueIdentifier -> Criteria.ListCustomFields(this.id)
else -> Criteria.ValueCustomFields(this.id) else -> Criteria.ValueCustomFields(this.id)
} }
} }
@Ignore
override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal
override fun localizedTitle(context: Context): String {
return this.name
}
override fun getDisplayName(context: Context): String {
return this.name
}
override val bottomSheetType: BottomSheetType
get() {
return when (this.type) {
Type.LIST.uniqueIdentifier -> BottomSheetType.LIST_STATIC
else -> BottomSheetType.NUMERIC_TEXT
}
}
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor> {
return when (this.type) {
Type.LIST.uniqueIdentifier -> {
val defaultValue: Any? by map
val data: RealmList<CustomFieldEntry>? by map
arrayListOf(
RowRepresentableEditDescriptor(defaultValue, staticData = data)
)
}
else -> {
val defaultValue: Double? by map
arrayListOf(
RowRepresentableEditDescriptor(
defaultValue, inputType = InputType.TYPE_CLASS_NUMBER
or InputType.TYPE_NUMBER_FLAG_DECIMAL
or InputType.TYPE_NUMBER_FLAG_SIGNED
)
)
}
}
}
} }

@ -12,13 +12,11 @@ import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.ModelException import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus 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.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
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.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable
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.NULL_TEXT
import net.pokeranalytics.android.util.extensions.toCurrency import net.pokeranalytics.android.util.extensions.toCurrency
@ -27,10 +25,7 @@ import java.util.*
import java.util.Currency import java.util.Currency
open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable, RowUpdatable { open class CustomFieldEntry : RealmObject(), NameManageable, RowRepresentable {
@Ignore
override val realmObjectClass: Class<out Identifiable> = CustomFieldEntry::class.java
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()

@ -1,20 +1,27 @@
package net.pokeranalytics.android.model.realm package net.pokeranalytics.android.model.realm
import android.content.Context import android.content.Context
import io.realm.* import io.realm.Realm
import io.realm.annotations.Ignore import io.realm.RealmList
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import io.realm.kotlin.where import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.Query import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.model.interfaces.CountableUsage
import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus
import net.pokeranalytics.android.model.interfaces.Editable
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.modules.filter.FilterableType import net.pokeranalytics.android.ui.interfaces.FilterableType
import net.pokeranalytics.android.ui.view.* import net.pokeranalytics.android.ui.view.ImageDecorator
import net.pokeranalytics.android.ui.view.rows.FilterCategoryRow import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rows.FilterItemRow import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
@ -23,10 +30,7 @@ import java.util.*
* It contains a list of [FilterCondition] describing the complete query to launch * 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 * 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 { open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, CountableUsage, ImageDecorator {
@Ignore
override val realmObjectClass: Class<out Identifiable> = Filter::class.java
companion object { companion object {
@ -35,6 +39,12 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
val filter = Filter() val filter = Filter()
filter.filterableTypeUniqueIdentifier = filterableTypeUniqueIdentifier filter.filterableTypeUniqueIdentifier = filterableTypeUniqueIdentifier
return filter return filter
//return realm.copyToRealm(filter)
}
// Get a queryWith by its id
fun getFilterBydId(realm: Realm, filterId: String): Filter? {
return realm.where<Filter>().equalTo("id", filterId).findFirst()
} }
inline fun <reified T : Filterable> queryOn(realm: Realm, query: Query, sortField: String? = null): RealmResults<T> { inline fun <reified T : Filterable> queryOn(realm: Realm, query: Query, sortField: String? = null): RealmResults<T> {
@ -43,6 +53,7 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
sortField?.let { sortField?.let {
realmQuery = realmQuery.sort(it) realmQuery = realmQuery.sort(it)
} }
val desc = realmQuery.description
return realmQuery.findAll() return realmQuery.findAll()
} }
} }
@ -71,9 +82,6 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
override var useCount: Int = 0 override var useCount: Int = 0
@Ignore
override val ownerClass: Class<out RealmModel> = Session::class.java
var filterConditions: RealmList<FilterCondition> = RealmList() var filterConditions: RealmList<FilterCondition> = RealmList()
private set private set
@ -87,25 +95,25 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
return FilterableType.ALL return FilterableType.ALL
} }
fun createOrUpdateFilterConditions(filterConditionRows: List<FilterItemRow>) { fun createOrUpdateFilterConditions(filterConditionRows: ArrayList<QueryCondition>) {
Timber.d("list of querys saving: ${filterConditionRows.map { it.id }}")
Timber.d("list of querys saving: ${filterConditionRows.map { it.queryCondition?.id }}")
Timber.d("list of querys previous: ${this.filterConditions.map { it.queryCondition.id }}") Timber.d("list of querys previous: ${this.filterConditions.map { it.queryCondition.id }}")
filterConditionRows filterConditionRows
.mapNotNull { .map {
it.queryCondition?.groupId it.groupId
} }
.distinct() .distinct()
.forEach { groupId -> .forEach { groupId ->
filterConditionRows filterConditionRows
.filter { .filter {
it.queryCondition?.groupId == groupId it.groupId == groupId
} }
.apply { .apply {
val conditions = this.mapNotNull { it.queryCondition }
Timber.d("list of querys: ${conditions.map { it.id }}") Timber.d("list of querys: ${this.map { it.id }}")
val newFilterCondition = FilterCondition(conditions, this.first().filterSectionRow) val casted = arrayListOf<QueryCondition>()
casted.addAll(this)
val newFilterCondition = FilterCondition(casted)
val previousCondition = filterConditions.filter { val previousCondition = filterConditions.filter {
it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator it.filterName == newFilterCondition.filterName && it.operator == newFilterCondition.operator
} }
@ -133,28 +141,35 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
Timber.d("list of saved queries ${filterConditions.map { it.queryCondition.id }}") Timber.d("list of saved queries ${filterConditions.map { it.queryCondition.id }}")
Timber.d("list of contains ${filterElementRow.id}") Timber.d("list of contains ${filterElementRow.id}")
val contained = filterConditions.flatMap { it.queryCondition.id }.contains(filterElementRow.id.first()) val contained = filterConditions.flatMap { it.queryCondition.id }.contains(filterElementRow.id.first())
Timber.d("is contained: $contained") Timber.d("list of : $contained")
return contained return contained
} }
fun filterCondition(filterElementRow: QueryCondition): FilterCondition? { /**
return filterConditions.firstOrNull { * Get the saved value for the given [filterElementRow]
it.queryCondition.id.contains(filterElementRow.id.first()) */
fun loadValueForElement(filterElementRow: QueryCondition) {
val filtered = filterConditions.filter {
it.queryCondition.id == filterElementRow.id
} }
if (filtered.isNotEmpty()) {
return filterElementRow.updateValueBy(filtered.first())
} }
}
inline fun <reified T : Filterable> results(firstField: String? = null, secondField: String? = null): RealmResults<T> {
inline fun <reified T : Filterable> query(firstField: String? = null, vararg remainingFields: String): RealmQuery<T> {
val realmQuery = realm.where<T>() val realmQuery = realm.where<T>()
if (firstField != null) { if (firstField != null && secondField != null) {
return this.query.queryWith(realmQuery).distinct(firstField, *remainingFields) return this.query.queryWith(realmQuery).distinct(firstField, secondField).findAll()
} }
return this.query.queryWith(realmQuery) if (firstField != null) {
return this.query.queryWith(realmQuery).distinct(firstField).findAll()
} }
inline fun <reified T : Filterable> results(firstField: String? = null, vararg remainingFields: String): RealmResults<T> { return this.query.queryWith(realmQuery).findAll()
return this.query<T>(firstField, *remainingFields).findAll()
} }
val query: Query val query: Query
@ -194,9 +209,11 @@ open class Filter : RealmObject(), RowRepresentable, RowUpdatable, Deletable, Us
} }
override fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
realm.executeTransaction {
val newName = value as String? ?: "" val newName = value as String? ?: ""
if (newName.isNotEmpty()) { if (newName.isNotEmpty()) {
name = newName name = newName
} }
} }
} }
}

@ -2,12 +2,10 @@ 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.LinkingObjects
import net.pokeranalytics.android.exceptions.PokerAnalyticsException import net.pokeranalytics.android.exceptions.PokerAnalyticsException
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.ui.view.rows.FilterSectionRow
import java.util.* import java.util.*
import kotlin.collections.ArrayList
open class FilterCondition() : RealmObject() { open class FilterCondition() : RealmObject() {
@ -16,7 +14,7 @@ open class FilterCondition() : RealmObject() {
this.sectionName = sectionName this.sectionName = sectionName
} }
constructor(filterElementRows: List<QueryCondition>, section: FilterSectionRow) : this(filterElementRows.first().baseId, section.name) { constructor(filterElementRows: ArrayList<QueryCondition>) : this(filterElementRows.first().baseId, filterElementRows.first().filterSectionRow.name) {
val row = filterElementRows.first() val row = filterElementRows.first()
this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName this.filterName ?: throw PokerAnalyticsException.FilterElementUnknownName
this.operator = row.operator.ordinal this.operator = row.operator.ordinal
@ -24,12 +22,11 @@ open class FilterCondition() : RealmObject() {
this.stringValue = row.customFieldId this.stringValue = row.customFieldId
} }
when (row) { when (row) {
is QueryCondition.SingleInt -> this.setValue(row.singleValue) is QueryCondition.SingleInt -> this.setValue(row.singleValue?:throw PokerAnalyticsException.FilterElementExpectedValueMissing)
is QueryCondition.SingleDate -> this.setValue(row.singleValue) is QueryCondition.SingleDate -> this.setValue(row.singleValue?:throw PokerAnalyticsException.FilterElementExpectedValueMissing)
is QueryCondition.ListOfDouble -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfDouble).listOfValues }) 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.ListOfInt -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfInt).listOfValues })
is QueryCondition.ListOfString -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfString).listOfValues }) is QueryCondition.ListOfString -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfString).listOfValues })
else -> {}
} }
} }
@ -51,10 +48,7 @@ open class FilterCondition() : RealmObject() {
var stringValue: String? = null var stringValue: String? = null
var operator: Int? = null var operator: Int? = null
@LinkingObjects("filterConditions") inline fun <reified T:Any > getValues(): ArrayList < T > {
val filters: RealmResults<Filter>? = null
inline fun <reified T> getValues(): ArrayList <T> {
return when (T::class) { return when (T::class) {
Int::class -> ArrayList<T>().apply { intValues?.map { add(it as T) } } Int::class -> ArrayList<T>().apply { intValues?.map { add(it as T) } }
Double::class -> ArrayList<T>().apply { doubleValues?.map { add(it as T) } } Double::class -> ArrayList<T>().apply { doubleValues?.map { add(it as T) } }
@ -63,17 +57,7 @@ open class FilterCondition() : RealmObject() {
} }
} }
fun <T> getv(clazz: Class<T>) : T { inline fun <reified T:Any > getValue(): 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) { return when (T::class) {
Int::class -> intValue ?: 0 Int::class -> intValue ?: 0
Double::class -> doubleValue?: 0.0 Double::class -> doubleValue?: 0.0
@ -107,5 +91,4 @@ open class FilterCondition() : RealmObject() {
fun setValue(value:String) { fun setValue(value:String) {
stringValue = value stringValue = value
} }
} }

@ -2,35 +2,29 @@ package net.pokeranalytics.android.model.realm
import android.content.Context import android.content.Context
import io.realm.Realm 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 io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.UsageCountable import net.pokeranalytics.android.model.interfaces.CountableUsage
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource 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.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable import net.pokeranalytics.android.ui.view.rowrepresentable.GameRow
import net.pokeranalytics.android.ui.view.rows.GamePropertiesRow import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable, RowUpdatable, UsageCountable { open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable, CountableUsage {
@Ignore
override val realmObjectClass: Class<out Identifiable> = Game::class.java
companion object { companion object {
val rowRepresentation : List<RowRepresentable> by lazy { val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>() val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME) rows.add(SimpleRow.NAME)
// rows.addAll(GameRow.values())
rows rows
} }
} }
@ -47,9 +41,6 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
// CountableUsage // CountableUsage
override var useCount: Int = 0 override var useCount: Int = 0
@Ignore
override val ownerClass: Class<out RealmModel> = Session::class.java
fun getNotNullShortName() : String { fun getNotNullShortName() : String {
this.shortName?.let { this.shortName?.let {
return it return it
@ -62,25 +53,21 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
} }
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation return Game.rowRepresentation
} }
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 -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
GamePropertiesRow.SHORT_NAME -> this.shortName ?: NULL_TEXT GameRow.SHORT_NAME -> this.shortName ?: NULL_TEXT
else -> return super.charSequenceForRow(row, context, 0) else -> return super.stringForRow(row)
} }
} }
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? { override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
return when (row) { return when (row) {
SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.name)) SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.name))
GamePropertiesRow.SHORT_NAME -> row.editingDescriptors(mapOf("defaultValue" to this.shortName)) GameRow.SHORT_NAME -> row.editingDescriptors(mapOf("defaultValue" to this.shortName))
else -> null else -> null
} }
} }
@ -88,7 +75,7 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
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? ?: ""
} }
} }
@ -107,30 +94,4 @@ open class Game : RealmObject(), NameManageable, StaticRowRepresentableDataSourc
override fun isValidForDelete(realm: Realm): Boolean { override fun isValidForDelete(realm: Realm): Boolean {
return realm.where<Session>().equalTo("game.id", id).findAll().isEmpty() 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()
}

@ -4,23 +4,17 @@ import android.content.Context
import com.google.android.libraries.places.api.model.Place import com.google.android.libraries.places.api.model.Place
import io.realm.Realm 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 io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
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.rowrepresentable.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(), NameManageable, RowRepresentable {
@Ignore
override val realmObjectClass: Class<out Identifiable> = Location::class.java
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()

@ -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()
}
} }

@ -6,13 +6,11 @@ import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.calcul.ReportDisplay
import net.pokeranalytics.android.calculus.Calculator import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Deletable
import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus 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.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
@ -21,9 +19,6 @@ import java.util.*
open class ReportSetup : RealmObject(), RowRepresentable, Deletable { open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
@Ignore
override val realmObjectClass: Class<out Identifiable> = ReportSetup::class.java
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -31,7 +26,7 @@ open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
var name: String = "" var name: String = ""
// The type of display of the report // The type of display of the report
var display: Int = ReportDisplay.TABLE.ordinal var display: Int = Calculator.Options.Display.TABLE.ordinal
/** /**
* A list of statIds to compute * A list of statIds to compute
@ -65,15 +60,15 @@ open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
/** /**
* Returns the Options based on the ReportSetup parameters * Returns the Options based on the ReportSetup parameters
*/ */
fun options(realm: Realm, reportDisplay: ReportDisplay): Calculator.Options { val options: Calculator.Options
get() {
val realm = Realm.getDefaultInstance()
val stats = this.statIds.map { Stat.valueByIdentifier(it) } val stats = this.statIds.map { Stat.valueByIdentifier(it) }
// Comparison criteria // Comparison criteria
val criteria = this.criteriaIds.map { Criteria.valueByIdentifier(it) } val criteria = this.criteriaIds.map { Criteria.valueByIdentifier(it) }
val customFields = this.criteriaCustomFieldIds.mapNotNull { realm.findById<CustomField>(it) } val customFields = this.criteriaCustomFieldIds.mapNotNull { realm.findById<CustomField>(it) }
val cfCriteria = customFields.map { it.criteria } val cfCriteria = customFields.map { it.criteria }
val allCriteria = mutableListOf<Criteria>() val allCriteria = mutableListOf<Criteria>()
@ -81,8 +76,8 @@ open class ReportSetup : RealmObject(), RowRepresentable, Deletable {
allCriteria.addAll(cfCriteria) allCriteria.addAll(cfCriteria)
return Calculator.Options( return Calculator.Options(
display = Calculator.Options.Display.values()[this.display],
stats = stats, stats = stats,
progressValues = reportDisplay.progressValues,
criterias = allCriteria, criterias = allCriteria,
filter = this.filter, filter = this.filter,
userGenerated = true, userGenerated = true,

@ -29,7 +29,7 @@ open class Result : RealmObject(), Filterable {
set(value) { set(value) {
field = value field = value
this.computeNumberOfRebuy() this.computeNumberOfRebuy()
this.computeNet(true) this.computeNet()
} }
/** /**
@ -38,7 +38,7 @@ open class Result : RealmObject(), Filterable {
var cashout: Double? = null var cashout: Double? = null
set(value) { set(value) {
field = value field = value
this.computeNet(true) this.computeNet()
if (value != null) { if (value != null) {
this.session?.end() this.session?.end()
} }
@ -50,18 +50,18 @@ open class Result : RealmObject(), Filterable {
var netResult: Double? = null var netResult: Double? = null
set(value) { set(value) {
// this.session?.bankroll?.let { bankroll -> this.session?.bankroll?.let { bankroll ->
// if (bankroll.live) { if (bankroll.live) {
// throw PAIllegalStateException("Can't set net result on a live bankroll") throw IllegalStateException("Can't set net result on a live bankroll")
// } }
// } ?: run { } ?: run {
// throw PAIllegalStateException("Session doesn't have any bankroll") throw IllegalStateException("Session doesn't have any bankroll")
// } }
field = value field = value
this.computeNet(false) this.computeNet()
if (value != null) { if (value != null) {
this.session?.end() this.session.end()
} }
} }
@ -75,10 +75,6 @@ open class Result : RealmObject(), Filterable {
* Tips * Tips
*/ */
var tips: Double? = null 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()
@ -102,37 +98,16 @@ open class Result : RealmObject(), Filterable {
/** /**
* Returns 1 if the session is positive * Returns 1 if the session is positive
*/ */
val isPositive: Int @Ignore
get() { val isPositive: Int = if (this.net >= 0.0) 1 else 0
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) { private fun computeNet() {
val transactionsSum = transactions.sumOf { it.amount }
// choose the method to compute the net val transactionsSum = transactions.sumByDouble { it.amount }
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 isLive = this.session?.isLive ?: true
if (isLive) {
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
@ -150,7 +125,7 @@ open class Result : RealmObject(), Filterable {
fun computeNumberOfRebuy() { fun computeNumberOfRebuy() {
this.session?.let { this.session?.let {
if (it.isCashGame()) { if (it.isCashGame()) {
it.cgBiggestBet?.let { bb -> it.cgBigBlind?.let { bb ->
if (bb > 0.0) { if (bb > 0.0) {
this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0) this.numberOfRebuy = (this.buyin ?: 0.0) / (bb * 100.0)
} else { } else {

@ -4,17 +4,16 @@ 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.LinkingObjects import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.StatFormattingException import net.pokeranalytics.android.calculus.StatFormattingException
import net.pokeranalytics.android.util.TextFormat
import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition 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.model.interfaces.Timed
import net.pokeranalytics.android.ui.graph.ObjectIdentifier
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.TextFormat
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
@ -61,9 +60,9 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
override var netDuration: Long = 0L override var netDuration: Long = 0L
fun computeStats() { fun computeStats() {
this.ratedNet = this.sessions?.sumOf { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0 this.ratedNet = this.sessions?.sumByDouble { it.computableResult?.ratedNet ?: 0.0 } ?: 0.0
this.estimatedHands = this.sessions?.sumOf { it.estimatedHands } ?: 0.0 this.estimatedHands = this.sessions?.sumByDouble { it.estimatedHands } ?: 0.0
this.bbNet = this.sessions?.sumOf { it.bbNet } ?: 0.0 this.bbNet = this.sessions?.sumByDouble { it.bbNet } ?: 0.0
this.breakDuration = this.sessions?.max("breakDuration")?.toLong() ?: 0L this.breakDuration = this.sessions?.max("breakDuration")?.toLong() ?: 0L
} }
@ -122,15 +121,15 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
override fun formattedValue(stat: Stat) : TextFormat { override fun formattedValue(stat: Stat) : TextFormat {
return when (stat) { return when (stat) {
Stat.NET_RESULT, Stat.AVERAGE -> stat.textFormat(this.ratedNet, currency = null) Stat.NET_RESULT, Stat.AVERAGE -> stat.format(this.ratedNet, currency = null)
Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> stat.textFormat(this.hourlyDuration, currency = null) Stat.HOURLY_DURATION, Stat.AVERAGE_HOURLY_DURATION -> stat.format(this.hourlyDuration, currency = null)
Stat.HOURLY_RATE -> stat.textFormat(this.hourlyRate, currency = null) Stat.HOURLY_RATE -> stat.format(this.hourlyRate, currency = null)
Stat.HANDS_PLAYED -> stat.textFormat(this.estimatedHands, currency = null) Stat.HANDS_PLAYED -> stat.format(this.estimatedHands, currency = null)
Stat.HOURLY_RATE_BB -> stat.textFormat(this.bbHourlyRate, currency = null) Stat.HOURLY_RATE_BB -> stat.format(this.bbHourlyRate, currency = null)
Stat.NET_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> { Stat.NET_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> {
val netBBPer100Hands = Stat.netBBPer100Hands(this.bbNet, this.estimatedHands) val netBBPer100Hands = Stat.netBBPer100Hands(this.bbNet, this.estimatedHands)
if (netBBPer100Hands != null) { if (netBBPer100Hands != null) {
return stat.textFormat(this.estimatedHands, currency = null) return stat.format(this.estimatedHands, currency = null)
} else { } else {
return TextFormat(NULL_TEXT) return TextFormat(NULL_TEXT)
} }
@ -139,8 +138,10 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
} }
} }
@Ignore // Timed
override val realmObjectClass: Class<out Identifiable> = SessionSet::class.java
override val objectIdentifier: ObjectIdentifier
get() = ObjectIdentifier(this.id, SessionSet::class.java)
} }

@ -2,27 +2,24 @@ package net.pokeranalytics.android.model.realm
import android.content.Context import android.content.Context
import io.realm.Realm 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 io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.UsageCountable import net.pokeranalytics.android.model.interfaces.CountableUsage
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource 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.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow import net.pokeranalytics.android.ui.view.rowrepresentable.TournamentFeatureRow
import net.pokeranalytics.android.ui.view.rows.TournamentFeatureRow
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
open class TournamentFeature : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource, UsageCountable { open class TournamentFeature : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable,
CountableUsage {
companion object { companion object {
val rowRepresentation : List<RowRepresentable> by lazy { val rowRepresentation : List<RowRepresentable> by lazy {
@ -33,9 +30,6 @@ open class TournamentFeature : RealmObject(), RowRepresentable, RowUpdatable, Na
} }
} }
@Ignore
override val realmObjectClass: Class<out Identifiable> = TournamentFeature::class.java
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -45,29 +39,22 @@ open class TournamentFeature : RealmObject(), RowRepresentable, RowUpdatable, Na
// CountableUsage // CountableUsage
override var useCount: Int = 0 override var useCount: Int = 0
@Ignore
override val ownerClass: Class<out RealmModel> = Session::class.java
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return this.name return this.name
} }
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation return TournamentFeature.rowRepresentation
} }
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 -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
else -> return super.charSequenceForRow(row, context, 0) else -> return super.stringForRow(row)
} }
} }
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? { override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
return row.editingDescriptors(mapOf( return row.editingDescriptors(mapOf(
"defaultValue" to this.name)) "defaultValue" to this.name))
} }

@ -3,26 +3,22 @@ package net.pokeranalytics.android.model.realm
import android.content.Context import android.content.Context
import io.realm.Realm 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 io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.model.interfaces.SaveValidityStatus import net.pokeranalytics.android.model.interfaces.SaveValidityStatus
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource 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.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow import net.pokeranalytics.android.ui.view.rowrepresentable.TournamentNameRow
import net.pokeranalytics.android.ui.view.rows.TournamentNameRow
import net.pokeranalytics.android.util.NULL_TEXT import net.pokeranalytics.android.util.NULL_TEXT
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentableDataSource, open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable {
RowUpdatable, RowRepresentable {
companion object { companion object {
val rowRepresentation : List<RowRepresentable> by lazy { val rowRepresentation : List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>() val rows = ArrayList<RowRepresentable>()
@ -32,9 +28,6 @@ open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentabl
} }
} }
@Ignore
override val realmObjectClass: Class<out Identifiable> = TournamentName::class.java
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -52,21 +45,17 @@ open class TournamentName : RealmObject(), NameManageable, StaticRowRepresentabl
} }
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation return TournamentName.rowRepresentation
} }
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 -> if (this.name.isNotEmpty()) this.name else NULL_TEXT
else -> return super.charSequenceForRow(row, context,0) else -> return super.stringForRow(row)
} }
} }
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? { override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
return row.editingDescriptors(mapOf("defaultValue" to this.name)) return row.editingDescriptors(mapOf("defaultValue" to this.name))
} }

@ -1,6 +1,7 @@
package net.pokeranalytics.android.model.realm package net.pokeranalytics.android.model.realm
import android.content.Context import android.content.Context
import com.github.mikephil.charting.data.Entry
import io.realm.Realm import io.realm.Realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
@ -11,36 +12,29 @@ import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.graph.Graph import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.view.* import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.view.rows.TransactionPropertiesRow import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.ui.view.rowrepresentable.TransactionRow
import net.pokeranalytics.android.util.TextFormat import net.pokeranalytics.android.util.TextFormat
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
import kotlin.math.abs import kotlin.collections.ArrayList
open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageable, StaticRowRepresentableDataSource, TimeFilterable,
Filterable, DatedBankrollGraphEntry {
companion object { open class Transaction : RealmObject(), Manageable, StaticRowRepresentableDataSource, RowRepresentable, TimeFilterable, Filterable, DatedValue,
GraphUnderlyingEntry {
fun newInstance(realm: Realm, bankroll: Bankroll, date: Date? = null, type: TransactionType, amount: Double, comment: String? = null, destination: Bankroll? = null, transferRate: Double? = null): Transaction {
val transaction = realm.copyToRealm(Transaction()) companion object {
transaction.date = date ?: Date()
transaction.amount = amount
transaction.type = type
transaction.bankroll = bankroll
transaction.comment = comment ?: ""
transaction.destination = destination
transaction.transferRate = transferRate
if (destination != null) { // we make sure transfers are negative
transaction.amount = abs(amount) * -1
}
return transaction val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.addAll(TransactionRow.values())
rows
} }
fun fieldNameForQueryType(queryCondition: Class<out QueryCondition>): String? { fun fieldNameForQueryType(queryCondition: Class<out QueryCondition>): String? {
@ -52,10 +46,7 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
QueryCondition.AnyDayOfWeek::class.java, QueryCondition.IsWeekEnd::class.java, QueryCondition.IsWeekDay::class.java -> "dayOfWeek" QueryCondition.AnyDayOfWeek::class.java, QueryCondition.IsWeekEnd::class.java, QueryCondition.IsWeekDay::class.java -> "dayOfWeek"
QueryCondition.AnyMonthOfYear::class.java -> "month" QueryCondition.AnyMonthOfYear::class.java -> "month"
QueryCondition.AnyYear::class.java -> "year" QueryCondition.AnyYear::class.java -> "year"
QueryCondition.PastDay::class.java, QueryCondition.IsToday::class.java, QueryCondition.PastDay::class.java, QueryCondition.IsToday::class.java, QueryCondition.WasYesterday::class.java, QueryCondition.WasTodayAndYesterday::class.java, QueryCondition.DuringThisYear::class.java, QueryCondition.DuringThisMonth::class.java, QueryCondition.DuringThisWeek::class.java -> "date"
QueryCondition.WasYesterday::class.java, QueryCondition.WasTodayAndYesterday::class.java,
QueryCondition.DuringThisYear::class.java, QueryCondition.DuringThisMonth::class.java,
QueryCondition.DuringThisWeek::class.java, QueryCondition.DateNotNull::class.java -> "date"
else -> null else -> null
} }
} }
@ -69,17 +60,10 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
// The bankroll of the transaction // The bankroll of the transaction
override var bankroll: Bankroll? = null var bankroll: Bankroll? = null
// The amount of the transaction // The amount of the transaction
override var amount: Double = 0.0 override var amount: Double = 0.0
set(value) {
field = value
computeRatedAmount()
}
// The amount of the transaction
var ratedAmount: Double = 0.0
// The date of the transaction // The date of the transaction
override var date: Date = Date() override var date: Date = Date()
@ -94,12 +78,6 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
// A user comment // A user comment
var comment: String = "" var comment: String = ""
// The destination Bankroll of a transfer
var destination: Bankroll? = null
// The rate of the transfer when bankrolls are in different currencies
var transferRate: Double? = null
// Timed interface // Timed interface
override var dayOfWeek: Int? = null override var dayOfWeek: Int? = null
override var month: Int? = null override var month: Int? = null
@ -109,34 +87,18 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
@Ignore @Ignore
override val viewType: Int = RowViewType.ROW_TRANSACTION.ordinal override val viewType: Int = RowViewType.ROW_TRANSACTION.ordinal
enum class Field(val identifier: String) {
RATED_AMOUNT("ratedAmount")
}
fun computeRatedAmount() {
val rate = this.bankroll?.currency?.rate ?: 1.0
this.ratedAmount = rate * this.amount
}
val displayAmount: Double
get() { // for transfers we want to show a positive value (in the feed for instance)
return if (this.destination == null) { this.amount } else { abs(this.amount) }
}
override fun updateValue(value: Any?, row: RowRepresentable) { override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) { when (row) {
TransactionPropertiesRow.BANKROLL -> bankroll = value as Bankroll? TransactionRow.BANKROLL -> bankroll = value as Bankroll?
TransactionPropertiesRow.TYPE -> type = value as TransactionType? TransactionRow.TYPE -> type = value as TransactionType?
TransactionPropertiesRow.AMOUNT -> amount = if (value == null) 0.0 else (value as String).toDouble() TransactionRow.AMOUNT -> amount = if (value == null) 0.0 else (value as String).toDouble()
TransactionPropertiesRow.COMMENT -> comment = value as String? ?: "" TransactionRow.COMMENT -> comment = value as String? ?: ""
TransactionPropertiesRow.DATE -> date = value as Date? ?: Date() TransactionRow.DATE -> date = value as Date? ?: Date()
TransactionPropertiesRow.DESTINATION -> destination = value as? Bankroll
TransactionPropertiesRow.RATE -> transferRate = (value as String?)?.toDouble()
} }
} }
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation() return rowRepresentation
} }
override fun isValidForSave(): Boolean { override fun isValidForSave(): Boolean {
@ -172,47 +134,26 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab
return SaveValidityStatus.VALID return SaveValidityStatus.VALID
} }
fun rowRepresentation(): List<RowRepresentable> { // GraphUnderlyingEntry
val rows = ArrayList<RowRepresentable>()
when (this.type?.kind) {
TransactionType.Value.TRANSFER.uniqueIdentifier -> {
if (this.bankroll != null && this.bankroll?.currency?.code != this.destination?.currency?.code) {
rows.addAll(TransactionPropertiesRow.transferRateRows)
} else {
rows.addAll(TransactionPropertiesRow.transferRows)
}
}
else -> {
rows.addAll(TransactionPropertiesRow.baseRows)
}
}
return rows
}
// GraphIdentifiableEntry
@Ignore
override val realmObjectClass: Class<out Identifiable> = Transaction::class.java
override fun entryTitle(context: Context): String { override fun entryTitle(context: Context): String {
return DateFormat.getDateInstance(DateFormat.SHORT).format(this.date) return DateFormat.getDateInstance(DateFormat.SHORT).format(this.date)
} }
override fun formattedValue(stat: Stat): TextFormat { override fun formattedValue(stat: Stat): TextFormat {
return stat.textFormat(this.amount, currency = this.bankroll?.utilCurrency) return stat.format(this.amount)
} }
override fun legendValues( override fun legendValues(
stat: Stat, stat: Stat,
total: Double, entry: Entry,
style: Graph.Style, style: GraphFragment.Style,
groupName: String, groupName: String,
context: Context context: Context
): LegendContent { ): LegendContent {
val entryValue = this.formattedValue(stat) val entryValue = this.formattedValue(stat)
val totalStatValue = stat.textFormat(total, currency = null) val totalStatValue = stat.format(entry.y.toDouble(), currency = null)
val leftName = context.getString(R.string.amount) val leftName = context.getString(R.string.amount)
return DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue, leftName = leftName) return DefaultLegendValues(this.entryTitle(context), entryValue, totalStatValue, leftName = leftName)
} }

@ -2,44 +2,27 @@ package net.pokeranalytics.android.model.realm
import android.content.Context import android.content.Context
import io.realm.Realm 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 net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.interfaces.DeleteValidityStatus
import net.pokeranalytics.android.model.interfaces.* import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.Localizable import net.pokeranalytics.android.ui.view.Localizable
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.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowUpdatable import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.ui.view.rows.SimpleRow import net.pokeranalytics.android.ui.view.rowrepresentable.TransactionTypeRow
import net.pokeranalytics.android.ui.view.rows.TransactionTypePropertiesRow
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
import java.util.* import java.util.*
import kotlin.collections.ArrayList
open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, NameManageable, StaticRowRepresentableDataSource, open class TransactionType : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable {
UsageCountable {
enum class Value(override var uniqueIdentifier: Int, val additive: Boolean) : IntIdentifiable, Localizable { enum class Value(val additive: Boolean) : Localizable {
WITHDRAWAL(false),
WITHDRAWAL(0, false), DEPOSIT(true),
DEPOSIT(1, true), BONUS(true);
BONUS(2, true),
STACKING_INCOMING(3, true),
STACKING_OUTGOING(4, false),
TRANSFER(5, false),
EXPENSE(6, false); // not created by default, only used for poker base import atm
companion object : IntSearchable<Value> {
override fun valuesInternal(): Array<Value> {
return values()
}
}
override val resId: Int? override val resId: Int?
get() { get() {
@ -47,52 +30,28 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
WITHDRAWAL -> R.string.withdrawal WITHDRAWAL -> R.string.withdrawal
DEPOSIT -> R.string.deposit DEPOSIT -> R.string.deposit
BONUS -> R.string.bonus BONUS -> R.string.bonus
STACKING_INCOMING -> R.string.stacking_incoming
STACKING_OUTGOING -> R.string.stacking_outgoing
TRANSFER -> R.string.transfer
EXPENSE -> R.string.expense
} }
} }
} }
companion object { companion object {
val rowRepresentation: List<RowRepresentable> by lazy { val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>() val rows = ArrayList<RowRepresentable>()
rows.add(SimpleRow.NAME) rows.add(SimpleRow.NAME)
rows.addAll(TransactionTypePropertiesRow.values()) rows.addAll(TransactionTypeRow.values())
rows rows
} }
fun getByValue(value: Value, realm: Realm): TransactionType { fun getByValue(value: Value, realm: Realm): TransactionType {
val type = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findFirst() val type = realm.where(TransactionType::class.java).equalTo("kind", value.ordinal).findFirst()
type?.let { type?.let {
return it return it
} }
throw PAIllegalStateException("Transaction type ${value.name} should exist in database!") throw IllegalStateException("Transaction type ${value.name} should exist in database!")
}
fun getOrCreate(realm: Realm, value: Value, context: Context): TransactionType {
return getOrCreate(realm, value.localizedTitle(context), value.additive)
} }
fun getOrCreate(realm: Realm, name: String, additive: Boolean): TransactionType {
val type = realm.where(TransactionType::class.java).equalTo("name", name).findFirst()
return if (type != null) {
type
} else {
val transactionType = TransactionType()
transactionType.name = name
transactionType.additive = additive
realm.copyToRealm(transactionType)
}
} }
}
@Ignore
override val realmObjectClass: Class<out Identifiable> = TransactionType::class.java
@PrimaryKey @PrimaryKey
override var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
@ -113,11 +72,6 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
// The predefined kind, if necessary, like: Withdrawal, deposit, or tips // The predefined kind, if necessary, like: Withdrawal, deposit, or tips
var kind: Int? = null var kind: Int? = null
override var useCount: Int = 0
@Ignore
override val ownerClass: Class<out RealmModel> = Transaction::class.java
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return this.name return this.name
} }
@ -126,32 +80,28 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
return rowRepresentation return rowRepresentation
} }
override fun charSequenceForRow( override fun stringForRow(row: RowRepresentable): String {
row: RowRepresentable,
context: Context,
tag: Int
): CharSequence {
return when (row) { return when (row) {
SimpleRow.NAME -> this.name SimpleRow.NAME -> this.name
else -> return super.charSequenceForRow(row, context, 0) else -> return super.stringForRow(row)
} }
} }
override fun boolForRow(row: RowRepresentable): Boolean { override fun boolForRow(row: RowRepresentable): Boolean {
return when (row) { return when (row) {
TransactionTypePropertiesRow.TRANSACTION_ADDITIVE -> this.additive TransactionTypeRow.TRANSACTION_ADDITIVE -> this.additive
else -> super.boolForRow(row) else -> super.boolForRow(row)
} }
} }
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? { override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
return row.editingDescriptors(mapOf("defaultValue" to this.name)) return row.editingDescriptors(mapOf("defaultValue" to this.name))
} }
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? ?: ""
TransactionTypePropertiesRow.TRANSACTION_ADDITIVE -> this.additive = value as Boolean? ?: false TransactionTypeRow.TRANSACTION_ADDITIVE -> this.additive = value as Boolean? ?: false
} }
} }
@ -164,13 +114,5 @@ open class TransactionType : RealmObject(), RowRepresentable, RowUpdatable, Name
return R.string.transaction_relationship_error return R.string.transaction_relationship_error
} }
override fun getFailedSaveMessage(status: SaveValidityStatus): Int {
return when (status) {
SaveValidityStatus.DATA_INVALID -> R.string.operation_type_empty_field_error
SaveValidityStatus.ALREADY_EXISTS -> R.string.duplicate_operation_type_error
else -> super.getFailedSaveMessage(status)
}
}
} }

@ -1,41 +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.util.UUID_SEPARATOR
import net.pokeranalytics.android.util.extensions.findById
import java.util.*
open class UserConfig : RealmObject() {
companion object {
fun getConfiguration(realm: Realm): UserConfig {
realm.where(UserConfig::class.java).findFirst()?.let { config ->
return config
}
return UserConfig()
}
}
@PrimaryKey
var id = UUID.randomUUID().toString()
var liveDealtHandsPerHour: Int = 250
var onlineDealtHandsPerHour: Int = 500
var transactionTypeIds: String = ""
fun setTransactionTypeIds(transactionTypes: Set<TransactionType>) {
this.transactionTypeIds = transactionTypes.joinToString(UUID_SEPARATOR) { it.id }
}
fun transactionTypes(realm: Realm): List<TransactionType> {
val ids = this.transactionTypeIds.split(UUID_SEPARATOR)
return ids.mapNotNull { realm.findById(it) }
}
}

@ -1,252 +0,0 @@
package net.pokeranalytics.android.model.realm.handhistory
import io.realm.RealmObject
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.Position
import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.ui.modules.handhistory.model.ActionReadRow
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.formatted
/***
* An extension to transform a list of ComputedAction into
* a more compact and read-friendly list of ActionReadRow
*/
fun List<Action>.compact(positions: LinkedHashSet<Position>, heroIndex: Int?): List<ActionReadRow> {
val rows = mutableListOf<ActionReadRow>()
this.forEach {
if (it.type == Action.Type.FOLD && rows.lastOrNull()?.action == Action.Type.FOLD) {
rows.lastOrNull()?.positions?.add(positions.elementAt(it.position))
} else {
rows.add(it.toReadRow(positions, heroIndex, null)) // TODO stack. The method is used for text export only atm
}
}
return rows
}
fun Action.toReadRow(positions: LinkedHashSet<Position>, heroIndex: Int?, stack: Double?): ActionReadRow {
val pos = positions.elementAt(this.position)
val isHero = (heroIndex == this.position)
var amount = this.amount
if (this.type?.isCall == true) {
amount = this.effectiveAmount
}
return ActionReadRow(mutableListOf(pos), this.position, this.type, amount, stack, isHero)
}
open class Action : RealmObject() {
enum class Type(override var resId: Int) : RowRepresentable {
POST_SB(R.string.posts_sb),
POST_BB(R.string.post_bb),
STRADDLE(R.string.straddle),
FOLD(R.string.fold),
CHECK(R.string.check),
CALL(R.string.call),
BET(R.string.bet),
POT(R.string.pot),
RAISE(R.string.raise),
UNDEFINED_ALLIN(R.string.allin),
CALL_ALLIN(R.string.callin),
BET_ALLIN(R.string.ballin),
RAISE_ALLIN(R.string.rallin);
val isBlind: Boolean
get() {
return when (this) {
POST_SB, POST_BB -> true
else -> false
}
}
val isSignificant: Boolean
get() {
return when (this) {
POST_SB, POST_BB, STRADDLE, BET, POT, RAISE, BET_ALLIN, RAISE_ALLIN -> true
UNDEFINED_ALLIN -> throw PAIllegalStateException("Can't ask for UNDEFINED_ALLIN")
else -> false
}
}
/***
* Tells if the action pulls the player out from any new decision
*/
val isPullOut: Boolean
get() {
return when (this) {
FOLD, BET_ALLIN, RAISE_ALLIN, CALL_ALLIN, UNDEFINED_ALLIN -> true
else -> false
}
}
/***
* Returns if the action is passive
*/
val isPassive: Boolean
get() {
return when (this) {
FOLD, CHECK -> true
else -> false
}
}
val isCall: Boolean
get() {
return when (this) {
CALL, CALL_ALLIN -> true
else -> false
}
}
val isAllin: Boolean
get() {
return when (this) {
UNDEFINED_ALLIN, BET_ALLIN, RAISE_ALLIN, CALL_ALLIN -> true
else -> false
}
}
val color: Int
get() {
return when (this) {
POST_SB, POST_BB, CALL, CHECK, CALL_ALLIN -> R.color.kaki_lighter
FOLD -> R.color.red
else -> R.color.green
}
}
val background: Int
get() {
return when (this) {
POST_SB, POST_BB, CALL, CHECK, CALL_ALLIN -> R.drawable.rounded_kaki_medium_rect
FOLD -> R.drawable.rounded_red_rect
else -> R.drawable.rounded_green_rect
}
}
override val viewType: Int = RowViewType.TITLE_GRID.ordinal
companion object {
val defaultTypes: List<Type> by lazy {
listOf(FOLD, CHECK, BET, POT, CALL, RAISE, UNDEFINED_ALLIN)
}
}
}
/***
* The street of the action
*/
private var streetIdentifier: Int = 0
var street: Street
set(value) {
this.streetIdentifier = value.ordinal
}
get() {
return streetIdentifier.let { Street.values()[it] }
}
/***
* The index of the action
*/
var index: Int = 0
/***
* The position of the user making the action
*/
var position: Int = 0
/***
* The type of action: check, fold, raise...
*/
private var typeIdentifier: Int? = null
var type: Type?
set(value) {
this.typeIdentifier = value?.ordinal
}
get() {
return typeIdentifier?.let { Type.values()[it] }
}
/***
* The amount linked for a bet, raise...
*/
var amount: Double? = null
set(value) {
field = value
// Timber.d("/// set value = $value")
}
var effectiveAmount: Double = 0.0
var positionRemainingStack: Double? = null
val isActionSignificant: Boolean
get() {
return this.type?.isSignificant ?: false
}
val formattedAmount: String?
get() {
val amount = when (this.type) {
Type.CALL, Type.CALL_ALLIN -> this.effectiveAmount
else -> this.amount
}
return amount?.formatted
}
fun toggleType(remainingStack: Double) {
when (this.type) {
Type.BET -> {
if (remainingStack == 0.0) {
this.type = Type.BET_ALLIN
}
}
Type.BET_ALLIN -> {
if (remainingStack > 0.0) {
this.type = Type.BET
}
}
Type.RAISE -> {
if (remainingStack == 0.0) {
this.type = Type.RAISE_ALLIN
}
}
Type.RAISE_ALLIN -> {
if (remainingStack > 0.0) {
this.type = Type.RAISE
}
}
Type.CALL -> {
if (remainingStack == 0.0) {
this.type = Type.CALL_ALLIN
}
}
Type.CALL_ALLIN -> {
if (remainingStack > 0.0) {
this.type = Type.CALL
}
}
else -> {}
}
}
val displayedAmount: Double?
get() {
if (this.type?.isCall == true) {
return this.effectiveAmount
}
return this.amount
}
}

@ -1,292 +0,0 @@
package net.pokeranalytics.android.model.realm.handhistory
import android.content.Context
import android.text.SpannableString
import android.text.TextUtils
import android.text.style.ForegroundColorSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatTextView
import io.realm.RealmObject
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import timber.log.Timber
interface CardProperty
/***
* Returns a CharSequence containing each card representation
*/
fun List<Card>.formatted(context: Context) : CharSequence? {
var span: CharSequence? = null
this.forEach {
val formatted = it.formatted(context)
span = if (span == null) {
formatted
} else {
TextUtils.concat(span, formatted)
}
}
return span
}
open class Card : RealmObject() {
companion object {
fun newInstance(value: Int? = null, suit: Suit? = null, index: Int = 0) : Card {
val card = Card()
value?.let { card.value = it }
suit?.let { card.suit = it}
card.index = index
return card
}
}
/***
* The Value of a Card
* Can represent all values from Deuce to Ace, or a blank
*/
class Value(var value: Int?) : CardProperty, RowRepresentable {
companion object {
val undefined: Value = Value(null)
val values: List<Value> by lazy {
val v = mutableListOf(Value(null)) // null for x
(2..14).forEach { v.add(Value(it)) }
v
}
fun format(value: Int?) : String {
return when(value) {
in 2..9 -> "$value"
10 -> "T"
11 -> "J"
12 -> "Q"
13 -> "K"
14 -> "A"
else -> "X"
}
}
}
override fun getDisplayName(context: Context): String {
return this.formatted
}
val formatted: String
get() { return format(this.value) }
override val viewType: Int = RowViewType.TITLE_GRID.ordinal
}
enum class Suit(val value: String) : CardProperty, RowRepresentable {
UNDEFINED("x"),
SPADES(""),
HEART(""),
DIAMOND(""),
CLOVER("");
companion object {
val baseSuits: List<Suit> by lazy {
listOf(SPADES, HEART, DIAMOND, CLOVER)
}
val displaySuits: List<Suit> by lazy {
listOf(SPADES, HEART, DIAMOND, CLOVER, UNDEFINED)
}
fun format(suit: Suit?) : String {
return suit?.value ?: UNDEFINED.value
}
fun color(suit: Suit?) : Int {
return suit?.color ?: R.color.white
}
fun valueOf(suit: Int): Suit {
return when (suit) {
0 -> SPADES
1 -> DIAMOND
2 -> CLOVER
3 -> HEART
else -> throw PAIllegalStateException("unknow value: $suit")
}
}
}
val color: Int
get() {
return when (this) {
UNDEFINED -> R.color.grey
SPADES -> R.color.black
HEART -> R.color.red
DIAMOND -> R.color.blue
CLOVER -> R.color.clover
}
}
override fun getDisplayName(context: Context): String {
return this.value
}
override val viewType: Int = RowViewType.TITLE_GRID.ordinal
val evaluatorSuit: Int?
get() {
return when (this) {
SPADES -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.SPADES
CLOVER -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.CLUBS
DIAMOND -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.DIAMONDS
HEART -> net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card.HEARTS
else -> null
}
}
}
/***
* The card value: 2..A
* can be undefined
*/
var value: Int? = null
/***
* Returns the formatted value of the card
*/
val formattedValue: String
get() { return Value.format(this.value) }
/***
* The card suit identifier: heart, spades...
*/
var suitIdentifier: Int? = null
/***
* Returns the suit of the card
*/
var suit: Suit?
get() {
return this.suitIdentifier?.let { Suit.values()[it] }
}
set(value) {
this.suitIdentifier = value?.ordinal
}
/***
* The card index in a list
*/
var index: Int = 0
/***
* Returns the street where the card belongs based on its [index]
*/
val street: Street
get() {
return when (this.index) {
in 0..2 -> Street.FLOP
3 -> Street.TURN
4 -> Street.RIVER
else -> throw PAIllegalStateException("Card doesn't belong to any street")
}
}
/***
* Formats the Card into a Spannable String with the appropriate color for the card suit
* [lineBreak] adds "\n" between the value and the suit
*/
fun formatted(context: Context, lineBreak: Boolean = false) : SpannableString {
val suit = Suit.format(this.suit)
val spannable = if (lineBreak) {
SpannableString(this.formattedValue + "\n" + suit)
} else {
SpannableString(this.formattedValue + suit)
}
val suitStart = spannable.indexOf(suit)
spannable.setSpan(
ForegroundColorSpan(context.getColor(Suit.color(this.suit))),
suitStart,
spannable.length,
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
)
// Timber.d("card format = $spannable")
return spannable
}
/***
* Returns a view showing the card [value] and [suit]
*/
fun view(context: Context, layoutInflater: LayoutInflater, root: ViewGroup, blank: Boolean = false): View {
val textView = layoutInflater.inflate(R.layout.view_card, root, false) as AppCompatTextView
if (blank) {
textView.text = "\n"
} else {
textView.text = this.formatted(context, true)
}
return textView
}
fun toEvaluatorCard():net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card? {
val rank = this.value
val suit = this.suit?.evaluatorSuit
return if (rank != null && suit != null) {
net.pokeranalytics.android.ui.modules.handhistory.evaluator.Card(rank - 2, suit)
} else {
null
}
}
fun copy(): Card {
val card = Card()
card.value = this.value
card.suit = this.suit
return card
}
val isWildCard: Boolean
get() {
return this.value == null || this.isSuitWildCard
}
val isSuitWildCard: Boolean
get() {
return this.suit == null || this.suit == Suit.UNDEFINED
}
}
fun List<Card>.putSuits(usedCards: MutableList<Card>) : List<Card> {
fun getLeastUsedValidSuit(value: Int, usedCards: MutableList<Card>, addedCards: List<Card>): Card.Suit {
val availableSuits = Card.Suit.baseSuits.toMutableList()
usedCards.filter { it.value == value }.forEach { availableSuits.remove(it.suit) }
val currentCards = this.drop(addedCards.size) + addedCards
val cardsPerSuit = availableSuits.associateWith { suit ->
currentCards.filter { it.suit == suit }
}
return cardsPerSuit.minByOrNull { it.value.count() }!!.key
}
val list = mutableListOf<Card>()
this.forEach { cardToClone ->
val card = cardToClone.copy()
if (card.value != null && card.isSuitWildCard) {
card.suit = getLeastUsedValidSuit(card.value!!, usedCards, list)
usedCards.add(card)
Timber.d("Selected suit for wildcard: ${card.suit?.value}, value = ${card.value}")
}
list.add(card)
}
return list
}

@ -1,775 +0,0 @@
package net.pokeranalytics.android.model.realm.handhistory
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatTextView
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.R
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.handhistory.HandSetup
import net.pokeranalytics.android.model.handhistory.Position
import net.pokeranalytics.android.model.handhistory.Street
import net.pokeranalytics.android.model.interfaces.*
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.modules.handhistory.evaluator.EvaluatorBridge
import net.pokeranalytics.android.ui.modules.handhistory.model.ActionReadRow
import net.pokeranalytics.android.ui.modules.handhistory.model.CardHolder
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.addLineReturn
import net.pokeranalytics.android.util.extensions.formatted
import net.pokeranalytics.android.util.extensions.fullDate
import timber.log.Timber
import java.util.*
import kotlin.math.max
data class PositionAmount(var position: Int, var amount: Double, var isAllin: Boolean)
open class HandHistory : RealmObject(), Deletable, RowRepresentable, Filterable, TimeFilterable,
CardHolder, Comparator<PositionAmount>, StakesHolder {
@PrimaryKey
override var id = UUID.randomUUID().toString()
@Ignore
override val realmObjectClass: Class<out Identifiable> = HandHistory::class.java
/***
* The date of the hand history
*/
var date: Date = Date()
set(value) {
field = value
this.updateTimeParameter(value)
}
init {
this.date = Date() // force the custom setter call
}
/***
* The session whose hand was played
*/
var session: Session? = null
/***
* The small blind
*/
var oldSmallBlind: Double? = null
set(value) {
field = value
// if (this.bigBlind == null && value != null) {
// this.bigBlind = value * 2
// }
}
/***
* The big blind
*/
var oldBigBlind: Double? = null
set(value) {
field = value
// if (this.smallBlind == null && value != null) {
// this.smallBlind = value / 2
// }
}
/***
* Big blind ante
*/
var bigBlindAnte: Boolean = false
/***
* The ante
*/
override var ante: Double? = 0.0
set(value) {
field = value
this.generateStakes()
this.defineHighestBet()
}
/***
* The blinds
*/
override var blinds: String? = null
set(value) {
field = value
this.generateStakes()
this.defineHighestBet()
}
override var biggestBet: Double? = null
/***
* The coded stakes
*/
override var stakes: String? = null
override val bankroll: Bankroll?
get() { return this.session?.bankroll }
override fun setHolderStakes(stakes: String?) {
this.stakes = stakes
}
override fun setHolderBiggestBet(biggestBet: Double?) {
this.biggestBet = biggestBet
}
/***
* Number of players in the hand
*/
var numberOfPlayers: Int = 9
/***
* Number of players in the hand
*/
var comment: String? = null
/***
* The position index of the hero
*/
var heroIndex: Int? = null
/***
* Indicates if the hero wins the hand
*/
var winnerPots: RealmList<WonPot> = RealmList()
/***
* The board
*/
var board: RealmList<Card> = RealmList()
/***
* The players actions
*/
var actions: RealmList<Action> = RealmList()
/***
* A list of players initial data: user, position, hand, stack
*/
var playerSetups: RealmList<PlayerSetup> = RealmList()
// Timed interface
override var dayOfWeek: Int? = null
override var month: Int? = null
override var year: Int? = null
override var dayOfMonth: Int? = null
/***
* Returns the indexes of all players
*/
val positionIndexes: IntRange
get() { return (0 until this.numberOfPlayers) }
// 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.
}
override fun deleteDependencies(realm: Realm) {
this.board.deleteAllFromRealm()
this.playerSetups.deleteAllFromRealm()
this.actions.deleteAllFromRealm()
}
/***
* Configures a hand history with a [handSetup]
*/
fun configure(handSetup: HandSetup, keepPlayers: Boolean = false) {
if (!keepPlayers) {
this.playerSetups.removeAll(this.playerSetups)
}
handSetup.tableSize?.let { this.numberOfPlayers = it }
handSetup.ante?.let { this.ante = it }
handSetup.blinds?.let { this.blinds = it }
this.session = handSetup.session
this.date = this.session?.handHistoryAutomaticDate ?: Date()
this.createActions(handSetup)
}
/***
* Creates the initial actions of the hand history
*/
private fun createActions(handSetup: HandSetup) {
this.actions.clear()
var blindValues = this.blindValues
if (blindValues.isNotEmpty()) {
blindValues.forEachIndexed { index, blind ->
val action = when(index) {
0 -> Action.Type.POST_SB
1 -> Action.Type.POST_BB
else -> null
}
action?.let { this.addAction(index, action, blind) }
}
} else {
this.addAction(0, Action.Type.POST_SB, this.oldSmallBlind)
this.addAction(1, Action.Type.POST_BB, this.oldBigBlind)
}
blindValues = blindValues.drop(2)
val positions = Position.positionsPerPlayers(this.numberOfPlayers)
handSetup.straddlePositions.forEachIndexed { index, position -> // position are sorted here
val positionIndex = positions.indexOf(position)
val amount = if (index < blindValues.size) { blindValues[index] } else null
this.addAction(positionIndex, Action.Type.STRADDLE, amount)
}
// var lastStraddler: Int? = null
// val totalActions = this.actions.size
// val startingPosition = lastStraddler?.let { it + 1 } ?: totalActions
// for (i in this.positionIndexes - 1) { // we don't add the BB / straddler by default in case of a walk
// this.addAction((startingPosition + i) % this.numberOfPlayers)
// }
}
/***
* Adds an action with the given [position], [type] and [amount] to the actions list
*/
private fun addAction(position: Int, type: Action.Type? = null, amount: Double? = null) {
val action = Action()
action.index = this.actions.size
action.position = position
action.type = type
action.amount = amount
action.effectiveAmount = amount ?: 0.0
this.actions.add(action)
}
/***
* Returns the board cards for a given [street]
*/
fun cardsForStreet(street: Street): MutableList<Card> {
return this.board.sortedBy { it.index }.take(street.totalBoardCards).toMutableList()
}
/***
* Returns the optional PlayerSetup object at the [position]
*/
fun playerSetupForPosition(position: Int): PlayerSetup? {
return this.playerSetups.firstOrNull { it.position == position }
}
override val cards: RealmList<Card>
get() { return this.board }
/***
* Returns the ante sum
*/
val anteSum: Double
get() {
return if (bigBlindAnte) {
this.biggestBet ?: 0.0
} else {
(this.ante ?: 0.0) * this.numberOfPlayers
}
}
/***
* Returns the sorted list of actions by index
*/
private val sortedActions: List<Action>
get() {
return this.actions.sortedBy { it.index }
}
/***
* Returns the list of undefined positions,
* meaning the positions where no PlayerSetup has been created
*/
fun undefinedPositions(): List<Position> {
val positions = Position.positionsPerPlayers(this.numberOfPlayers)
val copy = positions.clone() as LinkedHashSet<Position>
this.playerSetups.forEach {
copy.remove(positions.elementAt(it.position))
}
return copy.toList()
}
/***
* Creates and affect a PlayerSetup at the given [positionIndex]
*/
fun createPlayerSetup(positionIndex: Int): PlayerSetup {
val playerSetup = if (this.realm != null) {
this.realm.createObject(PlayerSetup::class.java) }
else {
PlayerSetup()
}
playerSetup.position = positionIndex
this.playerSetups.add(playerSetup)
return playerSetup
}
/***
* Returns the pot size at the start of the given [street]
*/
fun potSizeForStreet(street: Street): Double {
val sortedActions = this.sortedActions
val firstIndexOfStreet = sortedActions.firstOrNull { it.street == street }?.index
?: sortedActions.size
return this.anteSum + sortedActions.take(firstIndexOfStreet).sumOf { it.effectiveAmount }
}
@Ignore
override val viewType: Int = RowViewType.HAND_HISTORY.ordinal
override fun localizedString(context: Context): CharSequence {
val positions = Position.positionsPerPlayers(this.numberOfPlayers)
var string = ""
// Settings
val players = "${this.numberOfPlayers} ${context.getString(R.string.players)}"
val firstLineComponents = mutableListOf(this.date.fullDate(), players)
this.blinds?.let { firstLineComponents.add(it) }
// this.smallBlind?.let { sb ->
// this.bigBlind?.let { bb ->
// firstLineComponents.add("${sb.formatted}/${bb.formatted}")
// }
// }
this.ante?.let {
if (it > 0.0) {
firstLineComponents.add("ante ${this.ante}")
}
}
string = string.plus(firstLineComponents.joinToString(" - "))
string = string.addLineReturn(2)
// Comment
this.comment?.let { comment ->
string = string.plus(comment)
string = string.addLineReturn(2)
}
// Players
this.playerSetups.sortedBy { it.position }.forEach {
string = string.plus(localizedPlayerSetup(it, positions, context))
string = string.addLineReturn()
}
// Actions per street
val sortedActions = this.actions.sortedBy { it.index }
// Remove SUMMARY
Street.values().dropLast(1).forEach { street ->
string = string.addLineReturn(2)
val streetActions = sortedActions.filter { it.street == street }.compact(positions, this.heroIndex)
val streetCards = this.cardsForStreet(street)
// we want to show streets: with actions or when an allin is called
if (streetCards.size == street.totalBoardCards || streetActions.isNotEmpty()) {
val streetItems = mutableListOf<CharSequence>(context.getString(street.resId))
val potSize = this.potSizeForStreet(street)
if (potSize > 0) {
streetItems.add("(" + potSize.formatted + ")")
}
string = string.plus(streetItems.joinToString(" "))
if (streetCards.isNotEmpty()) {
string = string.addLineReturn()
string = string.plus(streetCards.formatted(context) ?: "")
}
string = string.addLineReturn()
string = string.plus("-----------")
string = string.addLineReturn()
streetActions.forEach { action ->
string = string.plus(localizedAction(action, context))
string = string.addLineReturn()
}
}
}
return string
}
fun anteForPosition(position: Position): Double {
return if (this.bigBlindAnte) {
if (position == Position.BB) {
this.biggestBet ?: 0.0
} else {
0.0
}
} else {
this.ante ?: 0.0
}
}
/***
* Returns a string representation of the [playerSetup]
*/
private fun localizedPlayerSetup(playerSetup: PlayerSetup, positions: LinkedHashSet<Position>, context: Context): String {
val playerItems = mutableListOf(positions.elementAt(playerSetup.position).value)
val isHero = (playerSetup.position == this.heroIndex)
if (isHero) {
val heroString = context.getString(R.string.hero)
playerItems.add("- $heroString")
}
if (playerSetup.cards.isNotEmpty()) {
playerItems.add("[${playerSetup.cards.formatted(context)}]")
}
playerSetup.stack?.let { stack ->
playerItems.add("- $stack")
}
return playerItems.joinToString(" ")
}
/***
* Returns a string representation of the [actionReadRow]
*/
private fun localizedAction(actionReadRow: ActionReadRow, context: Context): String {
val formattedPositions = actionReadRow.positions.joinToString(", ") { it.value }
val actionItems = mutableListOf(formattedPositions)
actionReadRow.action?.let { type ->
actionItems.add(context.getString(type.resId))
}
actionReadRow.amount?.let { amount ->
actionItems.add(amount.formatted)
}
return actionItems.joinToString(" ")
}
/***
* Returns if the hero has won the hand, or part of the pot
*/
val heroWins: Boolean?
get() {
return this.heroIndex?.let { heroIndex ->
this.largestWonPot?.let { pot ->
heroIndex == pot.position
} ?: run { null }
// heroIndex == this.largestWonPot?.position
// this.winnerPots.any { it.position == heroIndex }
} ?: run {
null
}
}
/***
* Creates a list of cards for the hand history to give a representation of the hand
* We will try to add a minimum of 5 cards using by priority:
* - the hero hand
* - the opponents hands
* - the board
*/
fun cardViews(context: Context, viewGroup: ViewGroup): List<View> {
val views = mutableListOf<View>()
val layoutInflater = LayoutInflater.from(context)
// Create all the possible cards list: hero, opponents, board
val cardSets = mutableListOf<List<Card>>()
// Hero
this.heroIndex?.let { hIndex ->
this.playerSetupForPosition(hIndex)?.cards?.let {
cardSets.add(it)
}
}
// Opponents
this.playerSetups.filter { it.cards.isNotEmpty() && it.position != this.heroIndex }.forEach {
cardSets.add(it.cards)
}
// Board
cardSets.add(this.board)
// Try to add cards but not too much
while (views.size < 5 && cardSets.isNotEmpty()) {
val cardSet = cardSets.removeAt(0)
if (views.isNotEmpty() && cardSet.isNotEmpty()) { // add separator with previous set of cards
val view = layoutInflater.inflate(R.layout.view_card_separator, viewGroup, false) as AppCompatTextView
views.add(view)
}
cardSet.forEach { views.add(it.view(context, layoutInflater, viewGroup)) }
}
// Add 5 blank cards if no card has been added
if (views.isEmpty()) {
val blankCard = Card()
(1..5).forEach { _ ->
val view = blankCard.view(context, layoutInflater, viewGroup, true)
view.setBackgroundResource(R.drawable.rounded_kaki_medium_rect)
views.add(view)
}
}
return views
}
data class Pot(var amount: Double, var level: Double, var positions: MutableSet<Int> = mutableSetOf())
/***
* Defines which positions win the hand
*/
fun defineWinnerPositions() {
val folds = this.sortedActions.filter { it.type == Action.Type.FOLD }.map { it.position }
val activePositions = this.positionIndexes.toMutableList()
activePositions.removeAll(folds)
val wonPots = when (activePositions.size) {
0 -> listOf() // no winner, everyone has fold. Should not happen
1 -> { // One player has not fold, typically BET / FOLD
val pot = WonPot()
pot.position = activePositions.first()
pot.amount = potSizeForStreet(Street.SUMMARY)
listOf(pot)
}
else -> { // Several players remains, typically BET/FOLD or CHECKS
this.wonPots(getPots(activePositions))
}
}
this.winnerPots.clear()
this.winnerPots.addAll(wonPots)
Timber.d("Pot won: ${this.winnerPots.size} for positions: ${this.winnerPots.map {it.position}} ")
}
/***
* Returns a list with all the different pots with the appropriate eligible players
*/
fun getPots(eligiblePositions: List<Int>): List<Pot> {
var runningPotAmount = 0.0
val pots = mutableListOf<Pot>()
Street.values().forEach { street ->
val streetActions = this.actions.filter { it.street == street }
val allinPositions = streetActions.filter { it.type?.isAllin == true }.map { it.position }
if (allinPositions.isEmpty()) {
runningPotAmount += streetActions.sumOf { it.effectiveAmount }
} else {
val amountsPerPosition = mutableListOf<PositionAmount>()
// get all committed amounts for the street by player, by allin
this.positionIndexes.map { position ->
val playerActions = streetActions.filter { it.position == position }
val sum = playerActions.sumOf { it.effectiveAmount }
amountsPerPosition.add(PositionAmount(position, sum, allinPositions.contains(position)))
}
amountsPerPosition.sortWith(this) // sort by value, then allin. Allin must be first of equal values sequence
// for each player
val streetPots = mutableListOf<Pot>()
amountsPerPosition.forEach { positionAmount ->
val amount = positionAmount.amount
val position = positionAmount.position
var rest = amount
var lastPotLevel = 0.0
// put invested amount in smaller pots
streetPots.forEach { pot ->
val added = pot.level - lastPotLevel
pot.amount += added
if (eligiblePositions.contains(position)) {
pot.positions.add(position)
}
rest -= added
lastPotLevel = pot.level
}
// Adds remaining chips to the running Pot
runningPotAmount += rest
// If the player is allin, create a new pot for the relevant amount
val isAllin = allinPositions.contains(position)
if (isAllin) {
streetPots.add(Pot(runningPotAmount, amount, mutableSetOf(position)))
runningPotAmount = 0.0
}
}
pots.addAll(streetPots)
}
}
// Create a pot with the remaining chips
if (runningPotAmount > 0.0) {
pots.add(Pot(runningPotAmount, 0.0, eligiblePositions.toMutableSet()))
}
return pots
}
private fun wonPots(pots: List<Pot>): Collection<WonPot> {
val wonPots = hashMapOf<Int, WonPot>()
pots.forEach { pot ->
if (pot.positions.size > 1) { // we only consider contested pots
val winningPositions = compareHands(pot.positions.toList())
// Distributes the pot for each winners
val share = pot.amount / winningPositions.size
winningPositions.forEach { p ->
val wp = wonPots[p]
if (wp == null) {
val wonPot = WonPot()
wonPot.position = p
wonPot.amount = share
wonPots[p] = wonPot
} else {
wp.amount += share
}
}
}
}
return wonPots.values
}
/***
* Compares the hands of the players at the given [activePositions]
* Returns the list of winning hands by position
*/
private fun compareHands(activePositions: List<Int>): List<Int> {
val usedFullCards = this.allFullCards.toMutableList()
val noWildCardBoard = this.board.putSuits(usedFullCards)
val noWildCardHands = activePositions.map {
this.playerSetupForPosition(it)?.cards?.putSuits(usedFullCards)
}
// Evaluate all hands
val results = noWildCardHands.map { cards ->
cards?.let { hand ->
EvaluatorBridge.playerHand(hand, noWildCardBoard)
} ?: run {
Int.MAX_VALUE
}
}
// Evaluate all hands
// val results = activePositions.map {
// this.playerSetupForPosition(it)?.cards?.let { hand ->
// EvaluatorBridge.playerHand(hand, this.board)
// } ?: run {
// Int.MAX_VALUE
// }
// }
// Check who has best score (EvaluatorBridge gives a lowest score for a better hand)
return results.minOrNull()?.let { best ->
val winners = mutableListOf<Int>()
results.forEachIndexed { index, i ->
if (i == best && i != Int.MAX_VALUE) {
winners.add(activePositions[index])
}
}
winners
} ?: run {
listOf<Int>() // type needed for build
}
}
/***
* return a negative integer, zero, or a positive integer as the
* first argument is less than, equal to, or greater than the
* second.
*/
override fun compare(o1: PositionAmount, o2: PositionAmount): Int {
return if (o1.amount == o2.amount) {
if (o1.isAllin) -1 else 1
} else {
(o1.amount - o2.amount).toInt()
}
}
val maxPlayerCards: Int
get() {
var max = 0
this.playerSetups.forEach {
max = max(it.cards.size, max)
}
return max
}
val usesWildcards: Boolean
get() {
val boardHasWildCard = this.cards.any { it.isWildCard }
val playerCardHasWildCard = this.playerSetups.any { it.cards.any { it.isWildCard } }
return boardHasWildCard || playerCardHasWildCard
}
private val allFullCards: List<Card>
get() {
val cards = mutableListOf<Card>()
cards.addAll(this.board)
this.playerSetups.forEach {
cards.addAll(it.cards)
}
return cards.filter { !it.isWildCard }
}
val allSuitWildCards: List<Card>
get() {
val cards = mutableListOf<Card>()
cards.addAll(this.board)
this.playerSetups.forEach {
cards.addAll(it.cards)
}
return cards.filter { it.isSuitWildCard }
}
private val largestWonPot: WonPot?
get() {
return if (this.winnerPots.isNotEmpty()) { // needed, otherwise maxBy crashes
this.winnerPots.maxBy { it.amount }
} else {
null
}
}
}

@ -1,31 +0,0 @@
package net.pokeranalytics.android.model.realm.handhistory
import io.realm.RealmList
import io.realm.RealmObject
import net.pokeranalytics.android.model.realm.Player
import net.pokeranalytics.android.ui.modules.handhistory.model.CardHolder
import net.pokeranalytics.android.ui.view.Localizable
open class PlayerSetup : RealmObject(), CardHolder, Localizable {
/***
* The player
*/
var player: Player? = null
/***
* The position at the table
*/
var position: Int = 0
/***
* The initial stack of the player
*/
var stack: Double? = null
/***
* The cards of the player
*/
override var cards: RealmList<Card> = RealmList()
}

@ -1,17 +0,0 @@
package net.pokeranalytics.android.model.realm.handhistory
import io.realm.RealmObject
open class WonPot: RealmObject() {
/***
* The position of the player
*/
var position: Int = 0
/***
* The amount won
*/
var amount: Double = 0.0
}

@ -1,14 +0,0 @@
package net.pokeranalytics.android.model.realm.rows
import android.content.Context
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.ui.view.RowRepresentable
class BankrollRow(var bankroll: Bankroll) : RowRepresentable {
override fun getDisplayName(context: Context): String {
return this.bankroll.name
}
}

@ -1,57 +0,0 @@
package net.pokeranalytics.android.model.realm.rows
import android.content.Context
import android.text.InputType
import io.realm.RealmList
import io.realm.annotations.Ignore
import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.CustomFieldEntry
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.RowViewType
class CustomFieldRow(var customField: CustomField) : RowRepresentable {
@Ignore
override var viewType: Int = RowViewType.TITLE_VALUE_ARROW.ordinal
override fun localizedTitle(context: Context): String {
return this.customField.name
}
override fun getDisplayName(context: Context): String {
return this.customField.name
}
override val bottomSheetType: BottomSheetType
get() {
return when (this.customField.type) {
CustomField.Type.LIST.uniqueIdentifier -> BottomSheetType.LIST_STATIC
else -> BottomSheetType.NUMERIC_TEXT
}
}
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor> {
return when (this.customField.type) {
CustomField.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
)
)
}
}
}
}

@ -0,0 +1,11 @@
package net.pokeranalytics.android.model.retrofit
import com.google.gson.annotations.SerializedName
/**
* CurrencyCode Converter mapping class
*/
class CurrencyConverterValue {
@SerializedName("val")
var value: Double? = 0.0
}

@ -1,42 +0,0 @@
package net.pokeranalytics.android.model.utils
import io.realm.Realm
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.TransactionType
import java.util.*
class DataUtils {
companion object {
/**
* Returns the number of sessions corresponding to the provided parameters
*/
fun sessionCount(realm: Realm, startDate: Date, endDate: Date?, net: Double): Int {
var sessionQuery = realm.where(Session::class.java)
.equalTo("startDate", startDate)
.equalTo("result.net", net)
endDate?.let {
sessionQuery = sessionQuery.equalTo("endDate", it)
}
return sessionQuery.findAll().size
}
/**
* Returns true if the provided parameters doesn't correspond to an existing transaction
*/
fun transactionUnicityCheck(realm: Realm, date: Date, amount: Double, type: TransactionType): Boolean {
val transactions = realm.where(Transaction::class.java)
.equalTo("date", date)
.equalTo("amount", amount)
.equalTo("type.id", type.id)
.findAll()
return transactions.isEmpty()
}
}
}

@ -5,7 +5,7 @@ import io.realm.Realm
import io.realm.Sort import io.realm.Sort
import net.pokeranalytics.android.model.realm.Location import net.pokeranalytics.android.model.realm.Location
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow import net.pokeranalytics.android.ui.view.rowrepresentable.SessionRow
/** /**
* Returns all significant parameters concatenated in a String * Returns all significant parameters concatenated in a String
@ -15,7 +15,7 @@ private fun Session.parameterRepresentation(context: Context): String {
var representation = "" var representation = ""
this.significantFields().forEach { this.significantFields().forEach {
representation += this.charSequenceForRow(it, context) representation += this.stringForRow(it, context)
} }
return representation return representation
@ -24,24 +24,24 @@ private fun Session.parameterRepresentation(context: Context): String {
/** /**
* Returns a list of fields used to determine which kind of session is favorite * Returns a list of fields used to determine which kind of session is favorite
*/ */
private fun Session.significantFields(): List<SessionPropertiesRow> { private fun Session.significantFields(): List<SessionRow> {
when (this.type) { when (this.type) {
Session.Type.TOURNAMENT.ordinal -> { Session.Type.TOURNAMENT.ordinal -> {
return listOf( return listOf(
SessionPropertiesRow.GAME, SessionRow.GAME,
SessionPropertiesRow.INITIAL_BUY_IN, SessionRow.INITIAL_BUY_IN,
SessionPropertiesRow.BANKROLL, SessionRow.BANKROLL,
SessionPropertiesRow.TABLE_SIZE, SessionRow.TABLE_SIZE,
SessionPropertiesRow.TOURNAMENT_NAME, SessionRow.TOURNAMENT_NAME,
SessionPropertiesRow.TOURNAMENT_TYPE SessionRow.TOURNAMENT_TYPE
) )
} }
Session.Type.CASH_GAME.ordinal -> { Session.Type.CASH_GAME.ordinal -> {
return listOf( return listOf(
SessionPropertiesRow.GAME, SessionRow.GAME,
SessionPropertiesRow.STAKES, SessionRow.BLINDS,
SessionPropertiesRow.BANKROLL, SessionRow.BANKROLL,
SessionPropertiesRow.TABLE_SIZE SessionRow.TABLE_SIZE
) )
} }
} }
@ -71,26 +71,27 @@ class FavoriteSessionFinder {
private const val FAVORITE_SIGNIFICANT_SESSIONS = 15L private const val FAVORITE_SIGNIFICANT_SESSIONS = 15L
/** /**
* Copies the favorite session parameters on the [session] * Copies the favorite session parameters on the [newSession]
*/ */
fun copyParametersFromFavoriteSession(session: Session, location: Location?, context: Context) { fun copyParametersFromFavoriteSession(newSession: Session, location: Location?, context: Context) {
val favoriteSession = favoriteSession(session.type, location, session.realm, context) val favoriteSession =
favoriteSession(newSession.type, location, newSession.realm, context)
favoriteSession?.let { fav -> favoriteSession?.let { fav ->
session.limit = fav.limit newSession.limit = fav.limit
session.game = fav.game newSession.game = fav.game
session.bankroll = fav.bankroll newSession.bankroll = fav.bankroll
session.tableSize = fav.tableSize newSession.tableSize = fav.tableSize
when (session.type) { when (newSession.type) {
Session.Type.CASH_GAME.ordinal -> { Session.Type.CASH_GAME.ordinal -> {
session.cgAnte = fav.cgAnte newSession.cgSmallBlind = fav.cgSmallBlind
session.cgBlinds = fav.cgBlinds newSession.cgBigBlind = fav.cgBigBlind
} }
Session.Type.TOURNAMENT.ordinal -> { Session.Type.TOURNAMENT.ordinal -> {
session.tournamentEntryFee = fav.tournamentEntryFee newSession.tournamentEntryFee = fav.tournamentEntryFee
} }
} }
} }

@ -12,33 +12,11 @@ import java.util.*
class Seed(var context:Context) : Realm.Transaction { class Seed(var context:Context) : Realm.Transaction {
companion object {
fun createDefaultTransactionTypes(values: Array<TransactionType.Value>, context: Context, realm: Realm) {
values.forEach { value ->
if (value != TransactionType.Value.EXPENSE) {
val existing = realm.where(TransactionType::class.java).equalTo("kind", value.uniqueIdentifier).findAll()
if (existing.isEmpty()) {
val type = TransactionType()
type.name = value.localizedTitle(context)
type.additive = value.additive
type.kind = value.uniqueIdentifier
type.lock = true
realm.insertOrUpdate(type)
}
}
}
}
}
override fun execute(realm: Realm) { override fun execute(realm: Realm) {
this.createDefaultGames(realm) this.createDefaultGames(realm)
this.createDefaultTournamentFeatures(realm) this.createDefaultTournamentFeatures(realm)
this.createDefaultCurrencyAndBankroll(realm) this.createDefaultCurrencyAndBankroll(realm)
createDefaultTransactionTypes(TransactionType.Value.values(), context, realm) this.createDefaultTransactionTypes(realm)
} }
private fun createDefaultTournamentFeatures(realm: Realm) { private fun createDefaultTournamentFeatures(realm: Realm) {
@ -78,4 +56,15 @@ class Seed(var context:Context) : Realm.Transaction {
} }
} }
private fun createDefaultTransactionTypes(realm: Realm) {
TransactionType.Value.values().forEachIndexed { index, value ->
val type = TransactionType()
type.name = value.localizedTitle(context)
type.additive = value.additive
type.kind = index
type.lock = true
realm.insertOrUpdate(type)
}
}
} }

@ -3,7 +3,6 @@ package net.pokeranalytics.android.model.utils
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.ModelException import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.SessionSet import net.pokeranalytics.android.model.realm.SessionSet
import kotlin.math.max import kotlin.math.max
@ -24,7 +23,7 @@ class SessionSetManager {
fun updateTimeline(session: Session) { fun updateTimeline(session: Session) {
if (!session.realm.isInTransaction) { if (!session.realm.isInTransaction) {
throw PAIllegalStateException("realm should be in transaction at this point") throw IllegalStateException("realm should be in transaction at this point")
} }
if (session.startDate == null) { if (session.startDate == null) {
@ -33,18 +32,11 @@ class SessionSetManager {
if (session.endDate == null) { if (session.endDate == null) {
throw ModelException("End date should never be null here") throw ModelException("End date should never be null here")
} }
val sessionSets = this.matchingSets(session)
cleanupSessionSets(session, sessionSets)
}
private fun matchingSets(session: Session) : RealmResults<SessionSet> {
val realm = session.realm
val endDate = session.endDate!! // tested above val endDate = session.endDate!! // tested above
val startDate = session.startDate!! val startDate = session.startDate!!
val realm = session.realm
val query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java) val query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java)
query query
@ -57,36 +49,12 @@ class SessionSetManager {
.greaterThanOrEqualTo("startDate", startDate) .greaterThanOrEqualTo("startDate", startDate)
.lessThanOrEqualTo("endDate", endDate) .lessThanOrEqualTo("endDate", endDate)
return query.findAll() val sessionGroups = query.findAll()
}
/**
* Multiple session sets update:
* Merges or splits session sets
* Does that by deleting then recreating
*/
private fun cleanupSessionSets(session: Session, sessionSets: RealmResults<SessionSet>) {
// get all endedSessions from sets
val allImpactedSessions = mutableSetOf<Session>()
sessionSets.forEach { set ->
set.sessions?.asIterable()?.let { allImpactedSessions.addAll(it) }
}
allImpactedSessions.add(session)
// delete all sets
sessionSets.deleteAllFromRealm()
allImpactedSessions.forEach { impactedSession ->
val sets = matchingSets(impactedSession)
this.updateTimeFrames(sets, impactedSession)
}
// Timber.d("netDuration 3 = : ${set.timeFrame?.netDuration}") this.updateTimeFrames(sessionGroups, session)
} }
/** /**
* Update the global timeline using the impacted [sessionSets] and the updated [session] * Update the global timeline using the impacted [sessionSets] and the updated [session]
*/ */
@ -186,7 +154,7 @@ class SessionSetManager {
fun removeFromTimeline(session: Session) { fun removeFromTimeline(session: Session) {
if (!session.realm.isInTransaction) { if (!session.realm.isInTransaction) {
throw PAIllegalStateException("realm should be in transaction at this point") throw IllegalStateException("realm should be in transaction at this point")
} }
val sessionSet = session.sessionSet val sessionSet = session.sessionSet

@ -0,0 +1,21 @@
package net.pokeranalytics.android.model.utils
import io.realm.Realm
import net.pokeranalytics.android.model.realm.Session
import java.util.*
class SessionUtils {
companion object {
/**
* Returns true if the provided parameters doesn't correspond to an existing session
*/
fun unicityCheck(realm: Realm, startDate: Date, endDate: Date, net: Double) : Boolean {
val sessions = realm.where(Session::class.java).equalTo("startDate", startDate).equalTo("endDate", endDate).equalTo("result.net", net).findAll()
return sessions.isEmpty()
}
}
}

@ -0,0 +1,90 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import io.realm.RealmResults
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
class BankrollActivity : PokerAnalyticsActivity() {
private lateinit var computableResults: RealmResults<ComputableResult>
private lateinit var bankrolls: RealmResults<Bankroll>
private lateinit var transactions: RealmResults<Transaction>
companion object {
fun newInstance(context: Context) {
val intent = Intent(context, BankrollActivity::class.java)
context.startActivity(intent)
}
/**
* Create a new instance for result
*/
fun newInstanceForResult(fragment: Fragment, requestCode: Int) {
val intent = Intent(fragment.requireContext(), BankrollActivity::class.java)
fragment.startActivityForResult(intent, requestCode)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_bankroll)
// this.computableResults = getRealm().where(ComputableResult::class.java).findAll() // ComputableResult are existing only if sessions are ended
// this.computableResults.addChangeListener { t, changeSet ->
//
// val bankrolls = mutableSetOf<Bankroll>()
// val indexes = mutableSetOf<Int>()
// indexes.addAll(changeSet.changes.toList())
// indexes.addAll(changeSet.insertions.toList())
// indexes.addAll(changeSet.deletions.toList())
// indexes.forEach { index ->
// t[index]?.session?.bankroll?.let { br ->
// bankrolls.add(br)
// }
// }
// this.computeBankrollReports(bankrolls)
// }
// this.bankrolls = getRealm().where(Bankroll::class.java).findAll() // ComputableResult are existing only if sessions are ended
// this.bankrolls.addChangeListener { _, changeSet ->
//
//
//
//
//
// }
// this.transactions = getRealm().where(Transaction::class.java).findAll() // ComputableResult are existing only if sessions are ended
// this.transactions.addChangeListener { t, changeSet ->
//
// val bankrolls = mutableSetOf<Bankroll>()
// val indexes = mutableSetOf<Int>()
// indexes.addAll(changeSet.changes.toList())
// indexes.addAll(changeSet.insertions.toList())
// indexes.addAll(changeSet.deletions.toList())
// indexes.forEach { index ->
// if (t.isNotEmpty()) {
// t[index]?.bankroll?.let { br ->
// bankrolls.add(br)
// }
// }
// }
// this.computeBankrollReports(bankrolls)
// }
}
fun computeBankrollReports(bankrolls: Collection<Bankroll>) {
}
}

@ -1,13 +1,14 @@
package net.pokeranalytics.android.ui.modules.bankroll package net.pokeranalytics.android.ui.activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.bankroll.BankrollReport import net.pokeranalytics.android.calculus.bankroll.BankrollReport
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.BankrollDetailsFragment
class BankrollDetailsActivity : BaseActivity() { class BankrollDetailsActivity : PokerAnalyticsActivity() {
companion object { companion object {
@ -17,7 +18,7 @@ class BankrollDetailsActivity : BaseActivity() {
* Default constructor * Default constructor
*/ */
fun newInstanceForResult(fragment: Fragment, bankrollReport: BankrollReport, requestCode: Int) { fun newInstanceForResult(fragment: Fragment, bankrollReport: BankrollReport, requestCode: Int) {
Companion.bankrollReport = bankrollReport this.bankrollReport = bankrollReport
val intent = Intent(fragment.requireContext(), BankrollDetailsActivity::class.java) val intent = Intent(fragment.requireContext(), BankrollDetailsActivity::class.java)
fragment.startActivityForResult(intent, requestCode) fragment.startActivityForResult(intent, requestCode)
} }

@ -1,49 +1,28 @@
package net.pokeranalytics.android.ui.activity package net.pokeranalytics.android.ui.activity
import android.app.Activity import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.BaseActivity import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.ui.fragment.SubscriptionFragment
class BillingActivity : BaseActivity() { class BillingActivity : PokerAnalyticsActivity() {
private enum class IntentKey(val keyName: String) {
SHOW_MESSAGE("showMessage"),
}
companion object { companion object {
fun newInstance(context: Context) {
fun newInstanceForResult(activity: Activity, showSessionMessage: Boolean) { val intent = Intent(context, BillingActivity::class.java)
val intent = Intent(activity, BillingActivity::class.java) context.startActivity(intent)
intent.putExtra(IntentKey.SHOW_MESSAGE.keyName, showSessionMessage)
activity.startActivityForResult(intent, RequestCode.SUBSCRIPTION.value)
}
fun newInstanceForResult(fragment: Fragment, showSessionMessage: Boolean) {
val intent = Intent(fragment.requireContext(), BillingActivity::class.java)
intent.putExtra(IntentKey.SHOW_MESSAGE.keyName, showSessionMessage)
fragment.startActivityForResult(intent, RequestCode.SUBSCRIPTION.value)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_billing) setContentView(R.layout.activity_billing)
initUI()
} }
private fun initUI() { override fun onResume() {
super.onResume()
val showSessionMessage = intent.getBooleanExtra(IntentKey.SHOW_MESSAGE.keyName, false)
val fragment = SubscriptionFragment.newInstance(showSessionMessage)
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.container, fragment)
fragmentTransaction.commit()
} }

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

Loading…
Cancel
Save