Stringprelude
make String implements Enumerable, Foldable
Text. A String is a sequence of Unicode characters, immutable like every other Kex value: every method here answers with a new string rather than changing the receiver.
A String is its own type, not a list of characters. It is Enumerable, so each, reduce, find and friends walk it one Char at a time, and the sequence operations that could reasonably answer in either currency pick the useful one: take, drop and sort hand back a String, while first and last hand back a Char. Use chars to cross over to a real list.
let line = " Hello, World "
line.trim.lowerCase.split(", ") # => ["hello", "world"]
line.trim.take(5) # => "Hello"
line.trim.chars.count(~upper?) # => 2
Strings interpolate with ${...}:
let name = "Ada"
"hello, ${name}" # => "hello, Ada"
reduce
Folds the string from the left, one Char at a time.
This is String's Enumerable primitive: map, filter, find, any? and the rest are defined in terms of it. Reach for it directly when you are accumulating something that is neither a string nor a list.
reduce(acc, f) : A -> (A -> Char -> A) -> A
Parameters
accA- the initial accumulator
fA -> Char -> A- combines the accumulator with each character
Returns: A — the final accumulator
Examples
Summing digit values
"12345".reduce(0) { |sum, c| sum ` c.codepoint - 48 } # => 15
Building a character histogram
"banana".reduce({}) do |counts, c|
counts.put(c.string, counts.get(c.string).or(0) ` 1)
end
# => {"a": 3, "b": 1, "n": 2}
mapChars
Applies f to every character and joins the results back into a String.
This is the string-preserving counterpart of map. map comes from Enumerable and collects into a list, because its type says (Char -> B) -> [B] and B need not be a Char at all. When you want a string out, name the operation that produces one.
mapChars(f) : (Char -> Char) -> String
Parameters
Returns: String — the mapped characters, as a string
Examples
"hi".mapChars(&.upperCase) # => "HI"
"hi".map(&.upperCase) # => ['H', 'I'] (a list, per Enumerable)
Shifting every character up one codepoint
"abc".mapChars do |c|
String.fromCodepoint(c.codepoint + 1).or("").first.or(c)
end
# => "bcd"
filter
Returns the characters satisfying pred, as a String.
filter(pred) : (Char -> Bool) -> String
Parameters
predChar -> Bool- kept when it answers
true
Returns: String — the matching characters, in order
Examples
Keeping only the digits
"a1b2c3".filter(~digit?) # => "123"
Stripping punctuation before comparing
"Hello, World!".filter(~alpha?).lowerCase # => "helloworld"
get
Returns the character at index i, counting from 0.
Answers None for an index past either end rather than failing, so it is safe to index with a computed position.
get(i) : Integer -> Char?
get(i) : Integer -> Char -> Char
Parameters
iInteger- the 0-based index
Returns: Char? — the character, or None when out of range
Examples
"hi".get(1) # => Just('i')
"hi".get(9) # => None
take
Returns the first n characters. A short string is returned whole, so take never fails on an n that is too large.
take(n) : Integer -> String
Parameters
nInteger- how many characters to keep
Returns: String — the leading n characters
Examples
"hello".take(2) # => "he"
"hello".take(99) # => "hello"
"hello".take(0) # => ""
Truncating for display
let preview = title.take(30) + (title.count > 30 then "…" else "")
drop
Returns everything after the first n characters. The complement of take: s.take(n) s.drop(n) is s+.
drop(n) : Integer -> String
Parameters
nInteger- how many characters to skip
Returns: String — the remaining characters
Examples
"hello".drop(2) # => "llo"
"hello".drop(99) # => ""
Removing a known prefix
let flag = "--verbose"
flag.startsWith?("--") then flag.drop(2) else flag # => "verbose"
reject
Returns the characters that do NOT satisfy pred: the complement of filter.
reject(pred) : (Char -> Bool) -> String
Parameters
predChar -> Bool- dropped when it answers
true
Returns: String — the characters that failed the predicate
Examples
"hello".reject { |c| c == 'l' } # => "heo"
Removing whitespace
"1 234 567".reject(~space?) # => "1234567"
indexOf
Returns the index of the first occurrence of c, or None when the character does not appear.
indexOf(c) : Char -> Integer?
Parameters
cChar- the character to look for
Returns: Integer? — the 0-based index, or None
Examples
"hello".indexOf('l') # => Just(2)
"hello".indexOf('z') # => None
Splitting a key=value pair at the first =
let pair = "host=localhost"
pair.indexOf('=').map { |i| (pair.take(i), pair.drop(i + 1)) }
# => Just(("host", "localhost"))
findIndex
Returns the index of the first character satisfying pred, or None.
The predicate counterpart of indexOf, which searches for one known character.
findIndex(pred) : (Char -> Bool) -> Integer?
Parameters
pred(Char) -> Bool- the test applied to each character
Returns: Integer? — the 0-based index, or None
Examples
"hello".findIndex { |c| c == 'l' } # => Just(2)
"hello".findIndex(~digit?) # => None
Finding where the leading indentation ends
" text".findIndex { |c| !c.space? } # => Just(4)
zip
Pairs each character with the element at the same index in other, stopping at the shorter of the two.
zip(other) : [Y] -> [(Char, Y)]
Parameters
other[Y]- the list to pair with
Returns: [(Char, Y)] — the pairs, in order
Examples
"ab".zip([1, 2]) # => [('a', 1), ('b', 2)]
"abc".zip([1, 2]) # => [('a', 1), ('b', 2)]
Numbering the characters
"abc".zip((0..2).items) # => [('a', 0), ('b', 1), ('c', 2)]
partition
Splits the string in two: the characters satisfying pred, then those that do not. One pass, both answers.
partition(pred) : (Char -> Bool) -> (String, String)
Parameters
predChar -> Bool- the test applied to each character
Returns: (String, String) — the matching and non-matching characters
Examples
"hello".partition { |c| c == 'l' } # => ("ll", "heo")
Separating digits from the rest
let (digits, other) = "a1b2".partition(~digit?)
digits # => "12"
other # => "ab"
enclose
Returns the string with wrapper added at both ends.
enclose(wrapper) : String -> String
enclose(wrapper) : String -> String -> String
Parameters
wrapperString- placed before and after the string
Returns: String — the wrapped string
Examples
"hello".enclose("*") # => "*hello*"
"hello".enclose("__") # => "__hello__"
Quoting a value for output
value.enclose("\"") # => "\"localhost\""
at
Returns the character at index i, counting from 0, or None when the index is out of range. The same as the one-argument get.
at(i) : Integer -> Char?
Parameters
iInteger- the 0-based index
Returns: Char? — the character, or None
Examples
"hello".at(1) # => Just('e')
"hello".at(9) # => None
split
Splits the string on every occurrence of sep, which may be a literal string or a Regex.
Separators at the ends produce empty parts, so splitting ",a," on "," gives three parts. Filter or trim afterwards when that is not wanted.
split(sep) : String | Regex -> [String]
split : [String]
Parameters
sepString | Regex- the separator to split on
Returns: [String] — the parts, in order
Examples
Splitting a delimited line
"a,b,c".split(",") # => ["a", "b", "c"]
"a, b, c".split(", ") # => ["a", "b", "c"]
Splitting on a pattern (needs using Regex)
"a1b22c".split(re`[0-9]+`) # => ["a", "b", "c"]
Empty parts at the edges are kept
",a,".split(",") # => ["", "a", ""]
indentRest
Indents every line but the first by prefix.
This is what splicing a multi-line value into an indented ${...} hole needs: the hole's own indentation already covers line one, and the rest have to catch up. Blank lines stay blank, and (through lines) a single trailing newline is dropped, so a block does not push a stray empty line into its slot.
indentRest(prefix) : String -> String
Parameters
prefixString- the indentation to add
Returns: String — the re-indented string
Examples
"a\nb\n".indentRest(" ") # => "a\n b"
"a".indentRest(" ") # => "a"
Splicing a block into a template
let body = "one\ntwo"
"items:\n ${body.indentRest(" ")}"
# => "items:\n one\n two"
replace
Replaces every literal occurrence of pattern with replacement.
The pattern is matched literally, not as a regular expression. An empty pattern matches at every character boundary, including both ends.
replace(pattern, replacement) : String -> String -> String
Parameters
patternString- the text to look for
replacementString- what to put in its place
Returns: String — the rewritten string
Examples
"a-b-c".replace("-", "`") # => "a`b+c"
"abc".replace("", "-") # => "-a-b-c-"
Normalising a path separator
"a\\b\\c".replace("\\", "/") # => "a/b/c"
substitute
Replaces every key of replacements with its value, in one pass over the map.
The keys are the placeholders exactly as written: no syntax is imposed and nothing is reserved, so $NAME$, __NAME__, {{name}} and %name% are all equally valid, and a template needs no escaping to be a template. $NAME$ is the convention to reach for when there is no reason to prefer another.
Note that a placeholder is NOT ${name}: that is interpolation, which the compiler resolves before this ever sees the string.
This is what a template wants instead of a chain of replace calls: the chain reads as a pipeline when it is really a substitution table, and it quietly depends on its own order, since an earlier replacement's OUTPUT is still visible to every later one.
Substitutions are applied in the map's canonical key order and each is applied to the result of the last, so a value that itself contains a key can still be rewritten by a later one. Keep values placeholder-free when that matters.
substitute(replacements) : {String: String} -> String
Parameters
replacements{String: String}- placeholder to replacement text
Returns: String — the filled-in template
Examples
"Hello, $WHO$!".substitute({"$WHO$": "world"}) # => "Hello, world!"
"$A$ and $B$".substitute({"$A$": "x", "$B$": "y"}) # => "x and y"
"__A__".substitute({"__A__": "any key works"}) # => "any key works"
A reusable template
let greeting = "Dear $NAME$,\n\nYour order $ID$ has shipped."
greeting.substitute({"$NAME$": "Ada", "$ID$": "A-1701"})
contains?
Returns true when sub appears anywhere in the string.
The search is literal and case-sensitive. Lower-case both sides to ignore case; use Regex when the needle is a pattern.
contains?(sub) : String -> Bool
Parameters
subString- the text to look for
Returns: Bool — true when it is present
Examples
"hello world".contains?("world") # => true
"hello world".contains?("xyz") # => false
"hello world".contains?("World") # => false
Ignoring case
"Hello".lowerCase.contains?("hello") # => true
startsWith?
Returns true when the string begins with prefix.
startsWith?(prefix) : String -> Bool
Parameters
prefixString- the expected opening text
Returns: Bool — true when the string starts with it
Examples
"hello".startsWith?("hel") # => true
"hello".startsWith?("llo") # => false
Recognising a command-line flag
args.filter { |a| a.startsWith?("--") }
endsWith?
Returns true when the string ends with suffix.
endsWith?(suffix) : String -> Bool
Parameters
suffixString- the expected closing text
Returns: Bool — true when the string ends with it
Examples
"hello".endsWith?("llo") # => true
"hello".endsWith?("hel") # => false
Selecting files by extension
paths.filter { |p| p.endsWith?(".kex") }
make Char
A single Unicode character.
Character literals are written with single quotes ('a') and are a different type from the one-character string "a". The classification methods (digit?, alpha?, space? and the rest) are what most character code needs; codepoint and String.fromCodepoint are the escape hatch to raw Unicode values.
"hello world".chars.count(~alpha?) # => 10
'a'.upperCase # => 'A'
'a'.string # => "a"
in?
Returns true when the character falls inside range, endpoints included.
in?(range) : Range<Char> -> Bool
Parameters
rangeRange<Char>- the inclusive character range
Returns: Bool — true when the character is in range
Examples
'b'.in?('a'..'z') # => true
'B'.in?('a'..'z') # => false
A hexadecimal-digit test
let hex?(c: Char) -> Bool = c.digit? || c.lowerCase.in?('a'..'f')
module String
Constructors for String values that are built from something other than text: a Unicode codepoint, or raw UTF-8 bytes.
function fromCodepoint
Builds a one-character string from a Unicode codepoint.
Answers None for a surrogate or a value outside the Unicode scalar range, so the result is always valid text. Char.codepoint is the inverse.
fromCodepoint(value) : Integer -> String?
Parameters
valueInteger- the Unicode codepoint
Returns: String? — the one-character string, or None
Examples
String.fromCodepoint(65) # => Just("A")
String.fromCodepoint(233) # => Just("é")
String.fromCodepoint(-1) # => None
Building the alphabet
(0..25).items.map { |i| String.fromCodepoint(97 + i).or("") }.join("")
# => "abcdefghijklmnopqrstuvwxyz"
function fromBytes
Rebuilds a string from its UTF-8 bytes: the inverse of bytes.
A String is TEXT, so bytes outside 0..255 or a malformed encoding answer None rather than a string that would decode to replacement characters. That makes it a decoding step you can check, not a lossy cast.
fromBytes(values) : [Byte] -> String?
Parameters
values[Byte]- the UTF-8 bytes
Returns: String? — the decoded string, or None
Examples
String.fromBytes([104, 105]) # => Just("hi")
String.fromBytes([195, 169]) # => Just("é")
String.fromBytes([255]) # => None
A round trip
String.fromBytes("héllo".bytes) # => Just("héllo")
make Tuple
A fixed-size group of values, written (a, b). Unlike a list, a tuple's size and the type of each position are part of its type, so the List methods do not apply to it: destructure it, match on it, or convert it with items.
let (name, age) = ("Ada", 36)
[1, 2, 3].partition { |n| n.even? } # => ([2], [1, 3])