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
389 assert_eq!("👨👩👧👦".width(), 2);
391
392 assert_eq!(input.unicode_truncate(4), ("123", 3));
393 assert_eq!(input.unicode_truncate(5), ("123👨👩👧👦", 5));
394 assert_eq!(input.unicode_truncate(6), ("123👨👩👧👦4", 6));
395 assert_eq!(input.unicode_truncate(20), (input, 8));
396 }
397 }
398
399 mod truncate_start {
400 use super::*;
401
402 #[test]
403 fn empty() {
404 assert_eq!("".unicode_truncate_start(4), ("", 0));
405 }
406
407 #[test]
408 fn zero_width() {
409 assert_eq!("ab".unicode_truncate_start(0), ("", 0));
410 assert_eq!("你好".unicode_truncate_start(0), ("", 0));
411 }
412
413 #[test]
414 fn less_than_limit() {
415 assert_eq!("abc".unicode_truncate_start(4), ("abc", 3));
416 assert_eq!("你".unicode_truncate_start(4), ("你", 2));
417 }
418
419 #[test]
420 fn at_boundary() {
421 assert_eq!("boundary".unicode_truncate_start(5), ("ndary", 5));
422 assert_eq!("你好吗".unicode_truncate_start(4), ("好吗", 4));
423 }
424
425 #[test]
426 fn not_boundary() {
427 assert_eq!("你好吗".unicode_truncate_start(3), ("吗", 2));
428 assert_eq!("你好吗".unicode_truncate_start(1), ("", 0));
429 }
430
431 #[test]
432 fn zero_width_char_in_middle() {
433 assert_eq!(
435 "y\u{0306}ey\u{0306}s".unicode_truncate_start(2),
436 ("y\u{0306}s", 2)
437 );
438 }
439
440 #[test]
441 fn remove_zero_width_char_at_boundary() {
442 assert_eq!("y\u{0306}es".unicode_truncate_start(2), ("es", 2));
444 }
445
446 #[test]
447 fn family_stays_together() {
448 let input = "123👨👩👧👦456";
449
450 assert_eq!("👨👩👧👦".width(), 2);
452
453 assert_eq!(input.unicode_truncate_start(4), ("456", 3));
454 assert_eq!(input.unicode_truncate_start(5), ("👨👩👧👦456", 5));
455 assert_eq!(input.unicode_truncate_start(6), ("3👨👩👧👦456", 6));
456 assert_eq!(input.unicode_truncate_start(20), (input, 8));
457 }
458 }
459
460 mod truncate_centered {
461 use super::*;
462
463 #[test]
464 fn empty() {
465 assert_eq!("".unicode_truncate_centered(4), ("", 0));
466 }
467
468 #[test]
469 fn zero_width() {
470 assert_eq!("ab".unicode_truncate_centered(0), ("", 0));
471 assert_eq!("你好".unicode_truncate_centered(0), ("", 0));
472 }
473
474 #[test]
475 fn less_than_limit() {
476 assert_eq!("abc".unicode_truncate_centered(4), ("abc", 3));
477 assert_eq!("你".unicode_truncate_centered(4), ("你", 2));
478 }
479
480 #[test]
482 fn truncate_exactly_one() {
483 assert_eq!("abcd".unicode_truncate_centered(3), ("abc", 3));
484 }
485
486 #[test]
487 fn at_boundary() {
488 assert_eq!(
489 "boundaryboundary".unicode_truncate_centered(5),
490 ("arybo", 5)
491 );
492 assert_eq!(
493 "你好吗你好吗你好吗".unicode_truncate_centered(4),
494 ("你好", 4)
495 );
496 }
497
498 #[test]
499 fn not_boundary() {
500 assert_eq!("你好吗你好吗".unicode_truncate_centered(3), ("吗", 2));
501 assert_eq!("你好吗你好吗".unicode_truncate_centered(1), ("", 0));
502 }
503
504 #[test]
505 fn zero_width_char_in_middle() {
506 assert_eq!(
508 "yy\u{0306}es".unicode_truncate_centered(2),
509 ("y\u{0306}e", 2)
510 );
511 }
512
513 #[test]
514 fn zero_width_char_at_boundary() {
515 assert_eq!(
518 "y\u{0306}ea\u{0306}b\u{0306}y\u{0306}ea\u{0306}b\u{0306}"
519 .unicode_truncate_centered(2),
520 ("b\u{0306}y\u{0306}", 2)
521 );
522 assert_eq!(
523 "ay\u{0306}ea\u{0306}b\u{0306}y\u{0306}ea\u{0306}b\u{0306}"
524 .unicode_truncate_centered(2),
525 ("a\u{0306}b\u{0306}", 2)
526 );
527 assert_eq!(
528 "y\u{0306}ea\u{0306}b\u{0306}y\u{0306}ea\u{0306}b\u{0306}a"
529 .unicode_truncate_centered(2),
530 ("b\u{0306}y\u{0306}", 2)
531 );
532 }
533
534 #[test]
535 fn control_char() {
536 use unicode_width::UnicodeWidthChar;
537 assert_eq!("\u{0019}".width(), 1);
538 assert_eq!('\u{0019}'.width(), None);
539 assert_eq!("\u{0019}".unicode_truncate(2), ("\u{0019}", 1));
540 }
541
542 #[test]
543 fn family_stays_together() {
544 let input = "123👨👩👧👦456";
545
546 assert_eq!("👨👩👧👦".width(), 2);
548
549 assert_eq!(input.unicode_truncate_centered(1), ("", 0));
550 assert_eq!(input.unicode_truncate_centered(2), ("👨👩👧👦", 2));
551 assert_eq!(input.unicode_truncate_centered(4), ("3👨👩👧👦4", 4));
552 assert_eq!(input.unicode_truncate_centered(6), ("23👨👩👧👦45", 6));
553 assert_eq!(input.unicode_truncate_centered(20), (input, 8));
554 }
555 }
556
557 #[test]
558 fn truncate_aligned() {
559 assert_eq!("abc".unicode_truncate_aligned(1, Alignment::Left), ("a", 1));
560 assert_eq!(
561 "abc".unicode_truncate_aligned(1, Alignment::Center),
562 ("b", 1)
563 );
564 assert_eq!(
565 "abc".unicode_truncate_aligned(1, Alignment::Right),
566 ("c", 1)
567 );
568 }
569
570 #[cfg(feature = "std")]
571 mod pad {
572 use super::*;
573
574 #[test]
575 fn zero_width() {
576 assert_eq!("你好".unicode_pad(0, Alignment::Left, true), "");
577 assert_eq!("你好".unicode_pad(0, Alignment::Left, false), "你好");
578 }
579
580 #[test]
581 fn less_than_limit() {
582 assert_eq!("你".unicode_pad(4, Alignment::Left, true), "你 ");
583 assert_eq!("你".unicode_pad(4, Alignment::Left, false), "你 ");
584 }
585
586 #[test]
587 fn width_at_boundary() {
588 assert_eq!("你好吗".unicode_pad(4, Alignment::Left, true), "你好");
589 assert_eq!("你好吗".unicode_pad(4, Alignment::Left, false), "你好吗");
590 }
591
592 #[test]
593 fn width_not_boundary() {
594 assert_eq!("你好吗".unicode_pad(3, Alignment::Left, true), "你 ");
596 assert_eq!("你好吗".unicode_pad(1, Alignment::Left, true), " ");
597 assert_eq!("你好吗".unicode_pad(3, Alignment::Left, false), "你好吗");
598
599 assert_eq!("你好吗".unicode_pad(3, Alignment::Center, true), "你 ");
600
601 assert_eq!("你好吗".unicode_pad(3, Alignment::Right, true), " 你");
602 }
603 }
604}