konst/string/
splitting.rs

1//! string splitting that requires Rust 1.64.0 to be efficient.
2
3use crate::{
4    iter::{IntoIterKind, IsIteratorKind},
5    string::{self, str_from, str_up_to},
6};
7
8use konst_macro_rules::iterator_shared;
9
10/// A const-equivalent of the [`str::split_once`] method.
11///
12/// This only accepts `&str` as the delimiter.
13///
14/// # Version compatibility
15///
16/// This requires the `"rust_1_64"` feature.
17///
18/// # Example
19///
20/// ```rust
21/// use konst::string;
22///
23/// assert_eq!(string::split_once("", "-"), None);
24/// assert_eq!(string::split_once("foo", "-"), None);
25/// assert_eq!(string::split_once("foo-", "-"), Some(("foo", "")));
26/// assert_eq!(string::split_once("foo-bar", "-"), Some(("foo", "bar")));
27/// assert_eq!(string::split_once("foo-bar-baz", "-"), Some(("foo", "bar-baz")));
28///
29/// ```
30#[cfg_attr(feature = "docsrs", doc(cfg(feature = "rust_1_64")))]
31pub const fn split_once<'a>(this: &'a str, delim: &str) -> Option<(&'a str, &'a str)> {
32    if delim.is_empty() {
33        // using split_at so that the pointer points within the string
34        Some(string::split_at(this, 0))
35    } else {
36        crate::option::map! {
37            string::find(this, delim, 0),
38            |pos| (str_up_to(this, pos), str_from(this, pos + delim.len()))
39        }
40    }
41}
42
43/// A const-equivalent of the [`str::rsplit_once`] method.
44///
45/// This only accepts `&str` as the delimiter.
46///
47/// # Version compatibility
48///
49/// This requires the `"rust_1_64"` feature.
50///
51/// # Example
52///
53/// ```rust
54/// use konst::string;
55///
56/// assert_eq!(string::rsplit_once("", "-"), None);
57/// assert_eq!(string::rsplit_once("foo", "-"), None);
58/// assert_eq!(string::rsplit_once("foo-", "-"), Some(("foo", "")));
59/// assert_eq!(string::rsplit_once("foo-bar", "-"), Some(("foo", "bar")));
60/// assert_eq!(string::rsplit_once("foo-bar-baz", "-"), Some(("foo-bar", "baz")));
61///
62/// ```
63#[cfg_attr(feature = "docsrs", doc(cfg(feature = "rust_1_64")))]
64pub const fn rsplit_once<'a>(this: &'a str, delim: &str) -> Option<(&'a str, &'a str)> {
65    if delim.is_empty() {
66        // using split_at so that the pointer points within the string
67        Some(string::split_at(this, this.len()))
68    } else {
69        crate::option::map! {
70            string::rfind(this, delim, this.len()),
71            |pos| (str_up_to(this, pos), str_from(this, pos + delim.len()))
72        }
73    }
74}
75
76//////////////////////////////////////////////////
77
78/// Const equivalent of [`str::split`], which only takes a `&str` delimiter.
79///
80/// # Version compatibility
81///
82/// This requires the `"rust_1_64"` feature.
83///
84/// # Example
85///
86/// ```rust
87/// use konst::string;
88/// use konst::iter::collect_const;
89///
90/// const STRS: [&str; 3] = collect_const!(&str => string::split("foo-bar-baz", "-"));
91///
92/// assert_eq!(STRS, ["foo", "bar", "baz"]);
93/// ```
94#[cfg_attr(feature = "docsrs", doc(cfg(feature = "rust_1_64")))]
95pub const fn split<'a, 'b>(this: &'a str, delim: &'b str) -> Split<'a, 'b> {
96    Split {
97        this,
98        state: if delim.is_empty() {
99            State::Empty(EmptyState::Start)
100        } else {
101            State::Normal { delim }
102        },
103    }
104}
105
106/// Const equivalent of [`str::rsplit`], which only takes a `&str` delimiter.
107///
108/// # Version compatibility
109///
110/// This requires the `"rust_1_64"` feature.
111///
112/// # Example
113///
114/// ```rust
115/// use konst::string;
116/// use konst::iter::collect_const;
117///
118/// const STRS: [&str; 3] = collect_const!(&str => string::rsplit("foo-bar-baz", "-"));
119///
120/// assert_eq!(STRS, ["baz", "bar", "foo"]);
121/// ```
122#[cfg_attr(feature = "docsrs", doc(cfg(feature = "rust_1_64")))]
123pub const fn rsplit<'a, 'b>(this: &'a str, delim: &'b str) -> RSplit<'a, 'b> {
124    split(this, delim).rev()
125}
126
127#[derive(Copy, Clone)]
128enum State<'a> {
129    Normal { delim: &'a str },
130    Empty(EmptyState),
131    Finished,
132}
133
134#[derive(Copy, Clone)]
135enum EmptyState {
136    Start,
137    Continue,
138}
139
140macro_rules! split_shared {
141    (is_forward = $is_forward:ident) => {
142        const fn next_from_empty(mut self, es: EmptyState) -> Option<(&'a str, Self)> {
143            match es {
144                EmptyState::Start => {
145                    self.state = State::Empty(EmptyState::Continue);
146                    Some(("", self))
147                }
148                EmptyState::Continue => {
149                    let this = self.this;
150
151                    if this.is_empty() {
152                        self.state = State::Finished;
153                    }
154                    let next_char = string::find_next_char_boundary(this.as_bytes(), 0);
155                    let (next_char, rem) = string::split_at(this, next_char);
156                    self.this = rem;
157                    Some((next_char, self))
158                }
159            }
160        }
161
162        const fn next_back_from_empty(mut self, es: EmptyState) -> Option<(&'a str, Self)> {
163            match es {
164                EmptyState::Start => {
165                    self.state = State::Empty(EmptyState::Continue);
166                    Some(("", self))
167                }
168                EmptyState::Continue => {
169                    let this = self.this;
170
171                    if self.this.is_empty() {
172                        self.state = State::Finished;
173                    }
174                    let next_char = string::find_prev_char_boundary(this.as_bytes(), this.len());
175                    let (rem, next_char) = string::split_at(this, next_char);
176                    self.this = rem;
177                    Some((next_char, self))
178                }
179            }
180        }
181
182        iterator_shared! {
183            is_forward = $is_forward,
184            item = &'a str,
185            iter_forward = Split<'a, 'b>,
186            iter_reversed = RSplit<'a, 'b>,
187            next(self){
188                let Self {
189                    this,
190                    state,
191                } = self;
192
193                match state {
194                    State::Normal{delim} => {
195                        match string::find(this, delim, 0) {
196                            Some(pos) => {
197                                self.this = str_from(this, pos + delim.len());
198                                Some((str_up_to(this, pos), self))
199                            }
200                            None => {
201                                self.this = "";
202                                self.state = State::Finished;
203                                Some((this, self))
204                            }
205                        }
206                    }
207                    State::Empty(es) => self.next_from_empty(es),
208                    State::Finished => None,
209                }
210            },
211            next_back{
212                let Self {
213                    this,
214                    state,
215                } = self;
216                match state {
217                    State::Normal{delim} => {
218                        match string::rfind(this, delim, this.len()) {
219                            Some(pos) => {
220                                self.this = str_up_to(this, pos);
221                                Some((str_from(this, pos + delim.len()), self))
222                            }
223                            None => {
224                                self.this = "";
225                                self.state = State::Finished;
226                                Some((this, self))
227                            }
228                        }
229                    }
230                    State::Empty(es) => self.next_back_from_empty(es),
231                    State::Finished => None,
232                }
233            },
234            fields = {this, state},
235        }
236    };
237}
238
239/// Const equivalent of `core::str::Split<'a, &'b str>`
240///
241/// This is constructed with [`split`] like this:
242/// ```rust
243/// # let string = "";
244/// # let delim = "";
245/// # let _ =
246/// konst::string::split(string, delim)
247/// # ;
248/// ```
249///
250/// # Version compatibility
251///
252/// This requires the `"rust_1_64"` feature.
253///
254#[cfg_attr(feature = "docsrs", doc(cfg(feature = "rust_1_64")))]
255pub struct Split<'a, 'b> {
256    this: &'a str,
257    state: State<'b>,
258}
259impl IntoIterKind for Split<'_, '_> {
260    type Kind = IsIteratorKind;
261}
262
263impl<'a, 'b> Split<'a, 'b> {
264    split_shared! {is_forward = true}
265
266    /// Gets the remainder of the string.
267    ///
268    /// # Example
269    ///
270    /// ```rust
271    /// let iter = konst::string::split("foo-bar-baz", "-");
272    /// assert_eq!(iter.remainder(), "foo-bar-baz");
273    ///
274    /// let (elem, iter) = iter.next().unwrap();
275    /// assert_eq!(elem, "foo");
276    /// assert_eq!(iter.remainder(), "bar-baz");
277    ///
278    /// let (elem, iter) = iter.next().unwrap();
279    /// assert_eq!(elem, "bar");
280    /// assert_eq!(iter.remainder(), "baz");
281    ///
282    /// let (elem, iter) = iter.next().unwrap();
283    /// assert_eq!(elem, "baz");
284    /// assert_eq!(iter.remainder(), "");
285    ///
286    /// ```
287    pub const fn remainder(&self) -> &'a str {
288        self.this
289    }
290}
291
292/// Const equivalent of `core::str::RSplit<'a, &'b str>`
293///
294/// This is constructed with [`rsplit`] like this:
295/// ```rust
296/// # let string = "";
297/// # let delim = "";
298/// # let _ =
299/// konst::string::rsplit(string, delim)
300/// # ;
301/// ```
302///
303/// # Version compatibility
304///
305/// This requires the `"rust_1_64"` feature.
306///
307#[cfg_attr(feature = "docsrs", doc(cfg(feature = "rust_1_64")))]
308pub struct RSplit<'a, 'b> {
309    this: &'a str,
310    state: State<'b>,
311}
312impl IntoIterKind for RSplit<'_, '_> {
313    type Kind = IsIteratorKind;
314}
315
316impl<'a, 'b> RSplit<'a, 'b> {
317    split_shared! {is_forward = false}
318
319    /// Gets the remainder of the string.
320    ///
321    /// # Example
322    ///
323    /// ```rust
324    /// let iter = konst::string::rsplit("foo-bar-baz", "-");
325    /// assert_eq!(iter.remainder(), "foo-bar-baz");
326    ///
327    /// let (elem, iter) = iter.next().unwrap();
328    /// assert_eq!(elem, "baz");
329    /// assert_eq!(iter.remainder(), "foo-bar");
330    ///
331    /// let (elem, iter) = iter.next().unwrap();
332    /// assert_eq!(elem, "bar");
333    /// assert_eq!(iter.remainder(), "foo");
334    ///
335    /// let (elem, iter) = iter.next().unwrap();
336    /// assert_eq!(elem, "foo");
337    /// assert_eq!(iter.remainder(), "");
338    ///
339    /// ```
340    pub const fn remainder(&self) -> &'a str {
341        self.this
342    }
343}