Added documentation + refactoring for CSV import

dev
Laurent 7 years ago
parent e65ef0f224
commit e17306f8b2
  1. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt
  2. 38
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt
  3. 95
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt
  4. 71
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt
  5. 14
      app/src/main/java/net/pokeranalytics/android/util/csv/TypedCSVField.kt

@ -30,7 +30,7 @@ import java.util.*
*/ */
class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource { class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource {
// Return the item as a Custom TypedField object // Return the item as a Custom TypedCSVField object
private val customField: CustomField private val customField: CustomField
get() { get() {
return this.item as CustomField return this.item as CustomField

@ -4,14 +4,19 @@ import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
/**
* The various sources of CSV
*/
enum class DataSource { enum class DataSource {
POKER_INCOME, POKER_INCOME,
POKER_BANKROLL_TRACKER, POKER_BANKROLL_TRACKER,
RUNGOOD RUNGOOD
} }
abstract class DataCSVDescriptor<T : RealmModel>(source: DataSource, vararg elements: Field) : CSVDescriptor(source, *elements) { /**
* A DataCSVDescriptor produces RealmModel instances for each row
*/
abstract class DataCSVDescriptor<T : RealmModel>(source: DataSource, vararg elements: CSVField) : CSVDescriptor(source, *elements) {
val realmModels = mutableListOf<RealmModel>() val realmModels = mutableListOf<RealmModel>()
@ -28,10 +33,19 @@ abstract class DataCSVDescriptor<T : RealmModel>(source: DataSource, vararg elem
} }
open class CSVDescriptor(var source: DataSource, vararg elements: Field) { /**
* A CSVDescriptor describes a CSV format by a source and a list of Fields
*/
abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField) {
protected var fields: List<Field> = listOf() /**
protected var fieldMapping: MutableMap<Field, Int> = mutableMapOf() * The CSVField list describing the CSV header format
*/
protected var fields: List<CSVField> = listOf()
/**
* The mapping of CSVField with their index in the CSV file
*/
protected var fieldMapping: MutableMap<CSVField, Int> = mutableMapOf()
init { init {
if (elements.size > 0) { if (elements.size > 0) {
@ -40,10 +54,16 @@ open class CSVDescriptor(var source: DataSource, vararg elements: Field) {
} }
companion object { companion object {
/**
* The list of all managed CSVDescriptors
*/
val all: List<CSVDescriptor> = val all: List<CSVDescriptor> =
listOf(SessionCSVDescriptor.pokerIncomeCash, SessionCSVDescriptor.pokerBankrollTracker, SessionCSVDescriptor.runGoodCashGames, SessionCSVDescriptor.runGoodTournaments) listOf(SessionCSVDescriptor.pokerIncomeCash, SessionCSVDescriptor.pokerBankrollTracker, SessionCSVDescriptor.runGoodCashGames, SessionCSVDescriptor.runGoodTournaments)
} }
/**
* Returns whether the [record] matches the CSVDescriptor
*/
fun matches(record: CSVRecord): Boolean { fun matches(record: CSVRecord): Boolean {
var count = 0 var count = 0
@ -63,9 +83,9 @@ open class CSVDescriptor(var source: DataSource, vararg elements: Field) {
return count == this.fields.size return count == this.fields.size
} }
open fun parse(realm: Realm, record: CSVRecord) { /**
* Method called when iterating on a CSVRecord
*/
} abstract fun parse(realm: Realm, record: CSVRecord)
} }

@ -9,24 +9,56 @@ import java.io.InputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.io.Reader import java.io.Reader
open class CSVImporter { class ImportException(message: String) : Exception(message)
/**
* A CSVImporter is a class in charge of parsing a CSV file and processing it
* When starting the parsing of a file, the instance will search for a CSVDescriptor, which describes
* the format of a CSV file.
* When finding a descriptor, the CSVImporter then continue to parse the file and delegates the parsing of each row
* to the CSVDescriptor
*/
open class CSVImporter(istream: InputStream) {
/**
* Number of commits required to commit a Realm transaction
*/
private val COMMIT_FREQUENCY = 100 private val COMMIT_FREQUENCY = 100
/**
var path: String? = null * The number of column indicating a valid record
var inputStream: InputStream? = null */
private val VALID_RECORD_COLUMNS = 4
constructor(istream: InputStream) { /**
inputStream = istream * The number of valid record to test for descriptor before throwing a File Format Exception
} */
private val VALID_RECORD_ATTEMPTS_BEFORE_THROWING_EXCEPTION = 5
constructor(filePath: String) {
path = filePath /**
} * The path of the CSV file
*/
private var path: String? = null
/**
* The InputStream containing a file content
*/
private var inputStream: InputStream? = istream
/**
* The current number of attempts at finding a valid CSVDescriptor
*/
private var descriptorFindingAttempts = 0
/**
* Stores the descriptors found
*/
private var usedDescriptors: MutableList<CSVDescriptor> = mutableListOf() private var usedDescriptors: MutableList<CSVDescriptor> = mutableListOf()
/**
* The currently used CSVDescriptor for parsing
*/
private var currentDescriptor: CSVDescriptor? = null private var currentDescriptor: CSVDescriptor? = null
/**
* Constructs a CSVParser object and starts parsing the CSV
*/
fun start() { fun start() {
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
@ -49,21 +81,31 @@ open class CSVImporter {
Timber.d("line $index") Timber.d("line $index")
if (currentDescriptor == null) { // find descriptor if (this.currentDescriptor == null) { // find descriptor
this.findDescriptor(record) this.currentDescriptor = this.findDescriptor(record)
} else {
if ((index + 1) % COMMIT_FREQUENCY == 0) { if (this.currentDescriptor == null) {
Timber.d("****** committing at ${index} sessions...") this.descriptorFindingAttempts++
if (record.size() > VALID_RECORD_COLUMNS && this.descriptorFindingAttempts > VALID_RECORD_ATTEMPTS_BEFORE_THROWING_EXCEPTION) {
throw ImportException("This type of file is not supported")
}
}
} else { // parse
val parsingIndex = index + 1
if (parsingIndex % COMMIT_FREQUENCY == 0) {
Timber.d("****** committing at $parsingIndex sessions...")
realm.commitTransaction() realm.commitTransaction()
realm.beginTransaction() realm.beginTransaction()
} }
currentDescriptor?.let { this.currentDescriptor?.let {
if (record.size() == 0) { if (record.size() == 0) {
this.usedDescriptors.add(it) this.usedDescriptors.add(it)
this.currentDescriptor = this.currentDescriptor = null // reset descriptor when encountering an empty line (multiple descriptors can be found in a single file)
null // reset descriptor when encountering an empty line (multiple descriptors can be found in a single file) this.descriptorFindingAttempts = 0
} else { } else {
it.parse(realm, record) it.parse(realm, record)
} }
@ -81,16 +123,19 @@ open class CSVImporter {
realm.close() realm.close()
} }
private fun findDescriptor(record: CSVRecord) { /**
* Search for a descriptor in the list of managed formats
*/
private fun findDescriptor(record: CSVRecord) : CSVDescriptor? {
CSVDescriptor.all.forEach { descriptor -> CSVDescriptor.all.forEach { descriptor ->
if (descriptor.matches(record)) { if (descriptor.matches(record)) {
this.currentDescriptor = descriptor this.currentDescriptor = descriptor
Timber.d("Identified source: ${descriptor.source}") Timber.d("Identified source: ${descriptor.source}")
return return descriptor
} }
} }
return null
} }
fun save(realm: Realm) { fun save(realm: Realm) {

@ -12,107 +12,113 @@ import net.pokeranalytics.android.util.extensions.setHourMinutes
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
import java.util.* import java.util.*
/**
* The enumeration of Session fields
*/
sealed class SessionField { sealed class SessionField {
data class Start( data class Start(
override var header: String, override var header: String,
override var callback: ((String) -> Date?)? = null, override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null override val dateFormat: String? = null
) : DateField ) : DateCSVField
data class StartTime( data class StartTime(
override var header: String, override var header: String,
override var callback: ((String) -> Date?)? = null, override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null override val dateFormat: String? = null
) : DateField ) : DateCSVField
data class End( data class End(
override var header: String, override var header: String,
override var callback: ((String) -> Date?)? = null, override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null override val dateFormat: String? = null
) : DateField ) : DateCSVField
data class EndTime( data class EndTime(
override var header: String, override var header: String,
override var callback: ((String) -> Date?)? = null, override var callback: ((String) -> Date?)? = null,
override val dateFormat: String? = null override val dateFormat: String? = null
) : DateField ) : DateCSVField
data class Buyin( data class Buyin(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberField ) : NumberCSVField
data class NetResult( data class NetResult(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberField ) : NumberCSVField
data class CashedOut( data class CashedOut(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberField ) : NumberCSVField
data class Break( data class Break(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberField ) : NumberCSVField
data class Tips( data class Tips(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberField ) : NumberCSVField
data class SmallBlind( data class SmallBlind(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberField ) : NumberCSVField
data class BigBlind( data class BigBlind(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberField ) : NumberCSVField
data class Blind(override var header: String, override var callback: ((String) -> Pair<Double, Double>?)? = null) : BlindField data class Blind(override var header: String, override var callback: ((String) -> Pair<Double, Double>?)? = null) : BlindCSVField
data class Game(override var header: String) : Field data class Game(override var header: String) : CSVField
data class Location(override var header: String) : Field data class Location(override var header: String) : CSVField
data class LocationType(override var header: String) : Field data class LocationType(override var header: String) : CSVField
data class Bankroll(override var header: String) : Field data class Bankroll(override var header: String) : CSVField
data class LimitType(override var header: String) : Field data class LimitType(override var header: String) : CSVField
data class Comment(override var header: String) : Field data class Comment(override var header: String) : CSVField
data class SessionType(override var header: String) : Field data class SessionType(override var header: String) : CSVField
data class TableSize(override var header: String) : Field data class TableSize(override var header: String) : CSVField
data class CurrencyCode(override var header: String) : Field data class CurrencyCode(override var header: String) : CSVField
data class TournamentName(override var header: String) : Field data class TournamentName(override var header: String) : CSVField
data class TournamentType(override var header: String) : Field data class TournamentType(override var header: String) : CSVField
data class CurrencyRate( data class CurrencyRate(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberField ) : NumberCSVField
data class TournamentPosition( data class TournamentPosition(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberField ) : 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) -> Double?)? = null,
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberField ) : NumberCSVField
} }
class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean, vararg elements: Field) : DataCSVDescriptor<Session>(source, *elements) { /**
* A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects
*/
class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean, vararg elements: CSVField) : DataCSVDescriptor<Session>(source, *elements) {
companion object { companion object {
val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor( val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor(
@ -216,6 +222,9 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
} }
/**
* Parses a [record] and return an optional Session
*/
override fun parseData(realm: Realm, record: CSVRecord): Session? { override fun parseData(realm: Realm, record: CSVRecord): Session? {
val session = Session.newInstance(realm, this.isTournament) val session = Session.newInstance(realm, this.isTournament)
@ -283,11 +292,11 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
val endDate = session.endDate val endDate = session.endDate
val net = session.result?.net val net = session.result?.net
if (startDate != null && endDate != null && net != null) { // valid session return if (startDate != null && endDate != null && net != null) { // valid session
val unique = SessionUtils.unicityCheck(realm, startDate, endDate, net) val unique = SessionUtils.unicityCheck(realm, startDate, endDate, net)
return if (unique) session else null if (unique) session else null
} else { // invalid session } else { // invalid session
return null null
} }
} }

@ -7,8 +7,10 @@ import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
/**
interface NumberField: TypedField<Double> { * An interface used to provide a Number flavour to a CSV field
*/
interface NumberCSVField: TypedCSVField<Double> {
val numberFormat: String? val numberFormat: String?
@ -29,7 +31,7 @@ interface NumberField: TypedField<Double> {
} }
} }
interface DateField : TypedField<Date> { interface DateCSVField : TypedCSVField<Date> {
val dateFormat: String? val dateFormat: String?
@ -46,7 +48,7 @@ interface DateField : TypedField<Date> {
} }
interface BlindField : TypedField<Pair<Double, Double>> { interface BlindCSVField : TypedCSVField<Pair<Double, Double>> {
override fun parse(value: String) : Pair<Double, Double>? { override fun parse(value: String) : Pair<Double, Double>? {
@ -68,11 +70,11 @@ interface BlindField : TypedField<Pair<Double, Double>> {
} }
interface TypedField<T> : Field { interface TypedCSVField<T> : CSVField {
fun parse(value: String) : T? fun parse(value: String) : T?
var callback: ((String) -> T?)? var callback: ((String) -> T?)?
} }
interface Field { interface CSVField {
val header: String val header: String
} }
Loading…
Cancel
Save