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.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.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.util.* import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class ImportFragment : RealmFragment() { class ImportFragment : RealmFragment(), ImportDelegate {
val coroutineContext: CoroutineContext val coroutineContext: CoroutineContext
get() = Dispatchers.Main 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.Realm
import io.realm.RealmModel import io.realm.RealmModel
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
import timber.log.Timber
/** /**
* The various sources of CSV * The various sources of CSV
@ -77,9 +78,8 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField)
if (index >= 0) { if (index >= 0) {
count++ count++
} }
} }
Timber.d("source= ${this.source.name} > total fields = ${this.fields.size}, identified = $count")
return count == this.fields.size return count == this.fields.size
} }

@ -11,6 +11,10 @@ import java.io.Reader
class ImportException(message: String) : Exception(message) 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 * 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 * 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) { 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 * Number of commits required to commit a Realm transaction
*/ */
@ -100,6 +109,7 @@ open class CSVImporter(istream: InputStream) {
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()
} }

@ -1,11 +1,14 @@
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.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.TransactionType
import net.pokeranalytics.android.model.utils.SessionUtils import net.pokeranalytics.android.model.utils.SessionUtils
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
@ -83,7 +86,22 @@ sealed class SessionField {
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberCSVField ) : 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 Game(override var header: String) : CSVField
data class Location(override var header: String) : CSVField data class Location(override var header: String) : CSVField
data class LocationType(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 * 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 { companion object {
val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor( val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor(
@ -142,17 +161,19 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
val pokerBankrollTracker: CSVDescriptor = SessionCSVDescriptor( val pokerBankrollTracker: CSVDescriptor = SessionCSVDescriptor(
DataSource.POKER_BANKROLL_TRACKER, DataSource.POKER_BANKROLL_TRACKER,
true, true,
SessionField.Start("starttime", dateFormat = "MM/dd/yyyy HH:mm"), SessionField.Start("starttime", dateFormat = "MM/dd/yy HH:mm"),
SessionField.End("endtime", dateFormat = "MM/dd/yyyy HH:mm"), SessionField.End("endtime", dateFormat = "MM/dd/yy HH:mm"),
SessionField.SessionType("variant"), SessionField.SessionType("variant"),
SessionField.Buyin("buyin"), SessionField.Buyin("buyin"),
SessionField.CashedOut("cashout"), SessionField.CashedOut("cashout"),
SessionField.Rebuy("rebuycosts"),
SessionField.Addon("addoncosts"),
SessionField.Break("breakminutes"), SessionField.Break("breakminutes"),
SessionField.LimitType("limit"), SessionField.LimitType("limit"),
SessionField.Game("game"), SessionField.Game("game"),
SessionField.Bankroll("currency"), // same as currency code SessionField.Bankroll("currency"), // same as currency code
SessionField.Location("location"), SessionField.Location("type"),
SessionField.Comment("sessionnote"), // SessionField.Comment("sessionnote"),
SessionField.Tips("expensesfromstack"), SessionField.Tips("expensesfromstack"),
SessionField.SmallBlind("smallblind"), SessionField.SmallBlind("smallblind"),
SessionField.BigBlind("bigblind"), SessionField.BigBlind("bigblind"),
@ -209,7 +230,8 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
SessionField.LocationType("Location Type"), SessionField.LocationType("Location Type"),
SessionField.Comment("Notes"), SessionField.Comment("Notes"),
SessionField.CurrencyCode("Currency"), SessionField.CurrencyCode("Currency"),
SessionField.Blind("Stakes", callback = { value -> // $10/20 SessionField.Blind("Stakes", callback = { value ->
// $10/20
value.drop(1) value.drop(1)
val blinds = value.split("/") val blinds = value.split("/")
if (blinds.size == 2) { 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 * 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) val session = Session.newInstance(realm, this.isTournament)
@ -233,6 +335,7 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
var bankrollName: String? = null var bankrollName: String? = null
var currencyCode: String? = null var currencyCode: String? = null
var currencyRate: Double? = null var currencyRate: Double? = null
var additionalBuyins = 0.0 // rebuy + addon
fields.forEach { field -> fields.forEach { field ->
@ -246,18 +349,34 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
is SessionField.End -> { is SessionField.End -> {
session.endDate = field.parse(value) session.endDate = field.parse(value)
} }
is SessionField.StartTime -> { session.startDate?.setHourMinutes(value) } is SessionField.StartTime -> {
is SessionField.EndTime -> { session.endDate?.setHourMinutes(value) } session.startDate?.setHourMinutes(value)
}
is SessionField.EndTime -> {
session.endDate?.setHourMinutes(value)
}
is SessionField.Buyin -> session.result?.buyin = field.parse(value) is SessionField.Buyin -> session.result?.buyin = field.parse(value)
is SessionField.CashedOut -> session.result?.cashout = 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.Tips -> session.result?.tips = field.parse(value)
is SessionField.Break -> { is SessionField.Break -> {
field.parse(value)?.let { field.parse(value)?.let {
session.breakDuration = it.toLong() * 60 * 1000 session.breakDuration = it.toLong() * 60 * 1000
} }
} }
is SessionField.Game -> session.game = realm.getOrCreate(value) is SessionField.Game -> {
is SessionField.Location -> session.location = realm.getOrCreate(value) 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.Bankroll -> bankrollName = value
is SessionField.LimitType -> session.limit = Limit.getInstance(value)?.ordinal is SessionField.LimitType -> session.limit = Limit.getInstance(value)?.ordinal
is SessionField.Comment -> session.comment = value is SessionField.Comment -> session.comment = value
@ -274,31 +393,44 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
session.type = type.ordinal 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.TournamentName -> session.tournamentName = realm.getOrCreate(value)
is SessionField.TournamentType -> session.tournamentType = TournamentType.getValueForLabel(value)?.ordinal is SessionField.TournamentType -> session.tournamentType =
is SessionField.TournamentNumberOfPlayers -> session.tournamentNumberOfPlayers = field.parse(value)?.toInt() TournamentType.getValueForLabel(value)?.ordinal
is SessionField.TournamentNumberOfPlayers -> session.tournamentNumberOfPlayers =
field.parse(value)?.toInt()
is SessionField.CurrencyCode -> currencyCode = value is SessionField.CurrencyCode -> currencyCode = value
is SessionField.CurrencyRate -> currencyRate = field.parse(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) session.bankroll = Bankroll.getOrCreate(realm, bankrollName ?: "Import", isLive, currencyCode, currencyRate)
val startDate = session.startDate val startDate = session.startDate
val endDate = session.endDate val endDate = session.endDate
val net = session.result?.net val net = session.result?.net
session.result?.buyin?.let {
session.result?.buyin = it + additionalBuyins
}
return if (startDate != null && endDate != null && net != null) { // valid session if (startDate != null && endDate != null && net != null) { // valid session
val unique = SessionUtils.unicityCheck(realm, startDate, endDate, net) if (SessionUtils.unicityCheck(realm, startDate, endDate, net)) {
if (unique) session else null return session
} else { // invalid session }
null
} }
session.deleteFromRealm()
return null
} }
} }

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

@ -5,4 +5,44 @@
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/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> </androidx.constraintlayout.widget.ConstraintLayout>

@ -351,4 +351,15 @@
<item name="android:textSize">28sp</item> <item name="android:textSize">28sp</item>
</style> </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> </resources>

Loading…
Cancel
Save