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 , curPrecision : {- The number of decimal places for this currency -} Natural } 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 = { cpWeekly : Optional WeekdayPat , cpYear : Optional MDYPat , cpMonth : Optional MDYPat , cpDay : Optional MDYPat } , default = { cpWeekly = None WeekdayPat , cpYear = None MDYPat , cpMonth = None MDYPat , cpDay = 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 TxOpts_ = {- Additional metadata to use when parsing a statement -} \(re : Type) -> { 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. -} re } , default = { toDate = "Date" , toAmount = "Amount" , toDesc = "Description" , toOther = [] : List Text , toDateFmt = "%0m/%0d/%Y" , toAmountFmt = "([-+])?([0-9]+)\\.?([0-9]+)?" } } let TxOpts = TxOpts_ Text 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 = { vmSign : {- Sign of value. True -> positive, False -> negative, None -> do not consider. -} Optional Bool , vmNum : {- Value of numerator to match. Do not consider numerator if none -} Optional Natural , vmDen : {- Value of denominator to match. Do not consider numerator if none -} Optional Natural , vmPrec : {- 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 = { vmSign = None Bool , vmNum = None Natural , vmDen = None Natural , vmPrec = 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 times a scaling factor BalanceN: the amount required to make the target account reach a balance PercentN: the amount required to make an account reach a given percentage -} < LookupN : Text | ConstN : Double | AmountN : Double | BalanceN : Double | PercentN : Double > let LinkedNumGetter = {- Means to get a numeric value from another entry -} { Type = { lngIndex : {- Index of the entry to link. -} Natural , lngScale : {- Factor by which to multiply the value of the linked entry. -} Double } , default = { lngScale = 1.0, lngIndex = 0 } } let LinkedEntryNumGetter = {- Means to get a numeric value from a statement row or another entry getter. Linked: a number referring to the entry on the 'from' side of the transaction (with 0 being the primary entry) Getter: a normal getter -} < Linked : LinkedNumGetter.Type | Getter : EntryNumGetter > 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) -> \(t : Type) -> { eAcnt : {- Pertains to account for this entry. -} a , eValue : {- Pertains to value for this entry. -} v , eComment : {- A short description of this entry (if none, use a blank string) -} Text , eTags : {- Pertains to the tags to describe this entry. -} List t } let EntryGetter = {- Means for getting an entry from a given row in a statement (debit side) -} \(n : Type) -> { Type = Entry EntryAcntGetter n TagID , default = { eComment = "", eTags = [] : List TagID } } let FromEntryGetter = {- Means for getting an entry from a given row in a statement (debit side) -} EntryGetter EntryNumGetter let ToEntryGetter = {- Means for getting an entry from a given row in a statement (credit side) -} EntryGetter LinkedEntryNumGetter let TxHalfGetter = {- Means of transforming one row in a statement to either the credit or debit half of a transaction -} \(e : Type) -> { Type = { thgAcnt : {- Account from which this transaction will be balanced. The value of the transaction will be assigned to this account unless other entries are specified (see below). This account (and its associated entry) will be denoted 'primary'. -} EntryAcntGetter , thgEntries : {- Means of getting additional entries from which this transaction will be balanced. If this list is empty, the total value of the transaction will be assigned to the value defined by 'tsgAcnt'. Otherwise, the entries specified here will be added to this side of this transaction, and their sum value will be subtracted from the total value of the transaction and assigned to 'tsgAcnt'. This is useful for situations where a particular transaction denotes values that come from multiple subaccounts. -} List e , thgComment : {- Comment for the primary entry -} Text , thgTags : {- Tags for the primary entry -} List TagID } , default = { thgTags = [] : List TagID , thgComment = "" , thgEntries = [] : List e } } let FromTxHalfGetter = TxHalfGetter FromEntryGetter.Type let ToTxHalfGetter = TxHalfGetter ToEntryGetter.Type let TxSubGetter = {- A means for transforming one row in a statement to a transaction -} { Type = { tsgValue : EntryNumGetter , tsgCurrency : EntryCurGetter , tsgFrom : (TxHalfGetter FromEntryGetter.Type).Type , tsgTo : (TxHalfGetter ToEntryGetter.Type).Type } , default = { tsgFrom = TxHalfGetter, tsgTo = TxHalfGetter } } let TxGetter = {- A means for transforming one row in a statement to a transaction -} { Type = { tgFrom : (TxHalfGetter FromEntryGetter.Type).Type , tgTo : (TxHalfGetter ToEntryGetter.Type).Type , tgScale : Double , tgCurrency : EntryCurGetter , tgOtherEntries : List TxSubGetter.Type } , default = { tgOtherEntries = [] : List TxSubGetter.Type , tgFrom = TxHalfGetter , tgTo = TxHalfGetter , tgScale = 1.0 } } 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 = { spDate : {- How to match the date column; if none match any date -} Optional DateMatcher , spVal : {- How to match the value column; if none match any value -} ValMatcher.Type , spDesc : {- Regular expression to match the description; if none match anythingS -} Optional re , spOther : {- How to match additional columns if present -} List (FieldMatcher_ re) , spTx : {- 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.Type , spTimes : {- Match at most this many rows; if none there is no limit -} Optional Natural , spPriority : {- In case of multiple matches, higher priority gets precedence. -} Integer } , default = { spDate = None DateMatcher , spVal = ValMatcher::{=} , spDesc = None Text , spOther = [] : List (FieldMatcher_ re) , spTx = None TxGetter.Type , spTimes = None Natural , spPriority = +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) -> { Type = { amtWhen : w, amtValue : v, amtDesc : Text, amtPriority : Integer } , default.amtPriority = +0 } let TransferType = {- 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 -} < TPercent | TBalance | TFixed > let TransferValue = {- Means to determine the value of a budget transfer. -} { Type = { tvVal : Double, tvType : TransferType } , default.tvType = TransferType.TFixed } 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).Type } let TaggedAcnt = {- An account with a tag -} { Type = { taAcnt : AcntID, taTags : List TagID } , default.taTags = [] : List TagID } let HistTransfer = {- A manually specified historical transfer -} Transfer TaggedAcnt.Type CurID DatePat TransferValue.Type let TransferAmount = Amount DatePat TransferValue.Type 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 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.Type , alloAmts : List (Amount w v).Type , 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 -} Double , 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 : Double, tbPercent : Double } 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. -} { tpDeductible : {- Initial amount to subtract from after-pretax-deductions -} Double , tpBrackets : {- 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 : Double > 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. -} Double , 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 SingleAlloAmount = \(v : Type) -> Amount {} v 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 MultiAlloAmount = \(v : Type) -> Amount Interval v let HourlyPeriod = {- Definition for a pay period denominated in hours -} { hpAnnualHours : {- Number of hours in one year -} Natural , hpDailyHours : {- Number of hours in one working day -} Natural , hpWorkingDays : {- Days which count as working days -} List Weekday } let PeriodType = {- Type of pay period. Hourly: pay period is denominated in hours Daily: pay period is denominated in working days (specified in a list) -} < Hourly : HourlyPeriod | Daily : List Weekday > let Period = {- Definition of a pay period -} { pType : {- Type of pay period -} PeriodType , pStart : {- Start date of the pay period. Must occur before first payment in this income stream is dispersed. -} Gregorian } let Income = {- Means to compute an income stream and how to allocate it -} { Type = { incGross : {- The value of the income stream. -} Double , incCurrency : {- The currency in which the income stream is denominated. -} CurID , incWhen : {- The dates on which the income stream is distributed. -} DatePat , incPayPeriod : {- Defines the period of time over which this income was earned (mostly used for taxes) -} Period , 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.Type , incToBal : {- The account to which to send the remainder of the income stream (if any) after all allocations have been applied. -} TaggedAcnt.Type , incPriority : Integer } , default = { incPretax = [] : List (SingleAllocation PretaxValue) , incTaxes = [] : List (SingleAllocation TaxValue) , incPosttaxx = [] : List (SingleAllocation PosttaxValue) , incPriority = +0 } } 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 = { tmFrom : {- List of accounts (which may be empty) to match with the starting account in a transfer. -} AcntSet.Type , tmTo : {- List of accounts (which may be empty) to match with the ending account in a transfer. -} AcntSet.Type , tmDate : {- If given, means to match the date of a transfer. -} Optional DateMatcher , tmVal : {- If given, means to match the value of a transfer. -} ValMatcher.Type } , default = { tmFrom = AcntSet.default , tmTo = AcntSet.default , tmDate = None DateMatcher , tmVal = ValMatcher.default } } let ShadowTransfer = {- A transaction analogous to another transfer with given properties. -} { stFrom : {- Source of this transfer -} TaggedAcnt.Type , stTo : {- Destination of this transfer. -} TaggedAcnt.Type , stCurrency : {- Currency of this transfer. -} CurID , 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 : TransferType , stRatio : {- Fixed multipler to translate value of matched transfer to this one. -} Double } let BudgetTransfer = {- A manually specified transaction for a budget -} HistTransfer let Budget = {- A hypothetical set of transactions (eg a "budget") to be generated and inserted into the database. -} { bgtLabel : {- A unique label for this budget. Can be useful to compare multiple potential futures. -} Text , bgtIncomes : List Income.Type , bgtPretax : List (MultiAllocation PretaxValue) , bgtTax : List (MultiAllocation TaxValue) , bgtPosttax : List (MultiAllocation PosttaxValue) , bgtTransfers : List BudgetTransfer , bgtShadowTransfers : List ShadowTransfer , bgtInterval : Optional Interval } in { CurID , AcntID , SqlConfig , Currency , Tag , TagID , Interval , TemporalScope , Gregorian , GregorianM , TimeUnit , Weekday , RepeatPat , MDYPat , ModPat , WeekdayPat , CronPat , DatePat , TxOpts , TxOpts_ , StatementParser , StatementParser_ , ValMatcher , YMDMatcher , DateMatcher , FieldMatcher , FieldMatcher_ , EntryNumGetter , LinkedEntryNumGetter , LinkedNumGetter , Field , FieldMap , Entry , FromEntryGetter , ToEntryGetter , EntryTextGetter , EntryCurGetter , EntryAcntGetter , Statement , History , Transfer , Income , Budget , Allocation , Amount , TransferMatcher , ShadowTransfer , AcntSet , TaggedAcnt , AccountTree , Account , Placeholder , PretaxValue , PosttaxValue , TaxBracket , TaxProgression , TaxMethod , TaxValue , TransferValue , TransferType , TxGetter , TxSubGetter , TxHalfGetter , FromTxHalfGetter , ToTxHalfGetter , HistTransfer , SingleAllocation , MultiAllocation , HourlyPeriod , Period , PeriodType , TransferAmount , MultiAlloAmount , SingleAlloAmount }