-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit c9f5d33
Showing
19 changed files
with
1,263 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
target/ | ||
project/target/ | ||
.cache | ||
*.swp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, "/", _ / _) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
Oops, something went wrong.