use konst::{string::Chars, try_opt};
use super::code_points::{CR, FF, LF};
pub struct Filtered<'a>(pub Chars<'a>);
impl<'a> std::fmt::Debug for Filtered<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Filtered").field(&self.0.as_str()).finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FilteredChar(char);
impl FilteredChar {
pub const fn from_str(s: &str) -> Self {
match Filtered::new(s).next() {
Some((c, stream)) => {
assert!(stream.next().is_none(), "unexpected remaining code points");
c
}
None => panic!("expect one code point"),
}
}
pub const fn is_digit(&self) -> bool {
self.0.is_ascii_digit()
}
pub(crate) const fn is_whitespace(&self) -> bool {
self.0.is_ascii_whitespace()
}
pub(crate) const fn is_non_printable(&self) -> bool {
matches!(self.0, '\u{0000}'..='\u{0008}' | '\u{000B}' | '\u{000E}'..='\u{001F}' | '\u{007F}')
}
}
impl<'a> Filtered<'a> {
pub const fn new(s: &'a str) -> Self {
Self(konst::string::chars(s))
}
pub const fn consume_digits(self) -> (&'a str, Self) {
let mut stream = self.copy();
loop {
match stream.copy().next() {
Some((fc, new_stream)) if fc.is_digit() => stream = new_stream,
_ => return (self.str_before(&stream), stream),
}
}
}
pub const fn first_n_code_points<const N: usize>(&self) -> FilteredCharVec<N> {
self.copy().consume_n_code_points().0
}
pub const fn consume_n_code_points<const N: usize>(mut self) -> (FilteredCharVec<N>, Self) {
let mut res = FilteredCharVec::EMPTY;
while res.len < N {
if let Some((fc, new_stream)) = self.copy().next() {
res.array[res.len] = fc.to_char();
self = new_stream;
res.len += 1;
} else {
break;
}
}
(res, self)
}
#[cfg(test)]
pub(crate) const fn assert_empty(&self) {
assert!(self.0.as_str().is_empty(), "unexpected remaining tokens")
}
#[cfg(test)]
pub(crate) fn assert_empty_or_report(&self) {
let s = self.0.as_str();
assert!(s.is_empty(), "unexpected remaining tokens: {s:?}")
}
}
impl Filtered<'_> {
pub const EMPTY: Self = Self(konst::string::chars(""));
pub const fn copy(&self) -> Self {
Self(self.0.copy())
}
pub const fn next(self) -> Option<(FilteredChar, Self)> {
if let Some((u, this)) = self.0.next() {
let u = match u {
FF => LF,
CR => {
if let Some((CR, this)) = this.copy().next() {
if let Some((LF, this)) = this.next() {
return Some((FilteredChar(LF), Self(this)));
} else {
LF
}
} else {
LF
}
}
'\u{0000}' => '\u{FFFD}',
u => u,
};
Some((FilteredChar(u), Self(this)))
} else {
None
}
}
pub(crate) const fn next_and_report(self) -> Option<(FilteredChar, bool, Self)> {
if let Some((u, this)) = self.0.next() {
let (u, is_filtered) = match u {
FF => (LF, true),
CR => (
if let Some((CR, this)) = this.copy().next() {
if let Some((LF, this)) = this.next() {
return Some((FilteredChar(LF), true, Self(this)));
} else {
LF
}
} else {
LF
},
true,
),
'\u{0000}' => ('\u{FFFD}', true),
u => (u, false),
};
Some((FilteredChar(u), is_filtered, Self(this)))
} else {
None
}
}
pub const fn next_two(self) -> Option<([FilteredChar; 2], Self)> {
let (a, this) = try_opt!(self.next());
let (b, this) = try_opt!(this.next());
Some(([a, b], this))
}
}
impl<'a> Filtered<'a> {
pub const fn str_before(&self, new_stream: &Self) -> &'a str {
let this = self.0.as_str();
let new_stream = new_stream.0.as_str();
assert!(this.len() >= new_stream.len());
konst::string::split_at(this, this.len() - new_stream.len()).0
}
pub const fn str_before_non_empty(&self, new_stream: &Self) -> Option<&'a str> {
let s = self.str_before(new_stream);
if s.is_empty() {
None
} else {
Some(s)
}
}
}
impl FilteredChar {
pub const fn to_char(self) -> char {
self.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct FilteredCharVec<const N: usize> {
array: [char; N],
len: usize,
}
impl<const N: usize> FilteredCharVec<N> {
const EMPTY: Self = Self {
array: ['\0'; N],
len: 0,
};
pub const fn new_filled(arr: [FilteredChar; N]) -> Self {
let mut array = ['\0'; N];
let mut i = 0;
while i < N {
array[i] = arr[i].to_char();
i += 1;
}
Self { array, len: N }
}
pub const fn to_chars_padding_zero(self) -> [char; N] {
self.array
}
pub const fn as_slice(&self) -> FilteredCharSlice<'_> {
FilteredCharSlice {
inner: self.as_char_slice(),
}
}
pub const fn as_char_slice(&self) -> &[char] {
self.array.split_at(self.len).0
}
pub const fn crop_and_fit<const M: usize>(self, remove_first_n: usize) -> FilteredCharVec<M> {
let mut res = FilteredCharVec::EMPTY;
if self.len > remove_first_n {
let new_len = self.len - remove_first_n;
assert!(new_len <= M);
let chars = self.array.split_at(remove_first_n).1;
while res.len < new_len {
res.array[res.len] = chars[res.len];
res.len += 1;
}
}
res
}
pub const fn truncate_and_fit<const M: usize>(self, keep_first_n: usize) -> FilteredCharVec<M> {
const fn min(a: usize, b: usize) -> usize {
if a > b {
b
} else {
a
}
}
let new_len = min(self.len, keep_first_n);
assert!(new_len <= M);
let mut res = FilteredCharVec::EMPTY;
while res.len < new_len {
res.array[res.len] = self.array[res.len];
res.len += 1;
}
res
}
pub const fn fit_or_keep_first_n<const M: usize>(self) -> FilteredCharVec<M> {
let mut res = FilteredCharVec::EMPTY;
let this = self.as_char_slice();
while res.len < M && res.len < this.len() {
res.array[res.len] = this[res.len];
res.len += 1;
}
res
}
pub const fn first(&self) -> Option<FilteredChar> {
if self.len > 0 {
Some(FilteredChar(self.array[0]))
} else {
None
}
}
pub const fn with_capacity<const M: usize>(self) -> FilteredCharVec<M> {
assert!(M >= self.len);
let mut res = FilteredCharVec::EMPTY;
while res.len < self.len {
res.array[res.len] = self.array[res.len];
}
res
}
pub const fn join_and_fit<const M: usize>(
self,
chars: FilteredCharSlice<'_>,
) -> FilteredCharVec<M> {
let chars = chars.to_char_slice();
assert!(self.len + chars.len() <= M);
let mut res = FilteredCharVec::EMPTY;
while res.len < self.len {
res.array[res.len] = chars[res.len];
res.len += 1;
}
let mut i = 0;
while i < chars.len() {
res.array[res.len] = chars[i];
res.len += 1;
i += 1;
}
res
}
}
#[derive(Debug, Clone, Copy)]
pub struct FilteredCharSlice<'a> {
inner: &'a [char],
}
impl<'a> FilteredCharSlice<'a> {
pub const fn to_char_slice(self) -> &'a [char] {
self.inner
}
}
#[cfg(test)]
mod tests {
use super::Filtered;
const _: () = {
let chars = Filtered::new("\\").first_n_code_points::<3>();
assert!(matches!(chars.as_char_slice(), ['\\']));
assert!(matches!(chars.crop_and_fit::<2>(1).as_char_slice(), []));
assert!(matches!(
chars.fit_or_keep_first_n::<2>().as_char_slice(),
['\\']
));
};
}