Skip to content

Commit

Permalink
Implement important fields parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
ILIYANGERMANOV committed Apr 15, 2023
1 parent 10ff0ee commit 27a578a
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 111 deletions.
64 changes: 30 additions & 34 deletions app/src/main/java/com/ivy/wallet/ui/csv/CSVViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package com.ivy.wallet.ui.csv

import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ivy.wallet.domain.deprecated.logic.csv.IvyFileReader
import com.ivy.wallet.ui.csv.domain.mappingFailure
import com.ivy.wallet.ui.csv.domain.parseImportantStatus
import com.ivy.wallet.ui.csv.domain.*
import com.opencsv.CSVReaderBuilder
import com.opencsv.validators.LineValidator
import com.opencsv.validators.RowValidator
Expand All @@ -29,8 +25,6 @@ class CSVViewModel @Inject constructor(

private var columns by mutableStateOf<CSVRow?>(null)
private var csv by mutableStateOf<List<CSVRow>?>(null)
private var transfer by mutableStateOf<TransferFields?>(null)
private var optional by mutableStateOf<OptionalFields?>(null)
private var successPercent by mutableStateOf<Double?>(null)
private var failedRows by mutableStateOf<List<CSVRow>?>(null)

Expand Down Expand Up @@ -106,39 +100,41 @@ class CSVViewModel @Inject constructor(
columns = columns,
csv = csv,
important = important(csv),
transfer = transfer,
optional = optional,
transfer = null,
optional = null,
successPercent = successPercent,
failedRows = failedRows,
)
}

@Composable
private fun important(csv: List<CSVRow>?): ImportantFields? {
val failure = mappingFailure()
val importantFields = ImportantFields(
amount = amount,
amountStatus = failure,
type = type,
typeStatus = failure,
date = date,
dateStatus = failure,
account = account,
accountStatus = failure,
accountCurrency = accountCurrency,
accountCurrencyStatus = failure,
)

return if (csv != null) {
val status = parseImportantStatus(csv, importantFields)
importantFields.copy(
amountStatus = status.amountStatus,
typeStatus = status.typeStatus,
dateStatus = status.dateStatus,
accountStatus = status.accountStatus,
accountCurrencyStatus = status.accountCurrencyStatus,
)
} else null
return produceState<ImportantFields?>(
initialValue = null,
csv, amount, type, date, account, accountCurrency,
) {
val result = withContext(Dispatchers.Default) {
if (csv != null) {
val sampleRows = csv.drop(1).take(10) // drop the header
ImportantFields(
amount = amount,
amountStatus = sampleRows.parseStatus(amount, ::parseAmount),
type = type,
typeStatus = sampleRows.parseStatus(type, ::parseTransactionType),
date = date,
dateStatus = sampleRows.parseStatus(date, ::parseDate),
account = account,
accountStatus = sampleRows.parseStatus(account, ::parseAccount),
accountCurrency = accountCurrency,
accountCurrencyStatus = sampleRows.parseStatus(
accountCurrency,
::parseAccountCurrency
),
)
} else null
}
value = result
}.value
}


Expand Down
87 changes: 10 additions & 77 deletions app/src/main/java/com/ivy/wallet/ui/csv/domain/ParseStatus.kt
Original file line number Diff line number Diff line change
@@ -1,92 +1,25 @@
package com.ivy.wallet.ui.csv.domain

import com.ivy.wallet.domain.data.TransactionType
import com.ivy.wallet.ui.csv.*
import kotlin.math.abs
import com.ivy.wallet.ui.csv.CSVRow
import com.ivy.wallet.ui.csv.ColumnMapping
import com.ivy.wallet.ui.csv.MappingStatus


data class ImportantStatus(
val amountStatus: MappingStatus,
val typeStatus: MappingStatus,
val dateStatus: MappingStatus,
val accountStatus: MappingStatus,
val accountCurrencyStatus: MappingStatus,
)

fun parseImportantStatus(
csv: List<CSVRow>,
important: ImportantFields,
): ImportantStatus {
val rows = csv.drop(1).take(10) // drop the header

return ImportantStatus(
amountStatus = parseAmountStatus(rows, important.amount),
typeStatus = parseTypeStatus(rows, important.type),
dateStatus = mappingFailure(),
accountStatus = mappingFailure(),
accountCurrencyStatus = mappingFailure(),
)
}

private fun parseAmountStatus(
rows: List<CSVRow>,
mapping: ColumnMapping<Int>
fun <T, M> List<CSVRow>.parseStatus(
mapping: ColumnMapping<M>,
parse: (String, M) -> T?
): MappingStatus = tryStatus {
val values = rows.values(mapping)
.mapNotNull {
val multiplier = when (mapping.metadata) {
-1000 -> 0.001
-100 -> 0.01
-10 -> 0.1
10 -> 10.0
100 -> 100.0
1000 -> 1000.0
else -> 1.0
}
it.toDoubleOrNull()?.times(multiplier)?.let(::abs)
}

MappingStatus(
sampleValues = values.map { it.toString() },
success = values.size == rows.size
)
}

private fun parseTypeStatus(
rows: List<CSVRow>,
mapping: ColumnMapping<TrnTypeMetadata>
): MappingStatus = tryStatus {
fun String.tryMeta(metaContains: String): Boolean {
return metaContains.isNotBlank() &&
this.contains(metaContains.trim(), ignoreCase = true)
val parsed = this.mapNotNull {
parse(it.values[mapping.index], mapping.metadata)
}

val values = rows.values(mapping)
.mapNotNull { value ->
val meta = mapping.metadata
with(value) {
when {
tryMeta(meta.expense) -> TransactionType.EXPENSE
tryMeta(meta.income) -> TransactionType.INCOME
tryMeta(meta.transfer ?: "") -> TransactionType.TRANSFER
value.contains("income", ignoreCase = true) -> TransactionType.INCOME
value.contains("expense", ignoreCase = true) -> TransactionType.EXPENSE
value.contains("transfer", ignoreCase = true) -> TransactionType.TRANSFER
else -> null
}
}
}

MappingStatus(
sampleValues = values.map { it.toString() },
success = values.size == rows.size
sampleValues = parsed.map { it.toString() },
success = parsed.size == this.size
)
}


private fun <T> List<CSVRow>.values(mapping: ColumnMapping<T>): List<String> =
map { it.values[mapping.index] }

private fun tryStatus(block: () -> MappingStatus): MappingStatus = try {
block()
} catch (e: Exception) {
Expand Down
163 changes: 163 additions & 0 deletions app/src/main/java/com/ivy/wallet/ui/csv/domain/Parser.kt
Original file line number Diff line number Diff line change
@@ -1,2 +1,165 @@
package com.ivy.wallet.ui.csv.domain

import com.ivy.wallet.domain.data.TransactionType
import com.ivy.wallet.ui.csv.DateMetadata
import com.ivy.wallet.ui.csv.TrnTypeMetadata
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import kotlin.math.abs

fun parseAmount(
value: String,
metadata: Int // a broken multiplier
): Double? = tryParse {
val multiplier = when (metadata) {
-10_000 -> 0.0001
-1_000 -> 0.001
-100 -> 0.01
-10 -> 0.1
10 -> 10.0
100 -> 100.0
1_000 -> 1_000.0
1_0000 -> 10_000.0
else -> 1.0
}
abs(value.toDouble() * multiplier)
}

fun parseTransactionType(
value: String,
metadata: TrnTypeMetadata
): TransactionType? {
fun String.tryMeta(metaContains: String): Boolean {
return metaContains.isNotBlank() &&
this.contains(metaContains.trim(), ignoreCase = true)
}

return tryParse {
with(value) {
when {
tryMeta(metadata.expense) -> TransactionType.EXPENSE
tryMeta(metadata.income) -> TransactionType.INCOME
tryMeta(metadata.transfer ?: "") -> TransactionType.TRANSFER
value.contains("income", ignoreCase = true) -> TransactionType.INCOME
value.contains("expense", ignoreCase = true) -> TransactionType.EXPENSE
value.contains("transfer", ignoreCase = true) -> TransactionType.TRANSFER
else -> null
}
}
}
}

// region Parse Date
var lastSuccessfulFormat: String? = null

fun parseDate(
value: String,
metadata: DateMetadata
): LocalDateTime? = tryParse {
val possibleFormats = possibleDateFormats(metadata)
if (lastSuccessfulFormat != null) {
try {
return@tryParse LocalDateTime.parse(
value,
DateTimeFormatter.ofPattern(lastSuccessfulFormat)
)
} catch (e: DateTimeParseException) {
// Ignore and continue trying other formats
lastSuccessfulFormat = null
}
}
for (format in possibleFormats) {
try {
return@tryParse LocalDateTime.parse(value, DateTimeFormatter.ofPattern(format))
} catch (e: DateTimeParseException) {
// Ignore and continue trying other formats
}
}
null
}

private fun possibleDateFormats(metadata: DateMetadata): List<String> {
return when (metadata) {
DateMetadata.DateFirst -> listOf(
"dd/MM/yyyy HH:mm:ss",
"dd-MM-yyyy HH:mm:ss",
"dd/MM/yyyy H:mm",
"dd-MM-yyyy H:mm",
"dd/MM/yyyy HH:mm",
"dd-MM-yyyy HH:mm",
"dd/MM/yyyy",
"dd-MM-yyyy",
"d MMM yyyy HH:mm:ss",
"d MMM yyyy H:mm",
"d MMM yyyy HH:mm",
"d MMM yyyy",
"dd MMM yyyy",
"yyyy/dd/MM HH:mm:ss",
"yyyy-dd-MM HH:mm:ss",
"yyyy/dd/MM H:mm",
"yyyy-dd-MM H:mm",
"yyyy/dd/MM HH:mm",
"yyyy-dd-MM HH:mm",
"yyyy/dd/MM",
"yyyy-dd-MM",
"yyyy d MMM HH:mm:ss",
"yyyy d MMM H:mm",
"yyyy d MMM HH:mm",
"yyyy d MMM",
"yyyy dd MMM"
)
DateMetadata.MonthFirst -> listOf(
"MM/dd/yyyy HH:mm:ss",
"MM-dd-yyyy HH:mm:ss",
"MM/dd/yyyy H:mm",
"MM-dd-yyyy H:mm",
"MM/dd/yyyy HH:mm",
"MM-dd-yyyy HH:mm",
"MM/dd/yyyy",
"MM-dd-yyyy",
"MMM d yyyy HH:mm:ss",
"MMM d yyyy H:mm",
"MMM d yyyy HH:mm",
"MMM d yyyy",
"MMM dd yyyy",
"yyyy/MM/dd HH:mm:ss",
"yyyy-MM-dd HH:mm:ss",
"yyyy/MM/dd H:mm",
"yyyy-MM-dd H:mm",
"yyyy/MM/dd HH:mm",
"yyyy-MM-dd HH:mm",
"yyyy/MM/dd",
"yyyy-MM-dd",
"yyyy MMM d HH:mm:ss",
"yyyy MMM d H:mm",
"yyyy MMM d HH:mm",
"yyyy MMM d",
"yyyy MMM dd"
)
}
}
// endregion

fun parseAccount(
value: String,
metadata: Unit,
): String? = notBlankTrimmedString(value)

fun parseAccountCurrency(
value: String,
metadata: Unit,
): String? = notBlankTrimmedString(value)



// region Util
private fun notBlankTrimmedString(value: String): String? =
value.trim().takeIf { it.isNotBlank() }

private fun <T> tryParse(block: () -> T): T? = try {
block()
} catch (e: Exception) {
null
}
// endregion

0 comments on commit 27a578a

Please sign in to comment.