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