Skip to content

Commit

Permalink
First commit. Version 0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
hrj committed May 28, 2013
0 parents commit c9f5d33
Show file tree
Hide file tree
Showing 19 changed files with 1,263 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target/
project/target/
.cache
*.swp
7 changes: 7 additions & 0 deletions base/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

libraryDependencies += "joda-time" % "joda-time" % "1.6.2"

libraryDependencies += "org.rogach" %% "scallop" % "0.9.2"

libraryDependencies += "com.typesafe" % "config" % "1.0.1"

90 changes: 90 additions & 0 deletions base/src/main/scala/co/uproot/abandon/Ast.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package co.uproot.abandon

object ASTHelper {
type NumericExpr = Expr[BigDecimal]
}

class InputError(msg: String) extends RuntimeException(msg)

import ASTHelper._

case class Date(year: Int, month: Int, day: Int) {
def formatYYYYMMDD = {
f"$year%4d / $month%d / $day%d"
}
}

case class AccountName(fullPath:Seq[String]) {
val name = fullPath.lastOption.getOrElse("")
val fullPathStr = fullPath.mkString(":")
override def toString = fullPathStr
val depth = fullPath.length
}

case class SingleTransaction(accName:AccountName, amount:Option[NumericExpr])

sealed class ASTEntry

case class PayeeDef(name:String) extends ASTEntry
case class TagDef(name:String) extends ASTEntry

sealed class ASTTangibleEntry extends ASTEntry

case class Transaction(date:Date, transactions:Seq[SingleTransaction], payeeOpt:Option[String]) extends ASTTangibleEntry

case class Definition[T](name:String, params:List[String], rhs:Expr[T]) extends ASTTangibleEntry {
def prettyPrint = "def %s(%s) = %s" format (name, params.mkString(", "), rhs.prettyPrint)
}

case class AccountDeclaration(name:AccountName, details:Map[String, Expr[_]]) extends ASTTangibleEntry

case class IncludeDirective(fileName:String) extends ASTEntry

sealed abstract class Expr[T] {
def evaluate(context:EvaluationContext[T]):T
def prettyPrint = toString
def getRefs:Seq[Ref]
}

trait LiteralValue[T] {
val value:T
def getRefs = Nil
def evaluate(context:EvaluationContext[T]):T = value
}

abstract class BooleanExpr extends Expr[Boolean] {
def evaluate(context:EvaluationContext[Boolean]): Boolean
}

case class BooleanLiteralExpr(val value:Boolean) extends BooleanExpr with LiteralValue[Boolean] {
}

case class NumericLiteralExpr(val value:BigDecimal) extends NumericExpr with LiteralValue[BigDecimal] {
override def prettyPrint = value.toString
}

case class FunctionExpr[T](val name:String, val arguments:Seq[Expr[T]]) extends Expr[T] {
def evaluate(context:EvaluationContext[T]): T = context.getValue(name, arguments.map(_.evaluate(context)))
override def prettyPrint = "%s(%s)" format (name, arguments.map(_.prettyPrint).mkString(", "))
def getRefs = Ref(name, arguments.length) +: arguments.flatMap(_.getRefs)
}

case class IdentifierExpr[T](val name:String) extends Expr[T] {
def evaluate(context:EvaluationContext[T]): T = context.getValue(name, Nil)
override def prettyPrint = name
def getRefs = Seq(Ref(name, 0))
}

abstract class BinaryNumericExpr(op1:NumericExpr, op2:NumericExpr, opChar:String, operation:(BigDecimal, BigDecimal)=>BigDecimal) extends NumericExpr {
def evaluate(context:EvaluationContext[BigDecimal]): BigDecimal = operation(op1.evaluate(context), op2.evaluate(context))
override def prettyPrint = op1.prettyPrint + " "+opChar+" " + op2.prettyPrint
def getRefs = op1.getRefs ++ op2.getRefs
}

case class AddExpr(val op1: NumericExpr, val op2: NumericExpr) extends BinaryNumericExpr(op1, op2, "+", _ + _)

case class SubExpr(val op1: NumericExpr, val op2: NumericExpr) extends BinaryNumericExpr(op1, op2, "-", _ - _)

case class MulExpr(val op1: NumericExpr, val op2: NumericExpr) extends BinaryNumericExpr(op1, op2, "*", _ * _)

case class DivExpr(val op1: NumericExpr, val op2: NumericExpr) extends BinaryNumericExpr(op1, op2, "/", _ / _)
87 changes: 87 additions & 0 deletions base/src/main/scala/co/uproot/abandon/Config.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package co.uproot.abandon

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

class AbandonCLIConf(arguments: Seq[String]) extends ScallopConf(arguments) {
val inputs = opt[List[String]]("input", short = 'i')
val reports = opt[List[String]]("report", short = 'r')
val config = opt[String]("config", short = 'c')
// val trail = trailArg[String]()
}

object SettingsHelper {
implicit class ConfigHelper(val config: Config) extends AnyVal {
def optional[T](path: String)(f: (Config, String) => T) = {

if (config.hasPath(path)) {
Some(f(config, path))
} else {
None
}
}
}

def getCompleteSettings(args: Seq[String]) = {
val cliConf = new AbandonCLIConf(args)
val configOpt = cliConf.config.get
configOpt match {
case Some(configFileName) =>
makeSettings(configFileName)
case _ =>
val inputs = cliConf.inputs.get.getOrElse(Nil)
val allReport = BalanceReportSettings("All Balances", None)
Right(Settings(inputs, Seq(allReport), ReportOptions(Nil)))
}
}

def makeSettings(configFileName: String) = {
val file = new java.io.File(configFileName)
if (file.exists) {
val config = ConfigFactory.parseFile(file).resolve()
try {
val inputs = config.getStringList("inputs").asScala.map(Processor.mkRelativeFileName(_, configFileName))
val reports = config.getConfigList("reports").asScala.map(makeReportSettings(_))
val reportOptions = config.optional("reportOptions") { _.getConfig(_) }
val isRight = reportOptions.map(_.optional("isRight") { _.getStringList(_).asScala }).flatten.getOrElse(Nil)
Right(Settings(inputs, reports, ReportOptions(isRight)))
} catch {
case e: ConfigException.Missing => Left(e.getMessage)
}
} else {
Left("Config file not found: " + configFileName)
}
}

def makeReportSettings(config: Config) = {
val title = config.getString("title")
val reportType = config.getString("type")
val accountMatch = config.optional("accountMatch") { _.getStringList(_).asScala }
reportType match {
case "balance" => BalanceReportSettings(title, accountMatch)
case "register" => RegisterReportSettings(title, accountMatch)
}
}
}

case class Settings(inputs: Seq[String], reports: Seq[ReportSettings], reportOptions: ReportOptions)

abstract class ReportSettings(val title:String, val accountMatch: Option[Seq[String]]) {
def isAccountMatching(name: String) = {
accountMatch.map(patterns => patterns.exists(name matches _)).getOrElse(true)
}
}

case class BalanceReportSettings(_title: String, _accountMatch: Option[Seq[String]]) extends ReportSettings(_title, _accountMatch) {
}

case class RegisterReportSettings(_title: String, _accountMatch: Option[Seq[String]]) extends ReportSettings(_title, _accountMatch) {
}

case class ReportOptions(isRight: Seq[String])

52 changes: 52 additions & 0 deletions base/src/main/scala/co/uproot/abandon/Eval.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package co.uproot.abandon

import com.sun.org.apache.xalan.internal.xsltc.compiler.LiteralExpr

case class Ref(name: String, argCount: Int)

object EvaluationContext {
private def ensureUnique[T](defs:Seq[Definition[T]]) = {
val duplicate = defs.map(_.name).combinations(2).find(e => e.head equals e.tail.head)
if (duplicate.isDefined) {
throw new InputError("Attempt to redefine value having name: " + duplicate.get.head)
}
}
}

class EvaluationContext[T](globalDefinitions: Seq[Definition[T]], localDefinitions: Seq[Definition[T]], literalFactory:(T) => Expr[T]) {
EvaluationContext.ensureUnique(globalDefinitions)
EvaluationContext.ensureUnique(localDefinitions)

private val localNames = localDefinitions.map(_.name)
private val definitions = globalDefinitions.filter(d => !localNames.contains(d.name)) ++ localDefinitions
// println("Context created\n" + definitions.map(_.prettyPrint).mkString("\n"))

private val defined = definitions.map(d => d.name -> d).toMap
definitions.foreach { d =>
d.rhs.getRefs foreach { ref =>
if (!defined.isDefinedAt(ref.name)) {
if (!d.params.contains(ref.name)) {
throw new InputError("Definition not found: " + ref.name)
}
} else {
if (defined(ref.name).params.length != ref.argCount) {
throw new InputError("Reference and Definition parameters don't match: " + ref.name)
}
}
}
}

private def mkContext(newLocalDefs:Seq[Definition[T]]) = {
// println("Making new context with", newLocalDefs.map(_.prettyPrint))
new EvaluationContext[T](globalDefinitions, newLocalDefs, literalFactory)
}

def isImmediatelyEvaluable(name: String) = true
def getValue(name: String, params:Seq[T]) = {
val d = defined(name)
assert(d.params.length == params.length)
val result = d.rhs.evaluate(mkContext(d.params.zip(params).map(pairs => Definition(pairs._1, Nil, literalFactory(pairs._2)))))
// println("evaluated", name, params, result)
result
}
}
22 changes: 22 additions & 0 deletions base/src/main/scala/co/uproot/abandon/Helper.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package co.uproot.abandon

object Helper {
val Zero = BigDecimal(0)
def maxElseZero(s: Iterable[Int]) = if (s.nonEmpty) s.max else 0

def filterByType[T](s: Seq[_ >: T])(implicit m: Manifest[T]) = s.collect({ case t: T => t })

def bold(s: String) = Console.BOLD + s + Console.RESET
def warn(s: String) = Console.RED + s + Console.RESET

val monthLabels = Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")

private var counter = 0
def getUniqueInt = {
synchronized {
val prev = counter
counter += 1
prev
}
}
}
Loading

0 comments on commit c9f5d33

Please sign in to comment.