CSV import update

csv
Laurent 7 years ago
parent 3385beced2
commit 90fe6ef40f
  1. 9
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt
  2. 4
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt
  3. 10
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt
  4. 174
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt
  5. 4
      app/src/main/res/layout/bottom_sheet_sum.xml
  6. 40
      app/src/main/res/layout/fragment_import.xml
  7. 11
      app/src/main/res/values/styles.xml

@ -12,13 +12,14 @@ 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.util.csv.CSVImporter
import net.pokeranalytics.android.util.csv.ImportDelegate
import net.pokeranalytics.android.util.csv.ImportException
import timber.log.Timber
import java.io.InputStream
import java.util.*
import kotlin.coroutines.CoroutineContext
class ImportFragment : RealmFragment() {
class ImportFragment : RealmFragment(), ImportDelegate {
val coroutineContext: CoroutineContext
get() = Dispatchers.Main
@ -81,4 +82,10 @@ class ImportFragment : RealmFragment() {
}
// ImportDelegate
override fun parsingCountUpdate(count: Int) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

@ -3,6 +3,7 @@ package net.pokeranalytics.android.util.csv
import io.realm.Realm
import io.realm.RealmModel
import org.apache.commons.csv.CSVRecord
import timber.log.Timber
/**
* The various sources of CSV
@ -77,9 +78,8 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField)
if (index >= 0) {
count++
}
}
Timber.d("source= ${this.source.name} > total fields = ${this.fields.size}, identified = $count")
return count == this.fields.size
}

@ -11,6 +11,10 @@ import java.io.Reader
class ImportException(message: String) : Exception(message)
interface ImportDelegate {
fun parsingCountUpdate(count: Int)
}
/**
* A CSVImporter is a class in charge of parsing a CSV file and processing it
* When starting the parsing of a file, the instance will search for a CSVDescriptor, which describes
@ -20,6 +24,11 @@ class ImportException(message: String) : Exception(message)
*/
open class CSVImporter(istream: InputStream) {
/**
* The object being notified of the import progress
*/
var delegate: ImportDelegate? = null
/**
* Number of commits required to commit a Realm transaction
*/
@ -100,6 +109,7 @@ open class CSVImporter(istream: InputStream) {
val parsingIndex = index + 1
if (parsingIndex % COMMIT_FREQUENCY == 0) {
Timber.d("****** committing at $parsingIndex sessions...")
this.delegate?.parsingCountUpdate(parsingIndex)
realm.commitTransaction()
realm.beginTransaction()
}

@ -1,11 +1,14 @@
package net.pokeranalytics.android.util.csv
import io.realm.Realm
import io.realm.RealmModel
import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.TransactionType
import net.pokeranalytics.android.model.utils.SessionUtils
import net.pokeranalytics.android.util.extensions.getOrCreate
import net.pokeranalytics.android.util.extensions.setHourMinutes
@ -83,7 +86,22 @@ sealed class SessionField {
override val numberFormat: String? = null
) : NumberCSVField
data class Blind(override var header: String, override var callback: ((String) -> Pair<Double, Double>?)? = null) : BlindCSVField
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
@ -118,7 +136,8 @@ sealed class SessionField {
/**
* A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects
*/
class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean, vararg elements: CSVField) : DataCSVDescriptor<Session>(source, *elements) {
class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean, vararg elements: CSVField) :
DataCSVDescriptor<RealmModel>(source, *elements) {
companion object {
val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor(
@ -142,17 +161,19 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
val pokerBankrollTracker: CSVDescriptor = SessionCSVDescriptor(
DataSource.POKER_BANKROLL_TRACKER,
true,
SessionField.Start("starttime", dateFormat = "MM/dd/yyyy HH:mm"),
SessionField.End("endtime", dateFormat = "MM/dd/yyyy HH:mm"),
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("location"),
SessionField.Comment("sessionnote"),
SessionField.Location("type"),
// SessionField.Comment("sessionnote"),
SessionField.Tips("expensesfromstack"),
SessionField.SmallBlind("smallblind"),
SessionField.BigBlind("bigblind"),
@ -209,7 +230,8 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
SessionField.LocationType("Location Type"),
SessionField.Comment("Notes"),
SessionField.CurrencyCode("Currency"),
SessionField.Blind("Stakes", callback = { value -> // $10/20
SessionField.Blind("Stakes", callback = { value ->
// $10/20
value.drop(1)
val blinds = value.split("/")
if (blinds.size == 2) {
@ -222,10 +244,90 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
}
private enum class DataType {
TRANSACTION,
SESSION;
companion object {
fun valueForString(type: String): DataType? {
return when (type) {
"Deposit/Payout" -> TRANSACTION
"Cash Game", "Tournament" -> SESSION
else -> null
}
}
}
}
/**
* Parses a [record] and return an optional Session
*/
override fun parseData(realm: Realm, record: CSVRecord): Session? {
override fun parseData(realm: Realm, record: CSVRecord): RealmModel? {
var dataType: DataType? = null
val typeField = fields.firstOrNull { it is SessionField.SessionType }
typeField?.let { field ->
this.fieldMapping[field]?.let { index ->
val typeValue = record.get(index)
dataType = DataType.valueForString(typeValue)
}
}
return when (dataType) {
DataType.TRANSACTION -> parseTransaction(realm, record)
else -> parseSession(realm, record)
}
}
fun parseTransaction(realm: Realm, record: CSVRecord): Transaction? {
var date: Date? = null
var amount: Double? = null
var type: TransactionType? = null
var currencyCode: String? = null
var currencyRate: Double? = null
fields.forEach { field ->
val index = this.fieldMapping[field]
if (index != null) {
val value = record.get(index)
when (field) {
is SessionField.Start -> {
date = field.parse(value)
}
is SessionField.Buyin -> amount = field.parse(value)
is SessionField.SessionType -> {
type = realm.getOrCreate(value)
}
is SessionField.CurrencyCode -> currencyCode = value
is SessionField.CurrencyRate -> currencyRate = field.parse(value)
else -> {
}
}
}
}
if (date != null && amount != null && type != null && currencyCode != null) {
val transaction = realm.copyToRealm(Transaction())
transaction.date = date!!
transaction.amount = amount!!
transaction.type = type
val bankroll = Bankroll.getOrCreate(realm, currencyCode!!, currencyRate = currencyRate)
transaction.bankroll = bankroll
return transaction
}
return null
}
fun parseSession(realm: Realm, record: CSVRecord): Session? {
val session = Session.newInstance(realm, this.isTournament)
@ -233,6 +335,7 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
var bankrollName: String? = null
var currencyCode: String? = null
var currencyRate: Double? = null
var additionalBuyins = 0.0 // rebuy + addon
fields.forEach { field ->
@ -246,18 +349,34 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
is SessionField.End -> {
session.endDate = field.parse(value)
}
is SessionField.StartTime -> { session.startDate?.setHourMinutes(value) }
is SessionField.EndTime -> { session.endDate?.setHourMinutes(value) }
is SessionField.StartTime -> {
session.startDate?.setHourMinutes(value)
}
is SessionField.EndTime -> {
session.endDate?.setHourMinutes(value)
}
is SessionField.Buyin -> session.result?.buyin = field.parse(value)
is SessionField.CashedOut -> session.result?.cashout = field.parse(value)
is SessionField.Addon -> additionalBuyins += field.parse(value) ?: 0.0
is SessionField.Rebuy -> additionalBuyins += field.parse(value) ?: 0.0
is SessionField.Tips -> session.result?.tips = field.parse(value)
is SessionField.Break -> {
field.parse(value)?.let {
session.breakDuration = it.toLong() * 60 * 1000
}
}
is SessionField.Game -> session.game = realm.getOrCreate(value)
is SessionField.Location -> session.location = realm.getOrCreate(value)
is SessionField.Game -> {
if (value.isNotEmpty()) {
session.game = realm.getOrCreate(value)
} else {
}
}
is SessionField.Location -> {
if (value.isNotEmpty()) {
session.location = realm.getOrCreate(value)
} else {
}
}
is SessionField.Bankroll -> bankrollName = value
is SessionField.LimitType -> session.limit = Limit.getInstance(value)?.ordinal
is SessionField.Comment -> session.comment = value
@ -274,16 +393,24 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
session.type = type.ordinal
}
}
is SessionField.TournamentPosition -> session.result?.tournamentFinalPosition = field.parse(value)?.toInt()
is SessionField.TournamentPosition -> session.result?.tournamentFinalPosition =
field.parse(value)?.toInt()
is SessionField.TournamentName -> session.tournamentName = realm.getOrCreate(value)
is SessionField.TournamentType -> session.tournamentType = TournamentType.getValueForLabel(value)?.ordinal
is SessionField.TournamentNumberOfPlayers -> session.tournamentNumberOfPlayers = field.parse(value)?.toInt()
is SessionField.TournamentType -> session.tournamentType =
TournamentType.getValueForLabel(value)?.ordinal
is SessionField.TournamentNumberOfPlayers -> session.tournamentNumberOfPlayers =
field.parse(value)?.toInt()
is SessionField.CurrencyCode -> currencyCode = value
is SessionField.CurrencyRate -> currencyRate = field.parse(value)
else -> { }
else -> {
}
}
}
}
if (bankrollName.isNullOrEmpty()) {
bankrollName = "Import"
}
session.bankroll = Bankroll.getOrCreate(realm, bankrollName ?: "Import", isLive, currencyCode, currencyRate)
@ -291,13 +418,18 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
val startDate = session.startDate
val endDate = session.endDate
val net = session.result?.net
session.result?.buyin?.let {
session.result?.buyin = it + additionalBuyins
}
return if (startDate != null && endDate != null && net != null) { // valid session
val unique = SessionUtils.unicityCheck(realm, startDate, endDate, net)
if (unique) session else null
} else { // invalid session
null
if (startDate != null && endDate != null && net != null) { // valid session
if (SessionUtils.unicityCheck(realm, startDate, endDate, net)) {
return session
}
}
session.deleteFromRealm()
return null
}

@ -15,7 +15,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="+ 1000 $"
tools:text="+ 1000 $"
app:layout_constraintEnd_toStartOf="@+id/button2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
@ -30,7 +30,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="+ 2000 $"
tools:text="+ 2000 $"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/button1"

@ -5,4 +5,44 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/counter"
style="@style/PokerAnalyticsTheme.TextView.Importcount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="550"
app:layout_constraintBottom_toTopOf="@id/save"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/save"
style="@style/PokerAnalyticsTheme.Button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toTopOf="@id/cancel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@string/save" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/cancel"
style="@style/PokerAnalyticsTheme.Button.Borderless"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@string/cancel" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -351,4 +351,15 @@
<item name="android:textSize">28sp</item>
</style>
<!-- Import -->
<style name="PokerAnalyticsTheme.TextView.Importcount">
<item name="android:textSize">72sp</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>

Loading…
Cancel
Save