Custom field management in CSV import/export

od
Laurent 6 years ago
parent d964be2d16
commit 72d2dbf766
  1. 12
      app/src/main/java/net/pokeranalytics/android/model/TournamentType.kt
  2. 28
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt
  3. 34
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt
  4. 12
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt
  5. 1
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt
  6. 18
      app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt
  7. 4
      app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt
  8. 70
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt
  9. 66
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt
  10. 4
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt
  11. 2
      app/src/main/java/net/pokeranalytics/android/util/csv/TransactionCSVDescriptor.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<TournamentType>
get() {
return TournamentType.values() as List<TournamentType>
return values() as List<TournamentType>
}
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
}
}

@ -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<out Identifiable> = 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)
}
}

@ -75,14 +75,20 @@ abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg el
this.realmModelIds.clear()
}
open fun prepareCSVExport() {
}
fun toCSV(dataSequence: List<T>): String {
prepareCSVExport()
val lines = mutableListOf<String>()
lines.add(this.csvHeaders)
dataSequence.forEach { data ->
val line = mutableListOf<String>()
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<CSVField> = listOf()
protected var staticFields: MutableList<CSVField> = mutableListOf()
/**
* A list of dynamic CSVField, described in the CSV header
*/
// protected var dynamicFields: MutableList<CSVField> = 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<String>()
this.fields.forEach {
this.staticFields.forEach {
headers.add(it.header)
}
return headers.joinToString(",")

@ -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<Double> {
val numberFormat: String?
companion object {
fun defaultParse(value: String) : Double? {
@ -170,6 +169,14 @@ interface BooleanCSVField : TypedCSVField<Boolean> {
}
}
interface CustomFieldCSVField : CSVField {
var customField: CustomField
companion object {
val separator: String = "::CF"
}
}
interface TypedCSVField<T> : CSVField {
fun parse(value: String) : T?
fun format(data: T?): String?
@ -179,7 +186,6 @@ interface TypedCSVField<T> : CSVField {
interface CSVField {
companion object {
const val delimiter = "\""
const val separator = "|"
}

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

@ -14,7 +14,10 @@ import org.apache.commons.csv.CSVRecord
import timber.log.Timber
import java.util.*
abstract class PACSVDescriptor<T : Identifiable>(source: DataSource, private var isTournament: Boolean?, vararg elements: CSVField) : DataCSVDescriptor<T>(source, *elements) {
abstract class PACSVDescriptor<T : Identifiable>(source: DataSource,
private var isTournament: Boolean?,
vararg elements: CSVField)
: DataCSVDescriptor<T>(source, *elements) {
private var sameDaySessionCount: Int = 0
private var currentDay: String = ""
@ -40,7 +43,7 @@ abstract class PACSVDescriptor<T : Identifiable>(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<T : Identifiable>(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 -> {
}
}

@ -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"),

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

@ -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<Double, Double>?)? = null) :
BlindCSVField
data class Blind(override var header: String,
override var callback: ((String) -> Pair<Double, Double>?)? = 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
}

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

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

Loading…
Cancel
Save