1use std::collections::HashMap;
12use std::time::Duration;
13
14use chrono::{DateTime, Local, Utc};
15
16pub fn money(v: f64, currency: &str) -> String {
27 let magnitude = format!("{:.2}", v.abs());
28 let sign = if v < 0.0 && magnitude != "0.00" {
29 "-"
30 } else {
31 ""
32 };
33 match currency {
34 "USD" => format!("{sign}${magnitude}"),
35 "CNY" => format!("{sign}¥{magnitude}"),
36 _ => format!("{sign}{magnitude} {currency}"),
38 }
39}
40
41pub fn usd(v: f64) -> String {
43 money(v, "USD")
44}
45
46pub fn local_time_hm(when: DateTime<Utc>) -> String {
47 when.with_timezone(&Local).format("%H:%M").to_string()
48}
49
50pub fn local_time_hms(when: DateTime<Utc>) -> String {
51 when.with_timezone(&Local).format("%H:%M:%S").to_string()
52}
53
54pub fn updated_at_hm(now: DateTime<Utc>, cache_age: Option<Duration>) -> String {
55 match cache_age {
56 Some(age) => local_time_hm(now - chrono::Duration::from_std(age).unwrap_or_default()),
57 None => "—".to_string(),
58 }
59}
60
61pub fn updated_at_hms(now: DateTime<Utc>, cache_age: Option<Duration>) -> String {
62 match cache_age {
63 Some(age) => local_time_hms(now - chrono::Duration::from_std(age).unwrap_or_default()),
64 None => "—".to_string(),
65 }
66}
67
68pub fn substitute(template: &str, values: &HashMap<&str, String>) -> String {
75 let mut out = String::with_capacity(template.len());
76 let mut rest = template;
77 while !rest.is_empty() {
78 match rest.find('{') {
79 None => {
80 out.push_str(rest);
81 break;
82 }
83 Some(open) => {
84 out.push_str(&rest[..open]);
86 let after_open = &rest[open + 1..];
87 if let Some(close) = after_open.find('}') {
88 let key = &after_open[..close];
89 if let Some(val) = values.get(key) {
90 out.push_str(val);
91 rest = &after_open[close + 1..];
92 continue;
93 }
94 }
95 out.push('{');
97 rest = after_open;
98 }
99 }
100 }
101 out
102}
103
104pub fn placeholders<I, V>(pairs: I) -> HashMap<&'static str, String>
106where
107 I: IntoIterator<Item = (&'static str, V)>,
108 V: Into<String>,
109{
110 pairs.into_iter().map(|(k, v)| (k, v.into())).collect()
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 fn pm(pairs: &[(&'static str, &str)]) -> HashMap<&'static str, String> {
118 placeholders(pairs.iter().map(|(k, v)| (*k, v.to_string())))
119 }
120
121 #[test]
122 fn single_substitution() {
123 let v = pm(&[("session_pct", "42")]);
124 assert_eq!(substitute("{session_pct}%", &v), "42%");
125 }
126
127 #[test]
128 fn usd_keeps_the_sign_outside_the_symbol() {
129 assert_eq!(usd(74.5), "$74.50");
130 assert_eq!(usd(0.0), "$0.00");
131 assert_eq!(usd(-5.71), "-$5.71");
132 assert_eq!(usd(-0.0), "$0.00");
135 assert_eq!(usd(-0.001), "$0.00");
137 assert_eq!(usd(-0.006), "-$0.01");
139 }
140
141 #[test]
142 fn money_places_the_sign_ahead_of_every_currency() {
143 assert_eq!(money(20.0, "CNY"), "¥20.00");
144 assert_eq!(money(-20.0, "CNY"), "-¥20.00");
145 assert_eq!(money(3.5, "EUR"), "3.50 EUR");
148 assert_eq!(money(-3.5, "EUR"), "-3.50 EUR");
149 assert_eq!(money(-5.71, "USD"), usd(-5.71));
151 assert_eq!(money(-0.0, "CNY"), "¥0.00");
152 }
153
154 #[test]
155 fn multiple_substitutions() {
156 let v = pm(&[("a", "1"), ("b", "2")]);
157 assert_eq!(substitute("{a}-{b}-{a}", &v), "1-2-1");
158 }
159
160 #[test]
161 fn unknown_placeholder_passes_through() {
162 let v = pm(&[("a", "1")]);
163 assert_eq!(substitute("{a} {unknown}", &v), "1 {unknown}");
164 }
165
166 #[test]
167 fn no_re_substitution_in_replacement_text() {
168 let v = pm(&[("a", "{a}"), ("b", "X")]);
170 assert_eq!(substitute("{b}{a}{b}", &v), "X{a}X");
171 }
172
173 #[test]
174 fn empty_template() {
175 let v = pm(&[("a", "1")]);
176 assert_eq!(substitute("", &v), "");
177 }
178
179 #[test]
180 fn template_without_braces() {
181 let v = pm(&[("a", "1")]);
182 assert_eq!(substitute("hello world", &v), "hello world");
183 }
184
185 #[test]
186 fn unmatched_open_brace_is_literal() {
187 let v = pm(&[("a", "1")]);
188 assert_eq!(substitute("{a {x", &v), "{a {x");
189 }
190
191 #[test]
192 fn placeholders_with_underscores_and_digits() {
193 let v = pm(&[("session_pct_2", "x")]);
194 assert_eq!(substitute("{session_pct_2}", &v), "x");
195 }
196
197 #[test]
198 fn utf8_around_braces() {
199 let v = pm(&[("x", "→")]);
200 assert_eq!(substitute("α{x}β", &v), "α→β");
201 }
202}