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");