Skip to main content

goish/
strings.rs

1// strings: Go's strings package, ported.
2//
3//   Go                                goish
4//   ───────────────────────────────   ──────────────────────────────────
5//   strings.Contains(s, "x")          strings::Contains(s, "x")
6//   strings.HasPrefix(s, "p")         strings::HasPrefix(s, "p")
7//   strings.HasSuffix(s, "p")         strings::HasSuffix(s, "p")
8//   strings.Index(s, "x")             strings::Index(s, "x")        // -1 if absent
9//   strings.LastIndex(s, "x")         strings::LastIndex(s, "x")
10//   strings.Count(s, "a")             strings::Count(s, "a")
11//   strings.Split(s, ",")             strings::Split(s, ",")        // → slice<string>
12//   strings.SplitN(s, ",", n)         strings::SplitN(s, ",", n)
13//   strings.Join(elems, ",")          strings::Join(&elems, ",")
14//   strings.Replace(s, a, b, n)       strings::Replace(s, a, b, n)  // n<0 = all
15//   strings.ReplaceAll(s, a, b)       strings::ReplaceAll(s, a, b)
16//   strings.ToUpper(s)                strings::ToUpper(s)
17//   strings.ToLower(s)                strings::ToLower(s)
18//   strings.TrimSpace(s)              strings::TrimSpace(s)
19//   strings.Trim(s, "x")              strings::Trim(s, "x")
20//   strings.TrimPrefix(s, "p")        strings::TrimPrefix(s, "p")
21//   strings.TrimSuffix(s, "p")        strings::TrimSuffix(s, "p")
22//   strings.Fields(s)                 strings::Fields(s)
23//   strings.Repeat(s, n)              strings::Repeat(s, n)
24//   strings.EqualFold(s, t)           strings::EqualFold(s, t)
25//
26// All functions take `impl AsRef<str>` so users can pass `String`, `&String`,
27// or `&str` without spelling out the conversion.
28
29use crate::types::{int, slice, string};
30
31pub fn Contains(s: impl AsRef<str>, substr: impl AsRef<str>) -> bool {
32    s.as_ref().contains(substr.as_ref())
33}
34
35/// strings.Compare(a, b) — returns -1 / 0 / 1 per lexicographic order.
36pub fn Compare(a: impl AsRef<str>, b: impl AsRef<str>) -> int {
37    use std::cmp::Ordering::*;
38    match a.as_ref().cmp(b.as_ref()) {
39        Less => -1,
40        Equal => 0,
41        Greater => 1,
42    }
43}
44
45/// strings.Clone(s) — returns a fresh copy of s. In Go this disentangles
46/// a string's underlying storage; in goish `String` is owned so it's a
47/// plain clone.
48pub fn Clone(s: impl AsRef<str>) -> string {
49    s.as_ref().to_string()
50}
51
52pub fn HasPrefix(s: impl AsRef<str>, prefix: impl AsRef<str>) -> bool {
53    s.as_ref().starts_with(prefix.as_ref())
54}
55
56pub fn HasSuffix(s: impl AsRef<str>, suffix: impl AsRef<str>) -> bool {
57    s.as_ref().ends_with(suffix.as_ref())
58}
59
60/// strings.Index — byte index of first occurrence, or -1.
61pub fn Index(s: impl AsRef<str>, substr: impl AsRef<str>) -> int {
62    match s.as_ref().find(substr.as_ref()) {
63        Some(i) => i as int,
64        None => -1,
65    }
66}
67
68pub fn LastIndex(s: impl AsRef<str>, substr: impl AsRef<str>) -> int {
69    match s.as_ref().rfind(substr.as_ref()) {
70        Some(i) => i as int,
71        None => -1,
72    }
73}
74
75pub fn Count(s: impl AsRef<str>, substr: impl AsRef<str>) -> int {
76    let s = s.as_ref();
77    let substr = substr.as_ref();
78    if substr.is_empty() {
79        return (s.chars().count() + 1) as int;
80    }
81    s.matches(substr).count() as int
82}
83
84pub fn Split(s: impl AsRef<str>, sep: impl AsRef<str>) -> slice<string> {
85    let s = s.as_ref();
86    let sep = sep.as_ref();
87    if sep.is_empty() {
88        return s.chars().map(|c| c.to_string()).collect();
89    }
90    s.split(sep).map(String::from).collect()
91}
92
93/// strings.SplitN — like Split but stops after n substrings (n<0 = all, n==0 = empty).
94pub fn SplitN(s: impl AsRef<str>, sep: impl AsRef<str>, n: int) -> slice<string> {
95    if n == 0 {
96        return slice::new();
97    }
98    let s = s.as_ref();
99    let sep = sep.as_ref();
100    if n < 0 {
101        return Split(s, sep);
102    }
103    s.splitn(n as usize, sep).map(String::from).collect()
104}
105
106pub fn Join(elems: &[string], sep: impl AsRef<str>) -> string {
107    elems.join(sep.as_ref())
108}
109
110/// strings.Replace — replace first n occurrences (n<0 = all).
111pub fn Replace(s: impl AsRef<str>, old: impl AsRef<str>, new: impl AsRef<str>, n: int) -> string {
112    let s = s.as_ref();
113    let old = old.as_ref();
114    let new = new.as_ref();
115    if n < 0 {
116        s.replace(old, new)
117    } else {
118        s.replacen(old, new, n as usize)
119    }
120}
121
122pub fn ReplaceAll(s: impl AsRef<str>, old: impl AsRef<str>, new: impl AsRef<str>) -> string {
123    s.as_ref().replace(old.as_ref(), new.as_ref())
124}
125
126pub fn ToUpper(s: impl AsRef<str>) -> string {
127    s.as_ref().to_uppercase()
128}
129
130pub fn ToLower(s: impl AsRef<str>) -> string {
131    s.as_ref().to_lowercase()
132}
133
134pub fn TrimSpace(s: impl AsRef<str>) -> string {
135    s.as_ref().trim().to_string()
136}
137
138pub fn TrimPrefix(s: impl AsRef<str>, prefix: impl AsRef<str>) -> string {
139    let s = s.as_ref();
140    s.strip_prefix(prefix.as_ref()).unwrap_or(s).to_string()
141}
142
143pub fn TrimSuffix(s: impl AsRef<str>, suffix: impl AsRef<str>) -> string {
144    let s = s.as_ref();
145    s.strip_suffix(suffix.as_ref()).unwrap_or(s).to_string()
146}
147
148pub fn Trim(s: impl AsRef<str>, cutset: impl AsRef<str>) -> string {
149    let cutset = cutset.as_ref().to_string();
150    s.as_ref().trim_matches(|c: char| cutset.contains(c)).to_string()
151}
152
153pub fn Fields(s: impl AsRef<str>) -> slice<string> {
154    s.as_ref().split_whitespace().map(String::from).collect()
155}
156
157pub fn Repeat(s: impl AsRef<str>, count: int) -> string {
158    if count < 0 {
159        panic!("strings: negative Repeat count");
160    }
161    s.as_ref().repeat(count as usize)
162}
163
164/// ASCII-only fold (Go does full Unicode; close enough for now).
165/// `strings.EqualFold(s, t)` — Unicode case-insensitive equality. Uses
166/// Rust's `char::to_lowercase` for folding so Greek/Latin/Cyrillic etc
167/// compare correctly (not just ASCII).
168pub fn EqualFold(s: impl AsRef<str>, t: impl AsRef<str>) -> bool {
169    let mut si = s.as_ref().chars();
170    let mut ti = t.as_ref().chars();
171    loop {
172        let a = si.next();
173        let b = ti.next();
174        match (a, b) {
175            (None, None) => return true,
176            (None, _) | (_, None) => return false,
177            (Some(a), Some(b)) => {
178                if a == b { continue; }
179                let a_low: Vec<char> = a.to_lowercase().collect();
180                let b_low: Vec<char> = b.to_lowercase().collect();
181                if a_low != b_low { return false; }
182            }
183        }
184    }
185}
186
187// ── strings.Builder ────────────────────────────────────────────────────
188//
189//   Go                                  goish
190//   ─────────────────────────────────   ──────────────────────────────────
191//   var b strings.Builder               let mut b = strings::Builder::new();
192//   b.WriteString("hello ")             b.WriteString("hello ");
193//   b.WriteByte('!')                    b.WriteByte(b'!');
194//   b.WriteRune('λ')                    b.WriteRune('λ');
195//   s := b.String()                     let s = b.String();
196//   n := b.Len()                        let n = b.Len();
197//   b.Reset()                           b.Reset();
198
199#[derive(Debug, Clone, Default)]
200pub struct Builder {
201    inner: string,
202}
203
204impl Builder {
205    pub fn new() -> Self { Builder::default() }
206
207    pub fn WriteString(&mut self, s: impl AsRef<str>) -> (int, crate::errors::error) {
208        let s = s.as_ref();
209        self.inner.push_str(s);
210        (s.len() as int, crate::errors::nil)
211    }
212
213    pub fn WriteByte(&mut self, b: crate::types::byte) -> crate::errors::error {
214        // Go's WriteByte takes a byte; we accept only ASCII-valid bytes since
215        // Builder backs to a String (UTF-8). Non-ASCII bytes panic (matches
216        // Go's runtime behavior on invalid UTF-8 conversion).
217        if b < 0x80 {
218            self.inner.push(b as char);
219            crate::errors::nil
220        } else {
221            crate::errors::New("strings.Builder: non-ASCII byte; use WriteRune")
222        }
223    }
224
225    pub fn WriteRune(&mut self, r: char) -> (int, crate::errors::error) {
226        let n = r.len_utf8();
227        self.inner.push(r);
228        (n as int, crate::errors::nil)
229    }
230
231    pub fn String(&self) -> string {
232        self.inner.clone()
233    }
234
235    /// `b.Cap()` — underlying capacity of the backing buffer. Used by
236    /// Go's tests; here we forward to String::capacity.
237    #[allow(non_snake_case)]
238    pub fn Cap(&self) -> int { self.inner.capacity() as int }
239
240    /// `b.Write(p)` — append raw bytes (must be valid UTF-8 for goish;
241    /// Go's Builder accepts arbitrary bytes since []byte ⊂ string there).
242    #[allow(non_snake_case)]
243    pub fn Write(&mut self, p: &[crate::types::byte]) -> (int, crate::errors::error) {
244        match std::str::from_utf8(p) {
245            Ok(s) => { self.inner.push_str(s); (p.len() as int, crate::errors::nil) },
246            Err(_) => (0, crate::errors::New("strings.Builder.Write: invalid UTF-8")),
247        }
248    }
249
250    pub fn Len(&self) -> int {
251        self.inner.len() as int
252    }
253
254    pub fn Reset(&mut self) {
255        self.inner.clear();
256    }
257
258    pub fn Grow(&mut self, n: int) {
259        if n > 0 {
260            self.inner.reserve(n as usize);
261        }
262    }
263
264    /// Lowercase alias for the polymorphic `len!()` macro.
265    pub fn len(&self) -> usize {
266        self.inner.len()
267    }
268}
269
270impl std::fmt::Display for Builder {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        write!(f, "{}", self.inner)
273    }
274}
275
276// ── strings.Reader ─────────────────────────────────────────────────────
277//
278//   Go                                  goish
279//   ─────────────────────────────────   ──────────────────────────────────
280//   r := strings.NewReader("hello")     let r = strings::NewReader("hello");
281//   n, err := r.Read(p)                 let (n, err) = r.Read(&mut p);
282
283#[derive(Debug, Clone)]
284pub struct Reader {
285    data: String,
286    pos: usize,
287}
288
289impl Reader {
290    pub fn Len(&self) -> int {
291        (self.data.len().saturating_sub(self.pos)) as int
292    }
293
294    pub fn Size(&self) -> crate::types::int64 {
295        self.data.len() as crate::types::int64
296    }
297
298    pub fn Read(&mut self, p: &mut [crate::types::byte]) -> (int, crate::errors::error) {
299        if self.pos >= self.data.len() {
300            return (0, crate::io::EOF());
301        }
302        let bytes = self.data.as_bytes();
303        let n = (bytes.len() - self.pos).min(p.len());
304        p[..n].copy_from_slice(&bytes[self.pos..self.pos + n]);
305        self.pos += n;
306        (n as int, crate::errors::nil)
307    }
308
309    pub fn ReadByte(&mut self) -> (crate::types::byte, crate::errors::error) {
310        let bytes = self.data.as_bytes();
311        if self.pos >= bytes.len() {
312            return (0, crate::io::EOF());
313        }
314        let b = bytes[self.pos];
315        self.pos += 1;
316        (b, crate::errors::nil)
317    }
318
319    pub fn UnreadByte(&mut self) -> crate::errors::error {
320        if self.pos == 0 {
321            return crate::errors::New("strings.Reader.UnreadByte: at beginning of string");
322        }
323        self.pos -= 1;
324        crate::errors::nil
325    }
326
327    /// `r.ReadAt(p, off)` — read at an absolute offset without advancing
328    /// the internal cursor. Matches Go's io.ReaderAt interface.
329    #[allow(non_snake_case)]
330    pub fn ReadAt(&self, p: &mut [crate::types::byte], off: crate::types::int64) -> (int, crate::errors::error) {
331        if off < 0 {
332            return (0, crate::errors::New("strings.Reader.ReadAt: negative offset"));
333        }
334        let off = off as usize;
335        if off >= self.data.len() {
336            return (0, crate::io::EOF());
337        }
338        let bytes = self.data.as_bytes();
339        let available = bytes.len() - off;
340        let n = available.min(p.len());
341        p[..n].copy_from_slice(&bytes[off..off + n]);
342        if n < p.len() {
343            (n as int, crate::io::EOF())
344        } else {
345            (n as int, crate::errors::nil)
346        }
347    }
348
349    pub fn Seek(&mut self, offset: crate::types::int64, whence: int) -> (crate::types::int64, crate::errors::error) {
350        let new_pos: i64 = match whence {
351            0 => offset,
352            1 => self.pos as i64 + offset,
353            2 => self.data.len() as i64 + offset,
354            _ => return (0, crate::errors::New("strings.Reader.Seek: invalid whence")),
355        };
356        if new_pos < 0 {
357            return (0, crate::errors::New("strings.Reader.Seek: negative position"));
358        }
359        self.pos = new_pos as usize;
360        (new_pos, crate::errors::nil)
361    }
362}
363
364impl std::io::Read for Reader {
365    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
366        let bytes = self.data.as_bytes();
367        if self.pos >= bytes.len() {
368            return Ok(0);
369        }
370        let n = (bytes.len() - self.pos).min(buf.len());
371        buf[..n].copy_from_slice(&bytes[self.pos..self.pos + n]);
372        self.pos += n;
373        Ok(n)
374    }
375}
376
377/// strings.NewReader(s) — construct a Reader over the string.
378#[allow(non_snake_case)]
379pub fn NewReader(s: impl Into<String>) -> Reader {
380    Reader { data: s.into(), pos: 0 }
381}
382
383// ── strings.Replacer ───────────────────────────────────────────────────
384//
385//   Go                                  goish
386//   ─────────────────────────────────   ──────────────────────────────────
387//   r := strings.NewReplacer("a","1", "b","2")
388//                                       let r = strings::NewReplacer(&["a","1", "b","2"]);
389//   r.Replace(s)                        r.Replace(s)
390
391#[derive(Debug, Clone)]
392pub struct Replacer {
393    pairs: Vec<(String, String)>,
394}
395
396impl Replacer {
397    pub fn Replace(&self, s: impl AsRef<str>) -> string {
398        let s = s.as_ref();
399        let mut out = String::with_capacity(s.len());
400        let bytes = s.as_bytes();
401        let mut i = 0usize;
402        'outer: while i < bytes.len() {
403            for (old, new) in &self.pairs {
404                if old.is_empty() { continue; }
405                let ob = old.as_bytes();
406                if i + ob.len() <= bytes.len() && &bytes[i..i + ob.len()] == ob {
407                    out.push_str(new);
408                    i += ob.len();
409                    continue 'outer;
410                }
411            }
412            // No pair matched; copy one UTF-8 char.
413            let ch = s[i..].chars().next().unwrap();
414            out.push(ch);
415            i += ch.len_utf8();
416        }
417        out
418    }
419
420    pub fn WriteString<W: std::io::Write>(&self, w: &mut W, s: impl AsRef<str>) -> (int, crate::errors::error) {
421        let result = self.Replace(s);
422        match w.write(result.as_bytes()) {
423            Ok(n) => (n as int, crate::errors::nil),
424            Err(e) => (0, crate::errors::New(&e.to_string())),
425        }
426    }
427}
428
429/// strings.NewReplacer("old1","new1","old2","new2",...)
430///
431/// Takes a slice of alternating old/new strings. Panics on odd count.
432#[allow(non_snake_case)]
433pub fn NewReplacer(pairs: &[impl AsRef<str>]) -> Replacer {
434    if pairs.len() % 2 != 0 {
435        panic!("strings.NewReplacer: odd argument count");
436    }
437    let pairs = pairs.chunks(2)
438        .map(|c| (c[0].as_ref().to_string(), c[1].as_ref().to_string()))
439        .collect();
440    Replacer { pairs }
441}
442
443// ── strings.Map ────────────────────────────────────────────────────────
444//
445//   Go                                  goish
446//   ─────────────────────────────────   ──────────────────────────────────
447//   strings.Map(fn, s)                  strings::Map(|r| …, s)
448//
449// fn returns a char; if it returns '\0' the rune is dropped (Go uses
450// negative rune; we use '\0' since char cannot be negative).
451
452#[allow(non_snake_case)]
453pub fn Map(mut f: impl FnMut(char) -> char, s: impl AsRef<str>) -> string {
454    let mut out = String::with_capacity(s.as_ref().len());
455    for c in s.as_ref().chars() {
456        let r = f(c);
457        if r != '\0' {
458            out.push(r);
459        }
460    }
461    out
462}
463
464// ── strings.ContainsAny / IndexAny / ContainsRune ──────────────────────
465
466#[allow(non_snake_case)]
467pub fn ContainsAny(s: impl AsRef<str>, chars: impl AsRef<str>) -> bool {
468    let set: Vec<char> = chars.as_ref().chars().collect();
469    s.as_ref().chars().any(|c| set.contains(&c))
470}
471
472#[allow(non_snake_case)]
473pub fn ContainsRune(s: impl AsRef<str>, r: char) -> bool {
474    s.as_ref().contains(r)
475}
476
477#[allow(non_snake_case)]
478pub fn IndexAny(s: impl AsRef<str>, chars: impl AsRef<str>) -> int {
479    let s = s.as_ref();
480    let set: Vec<char> = chars.as_ref().chars().collect();
481    for (i, c) in s.char_indices() {
482        if set.contains(&c) {
483            return i as int;
484        }
485    }
486    -1
487}
488
489#[allow(non_snake_case)]
490pub fn IndexByte(s: impl AsRef<str>, b: crate::types::byte) -> int {
491    s.as_ref().as_bytes().iter().position(|x| *x == b).map(|i| i as int).unwrap_or(-1)
492}
493
494#[allow(non_snake_case)]
495pub fn IndexRune(s: impl AsRef<str>, r: char) -> int {
496    let s = s.as_ref();
497    s.find(r).map(|i| i as int).unwrap_or(-1)
498}
499
500/// `strings.Cut(s, sep)` — slices s around the first instance of sep.
501/// Returns (before, after, found). If sep not in s, returns (s, "", false).
502#[allow(non_snake_case)]
503pub fn Cut(s: impl AsRef<str>, sep: impl AsRef<str>) -> (string, string, bool) {
504    let s = s.as_ref(); let sep = sep.as_ref();
505    match s.find(sep) {
506        Some(i) => (s[..i].to_string(), s[i + sep.len()..].to_string(), true),
507        None => (s.to_string(), String::new(), false),
508    }
509}
510
511/// `strings.CutPrefix(s, prefix)` — if prefix matches, returns (after, true); else (s, false).
512#[allow(non_snake_case)]
513pub fn CutPrefix(s: impl AsRef<str>, prefix: impl AsRef<str>) -> (string, bool) {
514    let s = s.as_ref(); let p = prefix.as_ref();
515    match s.strip_prefix(p) {
516        Some(rest) => (rest.to_string(), true),
517        None => (s.to_string(), false),
518    }
519}
520
521/// `strings.CutSuffix(s, suffix)` — if suffix matches, returns (before, true); else (s, false).
522#[allow(non_snake_case)]
523pub fn CutSuffix(s: impl AsRef<str>, suffix: impl AsRef<str>) -> (string, bool) {
524    let s = s.as_ref(); let suf = suffix.as_ref();
525    match s.strip_suffix(suf) {
526        Some(rest) => (rest.to_string(), true),
527        None => (s.to_string(), false),
528    }
529}
530
531/// `strings.TrimLeft(s, cutset)` — drop leading runes in `cutset`.
532#[allow(non_snake_case)]
533pub fn TrimLeft(s: impl AsRef<str>, cutset: impl AsRef<str>) -> string {
534    let cut: Vec<char> = cutset.as_ref().chars().collect();
535    s.as_ref().trim_start_matches(|c: char| cut.contains(&c)).to_string()
536}
537
538/// `strings.TrimRight(s, cutset)` — drop trailing runes in `cutset`.
539#[allow(non_snake_case)]
540pub fn TrimRight(s: impl AsRef<str>, cutset: impl AsRef<str>) -> string {
541    let cut: Vec<char> = cutset.as_ref().chars().collect();
542    s.as_ref().trim_end_matches(|c: char| cut.contains(&c)).to_string()
543}
544
545/// `strings.LastIndexByte(s, c)` — last index of the byte `c` in `s`, or -1.
546#[allow(non_snake_case)]
547pub fn LastIndexByte(s: impl AsRef<str>, c: crate::types::byte) -> int {
548    s.as_ref().as_bytes().iter().rposition(|&x| x == c).map(|i| i as int).unwrap_or(-1)
549}
550
551/// `strings.LastIndexAny(s, chars)` — greatest index of a rune in `chars`
552/// appearing in `s`, or -1.
553#[allow(non_snake_case)]
554pub fn LastIndexAny(s: impl AsRef<str>, chars: impl AsRef<str>) -> int {
555    let s = s.as_ref(); let chars = chars.as_ref();
556    let set: Vec<char> = chars.chars().collect();
557    let mut best: i64 = -1;
558    for (i, c) in s.char_indices() {
559        if set.contains(&c) { best = i as i64; }
560    }
561    best
562}
563
564/// `strings.IndexFunc(s, f)` — first index where f(rune) is true, or -1.
565#[allow(non_snake_case)]
566pub fn IndexFunc(s: impl AsRef<str>, mut f: impl FnMut(char) -> bool) -> int {
567    for (i, c) in s.as_ref().char_indices() {
568        if f(c) { return i as int; }
569    }
570    -1
571}
572
573/// `strings.LastIndexFunc(s, f)` — last index where f(rune) is true, or -1.
574#[allow(non_snake_case)]
575pub fn LastIndexFunc(s: impl AsRef<str>, mut f: impl FnMut(char) -> bool) -> int {
576    let mut best: i64 = -1;
577    for (i, c) in s.as_ref().char_indices() {
578        if f(c) { best = i as i64; }
579    }
580    best
581}
582
583/// `strings.TrimFunc(s, f)` — trim runes satisfying f from both ends.
584#[allow(non_snake_case)]
585pub fn TrimFunc(s: impl AsRef<str>, mut f: impl FnMut(char) -> bool) -> string {
586    s.as_ref().trim_matches(|c: char| f(c)).to_string()
587}
588
589/// `strings.TrimLeftFunc(s, f)` — trim runes satisfying f from the start.
590#[allow(non_snake_case)]
591pub fn TrimLeftFunc(s: impl AsRef<str>, mut f: impl FnMut(char) -> bool) -> string {
592    s.as_ref().trim_start_matches(|c: char| f(c)).to_string()
593}
594
595/// `strings.TrimRightFunc(s, f)` — trim runes satisfying f from the end.
596#[allow(non_snake_case)]
597pub fn TrimRightFunc(s: impl AsRef<str>, mut f: impl FnMut(char) -> bool) -> string {
598    s.as_ref().trim_end_matches(|c: char| f(c)).to_string()
599}
600
601/// `strings.SplitAfter(s, sep)` — like Split but keeps sep at the end of
602/// each chunk.
603#[allow(non_snake_case)]
604pub fn SplitAfter(s: impl AsRef<str>, sep: impl AsRef<str>) -> slice<string> {
605    let s = s.as_ref(); let sep = sep.as_ref();
606    if sep.is_empty() {
607        // Go's behavior: split after every rune.
608        return s.chars().map(|c| c.to_string()).collect();
609    }
610    let mut out = Vec::new();
611    let mut start = 0usize;
612    loop {
613        match s[start..].find(sep) {
614            Some(i) => {
615                let end = start + i + sep.len();
616                out.push(s[start..end].to_string());
617                start = end;
618            }
619            None => {
620                out.push(s[start..].to_string());
621                break;
622            }
623        }
624    }
625    out
626}
627
628/// `strings.FieldsFunc(s, f)` — split s around runs of runes where f is true.
629#[allow(non_snake_case)]
630pub fn FieldsFunc(s: impl AsRef<str>, mut f: impl FnMut(char) -> bool) -> slice<string> {
631    s.as_ref()
632        .split(|c: char| f(c))
633        .filter(|seg| !seg.is_empty())
634        .map(|seg| seg.to_string())
635        .collect()
636}
637
638#[allow(non_snake_case)]
639pub fn Title(s: impl AsRef<str>) -> string {
640    // Go's deprecated strings.Title: uppercase the first letter of each word.
641    let s = s.as_ref();
642    let mut out = String::with_capacity(s.len());
643    let mut at_word_boundary = true;
644    for c in s.chars() {
645        if c.is_whitespace() {
646            at_word_boundary = true;
647            out.push(c);
648        } else if at_word_boundary {
649            for uc in c.to_uppercase() {
650                out.push(uc);
651            }
652            at_word_boundary = false;
653        } else {
654            out.push(c);
655        }
656    }
657    out
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663
664    #[test]
665    fn contains_and_prefix() {
666        assert!(Contains("hello world", "world"));
667        assert!(!Contains("hello", "xyz"));
668        assert!(HasPrefix("foobar", "foo"));
669        assert!(HasSuffix("foobar", "bar"));
670    }
671
672    #[test]
673    fn index_returns_minus_one_when_absent() {
674        assert_eq!(Index("hello", "ll"), 2);
675        assert_eq!(Index("hello", "z"), -1);
676        assert_eq!(LastIndex("banana", "an"), 3);
677    }
678
679    #[test]
680    fn count_substr_and_empty() {
681        assert_eq!(Count("banana", "a"), 3);
682        assert_eq!(Count("xx", ""), 3); // Go: chars+1
683    }
684
685    #[test]
686    fn split_and_join() {
687        let v = Split("a,b,c", ",");
688        assert_eq!(v, vec!["a", "b", "c"]);
689        assert_eq!(Join(&v, "-"), "a-b-c");
690    }
691
692    #[test]
693    fn split_n_caps_results() {
694        let v = SplitN("a,b,c,d", ",", 2);
695        assert_eq!(v, vec!["a", "b,c,d"]);
696        let v = SplitN("a,b,c", ",", -1);
697        assert_eq!(v.len(), 3);
698        let v = SplitN("a,b,c", ",", 0);
699        assert!(v.is_empty());
700    }
701
702    #[test]
703    fn replace_and_replace_all() {
704        assert_eq!(Replace("aaa", "a", "b", 2), "bba");
705        assert_eq!(ReplaceAll("aaa", "a", "b"), "bbb");
706    }
707
708    #[test]
709    fn case_change() {
710        assert_eq!(ToUpper("hello"), "HELLO");
711        assert_eq!(ToLower("HELLO"), "hello");
712    }
713
714    #[test]
715    fn trim_variants() {
716        assert_eq!(TrimSpace("  hi  "), "hi");
717        assert_eq!(TrimPrefix("foobar", "foo"), "bar");
718        assert_eq!(TrimSuffix("foobar", "bar"), "foo");
719        assert_eq!(Trim("---abc--", "-"), "abc");
720    }
721
722    #[test]
723    fn fields_splits_on_whitespace() {
724        assert_eq!(Fields("  a  b\tc\n"), vec!["a", "b", "c"]);
725    }
726
727    #[test]
728    fn repeat_and_equalfold() {
729        assert_eq!(Repeat("ab", 3), "ababab");
730        assert!(EqualFold("HELLO", "hello"));
731        assert!(!EqualFold("hello", "world"));
732    }
733
734    #[test]
735    fn builder_writes_and_resets() {
736        let mut b = Builder::new();
737        b.WriteString("hello ");
738        b.WriteString("world");
739        b.WriteByte(b'!');
740        b.WriteRune('λ');
741        assert_eq!(b.String(), "hello world!λ");
742        assert_eq!(b.Len(), "hello world!λ".len() as int);
743        b.Reset();
744        assert_eq!(b.Len(), 0);
745    }
746
747    #[test]
748    fn builder_writerune_returns_bytes_written() {
749        let mut b = Builder::new();
750        let (n, _) = b.WriteRune('a');
751        assert_eq!(n, 1);
752        let (n, _) = b.WriteRune('λ');
753        assert_eq!(n, 2);
754        let (n, _) = b.WriteRune('漢');
755        assert_eq!(n, 3);
756    }
757
758    #[test]
759    fn reader_reads_bytes() {
760        let mut r = NewReader("hello");
761        let mut buf = [0u8; 3];
762        let (n, _) = r.Read(&mut buf);
763        assert_eq!(n, 3);
764        assert_eq!(&buf, b"hel");
765        let (n, _) = r.Read(&mut buf);
766        assert_eq!(n, 2);
767        assert_eq!(&buf[..2], b"lo");
768    }
769
770    #[test]
771    fn reader_seek() {
772        let mut r = NewReader("abcdef");
773        r.Seek(2, 0);
774        let (b, _) = r.ReadByte();
775        assert_eq!(b, b'c');
776        r.Seek(-1, 2);
777        let (b, _) = r.ReadByte();
778        assert_eq!(b, b'f');
779    }
780
781    #[test]
782    fn replacer_replaces_multiple() {
783        let r = NewReplacer(&["a", "1", "b", "2", "c", "3"]);
784        assert_eq!(r.Replace("abc cab"), "123 312");
785    }
786
787    #[test]
788    fn replacer_leaves_unmatched() {
789        let r = NewReplacer(&["foo", "FOO"]);
790        assert_eq!(r.Replace("foo bar baz"), "FOO bar baz");
791    }
792
793    #[test]
794    fn map_transforms_chars() {
795        let shout = Map(|c| c.to_ascii_uppercase(), "hello");
796        assert_eq!(shout, "HELLO");
797        let drop_vowels = Map(|c| if "aeiouAEIOU".contains(c) { '\0' } else { c }, "HELLO");
798        assert_eq!(drop_vowels, "HLL");
799    }
800
801    #[test]
802    fn contains_any_and_index_any() {
803        assert!(ContainsAny("hello", "xyz!o"));
804        assert!(!ContainsAny("hello", "xyz"));
805        assert_eq!(IndexAny("hello", "lo"), 2);
806        assert_eq!(IndexAny("abc", "xyz"), -1);
807        assert_eq!(IndexRune("héllo", 'é'), 1);
808        assert_eq!(IndexByte("hello", b'l'), 2);
809    }
810
811    #[test]
812    fn title_upcases_words() {
813        assert_eq!(Title("hello world"), "Hello World");
814    }
815}