From 72d2dbf766014cb979e1f32ca1ed76d2a438c0c0 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 19 May 2020 17:47:50 +0200 Subject: [PATCH] Custom field management in CSV import/export --- .../android/model/TournamentType.kt | 12 ++-- .../android/model/realm/CustomField.kt | 28 +++++++- .../android/util/csv/CSVDescriptor.kt | 34 +++++++-- .../android/util/csv/CSVField.kt | 12 +++- .../android/util/csv/CSVImporter.kt | 1 + .../android/util/csv/PACSVDescriptor.kt | 18 ++++- .../android/util/csv/ProductCSVDescriptors.kt | 4 +- .../android/util/csv/SessionCSVDescriptor.kt | 70 ++++++++++++++++++- .../android/util/csv/SessionField.kt | 66 ++++++++--------- .../csv/SessionTransactionCSVDescriptor.kt | 4 +- .../util/csv/TransactionCSVDescriptor.kt | 2 +- 11 files changed, 191 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt b/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt index c17b647a..ac7deb47 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt @@ -5,20 +5,20 @@ import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowViewType -enum class TournamentType : RowRepresentable { - MTT, - SNG; +enum class TournamentType(val label: String) : RowRepresentable { + MTT("MTT"), + SNG("SNG"); companion object { val all : List get() { - return TournamentType.values() as List + return values() as List } fun getValueForLabel(label: String) : TournamentType? { return when (label) { - "Single-Table" -> SNG - "Multi-Table" -> MTT + SNG.label, "Single-Table" -> SNG + MTT.label, "Multi-Table" -> MTT else -> null } } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt index 98266561..aa187757 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt @@ -29,6 +29,22 @@ import kotlin.collections.ArrayList 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 + customField + } + } + + } + @Ignore override val realmObjectClass: Class = CustomField::class.java @@ -283,6 +299,16 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa this.entriesToDelete.clear() } + fun getOrCreateEntry(value: String): CustomFieldEntry { + this.entries.firstOrNull { it.value == value }?.let { + return it + } ?: run { + val entry = this.addEntry() + entry.value = value + return entry + } + } + /** * Clean the entries if the type is not a list & remove the deleted entries from realm */ @@ -309,7 +335,7 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa val criteria: Criteria get() { return when (this.type) { - CustomField.Type.LIST.uniqueIdentifier -> Criteria.ListCustomFields(this.id) + Type.LIST.uniqueIdentifier -> Criteria.ListCustomFields(this.id) else -> Criteria.ValueCustomFields(this.id) } } 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 6c23ec7d..5379d292 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 @@ -75,14 +75,20 @@ abstract class DataCSVDescriptor(source: DataSource, vararg el this.realmModelIds.clear() } + open fun prepareCSVExport() { + + } + fun toCSV(dataSequence: List): String { + prepareCSVExport() + val lines = mutableListOf() lines.add(this.csvHeaders) dataSequence.forEach { data -> val line = mutableListOf() - this.fields.forEach { field -> + this.staticFields.forEach { field -> line.add(this.toCSV(data, field) ?: "") } lines.add(line.joinToString(",")) @@ -106,7 +112,13 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField) /** * The CSVField list describing the CSV header format */ - protected var fields: List = listOf() + protected var staticFields: MutableList = mutableListOf() + + /** + * A list of dynamic CSVField, described in the CSV header + */ +// protected var dynamicFields: MutableList = mutableListOf() + /** * The mapping of CSVField with their index in the CSV file */ @@ -114,7 +126,7 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField) init { if (elements.isNotEmpty()) { - this.fields = elements.toList() + this.staticFields = elements.toMutableList() } } @@ -139,7 +151,7 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField) var count = 0 val headers = record.toSet() - this.fields.forEach { field -> + this.staticFields.forEach { field -> val index = headers.indexOf(field.header) if (index >= 0) { @@ -147,15 +159,23 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField) count++ } } - val mandatoryFields = this.fields.filter { !it.optional } - Timber.d("source= ${this.source.name} > total fields = ${this.fields.size}, identified = $count") + val mandatoryFields = this.staticFields.filter { !it.optional } + Timber.d("source= ${this.source.name} > total fields = ${this.staticFields.size}, identified = $count") return count >= mandatoryFields.size } + /*** + * Method called when the descriptor has matched a record and has been identified + * as able to parse the file + */ + open fun hasMatched(realm: Realm, record: CSVRecord) { + + } + protected val csvHeaders: String get() { val headers = mutableListOf() - this.fields.forEach { + this.staticFields.forEach { headers.add(it.header) } return headers.joinToString(",") diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt index 3cedd81a..a9dadace 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt @@ -1,5 +1,6 @@ package net.pokeranalytics.android.util.csv +import net.pokeranalytics.android.model.realm.CustomField import net.pokeranalytics.android.model.realm.TournamentFeature import timber.log.Timber import java.text.DateFormat @@ -13,8 +14,6 @@ import java.util.* */ interface NumberCSVField: TypedCSVField { - val numberFormat: String? - companion object { fun defaultParse(value: String) : Double? { @@ -170,6 +169,14 @@ interface BooleanCSVField : TypedCSVField { } } +interface CustomFieldCSVField : CSVField { + var customField: CustomField + + companion object { + val separator: String = "::CF" + } +} + interface TypedCSVField : CSVField { fun parse(value: String) : T? fun format(data: T?): String? @@ -179,7 +186,6 @@ interface TypedCSVField : CSVField { interface CSVField { companion object { - const val delimiter = "\"" const val separator = "|" } 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 05b37957..4f631b0d 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 @@ -112,6 +112,7 @@ open class CSVImporter(istream: InputStream) { if (this.currentDescriptor == null) { // find descriptor this.currentDescriptor = this.findDescriptor(record) + this.currentDescriptor?.hasMatched(realm, record) if (this.currentDescriptor == null) { diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt index d68f8da7..2986e96e 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt @@ -14,7 +14,10 @@ import org.apache.commons.csv.CSVRecord import timber.log.Timber import java.util.* -abstract class PACSVDescriptor(source: DataSource, private var isTournament: Boolean?, vararg elements: CSVField) : DataCSVDescriptor(source, *elements) { +abstract class PACSVDescriptor(source: DataSource, + private var isTournament: Boolean?, + vararg elements: CSVField) + : DataCSVDescriptor(source, *elements) { private var sameDaySessionCount: Int = 0 private var currentDay: String = "" @@ -40,7 +43,7 @@ abstract class PACSVDescriptor(source: DataSource, private var var stackingIn: Double? = null var stackingOut: Double? = null - this.fields.forEach { field -> + this.staticFields.forEach { field -> this.fieldMapping[field]?.let { index -> @@ -196,6 +199,17 @@ abstract class PACSVDescriptor(source: DataSource, private var is SessionField.StackingOut -> { stackingOut = field.parse(value) } + is SessionField.ListCustomField -> { + val entry = field.customField.getOrCreateEntry(value) + session.customFieldEntries.add(entry) + } + is SessionField.NumberCustomField -> { + val customField = field.customField + val entry = CustomFieldEntry() + entry.numericValue = field.parse(value) + customField.entries.add(entry) + session.customFieldEntries.add(entry) + } else -> { } } diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt index 7b36c4a0..3a137468 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt @@ -212,7 +212,7 @@ class ProductCSVDescriptors { true, SessionField.Start("Start Date", dateFormat = "MM/dd/yy HH:mm:ss"), SessionField.End("End Date", dateFormat = "MM/dd/yy HH:mm:ss"), - SessionField.Break("Break", Calendar.SECOND), + SessionField.Break("Break"), SessionField.SessionType("Type"), SessionField.Live("Live"), SessionField.NumberOfTables("Tables"), @@ -229,7 +229,7 @@ class ProductCSVDescriptors { SessionField.CurrencyRate("Currency Rate"), SessionField.SmallBlind("Small Blind"), SessionField.BigBlind("Big Blind"), - SessionField.TournamentType("Tournament Type"), + SessionField.TournamentTypeName("Tournament Type"), SessionField.TournamentName("Tournament Name"), SessionField.TournamentEntryFee("Entry fee"), SessionField.TournamentNumberOfPlayers("Number of players"), 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 b4307189..4d7fc9ff 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,7 +1,10 @@ package net.pokeranalytics.android.util.csv import io.realm.Realm +import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.Limit +import net.pokeranalytics.android.model.TournamentType +import net.pokeranalytics.android.model.realm.CustomField import net.pokeranalytics.android.model.realm.Session import org.apache.commons.csv.CSVRecord @@ -11,6 +14,25 @@ import org.apache.commons.csv.CSVRecord class SessionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg elements: CSVField) : PACSVDescriptor(source, isTournament, *elements) { + override fun prepareCSVExport() { + + val realm = Realm.getDefaultInstance() + realm.where(CustomField::class.java).findAll().sort("name").forEach { customField -> + val header = customField.name + CustomFieldCSVField.separator + customField.type + val f = when (customField.type) { + CustomField.Type.LIST.ordinal -> { + SessionField.NumberCustomField(header, customField) + } + else -> { + SessionField.ListCustomField(header, customField) + } + } + this.staticFields.add(f) + } + realm.close() + + } + override fun parseData(realm: Realm, record: CSVRecord): Session? { return this.parseSession(realm, record) } @@ -41,16 +63,62 @@ class SessionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg el is SessionField.CurrencyRate -> field.format(data.bankroll?.currency?.rate) is SessionField.SmallBlind -> field.format(data.cgSmallBlind) is SessionField.BigBlind -> field.format(data.cgBigBlind) - is SessionField.TournamentType -> field.format(data.tournamentType) + is SessionField.TournamentType -> { + data.tournamentType?.let { tt -> + TournamentType.values()[tt].label + } + } is SessionField.TournamentName -> data.tournamentName?.name is SessionField.TournamentFeatures -> field.format(data.tournamentFeatures) is SessionField.TournamentEntryFee -> field.format(data.tournamentEntryFee) is SessionField.TournamentNumberOfPlayers -> field.format(data.tournamentNumberOfPlayers) is SessionField.TournamentPosition -> field.format(data.result?.tournamentFinalPosition) is SessionField.Comment -> data.comment + is SessionField.NumberCustomField -> { + val entry = data.customFieldEntries.first { it.customField == field.customField } + field.format(entry.numericValue) + } + is SessionField.ListCustomField -> data.customFieldEntries.first { it.customField == field.customField }.value else -> null } } + override fun hasMatched(realm: Realm, record: CSVRecord) { + super.hasMatched(realm, record) + + // identify if the headers has custom fields headers: + // create the custom fields if not existing + // adds the field to the dynamic list + val headers = record.toSet() + headers.filter { it.contains(CustomFieldCSVField.separator) }.forEach { header -> + + val cfProperties = header.split(CustomFieldCSVField.separator) + if (cfProperties.size != 2) { + throw PAIllegalStateException("A custom field header is wrongly formed: $header") + } + + val name = cfProperties.first() + val type = cfProperties.last().toInt() + val customField = CustomField.getOrCreate(realm, name, type) + + val field = when (customField.type) { + CustomField.Type.LIST.ordinal -> { + SessionField.NumberCustomField(header, customField) + } + else -> { + SessionField.ListCustomField(header, customField) + } + } + + val index = headers.indexOf(header) // get index in the record + if (index >= 0) { + this.fieldMapping[field] = index + } + this.staticFields.add(field) + + } + + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt index a9aea120..bfc97d64 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt @@ -1,6 +1,7 @@ package net.pokeranalytics.android.util.csv import net.pokeranalytics.android.exceptions.PAIllegalStateException +import net.pokeranalytics.android.model.realm.CustomField import net.pokeranalytics.android.model.realm.TournamentFeature import java.util.* @@ -14,8 +15,7 @@ sealed class TrField { data class Amount( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class BankrollName(override var header: String) : CSVField @@ -29,8 +29,7 @@ sealed class TrField { data class CurrencyRate( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class TransactionType(override var header: String) : CSVField @@ -70,33 +69,28 @@ sealed class SessionField { data class Duration( override var header: String, override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null, val randomTime: Boolean = false ) : NumberCSVField data class Buyin( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class NetResult( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class CashedOut( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class Break( override var header: String, var unit: Int = Calendar.MINUTE, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField { override fun parse(value: String): Double? { @@ -118,48 +112,42 @@ sealed class SessionField { data class Tips( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class SmallBlind( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class BigBlind( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class Rebuy( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class Addon( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class StackingIn( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class StackingOut( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField - data class Blind(override var header: String, override var callback: ((String) -> Pair?)? = null) : - BlindCSVField + data class Blind(override var header: String, + override var callback: ((String) -> Pair?)? = null + ) : BlindCSVField data class Live( override var header: String, @@ -190,8 +178,7 @@ sealed class SessionField { data class CurrencyRate( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class TournamentPosition( @@ -211,14 +198,23 @@ sealed class SessionField { data class TournamentEntryFee( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField data class TournamentPrizePool( override var header: String, - override var callback: ((String) -> Double?)? = null, - override val numberFormat: String? = null + override var callback: ((String) -> Double?)? = null ) : NumberCSVField + data class NumberCustomField( + override val header: String, + override var customField: CustomField, + override var callback: ((String) -> Double?)? = null + ) : CustomFieldCSVField, NumberCSVField + + data class ListCustomField( + override val header: String, + override var customField: CustomField + ) : CustomFieldCSVField + } diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt index e71a1585..5d720445 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt @@ -39,7 +39,7 @@ class SessionTransactionCSVDescriptor(source: DataSource, private var isTourname override fun parseData(realm: Realm, record: CSVRecord): Identifiable? { var dataType: DataType? = null - val typeField = fields.firstOrNull { it is SessionField.SessionType } + val typeField = staticFields.firstOrNull { it is SessionField.SessionType } typeField?.let { field -> this.fieldMapping[field]?.let { index -> val typeValue = record.get(index) @@ -65,7 +65,7 @@ class SessionTransactionCSVDescriptor(source: DataSource, private var isTourname var buyin: Double? = null var cashedOut: Double? = null - this.fields.forEach { field -> + this.staticFields.forEach { field -> val index = this.fieldMapping[field] if (index != null) { diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/TransactionCSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/TransactionCSVDescriptor.kt index 3b2db42d..978aa016 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/TransactionCSVDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/TransactionCSVDescriptor.kt @@ -24,7 +24,7 @@ class TransactionCSVDescriptor(source: DataSource, vararg elements: CSVField) : var currencyCode: String? = null var currencyRate: Double? = null - for (field in this.fields) { + for (field in this.staticFields) { val index = this.fieldMapping[field] if (index != null) {