#[must_use]
pub fn py_is_word(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
#[must_use]
pub fn char_at(s: &str, byte_idx: usize) -> Option<(usize, char)> {
s[byte_idx..].chars().next().map(|c| (byte_idx, c))
}
#[must_use]
pub fn char_before(s: &str, byte_idx: usize) -> Option<(usize, char)> {
s[..byte_idx].char_indices().next_back()
}
#[must_use]
pub fn walk_back_while<F>(s: &str, mut byte_idx: usize, mut pred: F) -> usize
where
F: FnMut(char) -> bool,
{
while byte_idx > 0
&& let Some((i, c)) = char_before(s, byte_idx)
&& pred(c)
{
byte_idx = i;
}
byte_idx
}
#[must_use]
pub fn walk_forward_while<F>(s: &str, mut byte_idx: usize, mut pred: F) -> usize
where
F: FnMut(char) -> bool,
{
while let Some((i, c)) = char_at(s, byte_idx)
&& pred(c)
{
byte_idx = i + c.len_utf8();
}
byte_idx
}
#[must_use]
pub fn str_find_from(s: &str, needle: &str, from: usize) -> Option<usize> {
if from > s.len() {
return None;
}
let mut from = from;
while from < s.len() && !s.is_char_boundary(from) {
from += 1;
}
s[from..].find(needle).map(|i| i + from)
}
#[must_use]
pub fn str_rfind_in(s: &str, needle: &str, start: usize, end: usize) -> Option<usize> {
let limit = end.min(s.len());
if needle.is_empty() || start > limit {
return None;
}
let capped = &s[..limit];
capped[start..]
.rfind(needle)
.map(|i| i + start)
.filter(|i| i + needle.len() <= limit)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn walks_respect_char_boundaries() {
let s = "a\u{00e9}b";
assert_eq!(walk_back_while(s, s.len(), |c| c != '\u{00e9}'), 3);
assert_eq!(walk_forward_while(s, 0, |c| c == 'a'), 1);
}
#[test]
fn rfind_within_bounds() {
assert_eq!(str_rfind_in("a.b.c", ".", 0, 3), Some(1));
assert_eq!(str_rfind_in("a.b.c", ".", 2, 5), Some(3));
assert_eq!(str_rfind_in("abc", "z", 0, 3), None);
}
}