pwncash/dhall/Types.dhall

1033 lines
26 KiB
Plaintext

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 -}
{ 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 =
{ 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
-}
< LookupN : Text | ConstN : Double | 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) ->
{ eAcnt :
{-
Pertains to account for this entry.
-}
a
, eValue :
{-
Pertains to value for this entry.
-}
v
, eCurrency :
{-
Pertains to value for this entry.
-}
c
, 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
-}
{ Type =
Entry EntryAcntGetter (Optional EntryNumGetter) EntryCurGetter TagID
, default = { eValue = None EntryNumGetter, eComment = "" }
}
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 =
{ 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
, 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
, 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) ->
{ 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 Double
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.
-}
Double
}
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
-}
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 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.
-}
Double
, 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 =
{ 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 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.
-}
Double
}
let BudgetTransferValue =
{-
Means to determine the value of a budget transfer.
-}
{ btVal : Double, 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.
-}
{ 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
}
in { CurID
, AcntID
, SqlConfig
, Currency
, Tag
, TagID
, Interval
, TemporalScope
, Gregorian
, GregorianM
, TimeUnit
, Weekday
, RepeatPat
, MDYPat
, ModPat
, WeekdayPat
, CronPat
, DatePat
, 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
}