1use crate::atomic::{OncePtr, PyAtomic, Radium};
3use crate::format::CharLen;
4use crate::wtf8::{CodePoint, Wtf8, Wtf8Buf};
5use crate::wtf8_index::Wtf8Index;
6use alloc::borrow::Cow;
7use ascii::{AsciiChar, AsciiStr, AsciiString};
8use core::fmt;
9use core::ops::{Bound, RangeBounds};
10use core::sync::atomic::Ordering::Relaxed;
11
12#[allow(non_camel_case_types)]
13pub type wchar_t = cfg_select! {
14 target_arch = "wasm32" => u32,
15 _ => libc::wchar_t,
16};
17
18#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
20pub enum StrKind {
21 Ascii,
22 Utf8,
23 Wtf8,
24}
25
26impl core::ops::BitOr for StrKind {
27 type Output = Self;
28
29 fn bitor(self, other: Self) -> Self {
30 match (self, other) {
31 (Self::Wtf8, _) | (_, Self::Wtf8) => Self::Wtf8,
32 (Self::Utf8, _) | (_, Self::Utf8) => Self::Utf8,
33 (Self::Ascii, Self::Ascii) => Self::Ascii,
34 }
35 }
36}
37
38impl StrKind {
39 #[must_use]
40 pub const fn is_ascii(&self) -> bool {
41 matches!(self, Self::Ascii)
42 }
43
44 #[must_use]
45 pub const fn is_utf8(&self) -> bool {
46 matches!(self, Self::Ascii | Self::Utf8)
47 }
48
49 #[inline(always)]
50 #[must_use]
51 pub fn can_encode(&self, code: CodePoint) -> bool {
52 match self {
53 Self::Ascii => code.is_ascii(),
54 Self::Utf8 => code.to_char().is_some(),
55 Self::Wtf8 => true,
56 }
57 }
58}
59
60pub trait DeduceStrKind {
61 fn str_kind(&self) -> StrKind;
62}
63
64impl DeduceStrKind for str {
65 fn str_kind(&self) -> StrKind {
66 if self.is_ascii() {
67 StrKind::Ascii
68 } else {
69 StrKind::Utf8
70 }
71 }
72}
73
74impl DeduceStrKind for Wtf8 {
75 fn str_kind(&self) -> StrKind {
76 if self.is_ascii() {
77 StrKind::Ascii
78 } else if self.is_utf8() {
79 StrKind::Utf8
80 } else {
81 StrKind::Wtf8
82 }
83 }
84}
85
86impl DeduceStrKind for String {
87 fn str_kind(&self) -> StrKind {
88 (**self).str_kind()
89 }
90}
91
92impl DeduceStrKind for Wtf8Buf {
93 fn str_kind(&self) -> StrKind {
94 (**self).str_kind()
95 }
96}
97
98impl<T: DeduceStrKind + ?Sized> DeduceStrKind for &T {
99 fn str_kind(&self) -> StrKind {
100 (**self).str_kind()
101 }
102}
103
104impl<T: DeduceStrKind + ?Sized> DeduceStrKind for Box<T> {
105 fn str_kind(&self) -> StrKind {
106 (**self).str_kind()
107 }
108}
109
110#[derive(Debug)]
111pub enum PyKindStr<'a> {
112 Ascii(&'a AsciiStr),
113 Utf8(&'a str),
114 Wtf8(&'a Wtf8),
115}
116
117const MAX_WALK_TO_INDEX: usize = 4;
126
127#[derive(Debug, Clone)]
128pub struct StrData {
129 data: Box<Wtf8>,
130 kind: StrKind,
131 len: StrLen,
132 index: Wtf8IndexSlot,
133}
134
135#[derive(Default)]
140struct Wtf8IndexSlot(OncePtr<Wtf8Index>);
141
142impl Wtf8IndexSlot {
143 #[inline(always)]
144 fn new() -> Self {
145 Self(OncePtr::new())
146 }
147
148 #[inline]
149 fn get_or_build(&self, data: &Wtf8, char_len: usize) -> &Wtf8Index {
150 let index = self
151 .0
152 .get_or_init(|| Box::new(Wtf8Index::new(data, char_len)));
153 unsafe { index.as_ref() }
155 }
156}
157
158impl fmt::Debug for Wtf8IndexSlot {
159 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
160 match self.0.get() {
161 Some(_) => f.write_str("<built>"),
162 None => f.write_str("<unbuilt>"),
163 }
164 }
165}
166
167impl Clone for Wtf8IndexSlot {
168 fn clone(&self) -> Self {
171 Self::new()
172 }
173}
174
175impl Drop for Wtf8IndexSlot {
176 fn drop(&mut self) {
177 if let Some(index) = self.0.get() {
178 drop(unsafe { Box::from_raw(index.as_ptr()) });
179 }
180 }
181}
182
183struct StrLen(PyAtomic<usize>);
184
185impl From<usize> for StrLen {
186 #[inline(always)]
187 fn from(value: usize) -> Self {
188 Self(Radium::new(value))
189 }
190}
191
192impl fmt::Debug for StrLen {
193 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
194 let len = self.0.load(Relaxed);
195 if len == usize::MAX {
196 f.write_str("<uncomputed>")
197 } else {
198 len.fmt(f)
199 }
200 }
201}
202
203impl StrLen {
204 #[inline(always)]
205 fn zero() -> Self {
206 0usize.into()
207 }
208
209 #[inline(always)]
210 fn uncomputed() -> Self {
211 usize::MAX.into()
212 }
213}
214
215impl Clone for StrLen {
216 fn clone(&self) -> Self {
217 Self(self.0.load(Relaxed).into())
218 }
219}
220
221impl Default for StrData {
222 fn default() -> Self {
223 Self {
224 data: <Box<Wtf8>>::default(),
225 kind: StrKind::Ascii,
226 len: StrLen::zero(),
227 index: Wtf8IndexSlot::new(),
228 }
229 }
230}
231
232impl From<Box<Wtf8>> for StrData {
233 fn from(value: Box<Wtf8>) -> Self {
234 let kind = value.str_kind();
237 unsafe { Self::new_str_unchecked(value, kind) }
238 }
239}
240
241impl From<Box<str>> for StrData {
242 #[inline]
243 fn from(value: Box<str>) -> Self {
244 let kind = value.str_kind();
247 unsafe { Self::new_str_unchecked(value.into(), kind) }
248 }
249}
250
251impl From<Box<AsciiStr>> for StrData {
252 #[inline]
253 fn from(value: Box<AsciiStr>) -> Self {
254 Self {
255 len: value.len().into(),
256 data: value.into(),
257 kind: StrKind::Ascii,
258 index: Wtf8IndexSlot::new(),
259 }
260 }
261}
262
263impl From<AsciiChar> for StrData {
264 fn from(ch: AsciiChar) -> Self {
265 AsciiString::from(ch).into_boxed_ascii_str().into()
266 }
267}
268
269impl From<char> for StrData {
270 fn from(ch: char) -> Self {
271 if let Ok(ch) = ascii::AsciiChar::from_ascii(ch) {
272 ch.into()
273 } else {
274 Self {
275 data: ch.to_string().into(),
276 kind: StrKind::Utf8,
277 len: 1.into(),
278 index: Wtf8IndexSlot::new(),
279 }
280 }
281 }
282}
283
284impl From<CodePoint> for StrData {
285 fn from(ch: CodePoint) -> Self {
286 if let Some(ch) = ch.to_char() {
287 ch.into()
288 } else {
289 Self {
290 data: Wtf8Buf::from(ch).into(),
291 kind: StrKind::Wtf8,
292 len: 1.into(),
293 index: Wtf8IndexSlot::new(),
294 }
295 }
296 }
297}
298
299impl StrData {
300 #[must_use]
304 pub unsafe fn new_str_unchecked(data: Box<Wtf8>, kind: StrKind) -> Self {
305 let len = match kind {
306 StrKind::Ascii => data.len().into(),
307 _ => StrLen::uncomputed(),
308 };
309 Self {
310 data,
311 kind,
312 len,
313 index: Wtf8IndexSlot::new(),
314 }
315 }
316
317 #[must_use]
321 pub unsafe fn new_with_char_len(data: Box<Wtf8>, kind: StrKind, char_len: usize) -> Self {
322 Self {
323 data,
324 kind,
325 len: char_len.into(),
326 index: Wtf8IndexSlot::new(),
327 }
328 }
329
330 #[inline]
331 pub const fn as_wtf8(&self) -> &Wtf8 {
332 &self.data
333 }
334
335 #[inline]
337 pub fn as_str(&self) -> Option<&str> {
338 self.kind
339 .is_utf8()
340 .then(|| unsafe { core::str::from_utf8_unchecked(self.data.as_bytes()) })
341 }
342
343 pub fn as_ascii(&self) -> Option<&AsciiStr> {
344 self.kind
345 .is_ascii()
346 .then(|| unsafe { AsciiStr::from_ascii_unchecked(self.data.as_bytes()) })
347 }
348
349 pub const fn kind(&self) -> StrKind {
350 self.kind
351 }
352
353 #[inline]
354 pub fn as_str_kind(&self) -> PyKindStr<'_> {
355 match self.kind {
356 StrKind::Ascii => {
357 PyKindStr::Ascii(unsafe { AsciiStr::from_ascii_unchecked(self.data.as_bytes()) })
358 }
359 StrKind::Utf8 => {
360 PyKindStr::Utf8(unsafe { core::str::from_utf8_unchecked(self.data.as_bytes()) })
361 }
362 StrKind::Wtf8 => PyKindStr::Wtf8(&self.data),
363 }
364 }
365
366 #[inline]
367 pub fn len(&self) -> usize {
368 self.data.len()
369 }
370
371 pub fn is_empty(&self) -> bool {
372 self.data.is_empty()
373 }
374
375 #[inline]
376 pub fn char_len(&self) -> usize {
377 match self.len.0.load(Relaxed) {
378 usize::MAX => self._compute_char_len(),
379 len => len,
380 }
381 }
382
383 #[cold]
384 fn _compute_char_len(&self) -> usize {
385 let len = if let Some(s) = self.as_str() {
386 s.chars().count()
388 } else {
389 self.data.code_points().count()
390 };
391 self.len.0.store(len, Relaxed);
393 len
394 }
395
396 pub fn char_index_to_byte(&self, index: usize) -> usize {
405 if self.kind.is_ascii() {
408 return index.min(self.data.len());
409 }
410 let char_len = self.char_len();
411 if index >= char_len {
412 return self.data.len();
413 }
414 self.index
415 .get_or_build(&self.data, char_len)
416 .byte_offset(&self.data, index)
417 }
418
419 fn char_index_to_byte_once(&self, index: usize) -> usize {
429 if index <= MAX_WALK_TO_INDEX {
430 return self
431 .data
432 .code_point_indices()
433 .nth(index)
434 .map_or(self.data.len(), |(byte, _)| byte);
435 }
436 let from_end = self.char_len() - index;
437 if from_end <= MAX_WALK_TO_INDEX {
438 return self
439 .data
440 .code_point_indices()
441 .nth_back(from_end - 1)
442 .map_or(self.data.len(), |(byte, _)| byte);
443 }
444 self.char_index_to_byte(index)
445 }
446
447 #[must_use]
454 pub fn char_range_to_bytes(&self, range: core::ops::Range<usize>) -> core::ops::Range<usize> {
455 if self.kind.is_ascii() {
456 return range;
457 }
458 let from_end = self.char_len() - range.end;
459 if range.start <= MAX_WALK_TO_INDEX && from_end <= MAX_WALK_TO_INDEX {
460 let start = self
464 .data
465 .code_point_indices()
466 .nth(range.start)
467 .map_or(self.data.len(), |(byte, _)| byte);
468 let end = match from_end {
469 0 => self.data.len(),
470 n => self
471 .data
472 .code_point_indices()
473 .nth_back(n - 1)
474 .map_or(self.data.len(), |(byte, _)| byte),
475 };
476 return start..end;
477 }
478 self.char_index_to_byte(range.start)..self.char_index_to_byte(range.end)
479 }
480
481 pub fn byte_to_char_index(&self, bytepos: usize) -> usize {
491 if self.kind.is_ascii() {
492 return bytepos;
493 }
494 let char_len = self.char_len();
495 self.index
496 .get_or_build(&self.data, char_len)
497 .char_index_at_byte(&self.data, bytepos, char_len)
498 }
499
500 pub fn nth_char(&self, index: usize) -> CodePoint {
501 match self.as_str_kind() {
502 PyKindStr::Ascii(s) => s[index].into(),
503 _ => self.data[self.char_index_to_byte_once(index)..]
504 .code_points()
505 .next()
506 .unwrap(),
507 }
508 }
509}
510
511impl core::fmt::Display for StrData {
512 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
513 self.data.fmt(f)
514 }
515}
516
517impl CharLen for StrData {
518 fn char_len(&self) -> usize {
519 self.char_len()
520 }
521}
522
523pub fn try_get_chars(s: &str, range: impl RangeBounds<usize>) -> Option<&str> {
524 let mut chars = s.chars();
525 let start = match range.start_bound() {
526 Bound::Included(&i) => i,
527 Bound::Excluded(&i) => i + 1,
528 Bound::Unbounded => 0,
529 };
530 for _ in 0..start {
531 chars.next()?;
532 }
533 let s = chars.as_str();
534 let range_len = match range.end_bound() {
535 Bound::Included(&i) => i + 1 - start,
536 Bound::Excluded(&i) => i - start,
537 Bound::Unbounded => return Some(s),
538 };
539 char_range_end(s, range_len).map(|end| &s[..end])
540}
541
542pub fn get_chars(s: &str, range: impl RangeBounds<usize>) -> &str {
543 try_get_chars(s, range).unwrap()
544}
545
546#[inline]
547#[must_use]
548pub fn char_range_end(s: &str, n_chars: usize) -> Option<usize> {
549 let i = match n_chars.checked_sub(1) {
550 Some(last_char_index) => {
551 let (index, c) = s.char_indices().nth(last_char_index)?;
552 index + c.len_utf8()
553 }
554 None => 0,
555 };
556 Some(i)
557}
558
559pub fn try_get_codepoints(w: &Wtf8, range: impl RangeBounds<usize>) -> Option<&Wtf8> {
560 let mut chars = w.code_points();
561 let start = match range.start_bound() {
562 Bound::Included(&i) => i,
563 Bound::Excluded(&i) => i + 1,
564 Bound::Unbounded => 0,
565 };
566 for _ in 0..start {
567 chars.next()?;
568 }
569 let s = chars.as_wtf8();
570 let range_len = match range.end_bound() {
571 Bound::Included(&i) => i + 1 - start,
572 Bound::Excluded(&i) => i - start,
573 Bound::Unbounded => return Some(s),
574 };
575 codepoint_range_end(s, range_len).map(|end| &s[..end])
576}
577
578pub fn get_codepoints(w: &Wtf8, range: impl RangeBounds<usize>) -> &Wtf8 {
579 try_get_codepoints(w, range).unwrap()
580}
581
582#[inline]
583#[must_use]
584pub fn codepoint_range_end(s: &Wtf8, n_chars: usize) -> Option<usize> {
585 let i = match n_chars.checked_sub(1) {
586 Some(last_char_index) => {
587 let (index, c) = s.code_point_indices().nth(last_char_index)?;
588 index + c.len_wtf8()
589 }
590 None => 0,
591 };
592 Some(i)
593}
594
595#[must_use]
596pub fn zfill(bytes: &[u8], width: usize) -> Option<Vec<u8>> {
598 if width <= bytes.len() {
599 return Some(bytes.to_vec());
600 }
601 let (sign, s) = match bytes.first() {
602 Some(_sign @ (b'+' | b'-')) => (unsafe { bytes.get_unchecked(..1) }, &bytes[1..]),
603 _ => (&b""[..], bytes),
604 };
605 let mut filled = Vec::new();
606 filled.try_reserve_exact(width).ok()?;
607 filled.extend_from_slice(sign);
608 filled.extend(core::iter::repeat_n(b'0', width - bytes.len()));
609 filled.extend_from_slice(s);
610 Some(filled)
611}
612
613#[must_use]
616pub fn to_ascii(value: &Wtf8) -> AsciiString {
617 let mut ascii = Vec::new();
618 for cp in value.code_points() {
619 if cp.is_ascii() {
620 ascii.push(cp.to_u32() as u8);
621 } else {
622 let c = cp.to_u32();
623 let hex = if c < 0x100 {
624 format!("\\x{c:02x}")
625 } else if c < 0x10000 {
626 format!("\\u{c:04x}")
627 } else {
628 format!("\\U{c:08x}")
629 };
630 ascii.append(&mut hex.into_bytes());
631 }
632 }
633 unsafe { AsciiString::from_ascii_unchecked(ascii) }
634}
635
636#[derive(Clone, Copy)]
637pub struct UnicodeEscapeCodepoint(pub CodePoint);
638
639impl fmt::Display for UnicodeEscapeCodepoint {
640 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
641 let c = self.0.to_u32();
642 if c >= 0x10000 {
643 write!(f, "\\U{c:08x}")
644 } else if c >= 0x100 {
645 write!(f, "\\u{c:04x}")
646 } else {
647 write!(f, "\\x{c:02x}")
648 }
649 }
650}
651
652pub mod levenshtein {
653 pub const MOVE_COST: usize = 2;
654 const CASE_COST: usize = 1;
655 const MAX_STRING_SIZE: usize = 40;
656
657 const fn substitution_cost(mut a: u8, mut b: u8) -> usize {
658 if (a & 31) != (b & 31) {
659 return MOVE_COST;
660 }
661 if a == b {
662 return 0;
663 }
664 if a.is_ascii_uppercase() {
665 a += b'a' - b'A';
666 }
667 if b.is_ascii_uppercase() {
668 b += b'a' - b'A';
669 }
670 if a == b { CASE_COST } else { MOVE_COST }
671 }
672
673 #[must_use]
674 pub fn levenshtein_distance(a: &[u8], b: &[u8], max_cost: usize) -> usize {
675 if a == b {
676 return 0;
677 }
678
679 let (mut a_bytes, mut b_bytes) = (a, b);
680 let (mut a_begin, mut a_end) = (0usize, a.len());
681 let (mut b_begin, mut b_end) = (0usize, b.len());
682
683 while a_end > 0 && b_end > 0 && (a_bytes[a_begin] == b_bytes[b_begin]) {
684 a_begin += 1;
685 b_begin += 1;
686 a_end -= 1;
687 b_end -= 1;
688 }
689 while a_end > 0
690 && b_end > 0
691 && (a_bytes[a_begin + a_end - 1] == b_bytes[b_begin + b_end - 1])
692 {
693 a_end -= 1;
694 b_end -= 1;
695 }
696 if a_end == 0 || b_end == 0 {
697 return (a_end + b_end) * MOVE_COST;
698 }
699 if a_end > MAX_STRING_SIZE || b_end > MAX_STRING_SIZE {
700 return max_cost + 1;
701 }
702
703 if b_end < a_end {
704 core::mem::swap(&mut a_bytes, &mut b_bytes);
705 core::mem::swap(&mut a_begin, &mut b_begin);
706 core::mem::swap(&mut a_end, &mut b_end);
707 }
708
709 if (b_end - a_end) * MOVE_COST > max_cost {
710 return max_cost + 1;
711 }
712
713 let mut buffer = [0usize; MAX_STRING_SIZE];
714
715 for (i, x) in buffer.iter_mut().take(a_end).enumerate() {
716 *x = (i + 1) * MOVE_COST;
717 }
718
719 let mut result = 0usize;
720 for (b_index, b_code) in b_bytes[b_begin..(b_begin + b_end)].iter().enumerate() {
721 result = b_index * MOVE_COST;
722 let mut distance = result;
723 let mut minimum = usize::MAX;
724 for (a_index, a_code) in a_bytes[a_begin..(a_begin + a_end)].iter().enumerate() {
725 let substitute = distance + substitution_cost(*b_code, *a_code);
726 distance = buffer[a_index];
727 let insert_delete = usize::min(result, distance) + MOVE_COST;
728 result = usize::min(insert_delete, substitute);
729
730 buffer[a_index] = result;
731 if result < minimum {
732 minimum = result;
733 }
734 }
735 if minimum > max_cost {
736 return max_cost + 1;
737 }
738 }
739 result
740 }
741}
742
743#[must_use]
745pub fn expandtabs(input: &Wtf8, tab_size: usize) -> Wtf8Buf {
746 if tab_size == 0 {
752 return input.code_points().filter(|ch| *ch != '\t').collect();
753 }
754
755 let tab_stop = tab_size;
756 let mut expanded_str = Wtf8Buf::with_capacity(input.len());
757 let mut tab_size = tab_stop;
758 let mut col_count = 0usize;
759 for ch in input.code_points() {
760 if ch == '\t' {
761 let num_spaces = tab_size - col_count;
762 col_count += num_spaces;
763 expanded_str.push_str(&" ".repeat(num_spaces));
764 } else {
765 expanded_str.push(ch);
766 if ch == '\r' || ch == '\n' {
767 col_count = 0;
768 tab_size = 0;
769 } else {
770 col_count += 1;
771 }
772 }
773 if col_count >= tab_size {
774 tab_size += tab_stop;
775 }
776 }
777 expanded_str
778}
779
780#[macro_export]
788macro_rules! ascii {
789 ($x:expr $(,)?) => {{
790 let s = const {
791 let s: &str = $x;
792 assert!(s.is_ascii(), "ascii!() argument is not an ascii string");
793 s
794 };
795 unsafe { $crate::vendored::ascii::AsciiStr::from_ascii_unchecked(s.as_bytes()) }
796 }};
797}
798pub use ascii;
799
800#[must_use]
806pub fn char_to_decimal(ch: char) -> Option<u8> {
807 let value = rustpython_unicode::Ucd::new(true).decimal(CodePoint::from(ch))?;
808 u8::try_from(value).ok()
809}
810
811#[must_use]
820pub fn transform_decimal_and_space_to_ascii(s: &str) -> Cow<'_, str> {
821 if s.is_ascii() {
822 return Cow::Borrowed(s);
823 }
824 let mut out = String::with_capacity(s.len());
825 for c in s.chars() {
826 if (c as u32) < 127 {
827 out.push(c);
828 } else if c.is_whitespace() {
829 out.push(' ');
830 } else if let Some(n) = char_to_decimal(c) {
831 out.push(char::from_digit(n.into(), 10).unwrap());
832 } else {
833 out.push('?');
834 break;
835 }
836 }
837 debug_assert!(out.is_ascii());
838 Cow::Owned(out)
839}
840
841#[cfg(test)]
842mod tests {
843 use super::*;
844
845 #[test]
846 fn char_to_decimal_values() {
847 assert_eq!(char_to_decimal('7'), Some(7));
848 assert_eq!(char_to_decimal('٣'), Some(3)); assert_eq!(char_to_decimal('𐴵'), Some(5)); assert_eq!(char_to_decimal('🯵'), Some(5)); assert_eq!(char_to_decimal('½'), None);
855 assert_eq!(char_to_decimal('가'), None);
856 }
857
858 #[test]
859 fn transform_decimal_and_space() {
860 assert!(matches!(
862 transform_decimal_and_space_to_ascii("123"),
863 Cow::Borrowed("123")
864 ));
865 assert_eq!(transform_decimal_and_space_to_ascii("١٢٣"), "123");
867 assert_eq!(transform_decimal_and_space_to_ascii("12३"), "123");
868 assert_eq!(transform_decimal_and_space_to_ascii("1٢3"), "123");
869 assert_eq!(transform_decimal_and_space_to_ascii("\u{3000}٣"), " 3");
871 assert_eq!(transform_decimal_and_space_to_ascii("0x١f"), "0x1f");
873 assert_eq!(transform_decimal_and_space_to_ascii("-١_٢"), "-1_2");
874 assert_eq!(transform_decimal_and_space_to_ascii("½가"), "?");
877 assert_eq!(transform_decimal_and_space_to_ascii("١٢가٣"), "12?");
878 assert_eq!(transform_decimal_and_space_to_ascii("١\u{7f}"), "1?");
879 }
880
881 #[test]
882 fn get_chars_basic() {
883 let s = "0123456789";
884 assert_eq!(get_chars(s, 3..7), "3456");
885 assert_eq!(get_chars(s, 3..7), &s[3..7]);
886
887 let s = "0유니코드 문자열9";
888 assert_eq!(get_chars(s, 3..7), "코드 문");
889
890 let s = "0😀😃😄😁😆😅😂🤣9";
891 assert_eq!(get_chars(s, 3..7), "😄😁😆😅");
892 }
893
894 fn expandtabs(input: &str, tab_size: usize) -> Wtf8Buf {
895 super::expandtabs(Wtf8::new(input), tab_size)
896 }
897
898 #[test]
899 fn expandtabs_with_zero_tab_size_drops_tabs() {
900 assert_eq!(expandtabs("a\tb", 0), Wtf8Buf::from("ab"));
904 assert_eq!(expandtabs("ab\tcd\tef", 0), Wtf8Buf::from("abcdef"));
905 assert_eq!(expandtabs("a\nb\tc", 0), Wtf8Buf::from("a\nbc"));
906 assert_eq!(expandtabs("á\tb", 0), Wtf8Buf::from("áb"));
907 assert_eq!(expandtabs("\ta", 0), Wtf8Buf::from("a"));
908 assert_eq!(expandtabs("\t", 0), Wtf8Buf::from(""));
909 assert_eq!(expandtabs("", 0), Wtf8Buf::from(""));
910 assert_eq!(expandtabs("no tabs", 0), Wtf8Buf::from("no tabs"));
911 }
912
913 #[test]
914 fn expandtabs_with_a_real_tab_size_is_unchanged() {
915 assert_eq!(expandtabs("a\tb", 8), Wtf8Buf::from("a b"));
916 assert_eq!(expandtabs("a\tb", 1), Wtf8Buf::from("a b"));
917 assert_eq!(expandtabs("abcd\te", 4), Wtf8Buf::from("abcd e"));
918 assert_eq!(expandtabs("a\nb\tc", 4), Wtf8Buf::from("a\nb c"));
919 assert_eq!(expandtabs("\ta", 4), Wtf8Buf::from(" a"));
920 }
921}