Skip to content

Instantly share code, notes, and snippets.

@hrj
Created August 19, 2018 17:19
Show Gist options
  • Save hrj/44524dd4264fb277c6453dffd5007f7c to your computer and use it in GitHub Desktop.
Save hrj/44524dd4264fb277c6453dffd5007f7c to your computer and use it in GitHub Desktop.
Import utilities for Indian bank statements into Abandon's shorthand format.
#!scala -save
!#
// From https://stackoverflow.com/questions/32488364/
object Parser {
def fromLine(line: String): List[String] = {
def recursive(
lineRemaining: String
, isWithinDoubleQuotes: Boolean
, valueAccumulator: String
, accumulator: List[String]
): List[String] = {
if (lineRemaining.isEmpty)
valueAccumulator :: accumulator
else
if (lineRemaining.head == '"')
if (isWithinDoubleQuotes)
if (lineRemaining.tail.nonEmpty && lineRemaining.tail.head == '"')
//escaped double quote
recursive(lineRemaining.drop(2), true, valueAccumulator + '"', accumulator)
else
//end of double quote pair (ignore whatever's between here and the next comma)
recursive(lineRemaining.dropWhile(_ != ','), false, valueAccumulator, accumulator)
else
//start of a double quote pair (ignore whatever's in valueAccumulator)
recursive(lineRemaining.drop(1), true, "", accumulator)
else
if (isWithinDoubleQuotes)
//scan to next double quote
recursive(
lineRemaining.dropWhile(_ != '"')
, true
, valueAccumulator + lineRemaining.takeWhile(_ != '"')
, accumulator
)
else
if (lineRemaining.head == ',')
//advance to next field value
recursive(
lineRemaining.drop(1)
, false
, ""
, valueAccumulator :: accumulator
)
else
//scan to next double quote or comma
recursive(
lineRemaining.dropWhile(char => (char != '"') && (char != ','))
, false
, valueAccumulator + lineRemaining.takeWhile(char => (char != '"') && (char != ','))
, accumulator
)
}
if (line.nonEmpty)
recursive(line, false, "", Nil).reverse
else
Nil
}
def fromLines(lines: List[String]): List[List[String]] =
lines.map(fromLine)
}
val fileName=args(0)
println("Importing from " + fileName)
case class EntryDate(year: String, month: String, day: String) {
val monthName = month.toInt match {
case 1 => "jan"
case 2 => "feb"
case 3 => "mar"
case 4 => "apr"
case 5 => "may"
case 6 => "jun"
case 7 => "jul"
case 8 => "aug"
case 9 => "sep"
case 10 => "oct"
case 11 => "nov"
case 12 => "dec"
}
def toLedgerFormat = s"$year/$monthName/$day"
}
case class Entry(date: EntryDate, amount: BigDecimal, narration: String) {
val account = if ((amount < 0) && (narration.startsWith("EAW") || narration.startsWith("ATW"))) {
"Drawings"
} else {
if (amount > 0) "Income:Suspense" else "Expense:Suspense"
}
def toLedgerFormat = f". ${date.toLedgerFormat} ${account}%-30s ${amount}%,15.2f ; $narration"
}
def parseDate(s:String) = {
val fields = s.split("/")
val year = "20" + fields(2).trim
val month = fields(1).trim
val day = fields(0).trim
EntryDate(year, month, day)
}
val lines = io.Source.fromFile(fileName).getLines.toList
val entries = lines.flatMap { l =>
val fields = Parser.fromLine(l).map(_.trim)
if (fields.size > 1) {
val narration = fields(1)
val entryOpt = util.Try(parseDate(fields(2))).map(valueDate => {
val debitAmount = BigDecimal(fields(3))
val creditAmount = BigDecimal(fields(4))
val amount = if (debitAmount > creditAmount) {
-debitAmount
} else {
creditAmount
}
Entry(valueDate, amount, narration)
}).toOption
if (entryOpt.isEmpty) {
println("Problem with " + l)
}
entryOpt
} else {
None
}
}.toList
println(entries.map(_.toLedgerFormat).mkString("\n"))
println("num entries: " + entries.size + " / " + lines.size)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment