Split

// `Split` — partition a UTF-8 string by separator.
//
// Public methods validate the haystack as UTF-8. Empty separator
// returns a single-element array with the original string (the most
// useful behavior — alternatives would be "split into codepoints" or
// "split into bytes" which have their own helpers in Codepoints /
// Bytes).

primitive Split
  fun on(s: String box, sep: String box)
    : (Array[String val] val | InvalidUtf8)
  =>
    """
    Split `s` on occurrences of `sep`. Adjacent separators produce
    empty-string entries. Returns a fresh val array of fresh val
    String slices (one per part).
    """
    match Bytes.first_bad_utf8_offset(s)
    | let off: USize => InvalidUtf8(off)
    | AllValid => _on(s, sep)
    end

  fun lines(s: String box)
    : (Array[String val] val | InvalidUtf8)
  =>
    """
    Split on line terminators: any of "\\r\\n", "\\n", or "\\r".
    Terminators are removed from the parts. A trailing terminator
    does NOT produce a trailing empty entry (mirrors common
    line-reader semantics).
    """
    match Bytes.first_bad_utf8_offset(s)
    | let off: USize => InvalidUtf8(off)
    | AllValid => _lines(s)
    end

  // ============================================================
  // Workhorses
  // ============================================================

  fun _on(s: String box, sep: String box): Array[String val] val =>
    let out = recover trn Array[String val] end
    if sep.size() == 0 then
      out.push(s.clone())
      return consume out
    end
    var pos: USize = 0
    while true do
      match Search._index_of(s, sep, pos)
      | let off: USize =>
        out.push(s.substring(
          ISize.from[USize](pos), ISize.from[USize](off)))
        pos = off + sep.size()
      | None =>
        out.push(s.substring(
          ISize.from[USize](pos), ISize.from[USize](s.size())))
        break
      end
    end
    consume out

  fun _lines(s: String box): Array[String val] val =>
    let out = recover trn Array[String val] end
    var pos: USize = 0
    let n = s.size()
    while pos < n do
      var i: USize = pos
      var end_byte: USize = n
      var next_pos: USize = n
      while i < n do
        try
          let c = s(i)?
          if c == 0x0A then
            end_byte = i
            next_pos = i + 1
            break
          elseif c == 0x0D then
            end_byte = i
            // Consume "\r\n" as a single terminator.
            next_pos =
              if ((i + 1) < n) and (try s(i + 1)? == 0x0A else false end)
              then i + 2
              else i + 1
              end
            break
          end
        end
        i = i + 1
      end
      out.push(s.substring(
        ISize.from[USize](pos), ISize.from[USize](end_byte)))
      pos = next_pos
    end
    consume out