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) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
this.createFakeSessions()
// this.createFakeSessions()
}
Patcher.patchBreaks()
}

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

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

@ -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<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
private fun observeRealmObjects() {

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

@ -5,7 +5,7 @@ import io.realm.RealmModel
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>()
@ -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 {
if (elements.size > 0) {
@ -36,17 +37,20 @@ open class CSVDescriptor(vararg elements: Field<*>) {
val all: List<CSVDescriptor> = listOf(SessionCSVDescriptor.pokerIncomeCash)
}
fun matches(headerMap: Map<String, Int>) : 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

@ -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<CSVDescriptor> = 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)
Timber.d("Starting import...")
realm.executeTransaction {
parser.forEachIndexed { index, record ->
val descriptor = this.findDescriptor(parser.headerMap)
Timber.d("line $index")
if (descriptor != null) {
this.descriptor = descriptor
this.parse(realm, descriptor, parser)
if (currentDescriptor == null) { // find descriptor
this.findDescriptor(record)
} 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) {
parser.records.forEach { record ->
descriptor.parse(realm, record)
Timber.d("Ending import...")
// 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) {
this.usedDescriptors.forEach { descriptor ->
if (descriptor is DataCSVDescriptor<*>) {
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
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<Session>(*elements) {
class SessionCSVDescriptor(var isTournament: Boolean, vararg elements: Field) : DataCSVDescriptor<Session>(*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)
this.fieldMapping[it]?.let { index ->
val value = record.get(index)
when (it) {
is SessionField.Start -> {
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.CashedOut -> session.result?.cashout = it.parse(value)
is SessionField.Blind -> {
// session.cgSmallBlind =
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 -> {
}
}
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.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 <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
*/

Loading…
Cancel
Save