Skip to content

Commit

Permalink
Merge pull request #91 from Entea/transaction_validation_date_range
Browse files Browse the repository at this point in the history
Transaction validation: date range
  • Loading branch information
hrj authored Sep 8, 2016
2 parents d570296 + 8273dff commit 9b3ff51
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 12 deletions.
1 change: 1 addition & 0 deletions base/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ libraryDependencies += Dependencies.config
libraryDependencies += scalaXML
libraryDependencies += scalaParserCombinators
libraryDependencies += scalatest % "test"
libraryDependencies += scalamock % "test"

2 changes: 1 addition & 1 deletion base/src/main/scala/co/uproot/abandon/Ast.scala
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,4 @@ case class Scope(entries: Seq[ASTEntry], parentOpt: Option[Scope]) extends ASTEn
def childScopes = Helper.filterByType[Scope](entries)

def allTransactions:Seq[ScopedTxn] = allLocalTransactions ++ childScopes.flatMap(_.allTransactions)
}
}
40 changes: 35 additions & 5 deletions base/src/main/scala/co/uproot/abandon/Config.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package co.uproot.abandon

import java.time.LocalDate
import java.time.format.DateTimeFormatter

import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory

import collection.JavaConverters._
import com.typesafe.config.ConfigObject
import org.rogach.scallop.ScallopConf
import SettingsHelper._
import com.typesafe.config.ConfigException
import scala.util.Try

class AbandonCLIConf(arguments: Seq[String]) extends ScallopConf(arguments) {
val inputs = opt[List[String]]("input", short = 'i')
Expand Down Expand Up @@ -69,16 +73,17 @@ object SettingsHelper {
if (file.exists) {
try {
val config = ConfigFactory.parseFile(file).resolve()
val inputs = config.getStringList("inputs").asScala.map(handleInput(_, configFileName)).flatten.toSeq.sorted
val inputs = config.getStringList("inputs").asScala.flatMap(handleInput(_, configFileName)).toSeq.sorted
val reports = config.getConfigList("reports").asScala.map(makeReportSettings(_))
val reportOptions = config.optConfig("reportOptions")
val isRight = reportOptions.map(_.optStringList("isRight")).flatten.getOrElse(Nil)
val isRight = reportOptions.flatMap(_.optStringList("isRight")).getOrElse(Nil)
val exportConfigs = config.optConfigList("exports").getOrElse(Nil)
val exports = exportConfigs.map(makeExportSettings)
val accountConfigs = config.optConfigList("accounts").getOrElse(Nil)
val accounts = accountConfigs.map(makeAccountSettings)
val eodConstraints = config.optConfigList("eodConstraints").getOrElse(Nil).map(makeEodConstraints(_))
Right(Settings(inputs, eodConstraints, accounts, reports, ReportOptions(isRight), exports, Some(file)))
val dateConstraints = config.optConfigList("dateConstraints").getOrElse(Nil).map(makeDateConstraints(_))
Right(Settings(inputs, eodConstraints ++ dateConstraints, accounts, reports, ReportOptions(isRight), exports, Some(file)))
} catch {
case e: ConfigException => Left(e.getMessage)
}
Expand All @@ -96,6 +101,13 @@ object SettingsHelper {
}
}

def makeDateConstraints(config: Config): DateConstraint = {
val from = AbandonParser.dateExpr(ParserHelper.scanner(config.getString("from"))).map(Some(_)).getOrElse(None)
val to = AbandonParser.dateExpr(ParserHelper.scanner(config.getString("to"))).map(Some(_)).getOrElse(None)

DateConstraint(from, to)
}

def makeReportSettings(config: Config) = {
val title = config.getString("title")
val reportType = config.getString("type")
Expand Down Expand Up @@ -204,9 +216,27 @@ case class NegativeConstraint(val accName: String) extends Constraint with SignC
val signStr = "negative"
}

case class DateConstraint(dateFrom: Option[Date], dateTo: Option[Date]) extends Constraint {
override def check(appState: AppState): Boolean = {
appState.accState.posts.find(post => {
val date = post.date

dateFrom.map(DateOrdering.compare(_, date) > 0).getOrElse(false) || dateTo.map(DateOrdering.compare(_, date) < 0).getOrElse(false)
}).foreach(post => {
throw new ConstraintError(
s"${post.name} is not in date range of " +
s"[${dateFrom.getOrElse("None")}, ${dateTo.getOrElse("None")}]. " +
s"Date is ${post.date.formatISO8601Ext}"
)
})

true
}
}

case class Settings(
inputs: Seq[String],
eodConstraints: Seq[Constraint],
constraints: Seq[Constraint],
accounts: Seq[AccountSettings],
reports: Seq[ReportSettings],
reportOptions: ReportOptions,
Expand Down
6 changes: 4 additions & 2 deletions base/src/main/scala/co/uproot/abandon/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,12 @@ object AbandonParser extends StandardTokenParsers with PackratParsers {
private lazy val conditionExpr = (numericExpr ~ comparisonExpr ~ numericExpr) ^^ { case (e1 ~ op ~ e2) => ConditionExpr(e1, op, e2)}
private lazy val comparisonExpr: PackratParser[String] = ((">" | "<" | "=") ~ "=" ^^ {case (o1 ~ o2) => o1 + o2}) | ">" | "<"

private lazy val compactTxFrag = (("." ~> (dateFrag | isoDateFrag) ~ accountName ~ numericExpr ~ eolComment) ^^ {
private lazy val compactTxFrag = (("." ~> dateExpr ~ accountName ~ numericExpr ~ eolComment) ^^ {
case date ~ accountName ~ amount ~ optComment =>
Transaction(date, List(Post(accountName, Option(amount), optComment)), None, None, Nil)
})

private lazy val txFrag = (((dateFrag | isoDateFrag) ~ (annotation?) ~ (payee?)) <~ eol) ~ (eolComment*) ~ (post+) ^^ {
private lazy val txFrag = ((dateExpr ~ (annotation?) ~ (payee?)) <~ eol) ~ (eolComment*) ~ (post+) ^^ {
case date ~ annotationOpt ~ optPayee ~ optComment ~ posts =>
val annotationStrOpt = annotationOpt.map(_.mkString(""))
Transaction(date, posts, annotationStrOpt, optPayee, optComment.flatten)
Expand All @@ -230,6 +230,8 @@ object AbandonParser extends StandardTokenParsers with PackratParsers {
case name ~ amount ~ commentOpt => Post(name, amount, commentOpt)
}

lazy val dateExpr = dateFrag | isoDateFrag

lazy val isoDateFrag = ((((integer <~ "-") ~ integer) <~ "-") ~ integer) ^? ({
case y ~ m ~ d if (isValidDate(y, m, d)) =>
Date(y, m, d)
Expand Down
67 changes: 67 additions & 0 deletions base/src/test/scala/co/uproot/abandon/DateConstraintTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package co.uproot.abandon

import java.time.{LocalDate, Month}

import org.scalamock.scalatest.MockFactory
import org.scalatest._

class DateConstraintTest extends FlatSpec with Matchers with BeforeAndAfterEach with MockFactory with OneInstancePerTest {

class MockableDetailedPost extends DetailedPost(new AccountName(Seq()), 0, None, None)

var appState: AppState = null

override def beforeEach() {
setupMocks()
}

it should "return true on valid posts" in {
val constraint = new DateConstraint(
Some(Date(2013, 1, 1)),
Some(Date(2013, 12, 31))
)

constraint.check(appState) should be(true)
}

it should "throw exception on end date violation" in {
val constraint = new DateConstraint(
Some(Date(2013, 1, 1)),
Some(Date(2013, 11, 1))
)
an [ConstraintError] should be thrownBy constraint.check(appState)

val constraintWithoutFrom = new DateConstraint(None, Some(Date(2013, 11, 1)))
an [ConstraintError] should be thrownBy constraintWithoutFrom.check(appState)
}

it should "throw exception on start date violation" in {
val constraint = new DateConstraint(
Some(Date(2013, 6, 1)),
Some(Date(2013, 11, 1))
)
an [ConstraintError] should be thrownBy constraint.check(appState)

val constraintWithoutTo = new DateConstraint(Some(Date(2013, 11, 1)), None)
an [ConstraintError] should be thrownBy constraintWithoutTo.check(appState)
}

it should "return true on empty dates" in {
val constraintWithoutTo = new DateConstraint(None, None)
constraintWithoutTo.check(appState) should be(true)
}


def setupMocks() {
val accState = stub[AccountState]
appState = new AppState(accState)

val posts: Seq[DetailedPost] = (1 to 12).map(month => {
val post = stub[MockableDetailedPost]
(post.date _).when().returns(new Date(2013, month, 1))
post
})

(accState.posts _).when().returns(posts)
}
}
2 changes: 1 addition & 1 deletion cli/src/main/scala/co/uproot/abandon/App.scala
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ object CLIMain {
val (parseError, astEntries, processedFiles) = Processor.parseAll(settings.inputs)
if (!parseError) {
val appState = Processor.process(astEntries,settings.accounts)
Processor.checkConstaints(appState, settings.eodConstraints)
Processor.checkConstaints(appState, settings.constraints)
settings.exports.foreach { exportSettings =>
val reportWriter = new ReportWriter(settings, exportSettings.outFiles)
println()
Expand Down
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ There are two examples in this directory.
If you are new to `abandon` then have a look at the `simple` example first.

The `complete` example showcases many features of abandon, and can be used as a reference
for managing your acutal accounts.
for managing your actual accounts.
5 changes: 5 additions & 0 deletions examples/complete/accounts.conf
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,8 @@ exports += {
}]

}

dateConstraints += {
from = "2012-01-01"
to = "2013-12-31"
}
1 change: 0 additions & 1 deletion examples/simple/accounts.conf
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,3 @@ reports += {
type = balance
accountMatch = ["^Income.*", "^Expenses.*"]
}

4 changes: 3 additions & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import Keys._

object Dependencies {
// Versions
val scalatestVersion = "3.0.0"
val scalatestVersion = "2.2.6"
val scallopVersion = "2.0.2"
val scalaMockVersion = "3.2.2"
val configVersion = "1.3.0"
val scalaXMLVersion = "1.0.5"
val scalaParserCombinatorsVersion = "1.0.4"
val scalaFXVersion = "8.0.92-R10"

// Libraries
val scalatest = "org.scalatest" %% "scalatest" % scalatestVersion
val scalamock = "org.scalamock" %% "scalamock-scalatest-support" % scalaMockVersion
val scallop = "org.rogach" %% "scallop" % scallopVersion
val config = "com.typesafe" % "config" % configVersion
val scalaXML = "org.scala-lang.modules" %% "scala-xml" % scalaXMLVersion
Expand Down

0 comments on commit 9b3ff51

Please sign in to comment.