let Map = https://prelude.dhall-lang.org/v21.1.0/Map/Type sha256:210c7a9eba71efbb0f7a66b3dcf8b9d3976ffc2bc0e907aadfb6aa29c333e8ed let List/map = https://prelude.dhall-lang.org/v21.1.0/List/map sha256:dd845ffb4568d40327f2a817eb42d1c6138b929ca758d50bc33112ef3c885680 let AccountTree : Type = {- Recursive type representing a tree of accounts. A node in the tree can either be an account (leaf) which has a name and description, or a placeholder (branch) which has name, description, and a non-empty list of accounts or other placeholders. -} forall (a : Type) -> forall ( Fix : < AccountF : { _1 : Text, _2 : Text } | PlaceholderF : { _1 : Text , _2 : Text , _3 : {- TODO nonempty? -} List a } > -> a ) -> a let AccountTreeF = {- Fixed type abstraction for an account tree. -} \(a : Type) -> < AccountF : { _1 : Text, _2 : Text } | PlaceholderF : { _1 : Text, _2 : Text, _3 : List a } > let Account : Text -> Text -> AccountTree = {- Smart constructor to build an account node in an account tree. -} \(desc : Text) -> \(name : Text) -> \(a : Type) -> let f = AccountTreeF a in \(Fix : f -> a) -> Fix (f.AccountF { _1 = desc, _2 = name }) let Placeholder : Text -> Text -> List AccountTree -> AccountTree = {- Smart constructor to build a placeholder node in an account tree. -} \(desc : Text) -> \(name : Text) -> \(children : List AccountTree) -> \(a : Type) -> let f = AccountTreeF a in \(Fix : f -> a) -> let apply = \(x : AccountTree) -> x a Fix in Fix ( f.PlaceholderF { _1 = desc , _2 = name , _3 = List/map AccountTree a apply children } ) let AcntID = {- A unique ID for an account; the exact ID associated with each account depends on the path in which each account exists in a tree. If the leaf node of an account's path is totally unique, this ID will be that leaf node. If not, then branch nodes will be appended to the front (delimited by underscores) until the ID is unique. This ID will be used throughout the config to refer to a specific account. -} Text let CurID = {- A unique, short (usually three uppercase characters) ID for a currency; this symbol will be used throughout the configuration to signify a particular currency -} Text let Currency = {- A unit of exchange. -} { curSymbol : CurID , curFullname : {- The full description of this currency (eg "Yugoslavian Twitcoin") -} Text } let TagID = {- A unique ID for a tag. This ID will be used throughout the configuration to refer to a specific tag. -} Text let Tag = {- A short metadata identifier associated with an account or entry. -} { tagID : TagID , tagDesc : {- A description to convey the meaning of this tag. -} Text } let SqlConfig {- TODO pgsql -} = {- How to connect to a SQL database. Only SQLite is supported, in which case the only parameter is the output path of the db file. -} < Sqlite : Text | Postgres > let Gregorian = {- A full date like 1976-04-01 -} { gYear : Natural, gMonth : Natural, gDay : Natural } let GregorianM = {- Like a Gregorian but without the month -} { gmYear : Natural, gmMonth : Natural } let Interval = {- An interval in time. If end is None, the interval ends at 'forever' -} { intStart : Gregorian, intEnd : Optional Gregorian } let TemporalScope = {- The range of times that will be considered when computing transactions. -} { budgetInterval : Interval, statementInterval : Interval } let TimeUnit = < Day | Week | Month | Year > let Weekday = < Mon | Tue | Wed | Thu | Fri | Sat | Sun > let RepeatPat = {- Means to match a repeated set of numeric values. -} { rpStart : {- Initial value to match -} Natural , rpBy : {- Distance between each repeated value -} Natural , rpRepeats : {- Number of repeats after initial value to match. If not given, this number continues until an upper bound determined from context. -} Optional Natural } let MDYPat = {- Means to match either a year, month, or day in a date (the matched component is determined by context) Single: match a single number Multi: match several numbers Repeat: match a repeated pattern After: match any number greater than a given value Before: match any number less than a given value Between: match any number between two values -} < Single : Natural | Multi : List Natural | Repeat : RepeatPat | After : Natural | Before : Natural | Between : { _between1 : Natural, _between2 : Natural } > let ModPat = {- Means to match a date using modular arithmetic. -} { Type = { mpStart : {- The starting date to begin matching. If not given, start at the beginning of whatever global time window is active. -} Optional Gregorian , mpBy : {- Numeric number of temporal units to repeat before next match. -} Natural , mpUnit : {- Unit of each interval -} TimeUnit , mpRepeats : {- Number of repeats to match. If not given, match all repeats until the end of the active global interval -} Optional Natural } , default = { mpStart = None Gregorian, mpRepeats = None Natural } } let WeekdayPat = {- Means to match a given day of week OnDay: Match a single weekday OnDays: Match multiple weekdays -} < OnDay : Weekday | OnDays : List Weekday > let CronPat = {- Means of matching dates according to their component parts. This is similar to 'cron' patterns in unix-like systems. -} { Type = { cronWeekly : Optional WeekdayPat , cronYear : Optional MDYPat , cronMonth : Optional MDYPat , cronDay : Optional MDYPat } , default = { cronWeekly = None WeekdayPat , cronYear = None MDYPat , cronMonth = None MDYPat , cronDay = None MDYPat } } let DatePat = {- Means of matching dates Cron: use cron-like date matching Mod: use modular temporal arithmetic matching -} < Cron : CronPat.Type | Mod : ModPat.Type > let Decimal = { whole : Natural, decimal : Natural, precision : Natural, sign : Bool } let TxOpts = {- Additional metadata to use when parsing a statement -} { Type = { toDate : {- Column title for date -} Text , toAmount : {- Column title for amount -} Text , toDesc : {- Column title for description -} Text , toOther : {- Titles of other columns to include; these will be available in a map for use in downstream processing (see 'Field') -} List Text , toDateFmt : {- Format of the date field as specified in the Data.Time.Format.formattime Haskell function. -} Text , toAmountFmt : {- Format of the amount field. Must include three fields for the sign, numerator, and denominator of the amount. -} Text } , default = { toDate = "Date" , toAmount = "Amount" , toDesc = "Description" , toOther = [] : List Text , toDateFmt = "%0m/%0d/%Y" , toAmountFmt = "([-+])?([0-9]+)\\.?([0-9]+)?" } } let Field = {- General key-value type -} \(k : Type) -> \(v : Type) -> { fKey : k, fVal : v } let FieldMap = {- Key-value type where key maps to a Map with key of the same type -} \(k : Type) -> \(v : Type) -> Field k (Map k v) let ValMatcher = {- Means to match a decimal value. -} { Type = { mvSign : {- Sign of value. True -> positive, False -> negative, None -> do not consider. -} Optional Bool , mvNum : {- Value of numerator to match. Do not consider numerator if none -} Optional Natural , mvDen : {- Value of denominator to match. Do not consider numerator if none -} Optional Natural , mvPrec : {- Precision of decimal to use when matching. This only affects the denominator, such that a query of '10.1' with a precision of 2 will have a denominator of '10' -} Natural } , default = { mvSign = None Bool , mvNum = None Natural , mvDen = None Natural , mvPrec = 2 } } let YMDMatcher = {- Means to match a given date with varying precision -} < Y : Natural | YM : GregorianM | YMD : Gregorian > let DateMatcher = {- Means to match either one discrete date or a range of dates -} < On : YMDMatcher | In : { _1 : YMDMatcher, _2 : Natural } > let FieldMatcher_ = {- Means to match a given field (either textual or numeric) -} \(re : Type) -> < Desc : Field Text re | Val : Field Text ValMatcher.Type > let FieldMatcher = FieldMatcher_ Text let EntryNumGetter = {- Means to get a numeric value from a statement row. LookupN: lookup the value from a field ConstN: a constant value AmountN: the value of the 'Amount' column -} < LookupN : Text | ConstN : Decimal | AmountN > let EntryTextGetter = {- Means to get a textual value from a statement row. ConstT: a constant value LookupT: lookup the value of a field MapT: use the value of a column as the key for a map Map2T: use the paired value of 2 columns as the key for a map -} \(t : Type) -> < ConstT : t | LookupT : Text | MapT : FieldMap Text t | Map2T : FieldMap { _1 : Text, _2 : Text } t > let EntryCurGetter = {- Means to get a currency ID from a statement row. -} EntryTextGetter CurID let EntryAcntGetter = {- Means to get an account ID from a statement row. -} EntryTextGetter AcntID let Entry = {- General type describing a single line item in an account. The polymorphism of this type allows representation of an actual line item itself as well as the means to get a line item from other data. -} \(a : Type) -> \(v : Type) -> \(c : Type) -> \(t : Type) -> { sAcnt : {- Pertains to account for this entry. -} a , sValue : {- Pertains to value for this entry. -} v , sCurrency : {- Pertains to value for this entry. -} c , sComment : {- A short description of this entry (if none, use a blank string) -} Text , sTags : {- Pertains to the tags to describe this entry. -} List t } let EntryGetter = {- Means for getting an entry from a given row in a statement -} { Type = Entry EntryAcntGetter (Optional EntryNumGetter) EntryCurGetter TagID , default = { sValue = None EntryNumGetter, sComment = "" } } let TxGetter = {- A means for transforming one row in a statement to a transaction Note that N-1 entries need to be specified to make a transaction, as the Nth entry will be balanced with the others. -} { tgEntries : {- A means of getting entries for this transaction (minimum 1) -} List EntryGetter.Type , tgCurrency : {- Currency against which entries in this transaction will be balanced -} EntryCurGetter , tgAcnt : {- Account in which entries in this transaction will be balanced -} EntryAcntGetter } let StatementParser_ = {- A recipe to match and transform a given entry in a statement to a transaction between 2 or more accounts. Polymorphism allows regular expressions to be computed and cached within the type during parsing. -} \(re : Type) -> { Type = { mDate : {- How to match the date column; if none match any date -} Optional DateMatcher , mVal : {- How to match the value column; if none match any value -} ValMatcher.Type , mDesc : {- Regular expression to match the description; if none match anythingS -} Optional re , mOther : {- How to match additional columns if present -} List (FieldMatcher_ re) , mTx : {- How to translate the matched statement row into entries for a transaction. If none, don't make a transaction (eg 'skip' this row in the statement). -} Optional TxGetter , mTimes : {- Match at most this many rows; if none there is no limit -} Optional Natural , mPriority : {- In case of multiple matches, higher priority gets precedence. -} Integer } , default = { mDate = None DateMatcher , mVal = ValMatcher::{=} , mDesc = None Text , mOther = [] : List (FieldMatcher_ re) , mTx = None TxGetter , mTimes = None Natural , mPriority = +0 } } let StatementParser = {- A statement parser specialized to raw regular expressions. -} StatementParser_ Text let Amount = {- A quantify of currency at a given time. -} \(w : Type) -> \(v : Type) -> { amtWhen : w, amtValue : v, amtDesc : Text } let Transfer = {- 1-1 transaction(s) between two accounts. -} \(a : Type) -> \(c : Type) -> \(w : Type) -> \(v : Type) -> { transFrom : a , transTo : a , transCurrency : c , transAmounts : List (Amount w v) } let HistTransfer = {- A manually specified historical transfer -} Transfer AcntID CurID DatePat Decimal let Statement = {- How to import a statement from local file(s). Statements are assumed to be tabular with one statement per row. -} { stmtPaths {- paths to statement files -} : List Text , stmtParsers {- parsers to match statements -} : List StatementParser.Type , stmtDelim {- file delimiter as a numeric char, usually either tab (9) or comma (44) -} : Natural , stmtTxOpts : TxOpts.Type , stmtSkipLines {- how many lines to skip before parsing statement -} : Natural } let History = {- How to generate historical transactions; either specified as manual transfers or via statements in files on local disk -} < HistTransfer : HistTransfer | HistStatement : Statement > let Exchange = {- A currency exchange. -} { xFromCur : {- Starting currency of the exchange. -} CurID , xToCur : {- Ending currency of the exchange. -} CurID , xAcnt : {- account in which the exchange will be documented. -} AcntID , xRate : {- The exchange rate between the currencies. -} Decimal } let BudgetCurrency = {- A 'currency' in the budget; either a fixed currency or an exchange -} < NoX : CurID | X : Exchange > let TaggedAcnt = {- An account with a tag -} { taAcnt : AcntID, taTags : List TagID } let Allocation = {- How to allocate a given budget stream. This can be thought of as a Transfer without an incoming account specified. -} \(w : Type) -> \(v : Type) -> { alloTo : TaggedAcnt , alloAmts : List (Amount w v) , alloCur : {-TODO allow exchanges here-} CurID } let PretaxValue = {- How to determine value of a pretax allocation. -} { preValue : {- The value to be deducted from gross income -} Decimal , prePercent : {- If true, value is interpreted as a percent of gross income instead of a fixed amount. -} Bool , preCategory : {- A category for this allocation. This is used when calculating taxes, which match against this to determine how much to deduct from the gross income stream. -} Text } let TaxBracket = {- A single tax bracket. Read as "every unit above limit is taxed at this percentage". -} { tbLowerLimit : Decimal, tbPercent : Decimal } let TaxProgression = {- A tax progression using a deductible and a series of tax brackets. This will cover simple cases of the US income tax as of 2017 and similar. -} { tbsDeductible : {- Initial amount to subtract from after-pretax-deductions -} Decimal , tbsBrackets : {- Tax brackets to apply after deductions (order does not matter, each entry will be sorted by limit) -} List TaxBracket } let TaxMethod = {- How to implement a given tax (either a progressive tax or a fixed percent) -} < TMBracket : TaxProgression | TMPercent : Decimal > let TaxValue = {- Means to determine value of an income tax allocation. -} { tvCategories : {- A list of categories corresponding to pretax allocations. Taxable income (from the perspective of this type) will be determined by subtracting matching allocations from gross income. -} List Text , tvMethod : TaxMethod } let PosttaxValue = {- Means to determine value of a post tax allocation. -} { postValue : {- The value to be deducted from income remaining after taxes. -} Decimal , postPercent : {- If true, subtract a percentage from the after-tax remainder instead of a fixed value. -} Bool } let SingleAllocation = {- An allocation specialized to an income stream (which means the timing is dictated by the income stream) -} Allocation {} let MultiAllocation = {- An allocation specialized to capturing multiple income streams within a given time period (useful for instances where an allocation might change independent of a change in income) -} Allocation Interval let Income = {- Means to compute an income stream and how to allocate it -} { Type = { incGross : {- The value of the income stream. -} Decimal , incCurrency : {- The currency in which the income stream is denominated. -} CurID , incWhen : {- The dates on which the income stream is distributed. -} DatePat , incPretax : List (SingleAllocation PretaxValue) , incTaxes : List (SingleAllocation TaxValue) , incPosttax : List (SingleAllocation PosttaxValue) , incFrom : {- The account in which the income is recorded. This must be an income AcntID, and is the only place income accounts may be specified in the entire budget. -} TaggedAcnt , incToBal : {- The account to which to send the remainder of the income stream (if any) after all allocations have been applied. -} TaggedAcnt } , default = { incPretax = [] : List (SingleAllocation PretaxValue) , incTaxes = [] : List (SingleAllocation TaxValue) , incPosttaxx = [] : List (SingleAllocation PosttaxValue) } } let AcntSet = {- A list of account IDs represented as a set. -} { Type = { asList : List AcntID , asInclude : {- If true, tests for account membership in this set will return true if the account is in the set. Invert this behavior otherwise. -} Bool } , default = { asList = [] : List AcntID, asInclude = False } } let TransferMatcher = {- Means to match a transfer (which will be used to "clone" it in some fashion) -} { Type = { smFrom : {- List of accounts (which may be empty) to match with the starting account in a transfer. -} AcntSet.Type , smTo : {- List of accounts (which may be empty) to match with the ending account in a transfer. -} AcntSet.Type , smDate : {- If given, means to match the date of a transfer. -} Optional DateMatcher , smVal : {- If given, means to match the value of a transfer. -} ValMatcher.Type } , default = { smFrom = AcntSet.default , smTo = AcntSet.default , smDate = None DateMatcher , smVal = ValMatcher.default } } let BudgetTransferType = {- The type of a budget transfer. BTFixed: Tranfer a fixed amount BTPercent: Transfer a percent of the source account to destination BTTarget: Transfer an amount such that the destination has a given target value -} < BTPercent | BTTarget | BTFixed > let ShadowTransfer = {- A transaction analogous to another transfer with given properties. -} { stFrom : {- Source of this transfer -} TaggedAcnt , stTo : {- Destination of this transfer. -} TaggedAcnt , stCurrency : {- Currency of this transfer. -} BudgetCurrency , stDesc : {- Description of this transfer. -} Text , stMatch : {- Means to match other transfers which will be used as the basis to compute this transfer. The date is taken as-is, the value is multiplied by a constant (see 'stRatio') and everything else is specified in other fields of this type. -} TransferMatcher.Type , stType : BudgetTransferType , stRatio : {- Fixed multipler to translate value of matched transfer to this one. -} Decimal } let BudgetTransferValue = {- Means to determine the value of a budget transfer. -} { btVal : Decimal, btType : BudgetTransferType } let BudgetTransfer = {- A manually specified transaction for a budget -} Transfer TaggedAcnt BudgetCurrency DatePat BudgetTransferValue let Budget = {- A hypothetical set of transactions (eg a "budget") to be generated and inserted into the database. -} { budgetLabel : {- A unique label for this budget. Can be useful to compare multiple potential futures. -} Text , incomes : List Income.Type , pretax : List (MultiAllocation PretaxValue) , tax : List (MultiAllocation TaxValue) , posttax : List (MultiAllocation PosttaxValue) , transfers : List BudgetTransfer , shadowTransfers : List ShadowTransfer } in { CurID , AcntID , SqlConfig , Currency , Tag , TagID , Interval , TemporalScope , Gregorian , GregorianM , TimeUnit , Weekday , RepeatPat , MDYPat , ModPat , WeekdayPat , CronPat , DatePat , Decimal , TxOpts , StatementParser , StatementParser_ , ValMatcher , YMDMatcher , DateMatcher , FieldMatcher , FieldMatcher_ , EntryNumGetter , Field , FieldMap , Entry , EntryGetter , EntryTextGetter , EntryCurGetter , EntryAcntGetter , Statement , History , Transfer , Income , Budget , Allocation , Amount , TransferMatcher , ShadowTransfer , AcntSet , BudgetCurrency , Exchange , TaggedAcnt , Account , Placeholder , PretaxValue , PosttaxValue , TaxBracket , TaxProgression , TaxMethod , TaxValue , BudgetTransferValue , BudgetTransferType , TxGetter , HistTransfer , SingleAllocation , MultiAllocation }