Sessions CSV export first commit

od
Laurent 6 years ago
parent 294b30e86d
commit 51a524116e
  1. 6
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  2. 2
      app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt
  3. 50
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt
  4. 24
      app/src/main/java/net/pokeranalytics/android/util/FileUtils.kt
  5. 2
      app/src/main/java/net/pokeranalytics/android/util/Global.kt
  6. 40
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt
  7. 62
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt
  8. 254
      app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt
  9. 49
      app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt
  10. 370
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt
  11. 38
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt
  12. 118
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt
  13. 2
      build.gradle
  14. 4
      gradle/wrapper/gradle-wrapper.properties

@ -48,9 +48,9 @@ typealias BB = Double
open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDataSource, RowRepresentable, Timed, open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDataSource, RowRepresentable, Timed,
TimeFilterable, Filterable, DatedBankrollGraphEntry { TimeFilterable, Filterable, DatedBankrollGraphEntry {
enum class Type { enum class Type(val value: String) {
CASH_GAME, CASH_GAME("Cash Game"),
TOURNAMENT; TOURNAMENT("Tournament");
companion object { companion object {

@ -53,7 +53,7 @@ open class TournamentFeature : RealmObject(), NameManageable, StaticRowRepresent
} }
override fun adapterRows(): List<RowRepresentable>? { override fun adapterRows(): List<RowRepresentable>? {
return TournamentFeature.rowRepresentation return rowRepresentation
} }
override fun stringForRow(row: RowRepresentable): String { override fun stringForRow(row: RowRepresentable): String {

@ -8,6 +8,7 @@ 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 android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm import io.realm.Realm
import kotlinx.android.synthetic.main.fragment_settings.* import kotlinx.android.synthetic.main.fragment_settings.*
@ -24,19 +25,25 @@ import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.openContactMail import net.pokeranalytics.android.ui.extensions.openContactMail
import net.pokeranalytics.android.ui.extensions.openPlayStorePage import net.pokeranalytics.android.ui.extensions.openPlayStorePage
import net.pokeranalytics.android.ui.extensions.openUrl import net.pokeranalytics.android.ui.extensions.openUrl
import net.pokeranalytics.android.ui.fragment.components.BaseFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SettingRow import net.pokeranalytics.android.ui.view.rowrepresentable.SettingRow
import net.pokeranalytics.android.util.FileUtils
import net.pokeranalytics.android.util.Preferences import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.URL import net.pokeranalytics.android.util.URL
import net.pokeranalytics.android.util.UserDefaults import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.billing.AppGuard import net.pokeranalytics.android.util.billing.AppGuard
import net.pokeranalytics.android.util.billing.IAPProducts import net.pokeranalytics.android.util.billing.IAPProducts
import net.pokeranalytics.android.util.csv.ProductCSVDescriptors
import net.pokeranalytics.android.util.extensions.fullDateTime
import net.pokeranalytics.android.util.extensions.shortDateTime
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.io.IOException
import java.util.* import java.util.*
class SettingsFragment : BaseFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource { class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource {
companion object { companion object {
@ -129,7 +136,7 @@ class SettingsFragment : BaseFragment(), RowRepresentableDelegate, StaticRowRepr
SettingRow.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path) SettingRow.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path)
SettingRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value) SettingRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value)
SettingRow.EXPORT_CSV -> { SettingRow.EXPORT_CSV -> {
this.csvExport()
} }
SettingRow.FOLLOW_US -> { SettingRow.FOLLOW_US -> {
when (position) { when (position) {
@ -151,6 +158,7 @@ class SettingsFragment : BaseFragment(), RowRepresentableDelegate, StaticRowRepr
} }
} }
/** /**
* Init UI * Init UI
*/ */
@ -202,4 +210,40 @@ class SettingsFragment : BaseFragment(), RowRepresentableDelegate, StaticRowRepr
} }
} }
private fun csvExport() {
val sessions = getRealm().where(Session::class.java).findAll().sort("startDate")
val csv = ProductCSVDescriptors.pokerAnalyticsAndroid.toCSV(sessions)
try {
val fileName = "sessions_${Date().shortDateTime()}"
FileUtils.writeToFile(csv, fileName, requireContext())
this.shareFile(fileName)
} catch (e: IOException) {
Toast.makeText(requireContext(), "File write failed: ${e.message}", Toast.LENGTH_LONG).show()
}
}
private fun shareFile(filePath: String) {
val intentShareFile = Intent(Intent.ACTION_SEND)
val fileWithinMyDir = File(filePath)
if (fileWithinMyDir.exists()) {
intentShareFile.type = "application/pdf"
intentShareFile.putExtra(
Intent.EXTRA_STREAM,
Uri.parse("file://$filePath")
)
intentShareFile.putExtra(
Intent.EXTRA_SUBJECT,
"Sharing File..."
)
intentShareFile.putExtra(Intent.EXTRA_TEXT, "Sharing File...")
startActivity(Intent.createChooser(intentShareFile, "Share File"))
}
}
} }

@ -0,0 +1,24 @@
package net.pokeranalytics.android.util
import android.content.Context
import java.io.OutputStreamWriter
class FileUtils {
companion object{
/***
* Writes a [string] into a file named [fileName], using a [context]
* Should be surrounded by a try/catch IOException
*/
fun writeToFile(string: String, fileName: String, context: Context) {
val outputStreamWriter = OutputStreamWriter(context.openFileOutput(fileName, Context.MODE_PRIVATE))
outputStreamWriter.write(string)
outputStreamWriter.close()
}
}
}

@ -1,5 +1,3 @@
package net.pokeranalytics.android.util package net.pokeranalytics.android.util
import android.content.Context
const val NULL_TEXT: String = "--" const val NULL_TEXT: String = "--"

@ -16,7 +16,7 @@ enum class DataSource {
POKER_ANALYTICS, POKER_ANALYTICS,
POKER_INCOME, POKER_INCOME,
POKER_BANKROLL_TRACKER, POKER_BANKROLL_TRACKER,
RUNGOOD, RUN_GOOD,
POKER_AGENT POKER_AGENT
} }
@ -75,24 +75,27 @@ abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg el
this.realmModelIds.clear() this.realmModelIds.clear()
} }
val csvHeaders: String fun toCSV(dataSequence: List<T>): String {
get() {
val headers = mutableListOf<String>()
this.fields.forEach {
headers.add(it.header)
}
return headers.joinToString(",")
}
fun toCSV(data: T): String { val lines = mutableListOf<String>()
lines.add(this.csvHeaders)
val fields = mutableListOf<String>() dataSequence.forEach { data ->
this.fields.forEach { val line = mutableListOf<String>()
// fields.add(it.toCSV(data)) this.fields.forEach { field ->
line.add(this.toCSV(data, field))
}
lines.add(line.joinToString(","))
} }
return fields.joinToString(",") return lines.joinToString("\n")
}
protected open fun toCSV(data: T, field: CSVField): String {
return ""
} }
// abstract fun
} }
/** /**
@ -149,4 +152,13 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField)
return count >= mandatoryfields.size return count >= mandatoryfields.size
} }
protected val csvHeaders: String
get() {
val headers = mutableListOf<String>()
this.fields.forEach {
headers.add(it.header)
}
return headers.joinToString(",")
}
} }

@ -1,5 +1,6 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import net.pokeranalytics.android.model.realm.TournamentFeature
import timber.log.Timber import timber.log.Timber
import java.text.DateFormat import java.text.DateFormat
import java.text.NumberFormat import java.text.NumberFormat
@ -52,6 +53,14 @@ interface NumberCSVField: TypedCSVField<Double> {
null null
} }
} }
override fun format(data: Double?): String? {
return if (data != null) {
NumberFormat.getInstance().format(data)
} else {
null
}
}
} }
interface IntCSVField: TypedCSVField<Int> { interface IntCSVField: TypedCSVField<Int> {
@ -69,19 +78,14 @@ interface IntCSVField: TypedCSVField<Int> {
null null
} }
} }
}
interface DataCSVField<T> : TypedCSVField<T> {
override fun parse(value: String): T? {
this.callback?.let { override fun format(data: Int?): String? {
return it(value) return if (data != null) {
NumberFormat.getInstance().format(data)
} else {
null
} }
return null
} }
} }
interface DateCSVField : TypedCSVField<Date> { interface DateCSVField : TypedCSVField<Date> {
@ -99,6 +103,29 @@ interface DateCSVField : TypedCSVField<Date> {
} }
} }
override fun format(data: Date?): String? {
return if (data != null) {
val formatter = if (dateFormat != null) SimpleDateFormat(dateFormat) else SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
formatter.format(data)
} else {
null
}
}
}
interface TournamentFeaturesCSVField : TypedCSVField<List<TournamentFeature>> {
override fun parse(value: String): List<TournamentFeature>? {
this.callback?.let {
return it(value)
}
return null
}
override fun format(data: List<TournamentFeature>?): String? {
return data?.joinToString(",") { it.name }
}
} }
interface BlindCSVField : TypedCSVField<Pair<Double, Double>> { interface BlindCSVField : TypedCSVField<Pair<Double, Double>> {
@ -121,16 +148,31 @@ interface BlindCSVField : TypedCSVField<Pair<Double, Double>> {
return null return null
} }
override fun format(data: Pair<Double, Double>?): String? {
data?.let {
val sb = NumberFormat.getInstance().format(data.first)
val bb = NumberFormat.getInstance().format(data.second)
return "$sb/$bb"
} ?: run {
return null
}
}
} }
interface BooleanCSVField : TypedCSVField<Boolean> { interface BooleanCSVField : TypedCSVField<Boolean> {
override fun parse(value: String): Boolean? { override fun parse(value: String): Boolean? {
return value == "1" return value == "1"
} }
override fun format(data: Boolean?): String {
return if (data != null && data) "1" else "0"
}
} }
interface TypedCSVField<T> : CSVField { interface TypedCSVField<T> : CSVField {
fun parse(value: String) : T? fun parse(value: String) : T?
fun format(data: T?): String?
var callback: ((String) -> T?)? var callback: ((String) -> T?)?
} }

@ -0,0 +1,254 @@
package net.pokeranalytics.android.util.csv
import net.pokeranalytics.android.model.interfaces.Identifiable
import io.realm.Realm
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.model.TournamentType
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.utils.DataUtils
import net.pokeranalytics.android.util.extensions.getOrCreate
import net.pokeranalytics.android.util.extensions.setHourMinutes
import org.apache.commons.csv.CSVRecord
import timber.log.Timber
import java.util.*
abstract class PACSVDescriptor<T : Identifiable>(source: DataSource, private var isTournament: Boolean?, vararg elements: CSVField) : DataCSVDescriptor<T>(source, *elements) {
private var sameDaySessionCount: Int = 0
private var currentDay: String = ""
private var startInSeconds: Double = 20 * 3600.0
/**
* Parses a [record] and return an optional Session
*/
protected fun parseSession(realm: Realm, record: CSVRecord): Session? {
val isTournament = isTournament ?: false
val session = Session.newInstance(realm, isTournament, managed = false)
var startDate: Date? = null
var endDate: Date? = null
var isLive = true
var bankrollName = ""
var currencyCode: String? = null
var currencyRate: Double? = null
var additionalBuyins = 0.0 // rebuy + addon
var stackingIn: Double? = null
var stackingOut: Double? = null
this.fields.forEach { field ->
this.fieldMapping[field]?.let { index ->
val value = record.get(index)
when (field) {
is SessionField.Start -> {
startDate = field.parse(value)
if (source == DataSource.POKER_AGENT) {
if (currentDay == value) {
sameDaySessionCount++
} else {
sameDaySessionCount = 0
}
currentDay = value
} else {}
}
is SessionField.End -> {
endDate = field.parse(value)
}
is SessionField.StartTime -> {
startDate?.setHourMinutes(value)
}
is SessionField.EndTime -> {
endDate?.setHourMinutes(value)
}
is SessionField.Duration -> {
val hoursDuration = field.parse(value) ?: throw PAIllegalStateException("null duration")
if (startDate != null) {
if (field.randomTime) {
if (sameDaySessionCount == 0) {
startInSeconds = 20 * 3600.0
} else {
startInSeconds -= hoursDuration * 3600.0
}
if (startInSeconds < 0) {
startInSeconds = 20 * 3600.0
// throw PAIllegalStateException("negative start: $startDate, start = $startInSeconds, net = ${session.result?.netResult}")
}
val hour = (startInSeconds / 3600.0).toInt()
val minutes = ((startInSeconds - hour * 3600.0) / 60.0).toInt()
val formattedTime = "$hour:$minutes"
startDate?.setHourMinutes(formattedTime)
}
val seconds = (hoursDuration * 3600.0).toInt()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.add(Calendar.SECOND, seconds)
endDate = calendar.time
} else {
throw PAIllegalStateException("start date ($startDate) + hoursDuration ($hoursDuration) required")
}
}
is SessionField.Buyin -> {
val buyin = field.parse(value)
session.result?.buyin = buyin
if (session.type == Session.Type.TOURNAMENT.ordinal) {
session.tournamentEntryFee = buyin
} else {
}
}
is SessionField.CashedOut -> session.result?.cashout = field.parse(value)
is SessionField.NetResult -> session.result?.netResult = field.parse(value)
is SessionField.SessionType -> {
Session.Type.getValueFromString(value)?.let { type ->
session.type = type.ordinal
}
}
is SessionField.Live -> isLive = field.parse(value) ?: false
is SessionField.NumberOfTables -> session.numberOfTables = field.parse(value) ?: 1
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()
}
}
is SessionField.LimitAndGame -> {
if (value.isNotEmpty()) {
var limitAndGame = value
for (someLimit in Limit.values()) {
if (value.startsWith(someLimit.longName)) {
session.limit = someLimit.ordinal
limitAndGame = limitAndGame.removePrefix(someLimit.longName)
break
}
}
session.game = realm.getOrCreate(limitAndGame.trim())
} else {
}
}
is SessionField.Game -> {
if (value.isNotEmpty()) {
session.game = realm.getOrCreate(value)
} else {
}
}
is SessionField.Location -> {
val trimmedValue = value.trim()
if (trimmedValue.isNotEmpty()) {
session.location = realm.getOrCreate(trimmedValue)
} else {
}
}
is SessionField.Bankroll -> bankrollName = value
is SessionField.LimitType -> session.limit = Limit.getInstance(value)?.ordinal
is SessionField.Comment -> session.comment = value
is SessionField.Blind -> { // 1/2
val blinds = field.parse(value)
session.cgSmallBlind = blinds?.first
session.cgBigBlind = blinds?.second
}
is SessionField.SmallBlind -> {
val sb = field.parse(value)
if (sb != null && sb > 0.0) {
session.cgSmallBlind = sb
} else {}
}
is SessionField.BigBlind -> {
val bb = field.parse(value)
if (bb != null && bb > 0.0) {
session.cgBigBlind = bb
} else {}
}
is SessionField.TableSize -> session.tableSize = TableSize.valueForLabel(value)
is SessionField.TournamentPosition -> session.result?.tournamentFinalPosition =
field.parse(value)
is SessionField.TournamentName -> {
if (value.isNotEmpty()) {
session.tournamentName = realm.getOrCreate(value)
} else {
}
}
is SessionField.TournamentTypeName -> session.tournamentType =
TournamentType.getValueForLabel(value)?.ordinal
is SessionField.TournamentNumberOfPlayers -> session.tournamentNumberOfPlayers =
field.parse(value)
is SessionField.TournamentEntryFee -> session.tournamentEntryFee = field.parse(value)
is SessionField.TournamentFeatures -> {
value.split(",").forEach { featureName ->
val tournamentFeature: TournamentFeature = realm.getOrCreate(featureName)
session.tournamentFeatures.add(tournamentFeature)
}
}
is SessionField.CurrencyCode -> currencyCode = value
is SessionField.CurrencyRate -> currencyRate = field.parse(value)
is SessionField.StackingIn -> {
stackingIn = field.parse(value)
}
is SessionField.StackingOut -> {
stackingOut = field.parse(value)
}
else -> {
}
}
}
}
if (bankrollName.isEmpty()) {
bankrollName = "Import"
}
val bankroll = Bankroll.getOrCreate(realm, bankrollName, isLive, currencyCode, currencyRate)
session.bankroll = bankroll
session.result?.buyin?.let {
session.result?.buyin = it + additionalBuyins
}
val net = session.result?.net
if (startDate != null && endDate != null && net != null) { // valid session
// session already in realm, we'd love not put it in Realm before doing the check
val count = DataUtils.sessionCount(realm, startDate!!, endDate!!, net)
if (count == 0) {
val managedSession = realm.copyToRealm(session)
managedSession.startDate = startDate
managedSession.endDate = endDate
if (stackingIn != null && stackingIn != 0.0) {
val type = TransactionType.getByValue(TransactionType.Value.STACKING_INCOMING, realm)
val transaction = Transaction.newInstance(realm, bankroll, startDate, type, stackingIn!!)
this.addAdditionallyCreatedIdentifiable(transaction)
}
if (stackingOut != null && stackingOut != 0.0) {
val type = TransactionType.getByValue(TransactionType.Value.STACKING_OUTGOING, realm)
val transaction = Transaction.newInstance(realm, bankroll, startDate, type, stackingOut!!)
this.addAdditionallyCreatedIdentifiable(transaction)
}
return managedSession
} else {
Timber.d("Session already exists(count=$count): sd=$startDate, ed=$endDate, net=$net")
}
} else {
Timber.d("Can't import session: sd=$startDate, ed=$endDate, net=$net")
}
return null
}
}

@ -16,7 +16,8 @@ class ProductCSVDescriptors {
pokerBankrollTracker, pokerBankrollTracker,
runGoodCashGames, runGoodCashGames,
runGoodTournaments, runGoodTournaments,
pokerAnalyticsiOS pokerAnalyticsiOS,
pokerAnalyticsAndroid
) )
private val pokerAgent: CSVDescriptor private val pokerAgent: CSVDescriptor
@ -57,7 +58,7 @@ class ProductCSVDescriptors {
private val pokerBankrollTracker: CSVDescriptor private val pokerBankrollTracker: CSVDescriptor
get() { get() {
return SessionCSVDescriptor( return SessionTransactionCSVDescriptor(
DataSource.POKER_BANKROLL_TRACKER, DataSource.POKER_BANKROLL_TRACKER,
true, true,
SessionField.Start("starttime", dateFormat = "MM/dd/yy HH:mm"), SessionField.Start("starttime", dateFormat = "MM/dd/yy HH:mm"),
@ -90,7 +91,7 @@ class ProductCSVDescriptors {
private val runGoodTournaments: CSVDescriptor private val runGoodTournaments: CSVDescriptor
get() { get() {
return SessionCSVDescriptor( return SessionCSVDescriptor(
DataSource.RUNGOOD, DataSource.RUN_GOOD,
true, true,
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"), SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"),
SessionField.StartTime("Start Time"), SessionField.StartTime("Start Time"),
@ -111,7 +112,7 @@ class ProductCSVDescriptors {
SessionField.TournamentName("Event Name"), SessionField.TournamentName("Event Name"),
SessionField.TournamentNumberOfPlayers("Total Players"), SessionField.TournamentNumberOfPlayers("Total Players"),
SessionField.TournamentPosition("Finished Place"), SessionField.TournamentPosition("Finished Place"),
SessionField.TournamentType("Single-Table/Multi-Table") SessionField.TournamentTypeName("Single-Table/Multi-Table")
) )
} }
@ -121,7 +122,7 @@ class ProductCSVDescriptors {
get() { get() {
return SessionCSVDescriptor( return SessionCSVDescriptor(
DataSource.RUNGOOD, DataSource.RUN_GOOD,
false, false,
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"), SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"),
SessionField.StartTime("Start Time", dateFormat = "HH:mm"), SessionField.StartTime("Start Time", dateFormat = "HH:mm"),
@ -155,7 +156,7 @@ class ProductCSVDescriptors {
) )
} }
private val pokerAnalyticsiOS: CSVDescriptor val pokerAnalyticsiOS: SessionCSVDescriptor
get() { get() {
return SessionCSVDescriptor( return SessionCSVDescriptor(
DataSource.POKER_ANALYTICS, DataSource.POKER_ANALYTICS,
@ -179,7 +180,7 @@ class ProductCSVDescriptors {
SessionField.CurrencyRate("Currency Rate"), SessionField.CurrencyRate("Currency Rate"),
SessionField.SmallBlind("Small Blind"), SessionField.SmallBlind("Small Blind"),
SessionField.BigBlind("Big Blind"), SessionField.BigBlind("Big Blind"),
SessionField.TournamentType("Tournament Type"), SessionField.TournamentTypeName("Tournament Type"),
SessionField.TournamentEntryFee("Entry fee"), SessionField.TournamentEntryFee("Entry fee"),
SessionField.TournamentNumberOfPlayers("Number of players"), SessionField.TournamentNumberOfPlayers("Number of players"),
SessionField.TournamentPrizePool("Prize Pool"), SessionField.TournamentPrizePool("Prize Pool"),
@ -188,6 +189,40 @@ class ProductCSVDescriptors {
) )
} }
val pokerAnalyticsAndroid: SessionCSVDescriptor
get() {
return SessionCSVDescriptor(
DataSource.POKER_ANALYTICS,
true,
SessionField.Start("Start Date", dateFormat = "MM/dd/yy HH:mm:ss"),
SessionField.End("End Date", dateFormat = "MM/dd/yy HH:mm:ss"),
SessionField.Break("Break", Calendar.SECOND),
SessionField.SessionType("Type"),
SessionField.Live("Live"),
SessionField.NumberOfTables("Tables"),
SessionField.Buyin("Buyin"),
SessionField.CashedOut("Cashed Out"),
SessionField.NetResult("Online Net"),
SessionField.Tips("Tips"),
SessionField.LimitType("Limit"),
SessionField.Game("Game"),
SessionField.TableSize("Table Size"),
SessionField.Location("Location"),
SessionField.Bankroll("Bankroll"),
SessionField.CurrencyCode("Currency Code"),
SessionField.CurrencyRate("Currency Rate"),
SessionField.SmallBlind("Small Blind"),
SessionField.BigBlind("Big Blind"),
SessionField.TournamentType("Tournament Type"),
SessionField.TournamentName("Tournament Name"),
SessionField.TournamentEntryFee("Entry fee"),
SessionField.TournamentNumberOfPlayers("Number of players"),
SessionField.TournamentFeatures("Number of players"),
SessionField.TournamentPrizePool("Prize Pool"),
SessionField.TournamentPosition("Position"),
SessionField.Comment("Comment")
)
}
} }
} }

@ -1,351 +1,55 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.TableSize
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.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.DataUtils
import net.pokeranalytics.android.util.extensions.getOrCreate
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.*
/** /**
* 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, isTournament: Boolean?, vararg elements: CSVField) :
DataCSVDescriptor<Identifiable>(source, *elements) { PACSVDescriptor<Session>(source, isTournament, *elements) {
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): Identifiable? {
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)
}
override fun parseData(realm: Realm, record: CSVRecord): Session? {
return this.parseSession(realm, record)
} }
private fun parseTransaction(realm: Realm, record: CSVRecord): Transaction? { override fun toCSV(data: Session, field: CSVField): String {
val string = when (field) {
var date: Date? = null is SessionField.Start -> field.format(data.startDate)
var type: TransactionType? = null is SessionField.End -> field.format(data.endDate)
var currencyCode: String? = null is SessionField.Break -> field.format(data.breakDuration.toDouble())
var currencyRate: Double? = null is SessionField.SessionType -> Session.Type.values()[data.type].value
is SessionField.Live -> field.format(data.isLive)
// Poker Bankroll Tracker specifics is SessionField.NumberOfTables -> field.format(data.numberOfTables)
var buyin: Double? = null is SessionField.Buyin -> field.format(data.result?.buyin)
var cashedOut: Double? = null is SessionField.CashedOut -> field.format(data.result?.cashout)
is SessionField.NetResult -> field.format(data.result?.netResult)
fields.forEach { field -> is SessionField.Tips -> field.format(data.result?.tips)
is SessionField.LimitType -> {
val index = this.fieldMapping[field] data.limit?.let { limit ->
if (index != null) { Limit.values()[limit].longName
val value = record.get(index)
when (field) {
is SessionField.Start -> {
date = field.parse(value)
}
is SessionField.Buyin -> buyin = field.parse(value)
is SessionField.CashedOut -> cashedOut = field.parse(value)
is SessionField.CurrencyCode -> currencyCode = value
is SessionField.CurrencyRate -> currencyRate = field.parse(value)
else -> {
}
} }
} }
} is SessionField.Game -> data.game?.name
is SessionField.TableSize -> data.tableSize?.toString()
val amount = if (buyin != null && buyin!! > 0) { is SessionField.Location -> data.location?.name
type = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm) is SessionField.Bankroll -> data.bankroll?.name
buyin!! * -1 is SessionField.CurrencyCode -> data.bankroll?.currency?.code
} else if (cashedOut != null && cashedOut!! > 0) { is SessionField.CurrencyRate -> field.format(data.bankroll?.currency?.rate)
type = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm) is SessionField.SmallBlind -> field.format(data.cgSmallBlind)
cashedOut is SessionField.BigBlind -> field.format(data.cgBigBlind)
} else { is SessionField.TournamentType -> field.format(data.tournamentType)
null is SessionField.TournamentName -> data.tournamentName?.name
} is SessionField.TournamentFeatures -> field.format(data.tournamentFeatures)
is SessionField.TournamentEntryFee -> field.format(data.tournamentEntryFee)
if (date != null && amount != null && type != null && currencyCode != null) { is SessionField.TournamentNumberOfPlayers -> field.format(data.tournamentNumberOfPlayers)
is SessionField.TournamentPosition -> field.format(data.result?.tournamentFinalPosition)
if (DataUtils.transactionUnicityCheck(realm, date!!, amount, type)) { is SessionField.Comment -> data.comment
else -> null
val bankroll = Bankroll.getOrCreate( }
realm, return string ?: ""
currencyCode!!,
currencyCode = currencyCode!!,
currencyRate = currencyRate
)
return Transaction.newInstance(realm, bankroll, date!!, type, amount)
} else {
Timber.d("Transaction already exists")
}
} else {
Timber.d("Can't import transaction: date=$date, amount=$amount, type=${type?.name}")
}
return null
}
private var sameDaySessionCount: Int = 0
private var currentday: String = ""
private var startInSeconds: Double = 20 * 3600.0
private fun parseSession(realm: Realm, record: CSVRecord): Session? {
val isTournament = isTournament ?: false
val session = Session.newInstance(realm, isTournament, managed = false)
var startDate: Date? = null
var endDate: Date? = null
var isLive = true
var bankrollName = ""
var currencyCode: String? = null
var currencyRate: Double? = null
var additionalBuyins = 0.0 // rebuy + addon
var stackingIn: Double? = null
var stackingOut: Double? = null
fields.forEach { field ->
this.fieldMapping[field]?.let { index ->
val value = record.get(index)
when (field) {
is SessionField.Start -> {
startDate = field.parse(value)
if (source == DataSource.POKER_AGENT) {
if (currentday == value) {
sameDaySessionCount++
} else {
sameDaySessionCount = 0
}
currentday = value
} else {}
}
is SessionField.End -> {
endDate = field.parse(value)
}
is SessionField.StartTime -> {
startDate?.setHourMinutes(value)
}
is SessionField.EndTime -> {
endDate?.setHourMinutes(value)
}
is SessionField.Duration -> {
val hoursDuration = field.parse(value) ?: throw PAIllegalStateException("null duration")
if (startDate != null) {
if (field.randomTime) {
if (sameDaySessionCount == 0) {
startInSeconds = 20 * 3600.0
} else {
startInSeconds -= hoursDuration * 3600.0
}
if (startInSeconds < 0) {
startInSeconds = 20 * 3600.0
// throw PAIllegalStateException("negative start: $startDate, start = $startInSeconds, net = ${session.result?.netResult}")
}
val hour = (startInSeconds / 3600.0).toInt()
val minutes = ((startInSeconds - hour * 3600.0) / 60.0).toInt()
val formattedTime = "$hour:$minutes"
startDate?.setHourMinutes(formattedTime)
}
val seconds = (hoursDuration * 3600.0).toInt()
val calendar = Calendar.getInstance()
calendar.time = startDate
calendar.add(Calendar.SECOND, seconds)
endDate = calendar.time
} else {
throw PAIllegalStateException("start date ($startDate) + hoursDuration ($hoursDuration) required")
}
}
is SessionField.Buyin -> {
val buyin = field.parse(value)
session.result?.buyin = buyin
if (session.type == Session.Type.TOURNAMENT.ordinal) {
session.tournamentEntryFee = buyin
} else {
}
}
is SessionField.CashedOut -> session.result?.cashout = field.parse(value)
is SessionField.NetResult -> session.result?.netResult = field.parse(value)
is SessionField.SessionType -> {
Session.Type.getValueFromString(value)?.let { type ->
session.type = type.ordinal
}
}
is SessionField.Live -> isLive = field.parse(value) ?: false
is SessionField.NumberOfTables -> session.numberOfTables = field.parse(value) ?: 1
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()
}
}
is SessionField.LimitAndGame -> {
if (value.isNotEmpty()) {
var limitAndGame = value
for (someLimit in Limit.values()) {
if (value.startsWith(someLimit.longName)) {
session.limit = someLimit.ordinal
limitAndGame = limitAndGame.removePrefix(someLimit.longName)
break
}
}
session.game = realm.getOrCreate(limitAndGame.trim())
} else {
}
}
is SessionField.Game -> {
if (value.isNotEmpty()) {
session.game = realm.getOrCreate(value)
} else {
}
}
is SessionField.Location -> {
val trimmedValue = value.trim()
if (trimmedValue.isNotEmpty()) {
session.location = realm.getOrCreate(trimmedValue)
} else {
}
}
is SessionField.Bankroll -> bankrollName = value
is SessionField.LimitType -> session.limit = Limit.getInstance(value)?.ordinal
is SessionField.Comment -> session.comment = value
is SessionField.Blind -> { // 1/2
val blinds = field.parse(value)
session.cgSmallBlind = blinds?.first
session.cgBigBlind = blinds?.second
}
is SessionField.SmallBlind -> {
val sb = field.parse(value)
if (sb != null && sb > 0.0) {
session.cgSmallBlind = sb
} else {}
}
is SessionField.BigBlind -> {
val bb = field.parse(value)
if (bb != null && bb > 0.0) {
session.cgBigBlind = bb
} else {}
}
is SessionField.TableSize -> session.tableSize = TableSize.valueForLabel(value)
is SessionField.TournamentPosition -> session.result?.tournamentFinalPosition =
field.parse(value)?.toInt()
is SessionField.TournamentName -> {
if (value.isNotEmpty()) {
session.tournamentName = realm.getOrCreate(value)
} else {
}
}
is SessionField.TournamentType -> session.tournamentType =
TournamentType.getValueForLabel(value)?.ordinal
is SessionField.TournamentNumberOfPlayers -> session.tournamentNumberOfPlayers =
field.parse(value)?.toInt()
is SessionField.TournamentEntryFee -> session.tournamentEntryFee = field.parse(value)
is SessionField.CurrencyCode -> currencyCode = value
is SessionField.CurrencyRate -> currencyRate = field.parse(value)
is SessionField.StackingIn -> {
stackingIn = field.parse(value)
}
is SessionField.StackingOut -> {
stackingOut = field.parse(value)
}
else -> {
}
}
}
}
if (bankrollName.isEmpty()) {
bankrollName = "Import"
}
val bankroll = Bankroll.getOrCreate(realm, bankrollName, isLive, currencyCode, currencyRate)
session.bankroll = bankroll
session.result?.buyin?.let {
session.result?.buyin = it + additionalBuyins
}
val net = session.result?.net
if (startDate != null && endDate != null && net != null) { // valid session
// session already in realm, we'd love not put it in Realm before doing the check
val count = DataUtils.sessionCount(realm, startDate!!, endDate!!, net)
if (count == 0) {
val managedSession = realm.copyToRealm(session)
managedSession.startDate = startDate
managedSession.endDate = endDate
if (stackingIn != null && stackingIn != 0.0) {
val type = TransactionType.getByValue(TransactionType.Value.STACKING_INCOMING, realm)
val transaction = Transaction.newInstance(realm, bankroll, startDate, type, stackingIn!!)
this.addAdditionallyCreatedIdentifiable(transaction)
}
if (stackingOut != null && stackingOut != 0.0) {
val type = TransactionType.getByValue(TransactionType.Value.STACKING_OUTGOING, realm)
val transaction = Transaction.newInstance(realm, bankroll, startDate, type, stackingOut!!)
this.addAdditionallyCreatedIdentifiable(transaction)
}
return managedSession
} else {
Timber.d("Session already exists(count=$count): sd=$startDate, ed=$endDate, net=$net")
}
} else {
Timber.d("Can't import session: sd=$startDate, ed=$endDate, net=$net")
}
return null
} }
} }

@ -1,16 +1,17 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.TournamentFeature
import java.util.* import java.util.*
sealed class TransactionField { //sealed class TransactionField {
//
data class TransactionType( // data class TransactionType(
override var header: String, // override var header: String,
override var callback: ((String) -> net.pokeranalytics.android.model.realm.TransactionType?)? = null // override var callback: ((String) -> net.pokeranalytics.android.model.realm.TransactionType?)? = null
) : DataCSVField<net.pokeranalytics.android.model.realm.TransactionType> // ) : DataCSVField<net.pokeranalytics.android.model.realm.TransactionType>
//
} //}
/** /**
* The enumeration of Session fields * The enumeration of Session fields
@ -156,7 +157,11 @@ sealed class SessionField {
data class TableSize(override var header: String) : CSVField data class TableSize(override var header: String) : CSVField
data class CurrencyCode(override var header: String) : CSVField data class CurrencyCode(override var header: String) : CSVField
data class TournamentName(override var header: String) : CSVField data class TournamentName(override var header: String) : CSVField
data class TournamentType(override var header: String) : CSVField data class TournamentTypeName(override var header: String) : CSVField
data class TournamentFeatures(override var header: String,
override var callback: ((String) -> List<TournamentFeature>?)? = null
) : TournamentFeaturesCSVField
data class CurrencyRate( data class CurrencyRate(
override var header: String, override var header: String,
@ -166,15 +171,18 @@ sealed class SessionField {
data class TournamentPosition( data class TournamentPosition(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Int?)? = null
override val numberFormat: String? = null ) : IntCSVField
) : NumberCSVField
data class TournamentNumberOfPlayers( data class TournamentNumberOfPlayers(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Int?)? = null
override val numberFormat: String? = null ) : IntCSVField
) : NumberCSVField
data class TournamentType(
override var header: String,
override var callback: ((String) -> Int?)? = null
) : IntCSVField
data class TournamentEntryFee( data class TournamentEntryFee(
override var header: String, override var header: String,

@ -0,0 +1,118 @@
package net.pokeranalytics.android.util.csv
import io.realm.Realm
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.realm.Bankroll
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.TransactionType
import net.pokeranalytics.android.model.utils.DataUtils
import org.apache.commons.csv.CSVRecord
import timber.log.Timber
import java.util.*
/**
* A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects
*/
class SessionTransactionCSVDescriptor(source: DataSource, private var isTournament: Boolean?, vararg elements: CSVField) :
PACSVDescriptor<Identifiable>(source, isTournament, *elements) {
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): Identifiable? {
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)
}
}
private fun parseTransaction(realm: Realm, record: CSVRecord): Transaction? {
var date: Date? = null
var type: TransactionType? = null
var currencyCode: String? = null
var currencyRate: Double? = null
// Poker Bankroll Tracker specifics
var buyin: Double? = null
var cashedOut: 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 -> buyin = field.parse(value)
is SessionField.CashedOut -> cashedOut = field.parse(value)
is SessionField.CurrencyCode -> currencyCode = value
is SessionField.CurrencyRate -> currencyRate = field.parse(value)
else -> {
}
}
}
}
val amount = if (buyin != null && buyin!! > 0) {
type = TransactionType.getByValue(TransactionType.Value.WITHDRAWAL, realm)
buyin!! * -1
} else if (cashedOut != null && cashedOut!! > 0) {
type = TransactionType.getByValue(TransactionType.Value.DEPOSIT, realm)
cashedOut
} else {
null
}
if (date != null && amount != null && type != null && currencyCode != null) {
if (DataUtils.transactionUnicityCheck(realm, date!!, amount, type)) {
val bankroll = Bankroll.getOrCreate(
realm,
currencyCode!!,
currencyCode = currencyCode!!,
currencyRate = currencyRate
)
return Transaction.newInstance(realm, bankroll, date!!, type, amount)
} else {
Timber.d("Transaction already exists")
}
} else {
Timber.d("Can't import transaction: date=$date, amount=$amount, type=${type?.name}")
}
return null
}
}

@ -10,7 +10,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.3' classpath 'com.android.tools.build:gradle:3.6.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'io.realm:realm-gradle-plugin:5.8.0' classpath 'io.realm:realm-gradle-plugin:5.8.0'
classpath 'com.google.gms:google-services:4.2.0' classpath 'com.google.gms:google-services:4.2.0'

@ -1,6 +1,6 @@
#Mon Aug 26 10:00:03 CEST 2019 #Wed May 13 12:21:50 CEST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

Loading…
Cancel
Save