smart_format/combinator/
truncate.rs1use core::fmt;
2
3use smart_string::DisplayExt;
4
5pub struct Truncate<T> {
16 inner: T,
17 max_chars: usize,
18}
19
20impl<T> Truncate<T> {
21 pub(crate) fn new(inner: T, max_chars: usize) -> Self {
22 Truncate { inner, max_chars }
23 }
24}
25
26impl<T: fmt::Display> fmt::Display for Truncate<T> {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 let max = self.max_chars;
29 let mut remaining = max;
30
31 self.inner.format_with(|s: Option<&str>| {
32 let Some(s) = s else { return Ok(()) };
33 if remaining == 0 {
34 return Ok(());
35 }
36
37 let char_count = s.chars().count();
38 if char_count <= remaining {
39 remaining -= char_count;
40 f.write_str(s)
41 } else {
42 let byte_end = s
44 .char_indices()
45 .nth(remaining)
46 .map(|(i, _)| i)
47 .unwrap_or(s.len());
48 remaining = 0;
49 f.write_str(&s[..byte_end])
50 }
51 })
52 }
53}
54
55pub struct TruncateWith<T> {
62 inner: T,
63 max_chars: usize,
64 tail: &'static str,
65}
66
67impl<T> TruncateWith<T> {
68 pub(crate) fn new(inner: T, max_chars: usize, tail: &'static str) -> Self {
69 TruncateWith {
70 inner,
71 max_chars,
72 tail,
73 }
74 }
75}
76
77impl<T: fmt::Display> fmt::Display for TruncateWith<T> {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 if self.max_chars == 0 {
80 return Ok(());
81 }
82
83 let tail_chars = self.tail.chars().count();
84
85 if tail_chars >= self.max_chars {
88 return Truncate::new(&self.inner, self.max_chars).fmt(f);
89 }
90
91 let content_budget = self.max_chars - tail_chars;
92
93 let mut total_chars: usize = 0;
95 self.inner.format_with(|s: Option<&str>| {
96 if let Some(s) = s {
97 total_chars += s.chars().count();
98 }
99 Ok(())
100 })?;
101
102 if total_chars <= self.max_chars {
103 self.inner.fmt(f)
105 } else {
106 let mut remaining = content_budget;
108 self.inner.format_with(|s: Option<&str>| {
109 let Some(s) = s else { return Ok(()) };
110 if remaining == 0 {
111 return Ok(());
112 }
113
114 let char_count = s.chars().count();
115 if char_count <= remaining {
116 remaining -= char_count;
117 f.write_str(s)
118 } else {
119 let byte_end = s
120 .char_indices()
121 .nth(remaining)
122 .map(|(i, _)| i)
123 .unwrap_or(s.len());
124 remaining = 0;
125 f.write_str(&s[..byte_end])
126 }
127 })?;
128 f.write_str(self.tail)
129 }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use crate::combinator::SmartFormat;
136
137 #[test]
138 fn truncate_shorter_than_limit() {
139 assert_eq!("hello", "hello".display_truncate(10).to_string());
140 }
141
142 #[test]
143 fn truncate_exact_limit() {
144 assert_eq!("hello", "hello".display_truncate(5).to_string());
145 }
146
147 #[test]
148 fn truncate_over_limit() {
149 assert_eq!("hel", "hello".display_truncate(3).to_string());
150 }
151
152 #[test]
153 fn truncate_zero() {
154 assert_eq!("", "hello".display_truncate(0).to_string());
155 }
156
157 #[test]
158 fn truncate_empty() {
159 assert_eq!("", "".display_truncate(5).to_string());
160 }
161
162 #[test]
163 fn truncate_unicode() {
164 assert_eq!("АБ", "АБВ".display_truncate(2).to_string());
166 }
167
168 #[test]
169 fn truncate_unicode_exact() {
170 assert_eq!("АБВ", "АБВ".display_truncate(3).to_string());
171 }
172
173 #[test]
174 fn truncate_with_no_truncation() {
175 assert_eq!("hi", "hi".display_truncate_with(10, "...").to_string());
176 }
177
178 #[test]
179 fn truncate_with_truncation() {
180 assert_eq!(
181 "hel...",
182 "hello world".display_truncate_with(6, "...").to_string()
183 );
184 }
185
186 #[test]
187 fn truncate_with_exact_fit() {
188 assert_eq!("hello", "hello".display_truncate_with(5, "...").to_string());
190 }
191
192 #[test]
193 fn truncate_with_tail_fills_budget() {
194 assert_eq!("hel", "hello".display_truncate_with(3, "...").to_string());
197 }
198
199 #[test]
200 fn truncate_with_tail_exceeds_budget() {
201 assert_eq!("he", "hello".display_truncate_with(2, "...").to_string());
203 }
204
205 #[test]
206 fn truncate_with_zero_budget() {
207 assert_eq!("", "hello".display_truncate_with(0, "...").to_string());
208 }
209
210 #[test]
211 fn truncate_with_unicode() {
212 assert_eq!("При…", "Привіт".display_truncate_with(4, "…").to_string());
214 }
215
216 #[test]
217 fn truncate_with_number() {
218 assert_eq!(
219 "123...",
220 123456789.display_truncate_with(6, "...").to_string()
221 );
222 }
223}