Coverage Summary for Class: InitialBalanceInput (com.georgeci.moneysurfer.feature.account.creation)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| InitialBalanceInput |
100%
(1/1)
|
100%
(3/3)
|
100%
(18/18)
|
100%
(15/15)
|
100%
(83/83)
|
package com.georgeci.moneysurfer.feature.account.creation
import com.georgeci.moneysurfer.domain.primitives.AccountType
/**
* Input filtering + validation for the account creation "Opening balance" field.
* Lives outside the ViewModel so the parsing rules can be unit-tested directly.
*/
object InitialBalanceInput {
/**
* Reduces a raw text-field value to a parseable money string: digits, an
* optional leading minus, and at most one decimal separator (',' normalised
* to '.'). Letters and any extra separators are dropped, so the field can
* never hold an unparseable amount.
*/
fun sanitize(raw: String): String {
val negative = raw.startsWith('-')
val digits = StringBuilder()
var separatorUsed = false
for (ch in raw) {
when {
ch.isDigit() -> digits.append(ch)
(ch == '.' || ch == ',') && !separatorUsed -> {
separatorUsed = true
digits.append('.')
}
}
}
return if (negative) "-$digits" else digits.toString()
}
/** Parses a [sanitize]d value to a [Double], or null while it is still incomplete (e.g. "" or "-"). */
fun parse(value: String): Double? = value.toDoubleOrNull()
/**
* The inline error to surface for [balance] given the selected [type], or
* null when the amount is acceptable. A negative opening balance is only
* valid for account types that can carry debt (see [allowsNegativeBalance]).
*/
fun errorFor(balance: String, type: AccountType): InitialBalanceError? {
val parsed = parse(balance) ?: return null
return if (parsed < 0 && !type.allowsNegativeBalance) {
InitialBalanceError.NEGATIVE_NOT_ALLOWED
} else {
null
}
}
}
/** Validation failures for the opening balance field. */
enum class InitialBalanceError { NEGATIVE_NOT_ALLOWED, }
/**
* Whether an account of this type can be created with a negative opening
* balance. Only credit-style accounts (Card) carry debt; cash/bank/savings
* balances start at zero or above.
*/
internal val AccountType.allowsNegativeBalance: Boolean
get() = this == AccountType.CARD