Slices, string-search, and trimming left and right

🤖 Generated with [ECA](https://eca.dev)

Co-Authored-By: eca-agent <git@eca.dev>
This commit is contained in:
Linus Björnstam 2026-10-05 09:44:37 +02:00
parent 16278c2c92
commit 5924aa27bc

View file

@ -1734,6 +1734,8 @@ Characters are written @code{#\a}, @code{#\space}, @code{#\newline}. They can be
(str "a" "b" "c") ;; "abc": any number of strings (str "a" "b" "c") ;; "abc": any number of strings
(string-upcase "björn") ;; "BJÖRN" (string-upcase "björn") ;; "BJÖRN"
(string-trim " hi ") ;; "hi" (string-trim " hi ") ;; "hi"
(string-trim-left " hi ") ;; "hi "
(string-trim-right " hi ") ;; " hi"
(string-pad-left "7" 3 #:with #\0) ;; "007" (string-pad-left "7" 3 #:with #\0) ;; "007"
(string-split "a,b,,c" ",") ;; ["a" "b" "" "c"]: a Vec (string-split "a,b,,c" ",") ;; ["a" "b" "" "c"]: a Vec
(string-join ["a" "b" "c"] ", ") ;; "a, b, c" (string-join ["a" "b" "c"] ", ") ;; "a, b, c"
@ -1747,7 +1749,7 @@ All of these compare strings ordinally, byte by byte, which for UTF-8 is the ord
@subsubsection{Cursors} @subsubsection{Cursors}
A cursor is a position in a string. It is as cheap as an int, but it always sits on a character boundary, and it can only be moved one character at a time, which is what makes it safe. Every cursor function takes the string as well as the cursor. A cursor is a position in a string. It is as cheap as an int, but it always sits on a character boundary, and it can only be moved one character at a time, which is what makes it safe. Every cursor function takes the string as well as the cursor. Positions are always cursors: nothing takes a byte index, and the searches answer cursors too.
@codeblock[#:lang "bjolang"]|{ @codeblock[#:lang "bjolang"]|{
(: capitalise (-> string string)) (: capitalise (-> string string))
@ -1769,6 +1771,15 @@ A cursor is a position in a string. It is as cheap as an int, but it always sits
(None s))) (None s)))
(first-word "hello there world") ;; "hello" (first-word "hello there world") ;; "hello"
;; string-search finds where a string starts; string-search-backward, the last one.
(: extension (-> string string))
(defun (extension path)
(match (string-search-backward path ".")
((Some dot) (substring path (string-cursor-next path dot) (string-cursor-end path)))
(None "")))
(extension "notes.backup.txt") ;; "txt"
}| }|
The folds walk a string for you, and are usually simpler than a cursor: The folds walk a string for you, and are usually simpler than a cursor:
@ -1785,6 +1796,42 @@ The folds walk a string for you, and are usually simpler than a cursor:
(count-vowels "Hello World") ;; 3 (count-vowels "Hello World") ;; 3
}| }|
@subsubsection{Slices}
@code{substring}, @code{string-trim} and @code{string-split} copy the text they answer. A slice is the same text without the copy: a string that shares the bytes of the string it was taken from. Only the forms ending in @code{/slice} make one:
@codeblock[#:lang "bjolang"]|{
(substring/slice s start end)
(string-trim/slice s)
(string-trim-left/slice s)
(string-trim-right/slice s)
(string-split/slice s sep)
}|
A slice is a @code{string} like any other. It equals, hashes and orders by its text, so it finds a map key with that text, and it can be passed anywhere a string can. That makes slices the thing to reach for when a scanner cuts many short pieces out of one text and only looks at them:
@codeblock[#:lang "bjolang"]|{
;; "name = Björn; age = 42" into a map, without copying a key or a value
;; before it is needed.
(: settings (-> string (Map string string)))
(defun (settings line)
(loop (:for field (string-split/slice line ";"))
(:let parts (string-split/slice field "="))
(:when (= (vec-length parts) 2))
(:acc m (mapping ((string-copy (string-trim/slice (vec-ref parts 0)))
. (string-copy (string-trim/slice (vec-ref parts 1))))))
=> m))
(map-ref (settings "name = Björn; age = 42") "age") ;; "42"
}|
Two things are different about a slice:
@itemlist{
@item{@strong{Its cursors are its source's.} A cursor found in a slice, by @code{string-search}, @code{string-index} or a regex, is a position in the string the slice was taken from, and @code{(string-cursor-start sl)} is where the slice begins in it.}
@item{@strong{It keeps all of its source alive.} A slice of ten bytes out of a file of a megabyte holds on to the megabyte. That is why the plain forms copy, and why the example copies what it keeps: @code{(string-copy s)} gives a string bytes of its own, and is @code{s} itself when it already has them.}
}
@subsubsection{Building strings} @subsubsection{Building strings}
Appending strings in a loop copies the whole string every time. Use a string builder instead, or the @code{stringing} collector, which uses one for you: Appending strings in a loop copies the whole string every time. Use a string builder instead, or the @code{stringing} collector, which uses one for you: