Strings and Text Processing

char_indices()

The .char_indices() method on str returns a std::str::CharIndices<'_> iterator that yields (usize, char) pairs, where the usize is the exact starting UTF-8 byte offset of that char within the string slice.

Why .char_indices() Differs from .chars().enumerate()

When a string contains multi-byte UTF-8 characters, the ordinal character index (0, 1, 2, ... from .chars().enumerate()) does not match the byte offset required for slicing (&s[byte_start..byte_end]). By contrast, .char_indices() always yields valid UTF-8 byte boundary offsets:

String: "Aé🦀B" (8 bytes total)

Byte index:    0      1   2      3   4   5   6      7
Bytes:       [ 41,    C3, A9,    F0, 9F, A6, 80,    42 ]
Chars:         'A'    'é'        '🦀'               'B'

.chars().enumerate() ──> (0, 'A'),  (1, 'é'),  (2, '🦀'),  (3, 'B')  (char ordinal)
.char_indices()      ──> (0, 'A'),  (1, 'é'),  (3, '🦀'),  (7, 'B')  (UTF-8 byte offset)
fn main() {
    let text = "Aé🦀B";

    println!("char_indices() vs chars().enumerate():");
    for ((char_ord, _), (byte_idx, ch)) in text.chars().enumerate().zip(text.char_indices()) {
        println!(
            "  char #{char_ord}: '{ch}' starts at byte offset {byte_idx} (len_utf8 = {})",
            ch.len_utf8()
        );
    }
}
char_indices() vs chars().enumerate():
  char #0: 'A' starts at byte offset 0 (len_utf8 = 1)
  char #1: 'é' starts at byte offset 1 (len_utf8 = 2)
  char #2: '🦀' starts at byte offset 3 (len_utf8 = 4)
  char #3: 'B' starts at byte offset 7 (len_utf8 = 1)

Safe Substring Slicing Using .char_indices()

Because every byte_idx returned by .char_indices() is guaranteed to lie on a valid UTF-8 character boundary (and byte_idx + ch.len_utf8() is the exact end boundary of that character), you can safely slice the original &str without risking a runtime panic:

/// Returns a `&str` containing at most the first `max_chars` Unicode characters.
fn truncate_chars(s: &str, max_chars: usize) -> &str {
    match s.char_indices().nth(max_chars) {
        Some((byte_idx, _ch)) => &s[..byte_idx],
        None => s,
    }
}

fn main() {
    let greeting = "স্বাগতম Rust";
    println!("First 4 chars: '{}'", truncate_chars(greeting, 4));
    println!("First 20 chars: '{}'", truncate_chars(greeting, 20));
}
First 4 chars: 'স্বা'
First 20 chars: 'স্বাগতম Rust'