Skip to main content

patterns/
render.rs

1use patterns_macros::{because, source};
2
3pub trait Named {
4    fn text(&self) -> &'static str;
5}
6
7pub fn named<T: Named>(value: &T) -> &'static str {
8    value.text()
9}
10
11pub fn named_text<T: Named>(value: &T) -> String {
12    String::from(value.text())
13}
14because!(named_text, "the display text of a named value as an owned string, for a page that answers the text rather than lending it");
15
16pub fn digits<T: std::fmt::Display>(value: T) -> String {
17    format!("{}", value)
18}
19
20pub fn label(name: &str, value: &str) -> String {
21    format!("{}={}", name, value)
22}
23
24pub fn joined(parts: &[String], between: &str) -> String {
25    parts.join(between)
26}
27
28pub fn numbered(stem: &str, at: u32, places: usize) -> String {
29    format!("{}{:0>width$}", stem, at, width = places)
30}
31because!(named, "the display text for a type that has one, so app never writes a user-facing string");
32because!(digits, "a number as text, kept separate from named because a number has no vocabulary to name it");
33because!(label, "a name and its value joined, the shape almost every rendered line takes");
34because!(numbered, "a name with a number padded to a fixed width, so a run of saved files sorts in the order it was made rather than by the first digit of the count");
35because!(joined, "several rendered parts into one line, so a separator is stated once here rather than at every call");
36
37pub trait Shown<M> {
38    fn shown(&self) -> &'static str;
39}
40
41pub fn shown_as<M, T: Shown<M>>(value: &T) -> &'static str {
42    value.shown()
43}
44because!(Shown, "a second and further display text on a type that already has a Named one, told apart by a marker, so a vocabulary can carry a key, a label and a description without three enums");
45because!(shown_as, "the text a marker selects on a value, the way value_of reads a marker's table, so app never writes a user-facing string for any of them");
46
47pub trait Texted<M> {
48    fn stem(&self) -> &'static str;
49    fn number(&self) -> Option<u64>;
50    fn tail(&self) -> &'static str;
51}
52
53pub fn texted<M, T: Texted<M>>(value: &T) -> String {
54    let mut out = String::from(value.stem());
55    if let Some(number) = value.number() {
56        out.push_str(&digits(number));
57    }
58    out.push_str(value.tail());
59    out
60}
61because!(Texted, "a display text that carries a number, split into the words before it, the constant, and the words after, so a spec never writes a digit into a string and the number is the one the constant states");
62because!(texted, "the three parts of a Texted value joined into the text a reader sees, the only place a stem and its number meet");
63
64#[derive(Clone, Copy, Debug, PartialEq)]
65pub enum Piece<P> {
66    Say(P),
67    First,
68    Second,
69    Third,
70    Fourth,
71    Number(u64),
72    Wide(u128),
73    Grouped(u64),
74    Fixed(f64, usize),
75}
76because!(Piece, "one part of a text with holes: a phrase the spec names, one of four values filled in at render time, or a number read from a constant, so no sentence with a figure in it is ever spelled out");
77
78pub fn filled<P: Named>(pieces: &[Piece<P>], first: &str, second: &str, third: &str, fourth: &str) -> String {
79    let mut out = String::new();
80    for piece in pieces {
81        match piece {
82            Piece::Say(phrase) => out.push_str(phrase.text()),
83            Piece::First => out.push_str(first),
84            Piece::Second => out.push_str(second),
85            Piece::Third => out.push_str(third),
86            Piece::Fourth => out.push_str(fourth),
87            Piece::Number(n) => out.push_str(&digits(n)),
88            Piece::Wide(n) => out.push_str(&digits(n)),
89            Piece::Grouped(n) => out.push_str(&grouped(*n)),
90            Piece::Fixed(value, places) => out.push_str(&fixed(*value, *places)),
91        }
92    }
93    out
94}
95because!(filled, "a text with holes rendered, the phrases in order with the four values and the numbers in their places, the one way a label with a token name or a figure in it is made");
96
97pub fn fixed(value: f64, places: usize) -> String {
98    format!("{:.*}", places, value)
99}
100because!(fixed, "a number with a fixed count of places after the point, the form a multiplier or a percent is shown in");
101
102const GROUP_MARK: u8 = 44;
103because!(GROUP_MARK, ThousandsGrouping, "the comma set between groups, written as its code point because a character literal is a value from nowhere in this zone");
104
105const GROUP: usize = 3;
106because!(GROUP, ThousandsGrouping, "the digits between separators when a large number is written for a reader");
107
108pub struct ThousandsGrouping;
109source!(ThousandsGrouping, "the convention of splitting a written number into groups of three digits from the right, which the dashboard's readers expect in every figure over a thousand");
110
111pub fn grouped(value: u64) -> String {
112    let plain = digits(value);
113    let mut out = String::with_capacity(plain.len() + plain.len() / GROUP);
114    for (at, c) in plain.chars().enumerate() {
115        if at > 0 && (plain.len() - at) % GROUP == 0 {
116            out.push(GROUP_MARK as char);
117        }
118        out.push(c);
119    }
120    out
121}
122because!(grouped, "a whole number with a separator every three digits from the right, the way a reader expects a large figure written");
123
124pub trait Worded {
125    fn text(&self) -> String;
126    fn ink(&self) -> Option<u32>;
127}
128because!(Worded, "a run of a title as its text and the ink it is drawn in, read off a spec's title vocabulary so a chart title is assembled here without naming a token");
129
130pub fn worded_text<T: Worded>(value: &T) -> String {
131    value.text()
132}
133because!(worded_text, "the text of one title run");
134
135pub fn worded_ink<T: Worded>(value: &T) -> Option<u32> {
136    value.ink()
137}
138because!(worded_ink, "the ink of one title run, or none for a plain word");