1use std::collections::HashMap;
12use std::time::Duration;
13
14use chrono::{DateTime, Local, Utc};
15
16pub fn local_time_hm(when: DateTime<Utc>) -> String {
17 when.with_timezone(&Local).format("%H:%M").to_string()
18}
19
20pub fn local_time_hms(when: DateTime<Utc>) -> String {
21 when.with_timezone(&Local).format("%H:%M:%S").to_string()
22}
23
24pub fn updated_at_hm(now: DateTime<Utc>, cache_age: Option<Duration>) -> String {
25 match cache_age {
26 Some(age) => local_time_hm(now - chrono::Duration::from_std(age).unwrap_or_default()),
27 None => "—".to_string(),
28 }
29}
30
31pub fn updated_at_hms(now: DateTime<Utc>, cache_age: Option<Duration>) -> String {
32 match cache_age {
33 Some(age) => local_time_hms(now - chrono::Duration::from_std(age).unwrap_or_default()),
34 None => "—".to_string(),
35 }
36}
37
38pub fn substitute(template: &str, values: &HashMap<&str, String>) -> String {
45 let mut out = String::with_capacity(template.len());
46 let mut rest = template;
47 while !rest.is_empty() {
48 match rest.find('{') {
49 None => {
50 out.push_str(rest);
51 break;
52 }
53 Some(open) => {
54 out.push_str(&rest[..open]);
56 let after_open = &rest[open + 1..];
57 if let Some(close) = after_open.find('}') {
58 let key = &after_open[..close];
59 if let Some(val) = values.get(key) {
60 out.push_str(val);
61 rest = &after_open[close + 1..];
62 continue;
63 }
64 }
65 out.push('{');
67 rest = after_open;
68 }
69 }
70 }
71 out
72}
73
74pub fn placeholders<I, V>(pairs: I) -> HashMap<&'static str, String>
76where
77 I: IntoIterator<Item = (&'static str, V)>,
78 V: Into<String>,
79{
80 pairs.into_iter().map(|(k, v)| (k, v.into())).collect()
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 fn pm(pairs: &[(&'static str, &str)]) -> HashMap<&'static str, String> {
88 placeholders(pairs.iter().map(|(k, v)| (*k, v.to_string())))
89 }
90
91 #[test]
92 fn single_substitution() {
93 let v = pm(&[("session_pct", "42")]);
94 assert_eq!(substitute("{session_pct}%", &v), "42%");
95 }
96
97 #[test]
98 fn multiple_substitutions() {
99 let v = pm(&[("a", "1"), ("b", "2")]);
100 assert_eq!(substitute("{a}-{b}-{a}", &v), "1-2-1");
101 }
102
103 #[test]
104 fn unknown_placeholder_passes_through() {
105 let v = pm(&[("a", "1")]);
106 assert_eq!(substitute("{a} {unknown}", &v), "1 {unknown}");
107 }
108
109 #[test]
110 fn no_re_substitution_in_replacement_text() {
111 let v = pm(&[("a", "{a}"), ("b", "X")]);
113 assert_eq!(substitute("{b}{a}{b}", &v), "X{a}X");
114 }
115
116 #[test]
117 fn empty_template() {
118 let v = pm(&[("a", "1")]);
119 assert_eq!(substitute("", &v), "");
120 }
121
122 #[test]
123 fn template_without_braces() {
124 let v = pm(&[("a", "1")]);
125 assert_eq!(substitute("hello world", &v), "hello world");
126 }
127
128 #[test]
129 fn unmatched_open_brace_is_literal() {
130 let v = pm(&[("a", "1")]);
131 assert_eq!(substitute("{a {x", &v), "{a {x");
132 }
133
134 #[test]
135 fn placeholders_with_underscores_and_digits() {
136 let v = pm(&[("session_pct_2", "x")]);
137 assert_eq!(substitute("{session_pct_2}", &v), "x");
138 }
139
140 #[test]
141 fn utf8_around_braces() {
142 let v = pm(&[("x", "→")]);
143 assert_eq!(substitute("α{x}β", &v), "α→β");
144 }
145}