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. 376
      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,
TimeFilterable, Filterable, DatedBankrollGraphEntry {
enum class Type {
CASH_GAME,
TOURNAMENT;
enum class Type(val value: String) {
CASH_GAME("Cash Game"),
TOURNAMENT("Tournament");
companion object {

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

@ -8,6 +8,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.Realm
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.openPlayStorePage
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.SettingRow
import net.pokeranalytics.android.util.FileUtils
import net.pokeranalytics.android.util.Preferences
import net.pokeranalytics.android.util.URL
import net.pokeranalytics.android.util.UserDefaults
import net.pokeranalytics.android.util.billing.AppGuard
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 java.io.File
import java.io.IOException
import java.util.*
class SettingsFragment : BaseFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource {
class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRepresentableDataSource {
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.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value)
SettingRow.EXPORT_CSV -> {
this.csvExport()
}
SettingRow.FOLLOW_US -> {
when (position) {
@ -151,6 +158,7 @@ class SettingsFragment : BaseFragment(), RowRepresentableDelegate, StaticRowRepr
}
}
/**
* 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
import android.content.Context
const val NULL_TEXT: String = "--"

@ -16,7 +16,7 @@ enum class DataSource {
POKER_ANALYTICS,
POKER_INCOME,
POKER_BANKROLL_TRACKER,
RUNGOOD,
RUN_GOOD,
POKER_AGENT
}
@ -75,24 +75,27 @@ abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg el
this.realmModelIds.clear()
}
val csvHeaders: String
get() {
val headers = mutableListOf<String>()
this.fields.forEach {
headers.add(it.header)
}
return headers.joinToString(",")
}
fun toCSV(dataSequence: List<T>): String {
fun toCSV(data: T): String {
val lines = mutableListOf<String>()
lines.add(this.csvHeaders)
val fields = mutableListOf<String>()
this.fields.forEach {
// fields.add(it.toCSV(data))
dataSequence.forEach { data ->
val line = mutableListOf<String>()
this.fields.forEach { field ->
line.add(this.toCSV(data, field))
}
return fields.joinToString(",")
lines.add(line.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
}
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
import net.pokeranalytics.android.model.realm.TournamentFeature
import timber.log.Timber
import java.text.DateFormat
import java.text.NumberFormat
@ -52,6 +53,14 @@ interface NumberCSVField: TypedCSVField<Double> {
null
}
}
override fun format(data: Double?): String? {
return if (data != null) {
NumberFormat.getInstance().format(data)
} else {
null
}
}
}
interface IntCSVField: TypedCSVField<Int> {
@ -69,19 +78,14 @@ interface IntCSVField: TypedCSVField<Int> {
null
}
}
}
interface DataCSVField<T> : TypedCSVField<T> {
override fun parse(value: String): T? {
this.callback?.let {
return it(value)
override fun format(data: Int?): String? {
return if (data != null) {
NumberFormat.getInstance().format(data)
} else {
null
}
return null
}
}
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>> {
@ -121,16 +148,31 @@ interface BlindCSVField : TypedCSVField<Pair<Double, Double>> {
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> {
override fun parse(value: String): Boolean? {
return value == "1"
}
override fun format(data: Boolean?): String {
return if (data != null && data) "1" else "0"
}
}
interface TypedCSVField<T> : CSVField {
fun parse(value: String) : T?
fun format(data: T?): String?
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,
runGoodCashGames,
runGoodTournaments,
pokerAnalyticsiOS
pokerAnalyticsiOS,
pokerAnalyticsAndroid
)
private val pokerAgent: CSVDescriptor
@ -57,7 +58,7 @@ class ProductCSVDescriptors {
private val pokerBankrollTracker: CSVDescriptor
get() {
return SessionCSVDescriptor(
return SessionTransactionCSVDescriptor(
DataSource.POKER_BANKROLL_TRACKER,
true,
SessionField.Start("starttime", dateFormat = "MM/dd/yy HH:mm"),
@ -90,7 +91,7 @@ class ProductCSVDescriptors {
private val runGoodTournaments: CSVDescriptor
get() {
return SessionCSVDescriptor(
DataSource.RUNGOOD,
DataSource.RUN_GOOD,
true,
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"),
SessionField.StartTime("Start Time"),
@ -111,7 +112,7 @@ class ProductCSVDescriptors {
SessionField.TournamentName("Event Name"),
SessionField.TournamentNumberOfPlayers("Total Players"),
SessionField.TournamentPosition("Finished Place"),
SessionField.TournamentType("Single-Table/Multi-Table")
SessionField.TournamentTypeName("Single-Table/Multi-Table")
)
}
@ -121,7 +122,7 @@ class ProductCSVDescriptors {
get() {
return SessionCSVDescriptor(
DataSource.RUNGOOD,
DataSource.RUN_GOOD,
false,
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"),
SessionField.StartTime("Start Time", dateFormat = "HH:mm"),
@ -155,7 +156,7 @@ class ProductCSVDescriptors {
)
}
private val pokerAnalyticsiOS: CSVDescriptor
val pokerAnalyticsiOS: SessionCSVDescriptor
get() {
return SessionCSVDescriptor(
DataSource.POKER_ANALYTICS,
@ -179,7 +180,7 @@ class ProductCSVDescriptors {
SessionField.CurrencyRate("Currency Rate"),
SessionField.SmallBlind("Small Blind"),
SessionField.BigBlind("Big Blind"),
SessionField.TournamentType("Tournament Type"),
SessionField.TournamentTypeName("Tournament Type"),
SessionField.TournamentEntryFee("Entry fee"),
SessionField.TournamentNumberOfPlayers("Number of players"),
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
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.interfaces.Identifiable
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.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.*
/**
* A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects
*/
class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean?, vararg elements: CSVField) :
DataCSVDescriptor<Identifiable>(source, *elements) {
private enum class DataType {
TRANSACTION,
SESSION;
companion object {
fun valueForString(type: String): DataType? {
return when (type) {
"Deposit/Payout" -> TRANSACTION
"Cash Game", "Tournament" -> SESSION
class SessionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg elements: CSVField) :
PACSVDescriptor<Session>(source, isTournament, *elements) {
override fun parseData(realm: Realm, record: CSVRecord): Session? {
return this.parseSession(realm, record)
}
override fun toCSV(data: Session, field: CSVField): String {
val string = when (field) {
is SessionField.Start -> field.format(data.startDate)
is SessionField.End -> field.format(data.endDate)
is SessionField.Break -> field.format(data.breakDuration.toDouble())
is SessionField.SessionType -> Session.Type.values()[data.type].value
is SessionField.Live -> field.format(data.isLive)
is SessionField.NumberOfTables -> field.format(data.numberOfTables)
is SessionField.Buyin -> field.format(data.result?.buyin)
is SessionField.CashedOut -> field.format(data.result?.cashout)
is SessionField.NetResult -> field.format(data.result?.netResult)
is SessionField.Tips -> field.format(data.result?.tips)
is SessionField.LimitType -> {
data.limit?.let { limit ->
Limit.values()[limit].longName
}
}
is SessionField.Game -> data.game?.name
is SessionField.TableSize -> data.tableSize?.toString()
is SessionField.Location -> data.location?.name
is SessionField.Bankroll -> data.bankroll?.name
is SessionField.CurrencyCode -> data.bankroll?.currency?.code
is SessionField.CurrencyRate -> field.format(data.bankroll?.currency?.rate)
is SessionField.SmallBlind -> field.format(data.cgSmallBlind)
is SessionField.BigBlind -> field.format(data.cgBigBlind)
is SessionField.TournamentType -> field.format(data.tournamentType)
is SessionField.TournamentName -> data.tournamentName?.name
is SessionField.TournamentFeatures -> field.format(data.tournamentFeatures)
is SessionField.TournamentEntryFee -> field.format(data.tournamentEntryFee)
is SessionField.TournamentNumberOfPlayers -> field.format(data.tournamentNumberOfPlayers)
is SessionField.TournamentPosition -> field.format(data.result?.tournamentFinalPosition)
is SessionField.Comment -> data.comment
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
}
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
return string ?: ""
}
}

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