Successfully imports basic CSVs

dev
Laurent 7 years ago
parent 1f04eb5d82
commit 7598c9391e
  1. 3
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  2. 16
      app/src/main/java/net/pokeranalytics/android/model/Limit.kt
  3. 2
      app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt
  4. 24
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  5. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt
  6. 18
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt
  7. 63
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt
  8. 49
      app/src/main/java/net/pokeranalytics/android/util/csv/Field.kt
  9. 97
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt
  10. 68
      app/src/main/java/net/pokeranalytics/android/util/csv/TypedField.kt
  11. 16
      app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt

@ -55,9 +55,10 @@ class PokerAnalyticsApplication : Application() {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}") Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
this.createFakeSessions() // this.createFakeSessions()
} }
Patcher.patchBreaks() Patcher.patchBreaks()
} }

@ -10,6 +10,21 @@ enum class Limit : RowRepresentable {
SPREAD, SPREAD,
MIXED; 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 val shortName: String
get() { get() {
return when (this) { return when (this) {
@ -36,4 +51,5 @@ enum class Limit : RowRepresentable {
override fun getDisplayName(context: Context): String { override fun getDisplayName(context: Context): String {
return this.longName return this.longName
} }
} }

@ -169,7 +169,7 @@ class SessionSetManager {
sessionSet.deleteFromRealm() sessionSet.deleteFromRealm()
sessions.forEach { sessions.forEach {
SessionSetManager.updateTimeline(it) updateTimeline(it)
} }
} }
} }

@ -1,11 +1,12 @@
package net.pokeranalytics.android.ui.activity package net.pokeranalytics.android.ui.activity
import android.Manifest
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Menu import androidx.core.app.ActivityCompat
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.android.synthetic.main.activity_home.* 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.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.adapter.HomePagerAdapter import net.pokeranalytics.android.ui.adapter.HomePagerAdapter
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.csv.CSVImporter
class HomeActivity : PokerAnalyticsActivity() { class HomeActivity : PokerAnalyticsActivity() {
@ -72,6 +74,26 @@ class HomeActivity : PokerAnalyticsActivity() {
observeRealmObjects() observeRealmObjects()
initUI() initUI()
checkFirstLaunch() 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<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
} }
private fun observeRealmObjects() { private fun observeRealmObjects() {

@ -34,7 +34,7 @@ import kotlin.collections.ArrayList
*/ */
class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource { class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource {
// Return the item as a Custom Field object // Return the item as a Custom TypedField object
private val customField: CustomField private val customField: CustomField
get() { get() {
return this.item as CustomField return this.item as CustomField

@ -5,7 +5,7 @@ import io.realm.RealmModel
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
abstract class DataCSVDescriptor<T : RealmModel>(vararg elements: Field<*>) : CSVDescriptor(*elements) { abstract class DataCSVDescriptor<T : RealmModel>(vararg elements: Field) : CSVDescriptor(*elements) {
val realmModels = mutableListOf<RealmModel>() val realmModels = mutableListOf<RealmModel>()
@ -22,9 +22,10 @@ abstract class DataCSVDescriptor<T : RealmModel>(vararg elements: Field<*>) : CS
} }
open class CSVDescriptor(vararg elements: Field<*>) { open class CSVDescriptor(vararg elements: Field) {
protected var fields: List<Field<*>> = listOf() protected var fields: List<Field> = listOf()
protected var fieldMapping: MutableMap<Field, Int> = mutableMapOf()
init { init {
if (elements.size > 0) { if (elements.size > 0) {
@ -36,17 +37,20 @@ open class CSVDescriptor(vararg elements: Field<*>) {
val all: List<CSVDescriptor> = listOf(SessionCSVDescriptor.pokerIncomeCash) val all: List<CSVDescriptor> = listOf(SessionCSVDescriptor.pokerIncomeCash)
} }
fun matches(headerMap: Map<String, Int>) : Boolean { fun matches(record: CSVRecord) : Boolean {
var count = 0 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) { if (index >= 0) {
count++ count++
} }
} }
return count == this.fields.size return count == this.fields.size

@ -2,51 +2,76 @@ package net.pokeranalytics.android.util.csv
import io.realm.Realm import io.realm.Realm
import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVParser import org.apache.commons.csv.CSVRecord
import timber.log.Timber import timber.log.Timber
import java.io.FileReader import java.io.FileReader
open class CSVImporter(var path: String) { open class CSVImporter(var path: String) {
private lateinit var descriptor: CSVDescriptor private var usedDescriptors: MutableList<CSVDescriptor> = mutableListOf()
private var currentDescriptor: CSVDescriptor? = null
private fun start(realm: Realm) { fun start() {
val realm = Realm.getDefaultInstance()
val reader = FileReader(this.path) val reader = FileReader(this.path)
val parser = CSVFormat.RFC4180.withFirstRecordAsHeader().parse(reader) val parser = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(reader)
Timber.d("Starting import...")
realm.executeTransaction {
parser.forEachIndexed { index, record ->
val descriptor = this.findDescriptor(parser.headerMap) Timber.d("line $index")
if (descriptor != null) { if (currentDescriptor == null) { // find descriptor
this.descriptor = descriptor this.findDescriptor(record)
this.parse(realm, descriptor, parser)
} else { } else {
Timber.d("No CSV descriptor found")
}
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")
} }
private fun findDescriptor(header: Map<String, Int>) : CSVDescriptor? {
CSVDescriptor.all.forEach {
if (it.matches(header)) {
return it
} }
} }
return null
} }
protected open fun parse(realm: Realm, descriptor: CSVDescriptor, parser : CSVParser) { Timber.d("Ending import...")
parser.records.forEach { record ->
descriptor.parse(realm, record) // this.save(realm)
realm.close()
}
private fun findDescriptor(record: CSVRecord) {
CSVDescriptor.all.forEach { descriptor ->
if (descriptor.matches(record)) {
this.currentDescriptor = descriptor
return
} }
} }
}
fun save(realm: Realm) { fun save(realm: Realm) {
this.usedDescriptors.forEach { descriptor ->
if (descriptor is DataCSVDescriptor<*>) { if (descriptor is DataCSVDescriptor<*>) {
realm.executeTransaction { realm.executeTransaction {
realm.copyToRealm((descriptor as DataCSVDescriptor<*>).realmModels) realm.copyToRealm(descriptor.realmModels)
}
} }
} }

@ -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<Double> {
val numberFormat: String?
override fun parse(value: String) : Double? {
val formatter = NumberFormat.getInstance()
return formatter.parse(value).toDouble()
}
}
interface DateField : Field<Date> {
val dateFormat: String?
override fun parse(value: String) : Date? {
val formatter = DateFormat.getDateInstance()
return formatter.parse(value)
}
}
interface BlindField : Field<Double> {
override fun parse(value: String) : Double? {
return null
}
}
interface Field<T> {
val header: String
var callback: (() -> Unit)?
fun parse(value: String) : T?
}

@ -1,39 +1,90 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.utils.SessionUtils import net.pokeranalytics.android.model.utils.SessionUtils
import net.pokeranalytics.android.util.extensions.getOrCreate
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
import timber.log.Timber
sealed class SessionField { sealed class SessionField {
data class Start(override var header: String, override var callback: (() -> Unit)? = null, override val dateFormat: String? = null) : DateField data class Start(
data class End(override var header: String, override var callback: (() -> Unit)? = null, override val dateFormat: String? = null) : DateField override var header: String,
override var callback: (() -> Unit)? = null,
data class Buyin(override var header: String, override var callback: (() -> Unit)? = null, override val numberFormat: String? = null) : AmountField override val dateFormat: String? = null
data class CashedOut(override var header: String, override var callback: (() -> Unit)? = null, override val numberFormat: String? = null) : AmountField ) : 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 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<Session>(*elements) { class SessionCSVDescriptor(var isTournament: Boolean, vararg elements: Field) : DataCSVDescriptor<Session>(*elements) {
companion object { companion object {
val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor(SessionField.Start("Start Time"), val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor(
false,
SessionField.Start("Start Time"),
SessionField.End("End Time"), SessionField.End("End Time"),
SessionField.Buyin("Buy In"), 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? { override fun parseData(realm: Realm, record: CSVRecord): Session? {
val session = Session() val session = Session.newInstance(realm, this.isTournament)
fields.forEach { fields.forEach {
val value = record.get(it.header) this.fieldMapping[it]?.let { index ->
val value = record.get(index)
when (it) { when (it) {
is SessionField.Start -> { is SessionField.Start -> {
session.startDate = it.parse(value) session.startDate = it.parse(value)
@ -43,12 +94,30 @@ class SessionCSVDescriptor(vararg elements: Field<*>) : DataCSVDescriptor<Sessio
} }
is SessionField.Buyin -> session.result?.buyin = it.parse(value) is SessionField.Buyin -> session.result?.buyin = it.parse(value)
is SessionField.CashedOut -> session.result?.cashout = it.parse(value) is SessionField.CashedOut -> session.result?.cashout = it.parse(value)
is SessionField.Blind -> { is SessionField.Tips -> session.result?.tips = it.parse(value)
// session.cgSmallBlind = 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 -> {
}
} }
else -> {}
} }
} }

@ -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<Double> {
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<Date> {
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<Double> {
override fun parse(value: String) : Double? {
return null
}
}
interface TypedField<T> : Field {
fun parse(value: String) : T?
}
interface NamedDataField : Field {
fun <T : NameManageable> getOrCreate(realm: Realm, clazz: Class<T>, name: String) : T {
return realm.getOrCreate(clazz, name)
}
}
interface Field {
val header: String
var callback: (() -> Unit)?
}

@ -6,10 +6,26 @@ import io.realm.RealmResults
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.where import io.realm.kotlin.where
import net.pokeranalytics.android.model.interfaces.CountableUsage 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.Session
import net.pokeranalytics.android.model.realm.TournamentFeature import net.pokeranalytics.android.model.realm.TournamentFeature
import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.model.realm.Transaction
fun <T: NameManageable> Realm.getOrCreate(clazz: Class<T>, 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 <reified T: NameManageable> Realm.getOrCreate(name: String) : T {
return this.getOrCreate(T::class.java, name)
}
/** /**
* Returns all entities of the [clazz] sorted with their default sorting * Returns all entities of the [clazz] sorted with their default sorting
*/ */

Loading…
Cancel
Save