diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt index 5559007c..5c358258 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt @@ -30,7 +30,7 @@ import java.util.* */ class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource { - // Return the item as a Custom TypedField object + // Return the item as a Custom TypedCSVField object private val customField: CustomField get() { return this.item as CustomField 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 a82324d6..f4ef3174 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 @@ -4,14 +4,19 @@ import io.realm.Realm import io.realm.RealmModel import org.apache.commons.csv.CSVRecord - +/** + * The various sources of CSV + */ enum class DataSource { POKER_INCOME, POKER_BANKROLL_TRACKER, RUNGOOD } -abstract class DataCSVDescriptor(source: DataSource, vararg elements: Field) : CSVDescriptor(source, *elements) { +/** + * A DataCSVDescriptor produces RealmModel instances for each row + */ +abstract class DataCSVDescriptor(source: DataSource, vararg elements: CSVField) : CSVDescriptor(source, *elements) { val realmModels = mutableListOf() @@ -28,10 +33,19 @@ abstract class DataCSVDescriptor(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 = listOf() - protected var fieldMapping: MutableMap = mutableMapOf() + /** + * The CSVField list describing the CSV header format + */ + protected var fields: List = listOf() + /** + * The mapping of CSVField with their index in the CSV file + */ + protected var fieldMapping: MutableMap = mutableMapOf() init { if (elements.size > 0) { @@ -40,10 +54,16 @@ open class CSVDescriptor(var source: DataSource, vararg elements: Field) { } companion object { + /** + * The list of all managed CSVDescriptors + */ val all: List = listOf(SessionCSVDescriptor.pokerIncomeCash, SessionCSVDescriptor.pokerBankrollTracker, SessionCSVDescriptor.runGoodCashGames, SessionCSVDescriptor.runGoodTournaments) } + /** + * Returns whether the [record] matches the CSVDescriptor + */ fun matches(record: CSVRecord): Boolean { var count = 0 @@ -63,9 +83,9 @@ open class CSVDescriptor(var source: DataSource, vararg elements: Field) { 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) } diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt index b988a824..1ac91e55 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt @@ -9,24 +9,56 @@ import java.io.InputStream import java.io.InputStreamReader 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 - - var path: String? = null - var inputStream: InputStream? = null - - constructor(istream: InputStream) { - inputStream = istream - } - - constructor(filePath: String) { - path = filePath - } - + /** + * The number of column indicating a valid record + */ + private val VALID_RECORD_COLUMNS = 4 + /** + * The number of valid record to test for descriptor before throwing a File Format Exception + */ + private val VALID_RECORD_ATTEMPTS_BEFORE_THROWING_EXCEPTION = 5 + + /** + * 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 = mutableListOf() + /** + * The currently used CSVDescriptor for parsing + */ private var currentDescriptor: CSVDescriptor? = null + /** + * Constructs a CSVParser object and starts parsing the CSV + */ fun start() { val realm = Realm.getDefaultInstance() @@ -49,21 +81,31 @@ open class CSVImporter { Timber.d("line $index") - if (currentDescriptor == null) { // find descriptor - this.findDescriptor(record) - } else { + if (this.currentDescriptor == null) { // find descriptor + this.currentDescriptor = this.findDescriptor(record) - if ((index + 1) % COMMIT_FREQUENCY == 0) { - Timber.d("****** committing at ${index} sessions...") + if (this.currentDescriptor == null) { + 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.beginTransaction() } - currentDescriptor?.let { + this.currentDescriptor?.let { if (record.size() == 0) { this.usedDescriptors.add(it) - this.currentDescriptor = - null // reset descriptor when encountering an empty line (multiple descriptors can be found in a single file) + this.currentDescriptor = null // reset descriptor when encountering an empty line (multiple descriptors can be found in a single file) + this.descriptorFindingAttempts = 0 } else { it.parse(realm, record) } @@ -81,16 +123,19 @@ open class CSVImporter { 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 -> if (descriptor.matches(record)) { this.currentDescriptor = descriptor Timber.d("Identified source: ${descriptor.source}") - return + return descriptor } } - + return null } fun save(realm: Realm) { 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 9475514b..d33e719f 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 @@ -12,107 +12,113 @@ import net.pokeranalytics.android.util.extensions.setHourMinutes import org.apache.commons.csv.CSVRecord import java.util.* +/** + * The enumeration of Session fields + */ sealed class SessionField { data class Start( override var header: String, override var callback: ((String) -> Date?)? = null, override val dateFormat: String? = null - ) : DateField + ) : DateCSVField data class StartTime( override var header: String, override var callback: ((String) -> Date?)? = null, override val dateFormat: String? = null - ) : DateField + ) : DateCSVField data class End( override var header: String, override var callback: ((String) -> Date?)? = null, override val dateFormat: String? = null - ) : DateField + ) : DateCSVField data class EndTime( override var header: String, override var callback: ((String) -> Date?)? = null, override val dateFormat: String? = null - ) : DateField + ) : DateCSVField data class Buyin( override var header: String, override var callback: ((String) -> Double?)? = null, override val numberFormat: String? = null - ) : NumberField + ) : NumberCSVField data class NetResult( override var header: String, override var callback: ((String) -> Double?)? = null, override val numberFormat: String? = null - ) : NumberField + ) : NumberCSVField data class CashedOut( override var header: String, override var callback: ((String) -> Double?)? = null, override val numberFormat: String? = null - ) : NumberField + ) : NumberCSVField data class Break( override var header: String, override var callback: ((String) -> Double?)? = null, override val numberFormat: String? = null - ) : NumberField + ) : NumberCSVField data class Tips( override var header: String, override var callback: ((String) -> Double?)? = null, override val numberFormat: String? = null - ) : NumberField + ) : NumberCSVField data class SmallBlind( override var header: String, override var callback: ((String) -> Double?)? = null, override val numberFormat: String? = null - ) : NumberField + ) : NumberCSVField data class BigBlind( override var header: String, override var callback: ((String) -> Double?)? = null, override val numberFormat: String? = null - ) : NumberField - - data class Blind(override var header: String, override var callback: ((String) -> Pair?)? = null) : BlindField - data class Game(override var header: String) : Field - data class Location(override var header: String) : Field - data class LocationType(override var header: String) : Field - data class Bankroll(override var header: String) : Field - data class LimitType(override var header: String) : Field - data class Comment(override var header: String) : Field - data class SessionType(override var header: String) : Field - data class TableSize(override var header: String) : Field - data class CurrencyCode(override var header: String) : Field - data class TournamentName(override var header: String) : Field - data class TournamentType(override var header: String) : Field + ) : NumberCSVField + + data class Blind(override var header: String, override var callback: ((String) -> Pair?)? = null) : BlindCSVField + data class Game(override var header: String) : CSVField + data class Location(override var header: String) : CSVField + data class LocationType(override var header: String) : CSVField + data class Bankroll(override var header: String) : CSVField + data class LimitType(override var header: String) : CSVField + data class Comment(override var header: String) : CSVField + data class SessionType(override var header: String) : CSVField + 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 CurrencyRate( override var header: String, override var callback: ((String) -> Double?)? = null, override val numberFormat: String? = null - ) : NumberField + ) : NumberCSVField data class TournamentPosition( override var header: String, override var callback: ((String) -> Double?)? = null, override val numberFormat: String? = null - ) : NumberField + ) : NumberCSVField data class TournamentNumberOfPlayers( override var header: String, override var callback: ((String) -> Double?)? = null, override val numberFormat: String? = null - ) : NumberField + ) : NumberCSVField } -class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean, vararg elements: Field) : DataCSVDescriptor(source, *elements) { +/** + * A SessionCSVDescriptor is a CSVDescriptor specialized in parsing Session objects + */ +class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean, vararg elements: CSVField) : DataCSVDescriptor(source, *elements) { companion object { 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? { val session = Session.newInstance(realm, this.isTournament) @@ -283,11 +292,11 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean val endDate = session.endDate 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) - return if (unique) session else null + if (unique) session else null } else { // invalid session - return null + null } } diff --git a/app/src/main/java/net/pokeranalytics/android/util/csv/TypedField.kt b/app/src/main/java/net/pokeranalytics/android/util/csv/TypedCSVField.kt similarity index 82% rename from app/src/main/java/net/pokeranalytics/android/util/csv/TypedField.kt rename to app/src/main/java/net/pokeranalytics/android/util/csv/TypedCSVField.kt index 7f0c9d5b..3e7e6659 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/csv/TypedField.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/csv/TypedCSVField.kt @@ -7,8 +7,10 @@ import java.text.ParseException import java.text.SimpleDateFormat import java.util.* - -interface NumberField: TypedField { +/** + * An interface used to provide a Number flavour to a CSV field + */ +interface NumberCSVField: TypedCSVField { val numberFormat: String? @@ -29,7 +31,7 @@ interface NumberField: TypedField { } } -interface DateField : TypedField { +interface DateCSVField : TypedCSVField { val dateFormat: String? @@ -46,7 +48,7 @@ interface DateField : TypedField { } -interface BlindField : TypedField> { +interface BlindCSVField : TypedCSVField> { override fun parse(value: String) : Pair? { @@ -68,11 +70,11 @@ interface BlindField : TypedField> { } -interface TypedField : Field { +interface TypedCSVField : CSVField { fun parse(value: String) : T? var callback: ((String) -> T?)? } -interface Field { +interface CSVField { val header: String } \ No newline at end of file