From 7598c9391e16d41891f6e4b00b61bbfa7b2d64ab Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 21 May 2019 17:59:04 +0200 Subject: [PATCH] Successfully imports basic CSVs --- .../android/PokerAnalyticsApplication.kt | 3 +- .../net/pokeranalytics/android/model/Limit.kt | 16 +++ .../android/model/utils/SessionSetManager.kt | 2 +- .../android/ui/activity/HomeActivity.kt | 24 +++- .../fragment/data/CustomFieldDataFragment.kt | 2 +- .../android/util/csv/CSVDescriptor.kt | 18 +-- .../android/util/csv/CSVImporter.kt | 71 +++++++---- .../pokeranalytics/android/util/csv/Field.kt | 49 -------- .../android/util/csv/SessionCSVDescriptor.kt | 115 ++++++++++++++---- .../android/util/csv/TypedField.kt | 68 +++++++++++ .../util/extensions/RealmExtensions.kt | 16 +++ 11 files changed, 278 insertions(+), 106 deletions(-) delete mode 100644 app/src/main/java/net/pokeranalytics/android/util/csv/Field.kt create mode 100644 app/src/main/java/net/pokeranalytics/android/util/csv/TypedField.kt diff --git a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt index d9c7f7fa..2f478421 100644 --- a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt +++ b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt @@ -55,9 +55,10 @@ class PokerAnalyticsApplication : Application() { if (BuildConfig.DEBUG) { Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}") - this.createFakeSessions() +// this.createFakeSessions() } + Patcher.patchBreaks() } diff --git a/app/src/main/java/net/pokeranalytics/android/model/Limit.kt b/app/src/main/java/net/pokeranalytics/android/model/Limit.kt index 86515b79..4eb28451 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/Limit.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/Limit.kt @@ -10,6 +10,21 @@ enum class Limit : RowRepresentable { SPREAD, MIXED; + companion object { + + fun getInstance(value: String) : Limit? { + return when (value) { + "No Limit" -> NO + "Pot Limit" -> POT + "Fixed Limit" -> FIXED + "Mixed Limit" -> MIXED + "Spread Limit" -> SPREAD + else -> null + } + } + + } + val shortName: String get() { return when (this) { @@ -36,4 +51,5 @@ enum class Limit : RowRepresentable { override fun getDisplayName(context: Context): String { return this.longName } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt b/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt index c62b84c3..95fea7f9 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt @@ -169,7 +169,7 @@ class SessionSetManager { sessionSet.deleteFromRealm() sessions.forEach { - SessionSetManager.updateTimeline(it) + updateTimeline(it) } } } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt index 03cddf08..b67c534e 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt @@ -1,11 +1,12 @@ package net.pokeranalytics.android.ui.activity +import android.Manifest import android.app.KeyguardManager import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle -import android.view.Menu +import androidx.core.app.ActivityCompat import com.google.android.material.bottomnavigation.BottomNavigationView import io.realm.RealmResults import kotlinx.android.synthetic.main.activity_home.* @@ -15,6 +16,7 @@ import net.pokeranalytics.android.model.realm.Currency import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity import net.pokeranalytics.android.ui.adapter.HomePagerAdapter import net.pokeranalytics.android.util.billing.AppGuard +import net.pokeranalytics.android.util.csv.CSVImporter class HomeActivity : PokerAnalyticsActivity() { @@ -72,6 +74,26 @@ class HomeActivity : PokerAnalyticsActivity() { observeRealmObjects() initUI() checkFirstLaunch() +// csv() + + } + + fun csv() { + + val path = "sdcard/Download/AllCashGames.csv" + val csv = CSVImporter(path) + csv.start() + + ActivityCompat.requestPermissions( + this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSION_REQUEST_ACCESS_FINE_LOCATION + ) + + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + } private fun observeRealmObjects() { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt index abdaee0a..cae1fe35 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt @@ -34,7 +34,7 @@ import kotlin.collections.ArrayList */ class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource { - // Return the item as a Custom Field object + // Return the item as a Custom TypedField object private val customField: CustomField get() { return this.item as CustomField diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt index 27207e37..81eb3d6b 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt @@ -5,7 +5,7 @@ import io.realm.RealmModel import org.apache.commons.csv.CSVRecord -abstract class DataCSVDescriptor(vararg elements: Field<*>) : CSVDescriptor(*elements) { +abstract class DataCSVDescriptor(vararg elements: Field) : CSVDescriptor(*elements) { val realmModels = mutableListOf() @@ -22,9 +22,10 @@ abstract class DataCSVDescriptor(vararg elements: Field<*>) : CS } -open class CSVDescriptor(vararg elements: Field<*>) { +open class CSVDescriptor(vararg elements: Field) { - protected var fields: List> = listOf() + protected var fields: List = listOf() + protected var fieldMapping: MutableMap = mutableMapOf() init { if (elements.size > 0) { @@ -36,17 +37,20 @@ open class CSVDescriptor(vararg elements: Field<*>) { val all: List = listOf(SessionCSVDescriptor.pokerIncomeCash) } - fun matches(headerMap: Map) : Boolean { + fun matches(record: CSVRecord) : Boolean { var count = 0 - val headers = headerMap.keys + val headers = record.toSet() - this.fields.forEach { + this.fields.forEach { field -> + + val index = headers.indexOf(field.header) + this.fieldMapping[field] = index - val index = headers.indexOf(it.header) if (index >= 0) { count++ } + } return count == this.fields.size diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt index 851cfdcf..0f46c5ef 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt @@ -2,51 +2,76 @@ package net.pokeranalytics.android.util.csv import io.realm.Realm import org.apache.commons.csv.CSVFormat -import org.apache.commons.csv.CSVParser +import org.apache.commons.csv.CSVRecord import timber.log.Timber import java.io.FileReader open class CSVImporter(var path: String) { - private lateinit var descriptor: CSVDescriptor + private var usedDescriptors: MutableList = mutableListOf() + private var currentDescriptor: CSVDescriptor? = null - private fun start(realm: Realm) { + fun start() { + + val realm = Realm.getDefaultInstance() val reader = FileReader(this.path) - val parser = CSVFormat.RFC4180.withFirstRecordAsHeader().parse(reader) + val parser = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(reader) - val descriptor = this.findDescriptor(parser.headerMap) + Timber.d("Starting import...") - if (descriptor != null) { - this.descriptor = descriptor - this.parse(realm, descriptor, parser) - } else { - Timber.d("No CSV descriptor found") - } + realm.executeTransaction { - } + parser.forEachIndexed { index, record -> + + Timber.d("line $index") - private fun findDescriptor(header: Map) : CSVDescriptor? { - CSVDescriptor.all.forEach { - if (it.matches(header)) { - return it + if (currentDescriptor == null) { // find descriptor + this.findDescriptor(record) + } else { + + currentDescriptor?.let { + if (record.size() == 0) { + this.usedDescriptors.add(it) + this.currentDescriptor = null // reset descriptor when encountering an empty line (multiple descriptors can be found in a single file) + } else { + it.parse(realm, record) + } + } ?: run { + throw IllegalStateException("CSVDescriptor should never be null here") + } + + } } } - return null + + Timber.d("Ending import...") + +// this.save(realm) + + realm.close() } - protected open fun parse(realm: Realm, descriptor: CSVDescriptor, parser : CSVParser) { - parser.records.forEach { record -> - descriptor.parse(realm, record) + private fun findDescriptor(record: CSVRecord) { + + CSVDescriptor.all.forEach { descriptor -> + if (descriptor.matches(record)) { + this.currentDescriptor = descriptor + return + } } + } fun save(realm: Realm) { - if (descriptor is DataCSVDescriptor<*>) { - realm.executeTransaction { - realm.copyToRealm((descriptor as DataCSVDescriptor<*>).realmModels) + this.usedDescriptors.forEach { descriptor -> + + if (descriptor is DataCSVDescriptor<*>) { + realm.executeTransaction { + realm.copyToRealm(descriptor.realmModels) + } } } diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/Field.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/Field.kt deleted file mode 100644 index 71f825e2..00000000 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/Field.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.pokeranalytics.android.util.csv - -import java.text.DateFormat -import java.text.NumberFormat -import java.util.* - - -interface AmountField: NumberField { - - override fun parse(value: String) : Double? { - val formatter = NumberFormat.getCurrencyInstance() - return formatter.parse(value).toDouble() - } - -} - -interface NumberField: Field { - val numberFormat: String? - - override fun parse(value: String) : Double? { - val formatter = NumberFormat.getInstance() - return formatter.parse(value).toDouble() - } -} - -interface DateField : Field { - val dateFormat: String? - - override fun parse(value: String) : Date? { - val formatter = DateFormat.getDateInstance() - return formatter.parse(value) - } - -} - -interface BlindField : Field { - - override fun parse(value: String) : Double? { - return null - } - -} - -interface Field { - val header: String - var callback: (() -> Unit)? - - fun parse(value: String) : T? -} diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt index 603c9b1e..d6efa567 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt @@ -1,55 +1,124 @@ package net.pokeranalytics.android.util.csv import io.realm.Realm +import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.utils.SessionUtils +import net.pokeranalytics.android.util.extensions.getOrCreate import org.apache.commons.csv.CSVRecord +import timber.log.Timber sealed class SessionField { - data class Start(override var header: String, override var callback: (() -> Unit)? = null, override val dateFormat: String? = null) : DateField - data class End(override var header: String, override var callback: (() -> Unit)? = null, override val dateFormat: String? = null) : DateField - - data class Buyin(override var header: String, override var callback: (() -> Unit)? = null, override val numberFormat: String? = null) : AmountField - data class CashedOut(override var header: String, override var callback: (() -> Unit)? = null, override val numberFormat: String? = null) : AmountField + data class Start( + override var header: String, + override var callback: (() -> Unit)? = null, + override val dateFormat: String? = null + ) : DateField + + data class End( + override var header: String, + override var callback: (() -> Unit)? = null, + override val dateFormat: String? = null + ) : DateField + + data class Buyin( + override var header: String, + override var callback: (() -> Unit)? = null, + override val numberFormat: String? = null + ) : NumberField + + data class CashedOut( + override var header: String, + override var callback: (() -> Unit)? = null, + override val numberFormat: String? = null + ) : NumberField + + data class Break( + override var header: String, + override var callback: (() -> Unit)? = null, + override val numberFormat: String? = null + ) : NumberField + + data class Tips( + override var header: String, + override var callback: (() -> Unit)? = null, + override val numberFormat: String? = null + ) : NumberField data class Blind(override var header: String, override var callback: (() -> Unit)? = null) : BlindField + data class Game(override var header: String, override var callback: (() -> Unit)? = null) : Field + data class Location(override var header: String, override var callback: (() -> Unit)? = null) : Field + data class Bankroll(override var header: String, override var callback: (() -> Unit)? = null) : Field + data class LimitType(override var header: String, override var callback: (() -> Unit)? = null) : Field + data class Comment(override var header: String, override var callback: (() -> Unit)? = null) : Field } -class SessionCSVDescriptor(vararg elements: Field<*>) : DataCSVDescriptor(*elements) { +class SessionCSVDescriptor(var isTournament: Boolean, vararg elements: Field) : DataCSVDescriptor(*elements) { companion object { - val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor(SessionField.Start("Start Time"), + val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor( + false, + SessionField.Start("Start Time"), SessionField.End("End Time"), SessionField.Buyin("Buy In"), - SessionField.CashedOut("Cashed Out")) + SessionField.CashedOut("Cashed Out"), + SessionField.Break("Break Minutes"), + SessionField.LimitType("Limit Type"), + SessionField.Game("Game"), + SessionField.Bankroll("Bankroll"), + SessionField.Location("Location"), + SessionField.Comment("Note"), + SessionField.Tips("Tips"), + SessionField.Blind("Stake") + ) } override fun parseData(realm: Realm, record: CSVRecord): Session? { - val session = Session() + val session = Session.newInstance(realm, this.isTournament) fields.forEach { - val value = record.get(it.header) - when (it) { - is SessionField.Start -> { - session.startDate = it.parse(value) - } - is SessionField.End -> { - session.endDate = it.parse(value) + this.fieldMapping[it]?.let { index -> + + val value = record.get(index) + when (it) { + is SessionField.Start -> { + session.startDate = it.parse(value) + } + is SessionField.End -> { + session.endDate = it.parse(value) + } + is SessionField.Buyin -> session.result?.buyin = it.parse(value) + is SessionField.CashedOut -> session.result?.cashout = it.parse(value) + is SessionField.Tips -> session.result?.tips = it.parse(value) + is SessionField.Break -> { + it.parse(value)?.let { + session.breakDuration = it.toLong() + } + } + is SessionField.Game -> session.game = realm.getOrCreate(value) + is SessionField.Location -> session.location = realm.getOrCreate(value) + is SessionField.Bankroll -> session.bankroll = realm.getOrCreate(value) + is SessionField.LimitType -> session.limit = Limit.getInstance(value)?.ordinal + is SessionField.Comment -> session.comment = value + is SessionField.Blind -> { // 1/2 + val strBlinds = value.split("/") + if (strBlinds.size > 1) { + session.cgBigBlind = strBlinds.last().toDouble() + session.cgSmallBlind = strBlinds[strBlinds.size - 2].toDouble() + } else { + Timber.d("Blinds could not be parsed: $value") + } + } + else -> { + } } - is SessionField.Buyin -> session.result?.buyin = it.parse(value) - is SessionField.CashedOut -> session.result?.cashout = it.parse(value) - is SessionField.Blind -> { -// session.cgSmallBlind = - } - else -> {} } - } val startDate = session.startDate diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/TypedField.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/TypedField.kt new file mode 100644 index 00000000..fd0b659a --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/TypedField.kt @@ -0,0 +1,68 @@ +package net.pokeranalytics.android.util.csv + +import io.realm.Realm +import net.pokeranalytics.android.model.interfaces.NameManageable +import net.pokeranalytics.android.util.extensions.getOrCreate +import timber.log.Timber +import java.text.DateFormat +import java.text.NumberFormat +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + + +interface NumberField: TypedField { + val numberFormat: String? + + override fun parse(value: String) : Double? { + val formatter = NumberFormat.getInstance() + + return try { + formatter.parse(value).toDouble() + } catch (e: ParseException) { + Timber.d("Unparseable number: $value") + null + } + } +} + +interface DateField : TypedField { + val dateFormat: String? + + override fun parse(value: String) : Date? { + + val formatter = if (dateFormat != null) SimpleDateFormat(dateFormat) else SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) + return try { + formatter.parse(value) + } catch (e: ParseException) { + Timber.d("Unparseable date: $value") + null + } + } + +} + +interface BlindField : TypedField { + + override fun parse(value: String) : Double? { + return null + } + +} + +interface TypedField : Field { + fun parse(value: String) : T? +} + +interface NamedDataField : Field { + + fun getOrCreate(realm: Realm, clazz: Class, name: String) : T { + return realm.getOrCreate(clazz, name) + } + +} + +interface Field { + val header: String + var callback: (() -> Unit)? +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt b/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt index 50d158ef..b2f60006 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt @@ -6,10 +6,26 @@ import io.realm.RealmResults import io.realm.Sort import io.realm.kotlin.where import net.pokeranalytics.android.model.interfaces.CountableUsage +import net.pokeranalytics.android.model.interfaces.NameManageable import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.TournamentFeature import net.pokeranalytics.android.model.realm.Transaction +fun Realm.getOrCreate(clazz: Class, name: String) : T { + val instance = this.where(clazz).equalTo("name", name).findFirst() + return if (instance != null) { + instance + } else { + val newInstance = clazz.newInstance() + newInstance.name = name + this.copyToRealm(newInstance) + } +} + +inline fun Realm.getOrCreate(name: String) : T { + return this.getOrCreate(T::class.java, name) +} + /** * Returns all entities of the [clazz] sorted with their default sorting */