1#![forbid(missing_docs, unsafe_code)]
11#![warn(clippy::arithmetic_side_effects)]
12#![cfg_attr(not(feature = "std"), no_std)]
13
14#![cfg_attr(
26 feature = "std",
27 doc = r##"
28Making sure the string is displayed in exactly number of columns by
29combining padding and truncating.
30
31```rust
32use unicode_truncate::UnicodeTruncateStr;
33use unicode_truncate::Alignment;
34use unicode_width::UnicodeWidthStr;
35
36let str = "你好吗".unicode_pad(5, Alignment::Left, true);
37assert_eq!(str, "你好 ");
38assert_eq!(str.width(), 5);
39```
40"##
41)]
42
43use itertools::{merge_join_by, Either};
44use unicode_segmentation::UnicodeSegmentation;
45use unicode_width::UnicodeWidthStr;
46
47#[derive(PartialEq, Eq, Debug, Copy, Clone)]
49pub enum Alignment {
50 Left,
52 Center,
54 Right,
56}
57
58pub trait UnicodeTruncateStr {
60 fn unicode_truncate(&self, max_width: usize) -> (&str, usize);
73
74 fn unicode_truncate_start(&self, max_width: usize) -> (&str, usize);
87
88 fn unicode_truncate_centered(&self, max_width: usize) -> (&str, usize);
101
102 #[inline]
120 fn unicode_truncate_aligned(&self, max_width: usize, align: Alignment) -> (&str, usize) {
121 match align {
122 Alignment::Left => self.unicode_truncate(max_width),
123 Alignment::Center => self.unicode_truncate_centered(max_width),
124 Alignment::Right => self.unicode_truncate_start(max_width),
125 }
126 }
127
128 #[cfg(feature = "std")]
142 fn unicode_pad(
143 &self,
144 target_width: usize,
145 align: Alignment,
146 truncate: bool,
147 ) -> std::borrow::Cow<'_, str>;
148}
149
150impl UnicodeTruncateStr for str {
151 #[inline]
152 fn unicode_truncate(&self, max_width: usize) -> (&str, usize) {
153 let (byte_index, new_width) = self
154 .grapheme_indices(true)
155 .map(|(byte_index, grapheme)| (byte_index, grapheme.width()))
157 .chain(core::iter::once((self.len(), 0)))
159 .scan(0, |sum: &mut usize, (byte_index, grapheme_width)| {
161 let current_width = *sum;
165 *sum = sum.checked_add(grapheme_width)?;
166 Some((byte_index, current_width))
167 })
168 .take_while(|&(_, current_width)| current_width <= max_width)
170 .last()
171 .unwrap_or((0, 0));
172
173 let result = self.get(..byte_index).unwrap();
175 debug_assert_eq!(result.width(), new_width);
176 (result, new_width)
177 }
178
179 #[inline]
180 fn unicode_truncate_start(&self, max_width: usize) -> (&str, usize) {
181 let (byte_index, new_width) = self
182 .grapheme_indices(true)
183 .rev()
185 .map(|(byte_index, grapheme)| (byte_index, grapheme.width()))
187 .scan(0, |sum: &mut usize, (byte_index, grapheme_width)| {
189 *sum = sum.checked_add(grapheme_width)?;
190 Some((byte_index, *sum))
191 })
192 .take_while(|&(_, current_width)| current_width <= max_width)
193 .last()
194 .unwrap_or((self.len(), 0));
195
196 let result = self.get(byte_index..).unwrap();
198 debug_assert_eq!(result.width(), new_width);
199 (result, new_width)
200 }
201
202 #[inline]
203 fn unicode_truncate_centered(&self, max_width: usize) -> (&str, usize) {
204 if max_width == 0 {
205 return ("", 0);
206 }
207
208 let original_width = self.width();
209 if original_width <= max_width {
210 return (self, original_width);
211 }
212
213 let min_removal_width = original_width.checked_sub(max_width).unwrap();
216
217 let less_than_half = min_removal_width.saturating_sub(10) / 2;
222
223 let from_start = self
224 .grapheme_indices(true)
225 .map(|(byte_index, grapheme)| (byte_index, grapheme.width()))
226 .scan(
229 (0usize, 0usize),
230 |(sum, prev_width), (byte_index, grapheme_width)| {
231 *sum = sum.checked_add(*prev_width)?;
232 *prev_width = grapheme_width;
233 Some((byte_index, *sum))
234 },
235 )
236 .skip_while(|&(_, removed)| removed < less_than_half);
238
239 let from_end = self
240 .grapheme_indices(true)
241 .map(|(byte_index, grapheme)| (byte_index, grapheme.width()))
242 .rev()
243 .scan(0usize, |sum, (byte_index, grapheme_width)| {
246 *sum = sum.checked_add(grapheme_width)?;
247 Some((byte_index, *sum))
248 })
249 .skip_while(|&(_, removed)| removed < less_than_half);
251
252 let (start_index, end_index, removed_width) = merge_join_by(
253 from_start,
254 from_end,
255 |&(_, start_removed), &(_, end_removed)| start_removed < end_removed,
257 )
258 .scan(
260 (0usize, 0usize, 0usize, 0usize),
261 |(start_removed, end_removed, start_index, end_index), position| {
262 match position {
263 Either::Left((idx, removed)) => {
264 *start_index = idx;
265 *start_removed = removed;
266 }
267 Either::Right((idx, removed)) => {
268 *end_index = idx;
269 *end_removed = removed;
270 }
271 }
272 let total_removed = start_removed.checked_add(*end_removed).unwrap();
274 Some((*start_index, *end_index, total_removed))
275 },
276 )
277 .find(|&(_, _, removed)| removed >= min_removal_width)
278 .unwrap_or((0, 0, original_width));
281
282 let result = self.get(start_index..end_index).unwrap();
284 let result_width = original_width.checked_sub(removed_width).unwrap();
286 debug_assert_eq!(result.width(), result_width);
287 (result, result_width)
288 }
289
290 #[cfg(feature = "std")]
291 #[inline]
292 fn unicode_pad(
293 &self,
294 target_width: usize,
295 align: Alignment,
296 truncate: bool,
297 ) -> std::borrow::Cow<'_, str> {
298 use std::borrow::Cow;
299
300 if !truncate && self.width() >= target_width {
301 return Cow::Borrowed(self);
302 }
303
304 let (truncated, columns) = self.unicode_truncate(target_width);
305 if columns == target_width {
306 return Cow::Borrowed(truncated);
307 }
308
309 let diff = target_width.saturating_sub(columns);
311 let (left_pad, right_pad) = match align {
312 Alignment::Left => (0, diff),
313 Alignment::Right => (diff, 0),
314 Alignment::Center => (diff / 2, diff.saturating_sub(diff / 2)),
315 };
316 debug_assert_eq!(diff, left_pad.saturating_add(right_pad));
317
318 let new_len = truncated
319 .len()
320 .checked_add(diff)
321 .expect("Padded result should fit in a new String");
322 let mut result = String::with_capacity(new_len);
323 for _ in 0..left_pad {
324 result.push(' ');
325 }
326 result += truncated;
327 for _ in 0..right_pad {
328 result.push(' ');
329 }
330 Cow::Owned(result)
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 mod truncate_end {
339 use super::*;
340
341 #[test]
342 fn empty() {
343 assert_eq!("".unicode_truncate(4), ("", 0));
344 }
345
346 #[test]
347 fn zero_width() {
348 assert_eq!("ab".unicode_truncate(0), ("", 0));
349 assert_eq!("你好".unicode_truncate(0), ("", 0));
350 }
351
352 #[test]
353 fn less_than_limit() {
354 assert_eq!("abc".unicode_truncate(4), ("abc", 3));
355 assert_eq!("你".unicode_truncate(4), ("你", 2));
356 }
357
358 #[test]
359 fn at_boundary() {
360 assert_eq!("boundary".unicode_truncate(5), ("bound", 5));
361 assert_eq!("你好吗".unicode_truncate(4), ("你好", 4));
362 }
363
364 #[test]
365 fn not_boundary() {
366 assert_eq!("你好吗".unicode_truncate(3), ("你", 2));
367 assert_eq!("你好吗".unicode_truncate(1), ("", 0));
368 }
369
370 #[test]
371 fn zero_width_char_in_middle() {
372 assert_eq!("y\u{0306}es".unicode_truncate(2), ("y\u{0306}e", 2));
374 }
375
376 #[test]
377 fn keep_zero_width_char_at_boundary() {
378 assert_eq!(
380 "y\u{0306}ey\u{0306}s".unicode_truncate(3),
381 ("y\u{0306}ey\u{0306}", 3)
382 );
383 }
384
385 #[test]
386 fn family_stays_together() {
387 let input = "123👨👩👧👦456";
388 assert_eq!(input.unicode_truncate(4), ("123", 3));
389 assert_eq!(input.unicode_truncate(8), ("123", 3));
390 assert_eq!(input.unicode_truncate(12), ("123👨👩👧👦4", 12));
391 assert_eq!(input.unicode_truncate(20), (input, 14));
392 }
393 }
394
395 mod truncate_start {
396 use super::*;
397
398 #[test]
399 fn empty() {
400 assert_eq!("".unicode_truncate_start(4), ("", 0));
401 }
402
403 #[test]
404 fn zero_width() {
405 assert_eq!("ab".unicode_truncate_start(0), ("", 0));
406 assert_eq!("你好".unicode_truncate_start(0), ("", 0));
407 }
408
409 #[test]
410 fn less_than_limit() {
411 assert_eq!("abc".unicode_truncate_start(4), ("abc", 3));
412 assert_eq!("你".unicode_truncate_start(4), ("你", 2));
413 }
414
415 #[test]
416 fn at_boundary() {
417 assert_eq!("boundary".unicode_truncate_start(5), ("ndary", 5));
418 assert_eq!("你好吗".unicode_truncate_start(4), ("好吗", 4));
419 }
420
421 #[test]
422 fn not_boundary() {
423 assert_eq!("你好吗".unicode_truncate_start(3), ("吗", 2));
424 assert_eq!("你好吗".unicode_truncate_start(1), ("", 0));
425 }
426
427 #[test]
428 fn zero_width_char_in_middle() {
429 assert_eq!(
431 "y\u{0306}ey\u{0306}s".unicode_truncate_start(2),
432 ("y\u{0306}s", 2)
433 );
434 }
435
436 #[test]
437 fn remove_zero_width_char_at_boundary() {
438 assert_eq!("y\u{0306}es".unicode_truncate_start(2), ("es", 2));
440 }
441
442 #[test]
443 fn family_stays_together() {
444 let input = "123👨👩👧👦456";
445 assert_eq!(input.unicode_truncate_start(4), ("456", 3));
446 assert_eq!(input.unicode_truncate_start(8), ("456", 3));
447 assert_eq!(input.unicode_truncate_start(12), ("3👨👩👧👦456", 12));
448 assert_eq!(input.unicode_truncate_start(20), (input, 14));
449 }
450 }
451
452 mod truncate_centered {
453 use super::*;
454
455 #[test]
456 fn empty() {
457 assert_eq!("".unicode_truncate_centered(4), ("", 0));
458 }
459
460 #[test]
461 fn zero_width() {
462 assert_eq!("ab".unicode_truncate_centered(0), ("", 0));
463 assert_eq!("你好".unicode_truncate_centered(0), ("", 0));
464 }
465
466 #[test]
467 fn less_than_limit() {
468 assert_eq!("abc".unicode_truncate_centered(4), ("abc", 3));
469 assert_eq!("你".unicode_truncate_centered(4), ("你", 2));
470 }
471
472 #[test]
474 fn truncate_exactly_one() {
475 assert_eq!("abcd".unicode_truncate_centered(3), ("abc", 3));
476 }
477
478 #[test]
479 fn at_boundary() {
480 assert_eq!(
481 "boundaryboundary".unicode_truncate_centered(5),
482 ("arybo", 5)
483 );
484 assert_eq!(
485 "你好吗你好吗你好吗".unicode_truncate_centered(4),
486 ("你好", 4)
487 );
488 }
489
490 #[test]
491 fn not_boundary() {
492 assert_eq!("你好吗你好吗".unicode_truncate_centered(3), ("吗", 2));
493 assert_eq!("你好吗你好吗".unicode_truncate_centered(1), ("", 0));
494 }
495
496 #[test]
497 fn zero_width_char_in_middle() {
498 assert_eq!(
500 "yy\u{0306}es".unicode_truncate_centered(2),
501 ("y\u{0306}e", 2)
502 );
503 }
504
505 #[test]
506 fn zero_width_char_at_boundary() {
507 assert_eq!(
510 "y\u{0306}ea\u{0306}b\u{0306}y\u{0306}ea\u{0306}b\u{0306}"
511 .unicode_truncate_centered(2),
512 ("b\u{0306}y\u{0306}", 2)
513 );
514 assert_eq!(
515 "ay\u{0306}ea\u{0306}b\u{0306}y\u{0306}ea\u{0306}b\u{0306}"
516 .unicode_truncate_centered(2),
517 ("a\u{0306}b\u{0306}", 2)
518 );
519 assert_eq!(
520 "y\u{0306}ea\u{0306}b\u{0306}y\u{0306}ea\u{0306}b\u{0306}a"
521 .unicode_truncate_centered(2),
522 ("b\u{0306}y\u{0306}", 2)
523 );
524 }
525
526 #[test]
527 fn control_char() {
528 use unicode_width::UnicodeWidthChar;
529 assert_eq!("\u{0019}".width(), 1);
530 assert_eq!('\u{0019}'.width(), None);
531 assert_eq!("\u{0019}".unicode_truncate(2), ("\u{0019}", 1));
532 }
533
534 #[test]
535 fn family_stays_together() {
536 let input = "123👨👩👧👦456";
537 assert_eq!(input.unicode_truncate_centered(4), ("", 0));
538 assert_eq!(input.unicode_truncate_centered(8), ("👨👩👧👦", 8));
539 assert_eq!(input.unicode_truncate_centered(12), ("23👨👩👧👦45", 12));
540 assert_eq!(input.unicode_truncate_centered(20), (input, 14));
541 }
542 }
543
544 #[test]
545 fn truncate_aligned() {
546 assert_eq!("abc".unicode_truncate_aligned(1, Alignment::Left), ("a", 1));
547 assert_eq!(
548 "abc".unicode_truncate_aligned(1, Alignment::Center),
549 ("b", 1)
550 );
551 assert_eq!(
552 "abc".unicode_truncate_aligned(1, Alignment::Right),
553 ("c", 1)
554 );
555 }
556
557 #[cfg(feature = "std")]
558 mod pad {
559 use super::*;
560
561 #[test]
562 fn zero_width() {
563 assert_eq!("你好".unicode_pad(0, Alignment::Left, true), "");
564 assert_eq!("你好".unicode_pad(0, Alignment::Left, false), "你好");
565 }
566
567 #[test]
568 fn less_than_limit() {
569 assert_eq!("你".unicode_pad(4, Alignment::Left, true), "你 ");
570 assert_eq!("你".unicode_pad(4, Alignment::Left, false), "你 ");
571 }
572
573 #[test]
574 fn width_at_boundary() {
575 assert_eq!("你好吗".unicode_pad(4, Alignment::Left, true), "你好");
576 assert_eq!("你好吗".unicode_pad(4, Alignment::Left, false), "你好吗");
577 }
578
579 #[test]
580 fn width_not_boundary() {
581 assert_eq!("你好吗".unicode_pad(3, Alignment::Left, true), "你 ");
583 assert_eq!("你好吗".unicode_pad(1, Alignment::Left, true), " ");
584 assert_eq!("你好吗".unicode_pad(3, Alignment::Left, false), "你好吗");
585
586 assert_eq!("你好吗".unicode_pad(3, Alignment::Center, true), "你 ");
587
588 assert_eq!("你好吗".unicode_pad(3, Alignment::Right, true), " 你");
589 }
590 }
591}