From 51a524116eeaf2cd35c677cc7403e3243ae46ef1 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 13 May 2020 17:22:17 +0200 Subject: [PATCH] Sessions CSV export first commit --- .../android/model/realm/Session.kt | 6 +- .../android/model/realm/TournamentFeature.kt | 2 +- .../android/ui/fragment/SettingsFragment.kt | 50 ++- .../pokeranalytics/android/util/FileUtils.kt | 24 ++ .../net/pokeranalytics/android/util/Global.kt | 2 - .../android/util/csv/CSVDescriptor.kt | 40 +- .../android/util/csv/CSVField.kt | 62 ++- .../android/util/csv/PACSVDescriptor.kt | 254 ++++++++++++ .../android/util/csv/ProductCSVDescriptors.kt | 49 ++- .../android/util/csv/SessionCSVDescriptor.kt | 370 ++---------------- .../android/util/csv/SessionField.kt | 38 +- .../csv/SessionTransactionCSVDescriptor.kt | 118 ++++++ build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 14 files changed, 630 insertions(+), 391 deletions(-) create mode 100644 app/src/main/java/net/pokeranalytics/android/util/FileUtils.kt create mode 100644 app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt create mode 100644 app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt index 79bfee91..d887521b 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt @@ -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 { diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt index d0f937e2..61bbe997 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/TournamentFeature.kt @@ -53,7 +53,7 @@ open class TournamentFeature : RealmObject(), NameManageable, StaticRowRepresent } override fun adapterRows(): List? { - return TournamentFeature.rowRepresentation + return rowRepresentation } override fun stringForRow(row: RowRepresentable): String { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt index b4244dfc..88dbf1c4 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt @@ -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")) + } + + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/FileUtils.kt b/app/src/main/java/net/pokeranalytics/android/util/FileUtils.kt new file mode 100644 index 00000000..22c20553 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/FileUtils.kt @@ -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() + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/Global.kt b/app/src/main/java/net/pokeranalytics/android/util/Global.kt index 5c7a43f4..4fb20690 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/Global.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/Global.kt @@ -1,5 +1,3 @@ package net.pokeranalytics.android.util -import android.content.Context - const val NULL_TEXT: String = "--" diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt index ded03d9d..c5da6ba8 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt @@ -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(source: DataSource, vararg el this.realmModelIds.clear() } - val csvHeaders: String - get() { - val headers = mutableListOf() - this.fields.forEach { - headers.add(it.header) - } - return headers.joinToString(",") - } + fun toCSV(dataSequence: List): String { - fun toCSV(data: T): String { + val lines = mutableListOf() + lines.add(this.csvHeaders) - val fields = mutableListOf() - this.fields.forEach { -// fields.add(it.toCSV(data)) + dataSequence.forEach { data -> + val line = mutableListOf() + 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 } + protected val csvHeaders: String + get() { + val headers = mutableListOf() + this.fields.forEach { + headers.add(it.header) + } + return headers.joinToString(",") + } + } diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt index 76e7c723..1fc52eb4 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt @@ -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 { null } } + + override fun format(data: Double?): String? { + return if (data != null) { + NumberFormat.getInstance().format(data) + } else { + null + } + } } interface IntCSVField: TypedCSVField { @@ -69,19 +78,14 @@ interface IntCSVField: TypedCSVField { null } } -} - - -interface DataCSVField : TypedCSVField { - - 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 { @@ -99,6 +103,29 @@ interface DateCSVField : TypedCSVField { } } + 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> { + + override fun parse(value: String): List? { + this.callback?.let { + return it(value) + } + return null + } + + override fun format(data: List?): String? { + return data?.joinToString(",") { it.name } + } + } interface BlindCSVField : TypedCSVField> { @@ -121,16 +148,31 @@ interface BlindCSVField : TypedCSVField> { return null } + override fun format(data: Pair?): 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 { 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 : CSVField { fun parse(value: String) : T? + fun format(data: T?): String? var callback: ((String) -> T?)? } diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt new file mode 100644 index 00000000..cd33448d --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/PACSVDescriptor.kt @@ -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(source: DataSource, private var isTournament: Boolean?, vararg elements: CSVField) : DataCSVDescriptor(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 + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt index ffc006d0..b4d91075 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt @@ -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") + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt index c3c1907e..863095b4 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt @@ -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(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 - 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) - } +class SessionCSVDescriptor(source: DataSource, isTournament: Boolean?, vararg elements: CSVField) : + PACSVDescriptor(source, isTournament, *elements) { + override fun parseData(realm: Realm, record: CSVRecord): Session? { + return this.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 -> { - } + 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 } } - } - - 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 + 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 + } + return string ?: "" } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt index db4d316a..dacd13ec 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt @@ -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 - -} +//sealed class TransactionField { +// +// data class TransactionType( +// override var header: String, +// override var callback: ((String) -> net.pokeranalytics.android.model.realm.TransactionType?)? = null +// ) : DataCSVField +// +//} /** * 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?)? = 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, diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt new file mode 100644 index 00000000..0125b615 --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/SessionTransactionCSVDescriptor.kt @@ -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(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 + } + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1d0eadaa..2aa87e09 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6ba7b0db..3b80afcf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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