CSV import update

csv
Laurent 7 years ago
parent 79c2152d9e
commit 9af997c6a0
  1. 7
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Manageable.kt
  2. 5
      app/src/main/java/net/pokeranalytics/android/model/realm/Filter.kt
  3. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  4. 39
      app/src/main/java/net/pokeranalytics/android/model/utils/DataUtils.kt
  5. 21
      app/src/main/java/net/pokeranalytics/android/model/utils/SessionUtils.kt
  6. 6
      app/src/main/java/net/pokeranalytics/android/ui/fragment/FiltersFragment.kt
  7. 73
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt
  8. 43
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt
  9. 75
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt
  10. 112
      app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt
  11. 265
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt
  12. 122
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt
  13. 41
      app/src/main/res/layout/fragment_import.xml
  14. 1
      app/src/main/res/values/strings.xml
  15. 20
      app/src/main/res/values/styles.xml

@ -46,6 +46,8 @@ interface NameManageable : Manageable {
} }
class Identificator(var id: String, var clazz: Class<out Identifiable>)
/** /**
* An interface associate a unique uniqueIdentifier to an object * An interface associate a unique uniqueIdentifier to an object
*/ */
@ -56,6 +58,11 @@ interface Identifiable : RealmModel {
*/ */
var id: String var id: String
val identificator: Identificator
get() {
return Identificator(this.id, this::class.java)
}
} }
/** /**

@ -42,11 +42,6 @@ open class Filter : RealmObject(), RowRepresentable, Editable, Deletable, Counta
//return realm.copyToRealm(filter) //return realm.copyToRealm(filter)
} }
// Get a queryWith by its id
fun getFilterBydId(realm: Realm, filterId: String): Filter? {
return realm.where<Filter>().equalTo("id", filterId).findFirst()
}
inline fun <reified T : Filterable> queryOn(realm: Realm, query: Query, sortField: String? = null): RealmResults<T> { inline fun <reified T : Filterable> queryOn(realm: Realm, query: Query, sortField: String? = null): RealmResults<T> {
val rootQuery = realm.where<T>() val rootQuery = realm.where<T>()
var realmQuery = query.queryWith(rootQuery) var realmQuery = query.queryWith(rootQuery)

@ -67,6 +67,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
} }
companion object { companion object {
fun newInstance(realm: Realm, isTournament: Boolean, bankroll: Bankroll? = null): Session { fun newInstance(realm: Realm, isTournament: Boolean, bankroll: Bankroll? = null): Session {
val session = Session() val session = Session()
session.result = Result() session.result = Result()
@ -76,6 +77,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
session.bankroll = realm.where<Bankroll>().findFirst() session.bankroll = realm.where<Bankroll>().findFirst()
} }
session.type = if (isTournament) Session.Type.TOURNAMENT.ordinal else Session.Type.CASH_GAME.ordinal session.type = if (isTournament) Session.Type.TOURNAMENT.ordinal else Session.Type.CASH_GAME.ordinal
return realm.copyToRealm(session) return realm.copyToRealm(session)
} }

@ -0,0 +1,39 @@
package net.pokeranalytics.android.model.utils
import io.realm.Realm
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.TransactionType
import java.util.*
class DataUtils {
companion object {
/**
* Returns true if the provided parameters doesn't correspond to an existing session
*/
fun sessionCount(realm: Realm, startDate: Date, endDate: Date, net: Double): Int {
val sessions = realm.where(Session::class.java)
.equalTo("startDate", startDate)
.equalTo("endDate", endDate)
.equalTo("result.net", net)
.findAll()
return sessions.size
}
/**
* Returns true if the provided parameters doesn't correspond to an existing transaction
*/
fun transactionUnicityCheck(realm: Realm, date: Date, amount: Double, type: TransactionType): Boolean {
val transactions = realm.where(Transaction::class.java)
.equalTo("date", date)
.equalTo("amount", amount)
.equalTo("type.id", type.id)
.findAll()
return transactions.isEmpty()
}
}
}

@ -1,21 +0,0 @@
package net.pokeranalytics.android.model.utils
import io.realm.Realm
import net.pokeranalytics.android.model.realm.Session
import java.util.*
class SessionUtils {
companion object {
/**
* Returns true if the provided parameters doesn't correspond to an existing session
*/
fun unicityCheck(realm: Realm, startDate: Date, endDate: Date, net: Double) : Boolean {
val sessions = realm.where(Session::class.java).equalTo("startDate", startDate).equalTo("endDate", endDate).equalTo("result.net", net).findAll()
return sessions.isEmpty()
}
}
}

@ -12,6 +12,7 @@ import kotlinx.android.synthetic.main.fragment_editable_data.recyclerView
import kotlinx.android.synthetic.main.fragment_filters.* import kotlinx.android.synthetic.main.fragment_filters.*
import kotlinx.android.synthetic.main.fragment_filters.view.toolbar import kotlinx.android.synthetic.main.fragment_filters.view.toolbar
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.ui.activity.FilterDetailsActivity import net.pokeranalytics.android.ui.activity.FilterDetailsActivity
@ -27,6 +28,7 @@ import net.pokeranalytics.android.ui.interfaces.FilterableType
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow import net.pokeranalytics.android.ui.view.rowrepresentable.FilterCategoryRow
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.sorted import net.pokeranalytics.android.util.extensions.sorted
import timber.log.Timber import timber.log.Timber
@ -173,7 +175,9 @@ open class FiltersFragment : RealmFragment(), StaticRowRepresentableDataSource,
val realm = getRealm() val realm = getRealm()
primaryKey?.let { primaryKey?.let {
currentFilter = realm.copyFromRealm(Filter.getFilterBydId(realm, it))
val filter = realm.findById<Filter>(it) ?: throw PAIllegalStateException("Can't find filter with id=$it")
currentFilter = realm.copyFromRealm(filter)
isUpdating = true isUpdating = true
} ?: run { } ?: run {
currentFilter = Filter.newInstance(this.filterableType.uniqueIdentifier) //realm.copyFromRealm(Filter.newInstanceForResult(realm, this.filterableType.ordinal)) currentFilter = Filter.newInstance(this.filterableType.uniqueIdentifier) //realm.copyFromRealm(Filter.newInstanceForResult(realm, this.filterableType.ordinal))

@ -4,18 +4,19 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_import.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.ResultCode
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.util.csv.CSVImporter import net.pokeranalytics.android.util.csv.CSVImporter
import net.pokeranalytics.android.util.csv.ImportDelegate import net.pokeranalytics.android.util.csv.ImportDelegate
import net.pokeranalytics.android.util.csv.ImportException import net.pokeranalytics.android.util.csv.ImportException
import timber.log.Timber import timber.log.Timber
import java.io.InputStream import java.io.InputStream
import java.text.NumberFormat
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -26,6 +27,7 @@ class ImportFragment : RealmFragment(), ImportDelegate {
private lateinit var filePath: String private lateinit var filePath: String
private lateinit var inputStream: InputStream private lateinit var inputStream: InputStream
private lateinit var importer: CSVImporter
fun setData(path: String) { fun setData(path: String) {
this.filePath = path this.filePath = path
@ -43,12 +45,34 @@ class ImportFragment : RealmFragment(), ImportDelegate {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
this.initUI()
this.startImport() this.startImport()
} }
fun startImport() { private fun initUI() {
this.imported.text = requireContext().getString(R.string.imported)
this.total.text = requireContext().getString(R.string.total)
this.save.isEnabled = false
this.save.setOnClickListener {
this.end()
}
var shouldDismissActivity = false this.cancel.setOnClickListener {
this.cancel()
this.end()
}
}
private fun startImport() {
// var shouldDismissActivity = false
this.importer = CSVImporter(inputStream)
this.importer.delegate = this
GlobalScope.launch(coroutineContext) { GlobalScope.launch(coroutineContext) {
@ -57,10 +81,9 @@ class ImportFragment : RealmFragment(), ImportDelegate {
Timber.d(">>> Start Import...") Timber.d(">>> Start Import...")
try { try {
val csv = CSVImporter(inputStream) importer.start()
csv.start()
} catch (e: ImportException) { } catch (e: ImportException) {
shouldDismissActivity = true // shouldDismissActivity = true
} }
val e = Date() val e = Date()
val duration = (e.time - s.time) / 1000.0 val duration = (e.time - s.time) / 1000.0
@ -69,23 +92,43 @@ class ImportFragment : RealmFragment(), ImportDelegate {
} }
test.await() test.await()
if (shouldDismissActivity) { // if (shouldDismissActivity) {
//
// activity?.let {
// it.setResult(ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value)
// it.finish()
// }
//
// } else {
// }
importDidFinish()
activity?.let { }
it.setResult(ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value)
it.finish()
}
} }
} private fun cancel() {
this.importer.cancel(getRealm())
}
private fun importDidFinish() {
this.save.isEnabled = true
}
private fun end() {
activity?.finish()
} }
val numberFormatter = NumberFormat.getNumberInstance()
// ImportDelegate // ImportDelegate
override fun parsingCountUpdate(count: Int) { override fun parsingCountUpdate(importedCount: Int, totalCount: Int) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. this.counter.text = this.numberFormatter.format(importedCount)
this.totalCounter.text = this.numberFormatter.format(totalCount)
} }
} }

@ -1,7 +1,10 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel import io.realm.kotlin.deleteFromRealm
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.Identificator
import net.pokeranalytics.android.util.extensions.findById
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
import timber.log.Timber import timber.log.Timber
@ -17,19 +20,30 @@ enum class DataSource {
/** /**
* A DataCSVDescriptor produces RealmModel instances for each row * A DataCSVDescriptor produces RealmModel instances for each row
*/ */
abstract class DataCSVDescriptor<T : RealmModel>(source: DataSource, vararg elements: CSVField) : CSVDescriptor(source, *elements) { abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg elements: CSVField) : CSVDescriptor(source, *elements) {
val realmModels = mutableListOf<RealmModel>() /**
* List of Realm object identificators
*/
val realmModelIds = mutableListOf<Identificator>()
abstract fun parseData(realm: Realm, record: CSVRecord): T? abstract fun parseData(realm: Realm, record: CSVRecord): T?
override fun parse(realm: Realm, record: CSVRecord) { override fun parse(realm: Realm, record: CSVRecord): Int {
val data = this.parseData(realm, record) val data = this.parseData(realm, record)
data?.let { data?.let {
this.realmModels.add(it) this.realmModelIds.add(it.identificator)
} }
return if (data != null) 1 else 0
}
override fun cancel(realm: Realm) {
realm.executeTransaction {
this.realmModelIds.forEach { identificator ->
realm.findById(identificator.clazz, identificator.id)?.deleteFromRealm()
}
}
} }
} }
@ -59,7 +73,19 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField)
* The list of all managed CSVDescriptors * The list of all managed CSVDescriptors
*/ */
val all: List<CSVDescriptor> = val all: List<CSVDescriptor> =
listOf(SessionCSVDescriptor.pokerIncomeCash, SessionCSVDescriptor.pokerBankrollTracker, SessionCSVDescriptor.runGoodCashGames, SessionCSVDescriptor.runGoodTournaments) listOf(ProductCSVDescriptors.pokerIncomeCash,
ProductCSVDescriptors.pokerBankrollTracker,
ProductCSVDescriptors.runGoodCashGames,
ProductCSVDescriptors.runGoodTournaments)
}
/**
* Method called when iterating on a CSVRecord
*/
abstract fun parse(realm: Realm, record: CSVRecord): Int
open fun cancel(realm: Realm) {
} }
/** /**
@ -83,9 +109,4 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField)
return count == this.fields.size return count == this.fields.size
} }
/**
* Method called when iterating on a CSVRecord
*/
abstract fun parse(realm: Realm, record: CSVRecord)
} }

@ -1,7 +1,10 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import android.os.Handler
import android.os.Looper
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 org.apache.commons.csv.CSVRecord
import timber.log.Timber import timber.log.Timber
import java.io.FileReader import java.io.FileReader
@ -12,7 +15,7 @@ import java.io.Reader
class ImportException(message: String) : Exception(message) class ImportException(message: String) : Exception(message)
interface ImportDelegate { interface ImportDelegate {
fun parsingCountUpdate(count: Int) fun parsingCountUpdate(importedCount: Int, totalCount: Int)
} }
/** /**
@ -60,11 +63,27 @@ open class CSVImporter(istream: InputStream) {
* Stores the descriptors found * Stores the descriptors found
*/ */
private var usedDescriptors: MutableList<CSVDescriptor> = mutableListOf() private var usedDescriptors: MutableList<CSVDescriptor> = mutableListOf()
/** /**
* The currently used CSVDescriptor for parsing * The currently used CSVDescriptor for parsing
*/ */
private var currentDescriptor: CSVDescriptor? = null private var currentDescriptor: CSVDescriptor? = null
/**
* The number of valid record parsed
*/
private var totalParsedRecords = 0
/**
* The number of successfully imported records
*/
private var importedRecords = 0
/**
* The CSV parser
*/
private lateinit var parser: CSVParser
/** /**
* Constructs a CSVParser object and starts parsing the CSV * Constructs a CSVParser object and starts parsing the CSV
*/ */
@ -80,7 +99,7 @@ open class CSVImporter(istream: InputStream) {
reader = InputStreamReader(this.inputStream) reader = InputStreamReader(this.inputStream)
} }
val parser = CSVFormat.DEFAULT.withAllowMissingColumnNames().parse(reader) this.parser = CSVFormat.DEFAULT.withAllowMissingColumnNames().parse(reader)
Timber.d("Starting import...") Timber.d("Starting import...")
@ -89,6 +108,7 @@ open class CSVImporter(istream: InputStream) {
parser.forEachIndexed { index, record -> parser.forEachIndexed { index, record ->
Timber.d("line $index") Timber.d("line $index")
this.notifyDelegate()
if (this.currentDescriptor == null) { // find descriptor if (this.currentDescriptor == null) { // find descriptor
this.currentDescriptor = this.findDescriptor(record) this.currentDescriptor = this.findDescriptor(record)
@ -106,10 +126,10 @@ open class CSVImporter(istream: InputStream) {
} else { // parse } else { // parse
// batch commit
val parsingIndex = index + 1 val parsingIndex = index + 1
if (parsingIndex % COMMIT_FREQUENCY == 0) { if (parsingIndex % COMMIT_FREQUENCY == 0) {
Timber.d("****** committing at $parsingIndex sessions...") Timber.d("****** committing at $parsingIndex sessions...")
this.delegate?.parsingCountUpdate(parsingIndex)
realm.commitTransaction() realm.commitTransaction()
realm.beginTransaction() realm.beginTransaction()
} }
@ -117,19 +137,27 @@ open class CSVImporter(istream: InputStream) {
this.currentDescriptor?.let { this.currentDescriptor?.let {
if (record.size() == 0) { if (record.size() == 0) {
this.usedDescriptors.add(it) this.usedDescriptors.add(it)
this.currentDescriptor = null // reset descriptor when encountering an empty line (multiple descriptors can be found in a single file) this.currentDescriptor =
null // reset descriptor when encountering an empty line (multiple descriptors can be found in a single file)
this.descriptorFindingAttempts = 0 this.descriptorFindingAttempts = 0
} else { } else {
it.parse(realm, record) val count = it.parse(realm, record)
this.importedRecords += count
this.totalParsedRecords++
this.notifyDelegate()
} }
} ?: run { } ?: run {
realm.cancelTransaction() realm.cancelTransaction()
throw IllegalStateException("CSVDescriptor should never be null here") throw ImportException("CSVDescriptor should never be null here")
} }
} }
} }
this.notifyDelegate()
realm.commitTransaction() realm.commitTransaction()
Timber.d("Ending import...") Timber.d("Ending import...")
@ -137,10 +165,16 @@ open class CSVImporter(istream: InputStream) {
realm.close() realm.close()
} }
private fun notifyDelegate() {
Handler(Looper.getMainLooper()).post {
this.delegate?.parsingCountUpdate(this.importedRecords, this.totalParsedRecords)
}
}
/** /**
* Search for a descriptor in the list of managed formats * Search for a descriptor in the list of managed formats
*/ */
private fun findDescriptor(record: CSVRecord) : CSVDescriptor? { private fun findDescriptor(record: CSVRecord): CSVDescriptor? {
CSVDescriptor.all.forEach { descriptor -> CSVDescriptor.all.forEach { descriptor ->
if (descriptor.matches(record)) { if (descriptor.matches(record)) {
@ -152,17 +186,26 @@ open class CSVImporter(istream: InputStream) {
return null return null
} }
fun save(realm: Realm) { // fun save(realm: Realm) {
//
// this.usedDescriptors.forEach { descriptor ->
//
// if (descriptor is DataCSVDescriptor<*>) {
// realm.executeTransaction {
// realm.copyToRealm(descriptor.realmModels)
// }
// }
// }
// }
fun cancel(realm: Realm) {
this.parser.close()
realm.refresh()
this.currentDescriptor?.cancel(realm)
this.usedDescriptors.forEach { descriptor -> this.usedDescriptors.forEach { descriptor ->
descriptor.cancel(realm)
if (descriptor is DataCSVDescriptor<*>) {
realm.executeTransaction {
realm.copyToRealm(descriptor.realmModels)
}
}
} }
} }
} }

@ -0,0 +1,112 @@
package net.pokeranalytics.android.util.csv
class ProductCSVDescriptors {
companion object {
val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor(
DataSource.POKER_INCOME,
false,
SessionField.Start("Start Time"),
SessionField.End("End Time"),
SessionField.Buyin("Buy In"),
SessionField.CashedOut("Cashed Out"),
SessionField.Break("Break Minutes"),
SessionField.LimitType("Limit Type"),
SessionField.Game("Game"),
SessionField.Bankroll("Bankroll"),
SessionField.Location("Location"),
SessionField.Location("Location Type"),
SessionField.Comment("Note"),
SessionField.Tips("Tips"),
SessionField.Blind("Stake")
)
val pokerBankrollTracker: CSVDescriptor = SessionCSVDescriptor(
DataSource.POKER_BANKROLL_TRACKER,
true,
SessionField.Start("starttime", dateFormat = "MM/dd/yy HH:mm"),
SessionField.End("endtime", dateFormat = "MM/dd/yy HH:mm"),
SessionField.SessionType("variant"),
SessionField.Buyin("buyin"),
SessionField.CashedOut("cashout"),
SessionField.Rebuy("rebuycosts"),
SessionField.Addon("addoncosts"),
SessionField.Break("breakminutes"),
SessionField.LimitType("limit"),
SessionField.Game("game"),
SessionField.Bankroll("currency"), // same as currency code
SessionField.Location("type"),
// SessionField.Comment("sessionnote"),
SessionField.Tips("expensesfromstack"),
SessionField.SmallBlind("smallblind"),
SessionField.BigBlind("bigblind"),
SessionField.TournamentNumberOfPlayers("player"),
SessionField.TournamentPosition("place"),
SessionField.TournamentName("mttname"),
SessionField.CurrencyCode("currency"),
SessionField.CurrencyRate("exchangerate"),
SessionField.TableSize("tablesize")
)
val runGoodTournaments: CSVDescriptor = SessionCSVDescriptor(
DataSource.RUNGOOD,
true,
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"),
SessionField.StartTime("Start Time"),
SessionField.End("End Date"),
SessionField.EndTime("End Time"),
SessionField.Buyin("Total Buy-In"),
SessionField.CashedOut("Winnings"),
SessionField.NetResult("Profit"),
SessionField.Break("Break"),
SessionField.LimitType("Limit Type"),
SessionField.Game("Game"),
SessionField.Bankroll("Bankroll"),
SessionField.TableSize("Table Type"),
SessionField.Location("Location"),
SessionField.LocationType("Location Type"),
SessionField.Comment("Notes"),
SessionField.CurrencyCode("Currency"),
SessionField.TournamentName("Event Name"),
SessionField.TournamentNumberOfPlayers("Total Players"),
SessionField.TournamentPosition("Finished Place"),
SessionField.TournamentType("Single-Table/Multi-Table")
)
val runGoodCashGames: CSVDescriptor = SessionCSVDescriptor(
DataSource.RUNGOOD,
false,
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"),
SessionField.StartTime("Start Time", dateFormat = "HH:mm"),
SessionField.End("End Date", dateFormat = "dd/MM/yyyy"),
SessionField.EndTime("End Time", dateFormat = "HH:mm"),
SessionField.Buyin("Total Buy-In"),
SessionField.CashedOut("Cashed Out"),
SessionField.NetResult("Profit"),
SessionField.Break("Break"),
SessionField.LimitType("Limit Type"),
SessionField.Game("Game"),
SessionField.Bankroll("Bankroll"),
SessionField.TableSize("Table Type"),
SessionField.Location("Location"),
SessionField.LocationType("Location Type"),
SessionField.Comment("Notes"),
SessionField.CurrencyCode("Currency"),
SessionField.Blind("Stakes", callback = { value ->
// $10/20
value.drop(1)
val blinds = value.split("/")
if (blinds.size == 2) {
return@Blind Pair(blinds.first().toDouble(), blinds.last().toDouble())
} else {
return@Blind null
}
})
)
}
}

@ -1,248 +1,26 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel
import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.TableSize import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.TournamentType import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.TransactionType import net.pokeranalytics.android.model.realm.TransactionType
import net.pokeranalytics.android.model.utils.SessionUtils import net.pokeranalytics.android.model.utils.DataUtils
import net.pokeranalytics.android.util.extensions.getOrCreate import net.pokeranalytics.android.util.extensions.getOrCreate
import net.pokeranalytics.android.util.extensions.setHourMinutes import net.pokeranalytics.android.util.extensions.setHourMinutes
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
import timber.log.Timber
import java.util.* import java.util.*
/**
* The enumeration of Session fields
*/
sealed class SessionField {
data class Start(
override var header: String,
override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null
) : DateCSVField
data class StartTime(
override var header: String,
override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null
) : DateCSVField
data class End(
override var header: String,
override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null
) : DateCSVField
data class EndTime(
override var header: String,
override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null
) : DateCSVField
data class Buyin(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class NetResult(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class CashedOut(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class Break(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class Tips(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class SmallBlind(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class BigBlind(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class Rebuy(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class Addon(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class Blind(override var header: String, override var callback: ((String) -> Pair<Double, Double>?)? = null) :
BlindCSVField
data class Game(override var header: String) : CSVField
data class Location(override var header: String) : CSVField
data class LocationType(override var header: String) : CSVField
data class Bankroll(override var header: String) : CSVField
data class LimitType(override var header: String) : CSVField
data class Comment(override var header: String) : CSVField
data class SessionType(override var header: String) : CSVField
data class TableSize(override var header: String) : CSVField
data class CurrencyCode(override var header: String) : CSVField
data class TournamentName(override var header: String) : CSVField
data class TournamentType(override var header: String) : CSVField
data class CurrencyRate(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class TournamentPosition(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class TournamentNumberOfPlayers(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
}
/** /**
* A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects * A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects
*/ */
class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean, vararg elements: CSVField) : class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean, vararg elements: CSVField) :
DataCSVDescriptor<RealmModel>(source, *elements) { DataCSVDescriptor<Identifiable>(source, *elements) {
companion object {
val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor(
DataSource.POKER_INCOME,
false,
SessionField.Start("Start Time"),
SessionField.End("End Time"),
SessionField.Buyin("Buy In"),
SessionField.CashedOut("Cashed Out"),
SessionField.Break("Break Minutes"),
SessionField.LimitType("Limit Type"),
SessionField.Game("Game"),
SessionField.Bankroll("Bankroll"),
SessionField.Location("Location"),
SessionField.Location("Location Type"),
SessionField.Comment("Note"),
SessionField.Tips("Tips"),
SessionField.Blind("Stake")
)
val pokerBankrollTracker: CSVDescriptor = SessionCSVDescriptor(
DataSource.POKER_BANKROLL_TRACKER,
true,
SessionField.Start("starttime", dateFormat = "MM/dd/yy HH:mm"),
SessionField.End("endtime", dateFormat = "MM/dd/yy HH:mm"),
SessionField.SessionType("variant"),
SessionField.Buyin("buyin"),
SessionField.CashedOut("cashout"),
SessionField.Rebuy("rebuycosts"),
SessionField.Addon("addoncosts"),
SessionField.Break("breakminutes"),
SessionField.LimitType("limit"),
SessionField.Game("game"),
SessionField.Bankroll("currency"), // same as currency code
SessionField.Location("type"),
// SessionField.Comment("sessionnote"),
SessionField.Tips("expensesfromstack"),
SessionField.SmallBlind("smallblind"),
SessionField.BigBlind("bigblind"),
SessionField.TournamentNumberOfPlayers("player"),
SessionField.TournamentPosition("place"),
SessionField.TournamentName("mttname"),
SessionField.CurrencyCode("currency"),
SessionField.CurrencyRate("exchangerate"),
SessionField.TableSize("tablesize")
)
val runGoodTournaments: CSVDescriptor = SessionCSVDescriptor(
DataSource.RUNGOOD,
true,
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"),
SessionField.StartTime("Start Time"),
SessionField.End("End Date"),
SessionField.EndTime("End Time"),
SessionField.Buyin("Total Buy-In"),
SessionField.CashedOut("Winnings"),
SessionField.NetResult("Profit"),
SessionField.Break("Break"),
SessionField.LimitType("Limit Type"),
SessionField.Game("Game"),
SessionField.Bankroll("Bankroll"),
SessionField.TableSize("Table Type"),
SessionField.Location("Location"),
SessionField.LocationType("Location Type"),
SessionField.Comment("Notes"),
SessionField.CurrencyCode("Currency"),
SessionField.TournamentName("Event Name"),
SessionField.TournamentNumberOfPlayers("Total Players"),
SessionField.TournamentPosition("Finished Place"),
SessionField.TournamentType("Single-Table/Multi-Table")
)
val runGoodCashGames: CSVDescriptor = SessionCSVDescriptor(
DataSource.RUNGOOD,
false,
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"),
SessionField.StartTime("Start Time", dateFormat = "HH:mm"),
SessionField.End("End Date", dateFormat = "dd/MM/yyyy"),
SessionField.EndTime("End Time", dateFormat = "HH:mm"),
SessionField.Buyin("Total Buy-In"),
SessionField.CashedOut("Cashed Out"),
SessionField.NetResult("Profit"),
SessionField.Break("Break"),
SessionField.LimitType("Limit Type"),
SessionField.Game("Game"),
SessionField.Bankroll("Bankroll"),
SessionField.TableSize("Table Type"),
SessionField.Location("Location"),
SessionField.LocationType("Location Type"),
SessionField.Comment("Notes"),
SessionField.CurrencyCode("Currency"),
SessionField.Blind("Stakes", callback = { value ->
// $10/20
value.drop(1)
val blinds = value.split("/")
if (blinds.size == 2) {
return@Blind Pair(blinds.first().toDouble(), blinds.last().toDouble())
} else {
return@Blind null
}
})
)
}
private enum class DataType { private enum class DataType {
TRANSACTION, TRANSACTION,
@ -264,7 +42,7 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
/** /**
* Parses a [record] and return an optional Session * Parses a [record] and return an optional Session
*/ */
override fun parseData(realm: Realm, record: CSVRecord): RealmModel? { override fun parseData(realm: Realm, record: CSVRecord): Identifiable? {
var dataType: DataType? = null var dataType: DataType? = null
val typeField = fields.firstOrNull { it is SessionField.SessionType } val typeField = fields.firstOrNull { it is SessionField.SessionType }
@ -282,7 +60,7 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
} }
fun parseTransaction(realm: Realm, record: CSVRecord): Transaction? { private fun parseTransaction(realm: Realm, record: CSVRecord): Transaction? {
var date: Date? = null var date: Date? = null
var amount: Double? = null var amount: Double? = null
@ -313,24 +91,32 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
if (date != null && amount != null && type != null && currencyCode != null) { if (date != null && amount != null && type != null && currencyCode != null) {
val transaction = realm.copyToRealm(Transaction()) if (DataUtils.transactionUnicityCheck(realm, date!!, amount!!, type!!)) {
transaction.date = date!!
transaction.amount = amount!!
transaction.type = type
val bankroll = Bankroll.getOrCreate(realm, currencyCode!!, currencyRate = currencyRate) val transaction = realm.copyToRealm(Transaction())
transaction.bankroll = bankroll transaction.date = date!!
transaction.amount = amount!!
transaction.type = type
return transaction val bankroll = Bankroll.getOrCreate(realm, currencyCode!!, currencyRate = currencyRate)
transaction.bankroll = bankroll
return transaction
} else {
Timber.d("Transaction already exists")
}
} else {
Timber.d("Can't import transaction: date=$date, amount=$amount, type=${type?.name}")
} }
return null return null
} }
fun parseSession(realm: Realm, record: CSVRecord): Session? { private fun parseSession(realm: Realm, record: CSVRecord): Session? {
val session = Session.newInstance(realm, this.isTournament) val session = Session.newInstance(realm, this.isTournament)
var isLive = true var isLive = true
var bankrollName: String? = null var bankrollName: String? = null
var currencyCode: String? = null var currencyCode: String? = null
@ -423,14 +209,17 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
} }
if (startDate != null && endDate != null && net != null) { // valid session if (startDate != null && endDate != null && net != null) { // valid session
if (SessionUtils.unicityCheck(realm, startDate, endDate, net)) { if (DataUtils.sessionCount(realm, startDate, endDate, net) == 1) { // session already in realm, we'd love not put it in Realm before doing the check
return session return session
} else {
Timber.d("Session already exists")
} }
} else {
Timber.d("Can't import session: sd=$startDate, ed=$endDate, net=$net")
} }
session.deleteFromRealm() session.deleteFromRealm()
return null return null
} }
} }

@ -0,0 +1,122 @@
package net.pokeranalytics.android.util.csv
import java.util.*
/**
* The enumeration of Session fields
*/
sealed class SessionField {
data class Start(
override var header: String,
override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null
) : DateCSVField
data class StartTime(
override var header: String,
override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null
) : DateCSVField
data class End(
override var header: String,
override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null
) : DateCSVField
data class EndTime(
override var header: String,
override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null
) : DateCSVField
data class Buyin(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class NetResult(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class CashedOut(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class Break(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class Tips(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class SmallBlind(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class BigBlind(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class Rebuy(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class Addon(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class Blind(override var header: String, override var callback: ((String) -> Pair<Double, Double>?)? = null) :
BlindCSVField
data class Game(override var header: String) : CSVField
data class Location(override var header: String) : CSVField
data class LocationType(override var header: String) : CSVField
data class Bankroll(override var header: String) : CSVField
data class LimitType(override var header: String) : CSVField
data class Comment(override var header: String) : CSVField
data class SessionType(override var header: String) : CSVField
data class TableSize(override var header: String) : CSVField
data class CurrencyCode(override var header: String) : CSVField
data class TournamentName(override var header: String) : CSVField
data class TournamentType(override var header: String) : CSVField
data class CurrencyRate(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class TournamentPosition(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class TournamentNumberOfPlayers(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
}

@ -5,9 +5,21 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/imported"
style="@style/PokerAnalyticsTheme.TextView.ImportLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="IMPORTED"
app:layout_constraintBottom_toTopOf="@id/counter"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.5"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/counter" android:id="@+id/counter"
style="@style/PokerAnalyticsTheme.TextView.Importcount" style="@style/PokerAnalyticsTheme.TextView.ImportCount"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="550" tools:text="550"
@ -18,12 +30,34 @@
app:layout_constraintVertical_bias="0.5" app:layout_constraintVertical_bias="0.5"
tools:visibility="visible" /> tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/total"
style="@style/PokerAnalyticsTheme.TextView.ImportLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="TOTAL"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/counter"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/totalCounter"
style="@style/PokerAnalyticsTheme.TextView.ImportSubCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="550"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/total"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/save" android:id="@+id/save"
style="@style/PokerAnalyticsTheme.Button" style="@style/PokerAnalyticsTheme.Button"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
app:layout_constraintBottom_toTopOf="@id/cancel" app:layout_constraintBottom_toTopOf="@id/cancel"
@ -36,7 +70,7 @@
style="@style/PokerAnalyticsTheme.Button.Borderless" style="@style/PokerAnalyticsTheme.Button.Borderless"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -44,5 +78,4 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:text="@string/cancel" /> android:text="@string/cancel" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -37,6 +37,7 @@
<string name="filter_currently_selected">The filter cannot be deleted because it is currently selected.</string> <string name="filter_currently_selected">The filter cannot be deleted because it is currently selected.</string>
<string name="custom_field">Custom field</string> <string name="custom_field">Custom field</string>
<string name="transaction_relationship_error">The item is used in one or more transactions&#8230;Please delete the linked transactions first</string> <string name="transaction_relationship_error">The item is used in one or more transactions&#8230;Please delete the linked transactions first</string>
<string name="imported">Imported</string>
<string name="address">Address</string> <string name="address">Address</string>
<string name="suggestions">Naming suggestions</string> <string name="suggestions">Naming suggestions</string>

@ -353,7 +353,16 @@
<!-- Import --> <!-- Import -->
<style name="PokerAnalyticsTheme.TextView.Importcount"> <style name="PokerAnalyticsTheme.TextView.ImportLabel">
<item name="android:textSize">20sp</item>
<item name="android:textColor">@color/white</item>
<item name="android:fontFamily">@font/roboto_light</item>
<item name="android:textAllCaps">true</item>
<item name="android:maxLines">1</item>
<item name="android:ellipsize">end</item>
</style>
<style name="PokerAnalyticsTheme.TextView.ImportCount">
<item name="android:textSize">72sp</item> <item name="android:textSize">72sp</item>
<item name="android:textColor">@color/white</item> <item name="android:textColor">@color/white</item>
<item name="android:fontFamily">@font/roboto_light</item> <item name="android:fontFamily">@font/roboto_light</item>
@ -362,4 +371,13 @@
<item name="android:ellipsize">end</item> <item name="android:ellipsize">end</item>
</style> </style>
<style name="PokerAnalyticsTheme.TextView.ImportSubCount">
<item name="android:textSize">28sp</item>
<item name="android:textColor">@color/white</item>
<item name="android:fontFamily">@font/roboto_light</item>
<item name="android:textAllCaps">true</item>
<item name="android:maxLines">1</item>
<item name="android:ellipsize">end</item>
</style>
</resources> </resources>

Loading…
Cancel
Save