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.
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)