Trimming the first character of a `str` in `const`

Any nicer ways to conditionally trim or split the first character of a string in a const function (on stable)? The best I could come up with is this:

if let Some((fst, rst)) = s.split_at_checked(1)
    && fst.as_bytes()[0] == b'#'
{
    s = rst;
}

This works if the string is empty, or the first character is multi-byte, but if the character I want to trim were itself multi-byte, things would get complicated.

The hoop-jumping is needed because both str indexing and comparison (even pattern matching) are behind const trait support, as is str::trim_start_matches. Moreover, str doesn't have a first() or split_first() methods.

I opened an ACP that proposes adding some methods that are in [_] but missing in str.

Also asked on Reddit.

2 Likes
const fn trim_first_if(input: &str, c: char) -> &str {
    let utf8 = input.as_bytes();
    let mut search_utf8 = [0; char::MAX_LEN_UTF8];
    let mut search = c.encode_utf8(&mut search_utf8).as_bytes();
    if let Some((mut fst, rem)) = utf8.split_at_checked(search.len()) {
        while let ([first_fst, rest_fst @ ..], [first_search, rest_search @ ..]) = (fst, search) {
            if *first_fst == *first_search {
                fst = rest_fst;
                search = rest_search;
            } else {
                return input;
            }
        }
        // SAFETY:
        // We trimmed at most the first Unicode scalar value (USV) from `input`;
        // thus `rem` must also be valid UTF-8 (i.e., `rem` is valid UTF-8 iff `input`
        // was valid UTF-8).
        unsafe { str::from_utf8_unchecked(rem) }
    } else {
        input
    }
}

Wouldn't exactly call that "nice" though.

I went with llogiq's clean and concise suggestion on Reddit:

if !s.is_empty() && s.as_bytes()[0] == b'#' {
    (_, s) = s.split_at(1);
}

I was a bit too stuck on the pattern matching idea to realize that I can simply swap the test and the split.

If anyone is interested, the full code is about parsing a CSS hex code with the leading '#' optional:

pub const fn hex<S, const N: usize>(mut s: &str) -> Color<[u8; N], S> {
    const {
        assert!(N == 3 || N == 4, "number of components must be 3 or 4");
    }
    // A bit of ceremony needed to make this work in const...
    if !s.is_empty() && s.as_bytes()[0] == b'#' {
        (_, s) = s.split_at(1);
    }
    let Ok(n) = u32::from_str_radix(s, 16) else {
        panic!("invalid hex string")
    };

    let n = if s.len() == 2 * N {
        n
    } else if s.len() == N {
        // Morton code interleaving trick: expand n by adding
        // a nibble of zeroes between each nibble
        // 0x123 -> 0x01_02_03
        let n = (n | n << 8) & 0x00_FF_00_FF;
        let n = (n | n << 4) & 0x0F_0F_0F_0F;
        // Duplicate nibbles into the gaps
        // 0x01_02_03 -> 0x11_22_33
        n | n << 4
    } else {
        panic!("invalid number of hex digits")
    };
    // The lowest N bytes are the components we want
    let bytes = n.to_be_bytes();
    let Some(&cs) = bytes.split_at(4 - N).1.as_array() else {
        panic!("should not happen: 4 - (4 - N) = N");
    };
    Color::new(cs)