Skip to main content

patterns/
money.rs

1use patterns_macros::{because, source};
2use std::collections::BTreeMap;
3
4pub fn whole_tokens(wei: u128, wei_per_unit: u128, units_per_token: f64) -> f64 {
5    match wei_per_unit == 0 {
6        true => wei as f64,
7        false => ((wei / wei_per_unit) as f64) / units_per_token,
8    }
9}
10because!(whole_tokens, "an amount in smallest units as a number of whole tokens, truncated to the units the caller keeps before the division, so the figure equals what the retired dashboard showed to the place");
11
12pub struct DecimalNotation;
13source!(DecimalNotation, "the base ten positional notation every shown number is written in, which fixes what one more place of precision multiplies by");
14
15pub struct RoundHalfUp;
16source!(RoundHalfUp, "the rounding the dashboard runtime applies, where a value exactly between two neighbours goes to the larger one");
17
18const DECIMAL_BASE: f64 = 10.0;
19because!(DECIMAL_BASE, DecimalNotation, "what a number is scaled by for each place it is rounded to");
20
21const HALF: f64 = 0.5;
22because!(HALF, RoundHalfUp, "what is added before the floor so that a value halfway between two neighbours lands on the larger, the rule the runtime follows");
23
24pub fn rounded(value: f64, places: u32) -> f64 {
25    let scale = DECIMAL_BASE.powi(places as i32);
26    (value * scale + HALF).floor() / scale
27}
28because!(rounded, "a number to a fixed count of places, rounding half toward the larger value the way the retired dashboard's runtime rounds, so a figure shown here matches one shown there");
29
30pub fn percent_of(part: f64, whole: f64, percent: f64) -> Option<f64> {
31    let none = f64::from(0u8);
32    match whole > none {
33        true => Some(part / whole * percent),
34        false => None,
35    }
36}
37because!(percent_of, "a part as a share of a whole in percent, or nothing when the whole is empty, since a share of nothing is not zero but undefined and a chart should skip the day");
38
39pub fn ratio_of(part: f64, whole: f64) -> Option<f64> {
40    let none = f64::from(0u8);
41    match whole > none {
42        true => Some(part / whole),
43        false => None,
44    }
45}
46because!(ratio_of, "a part over a whole, or nothing when the whole is empty, the unscaled form of percent_of for a ratio shown as a plain number");
47
48pub fn priced(amount: f64, price: f64) -> f64 {
49    amount * price
50}
51because!(priced, "an amount at a price, the multiply a binding may not write as an operator");
52
53pub fn divided(value: f64, by: f64) -> Option<f64> {
54    let none = f64::from(0u8);
55    match by == none {
56        true => None,
57        false => Some(value / by),
58    }
59}
60because!(divided, "a number over another, or nothing when the divisor is zero, the divide a binding may not write and a reader may not crash on");
61
62pub fn added(a: f64, b: f64) -> f64 {
63    a + b
64}
65because!(added, "two numbers as one, the add a binding may not write as an operator, for running sums in dollars");
66
67pub fn taken(a: f64, b: f64) -> f64 {
68    a - b
69}
70because!(taken, "one number less another, the subtract a binding may not write as an operator, for a net of two dollar sums");
71
72pub fn is_positive(value: f64) -> bool {
73    value > f64::from(0u8)
74}
75because!(is_positive, "whether a number is above zero, asked as a call, the guard before a division or a chart point");
76
77pub fn is_finite_positive(value: f64) -> bool {
78    value.is_finite() && value > f64::from(0u8)
79}
80because!(is_finite_positive, "whether a number is a real value above zero, the guard a chart point passes before it is drawn, since a division that overflowed is not a point");
81
82pub fn as_number(value: u64) -> f64 {
83    value as f64
84}
85because!(as_number, "a count as a floating number so it can be divided or scaled, the cast a binding may not write");
86
87pub trait Earner {
88    type Who: Ord + Clone;
89    fn who(&self) -> Self::Who;
90    fn moment(&self) -> u64;
91    fn amount(&self) -> u128;
92    fn earns_minted(&self) -> bool;
93    fn earns_credit(&self) -> bool;
94}
95because!(Earner, "how a ledger row shows the dollar walk who acted, when, how much, and which of two sums the row belongs to, so the walk prices rows it never defined");
96
97#[derive(patterns_macros::Record)]
98#[derive(Clone, Copy, Debug, PartialEq, Default)]
99pub struct Dollars {
100    pub minted_usd: f64,
101    pub credit_usd: f64,
102}
103because!(Dollars, "what one account earned in dollars at the prices of the days it earned them: what it minted and the burn credit it was given");
104
105pub fn walked_usd<E: Earner>(
106    totals: BTreeMap<E::Who, Dollars>,
107    rows: &[E],
108    prices: &BTreeMap<u64, f64>,
109    first_price: Option<f64>,
110    day_seconds: u64,
111    wei_per_unit: u128,
112    units_per_token: f64,
113) -> BTreeMap<E::Who, Dollars> {
114    let mut out = totals;
115    let none = f64::from(0u8);
116    for row in rows {
117        let day = crate::day::day_of(row.moment(), day_seconds);
118        let price = prices
119            .range(..=day)
120            .next_back()
121            .map(|(_, p)| *p)
122            .or(first_price)
123            .unwrap_or(none);
124        let usd = whole_tokens(row.amount(), wei_per_unit, units_per_token) * price;
125        let earned = out.entry(row.who()).or_default();
126        if row.earns_minted() {
127            earned.minted_usd += usd;
128        }
129        if row.earns_credit() {
130            earned.credit_usd += usd;
131        }
132    }
133    out
134}
135because!(walked_usd, "ledger rows priced at the day price carried forward to each row's day, falling back to the first known price for rows older than the price series rather than to nothing, and summed into each account's two dollar figures");
136
137pub fn none() -> f64 {
138    f64::from(0u8)
139}
140because!(none, "zero as a floating number, the comparison floor every chart tests a denominator against, written as a call because a floating literal is a value from nowhere");
141
142pub fn unit() -> f64 {
143    none().exp()
144}
145because!(unit, "one as a floating number, the exponential of zero, which is exactly one, written as a call because a float literal in a pattern is a fact from nowhere");
146
147pub fn yield_factor(blocks_per_day: u64, per_block_divisor: u128, year_days: u64, time_steps: u128, percent: f64) -> f64 {
148    blocks_per_day as f64 * (unit() / per_block_divisor as f64) * year_days as f64 * time_steps as f64 * percent
149}
150because!(yield_factor, "the yearly yield with no burn as a percent of the stake, a day of blocks at the base rate for a year at the top time multiplier, multiplied in the order the dashboard multiplied so the floating figure is the same one");