String
type String
Implements Monoid, Blankable, Enumerable, Foldable, Truthyable.
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 Enumerable Foldable
reduce(acc: A, f: (A -> Char -> A)) -> A
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.
Parameters
acc- the initial accumulator
f- combines the accumulator with each character
Returns 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
mapChars(f: (Char -> Char)) -> String
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.
Parameters
f- applied to each character
Returns 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 Enumerable
filter(pred: (Char -> Bool)) -> String
Returns the characters satisfying pred, as a String.
Parameters
pred- kept when it answers
true
Returns the matching characters, in order
Examples
Keeping only the digits
"a1b2c3".filter(~digit?) # => "123"
Stripping punctuation before comparing
"Hello, World!".filter(~alpha?).lowerCase # => "helloworld"
uniq
uniq : String
Returns the string with repeated characters removed, keeping the first occurrence of each.
Returns the distinct characters, in first-seen order
Examples
"banana".uniq # => "ban"
"hello".uniq # => "helo"
first
first : Char?
A String is its OWN type, not a [Char]: chars converts between them. So the sequence operations below are declared here rather than inherited from List, and they answer in String's terms: take/drop/sort hand back a String, while first/last hand back a Char. The List intrinsics they delegate to already understand the String representation, so there is one implementation for both backends.
Returns the first character, or None when the string is empty.
Returns the first character, or None
Examples
"hi".first # => Just('h')
"".first # => None
Testing an initial without risking an empty string
"hello".first.map(~upper?).or(false) # => false
last
last : Char?
Returns the last character, or None when the string is empty.
Returns the last character, or None
Examples
"hi".last # => Just('i')
"".last # => None
get
get(i: Integer) -> Char?
get(i: Integer, default: Char) -> Char
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.
Parameters
i- the 0-based index
Returns the character, or None when out of range
Examples
"hi".get(1) # => Just('i')
"hi".get(9) # => None
take
take(n: Integer) -> String
Returns the first n characters. A short string is returned whole, so take never fails on an n that is too large.
Parameters
n- how many characters to keep
Returns 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
drop(n: Integer) -> String
Returns everything after the first n characters. The complement of take: +s.take(n) + s.drop(n)+ is s.
Parameters
n- how many characters to skip
Returns 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
reject(pred: (Char -> Bool)) -> String
Returns the characters that do NOT satisfy pred: the complement of filter.
Parameters
pred- dropped when it answers
true
Returns the characters that failed the predicate
Examples
"hello".reject { |c| c == 'l' } # => "heo"
Removing whitespace
"1 234 567".reject(~space?) # => "1234567"
sort
sort : String
Returns the characters in ascending codepoint order.
Returns the sorted characters
Examples
"hello".sort # => "ehllo"
An anagram check
"listen".sort == "silent".sort # => true
indexOf
indexOf(c: Char) -> Integer?
Returns the index of the first occurrence of c, or None when the character does not appear.
Parameters
c- the character to look for
Returns 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
findIndex(pred: (Char -> Bool)) -> Integer?
Returns the index of the first character satisfying pred, or None.
The predicate counterpart of indexOf, which searches for one known character.
Parameters
pred- the test applied to each character
Returns 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
zip(other: [Y]) -> [(Char, Y)]
Pairs each character with the element at the same index in other, stopping at the shorter of the two.
Parameters
other- the list to pair with
Returns 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
partition(pred: (Char -> Bool)) -> (String, String)
Splits the string in two: the characters satisfying pred, then those that do not. One pass, both answers.
Parameters
pred- the test applied to each character
Returns 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"
count Foldable
count : Integer
Returns the number of characters: characters, not bytes. See bytes for the storage-level length.
Returns the character count
Examples
"hello".count # => 5
"".count # => 0
"é".count # => 1 (but "é".bytes.count is 2)
length
length : Integer
Returns the number of characters. The same as count, under the name familiar from other languages.
Returns the character count
Examples
"hello".length # => 5
graphemeCount
graphemeCount : Integer
Returns the number of user-perceived characters: extended grapheme clusters (Unicode UAX #29), which is not always count.
count answers the codepoint count, and most text is one codepoint per grapheme cluster, so the two usually agree. They part ways for a base character combined with a following mark, an emoji built from more than one codepoint, and "\r\n", which is one grapheme cluster over two codepoints: reach for graphemeCount over count wherever "how many characters does a person see" is the question, such as sizing text for display or truncating it at a boundary a reader would recognize.
Returns the grapheme cluster count
Examples
"hello".graphemeCount # => 5, same as count
"\r\n".graphemeCount # => 1 ("\r\n".count is 2: a CR codepoint and an LF codepoint)
A base character plus a combining mark
let acute = String.fromCodepoint(0x301).or("") # combining acute accent
let e = "e" + acute
e.count # => 2 (two codepoints: 'e' and the combining mark)
e.graphemeCount # => 1 (one character on screen)
empty?
empty? : Bool
Returns true when the string has no characters.
Note that a string of spaces is not empty: use blank? from the Blankable trait when whitespace should not count.
Returns true for the empty string
Examples
"".empty? # => true
"hi".empty? # => false
" ".empty? # => false
enclose
enclose(wrapper: String) -> String
enclose(left: String, right: String) -> String
Returns the string with wrapper added at both ends.
Parameters
wrapper- placed before and after the string
Returns the wrapped string
Examples
"hello".enclose("*") # => "*hello*"
"hello".enclose("__") # => "__hello__"
Quoting a value for output
value.enclose("\"") # => "\"localhost\""
at
at(i: Integer) -> Char?
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.
Parameters
i- the 0-based index
Returns the character, or None
Examples
"hello".at(1) # => Just('e')
"hello".at(9) # => None
second
second : Char?
Returns the second character, or None when the string is shorter than two characters.
Returns the second character, or None
Examples
"hi".second # => Just('i')
"h".second # => None
third
third : Char?
Returns the third character, or None when the string is shorter than three characters.
Returns the third character, or None
Examples
"hip".third # => Just('p')
"hi".third # => None
rest
rest : String
Returns everything after the first character. The empty string has no rest, and answers with itself.
Returns the string without its first character
Examples
"hello".rest # => "ello"
"h".rest # => ""
chars
chars : [Char]
Returns the string's characters as a list.
The bridge from String to the List methods that a string does not have of its own. Join back with .join("").
Returns the characters, in order
Examples
"hi".chars # => ['h', 'i']
Going out to List and back
"hello".chars.reverse.join("") # => "olleh"
bytes
bytes : [Byte]
Returns the string's UTF-8 encoding, one Byte per byte.
chars is the TEXT view of a string and bytes is the STORAGE view, so they differ for anything outside ASCII: "é" is one Char and two +Byte+s. Use bytes when the length that matters is the encoded one: a network frame, a file offset, a size limit.
String.fromBytes is the inverse.
Returns the UTF-8 bytes
Examples
"hi".bytes # => [104, 105]
"é".bytes # => [195, 169]
"é".chars # => ['é']
Measuring the encoded size
"héllo".count # => 5
"héllo".bytes.count # => 6
split
split : [String]
Splits the string into its individual characters, as one-character strings.
Use chars instead when you want Char values rather than strings.
Separator-less: one part per character, as the example above shows. Only the intrinsic had this form, so the walker answered "hi".split with "'this' used outside of a method context" while BEAM returned the parts. Declared BEFORE the separator form, as sort is in list.kex: the walker resolves a no-argument call against the first clause of that name.
Returns one string per character
Examples
"hi".split # => ["h", "i"]
split(sep: String | Regex) -> [String]
split : [String]
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.
Parameters
sep- the separator to split on
Returns 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", ""]
lines
lines : [String]
Splits the string into lines on \n.
A single trailing newline is dropped, so a file that ends in one does not produce a final empty line. Windows line endings are not stripped: split on \r\n explicitly, or trim each line, when that matters.
Returns the lines, without their newlines
Examples
"a\nb".lines # => ["a", "b"]
"a\nb\n".lines # => ["a", "b"]
"".lines # => []
Numbering the lines of a file
text.lines.zip((1..text.lines.count).items).each do |pair|
let (line, n) = pair
IO.printLine("${n}: ${line}")
end
indentRest
indentRest(prefix: String) -> String
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.
Parameters
prefix- the indentation to add
Returns 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
replace(pattern: String, replacement: String) -> String
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.
Parameters
pattern- the text to look for
replacement- what to put in its place
Returns 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
substitute(replacements: {String: String}) -> String
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.
Parameters
replacements- placeholder to replacement text
Returns 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"})
trim
trim : String
Removes leading and trailing whitespace. Whitespace inside the string is left alone.
Returns the trimmed string
Examples
" hello ".trim # => "hello"
"\n hi \t".trim # => "hi"
"a b".trim # => "a b"
Cleaning up user input
let name = IO.getLine.or("").trim
upperCase
upperCase : String
Returns the string with every character converted to upper case.
Returns the upper-cased string
Examples
"hello".upperCase # => "HELLO"
"Hello, ada".upperCase # => "HELLO, ADA"
Case-insensitive comparison
input.lowerCase == "yes"
lowerCase
lowerCase : String
Returns the string with every character converted to lower case.
Returns the lower-cased string
Examples
"HELLO".lowerCase # => "hello"
Normalising a lookup key
settings.get(key.trim.lowerCase)
capitalize
capitalize : String
Returns the string with its first character upper-cased and the rest LOWER-cased.
This matches Ruby's String#capitalize, so "hELLO" becomes "Hello", not "HELLO". Use upperCase when the whole string should shout.
It is also what building a type name from data needs: a generated type %name = ... inside a compiled do block requires an upper-case identifier, so a list of lower-case words has to be capitalized first.
Returns the capitalized string
Examples
"hello".capitalize # => "Hello"
"hELLO".capitalize # => "Hello" (tail is lower-cased)
"already Capital".capitalize # => "Already capital"
"".capitalize # => ""
Title-casing a sentence
"the quick brown fox".split(" ").map(~capitalize).join(" ")
# => "The Quick Brown Fox"
reverse
reverse : String
Returns the string with its characters in reverse order.
Returns the reversed string
Examples
"hello".reverse # => "olleh"
A palindrome check
let cleaned = "A man, a plan, a canal: Panama".filter(~alpha?).lowerCase
cleaned == cleaned.reverse # => true
contains?
contains?(sub: String) -> Bool
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.
Parameters
sub- the text to look for
Returns 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?
startsWith?(prefix: String) -> Bool
Returns true when the string begins with prefix.
Parameters
prefix- the expected opening text
Returns 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?
endsWith?(suffix: String) -> Bool
Returns true when the string ends with suffix.
Parameters
suffix- the expected closing text
Returns 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") }
From Monoid
repeat- Combines this value with itself
ntimes.
From Enumerable
map- Applies
fto each item and collects the results into a list. mapIndexed- Applies
fto each item and its 0-based position, and collects the results into a list. flatMap- Applies
fto each item, expecting a list back, and concatenates the results into one flat list. collect- Applies
fto each item, expecting anOptionalback, and returns the values that were present: unwrapped.
From Foldable
each- Calls
fwith each item, for its side effects. eachIndexed- Calls
fwith each item and its 0-based position, for its side effects. all?- Returns
truewhen every item satisfiespred. any?- Returns
truewhen at least one item satisfiespred. find- Returns the first item satisfying
pred, orNonewhen nothing does.
Defined in other modules
type 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"
string
string : String
Returns this character as a one-character String.
A Char and a String are distinct types, so converting between them is explicit. to(String) is the fallible conversion protocol and answers String? like every other conversion; string is the total one, because a character is always one character of text and has no failure case to report. It is the single-character counterpart of String.chars and [Char].join("").
Returns the character as text
Examples
'a'.string # => "a"
'a'.to(String) # => Just("a")
"hi".chars.map(&.string) # => ["h", "i"]
upperCase
upperCase : Char
Returns the upper-case form of this character, still as a Char.
Characters with no upper-case form (digits, punctuation) are returned unchanged.
Returns the upper-case character
Examples
'a'.upperCase # => 'A'
'1'.upperCase # => '1'
lowerCase
lowerCase : Char
Returns the lower-case form of this character, still as a Char.
Characters with no lower-case form are returned unchanged.
Returns the lower-case character
Examples
'A'.lowerCase # => 'a'
'!'.lowerCase # => '!'
digit?
digit? : Bool
Returns true when the character is a decimal digit, '0' through '9'.
Returns true for a decimal digit
Examples
'3'.digit? # => true
'a'.digit? # => false
Validating that a field is all digits
"12345".chars.all?(~digit?) # => true
alpha?
alpha? : Bool
Returns true when the character is alphabetic.
Returns true for a letter
Examples
'a'.alpha? # => true
'1'.alpha? # => false
' '.alpha? # => false
Keeping only letters
"Hello, World!".filter(~alpha?) # => "HelloWorld"
letter?
letter? : Bool
Returns true when the character is a letter. The Unicode-letter spelling of alpha?.
Returns true for a letter
Examples
'z'.letter? # => true
'-'.letter? # => false
upper?
upper? : Bool
Returns true when the character is upper case.
Characters without case (digits, punctuation) answer false.
Returns true for an upper-case letter
Examples
'A'.upper? # => true
'a'.upper? # => false
'1'.upper? # => false
Splitting a camelCase name
"camelCaseName".findIndex(~upper?) # => Just(5)
lower?
lower? : Bool
Returns true when the character is lower case.
Returns true for a lower-case letter
Examples
'a'.lower? # => true
'A'.lower? # => false
space?
space? : Bool
Returns true when the character is whitespace: a space, tab, newline or carriage return.
Returns true for whitespace
Examples
' '.space? # => true
'\t'.space? # => true
'a'.space? # => false
Counting the words on a line
"one two three".split(" ").reject(~empty?).count # => 3
codepoint
codepoint : Integer
Returns the character's Unicode codepoint.
String.fromCodepoint is the inverse.
Returns the codepoint
Examples
'A'.codepoint # => 65
'é'.codepoint # => 233
Converting a digit character to its value
'7'.codepoint - '0'.codepoint # => 7
in?
in?(range: Range<Char>) -> Bool
Returns true when the character falls inside range, endpoints included.
Parameters
range- the inclusive character range
Returns 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.
fromCodepoint
fromCodepoint(value: Integer) -> String?
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.
Parameters
value- the Unicode codepoint
Returns 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"
fromBytes
fromBytes(values: [Byte]) -> String?
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.
Parameters
values- the UTF-8 bytes
Returns 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")
type 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])
items
items : [Any]
Returns the tuple's elements as a list.
A tuple is not a list (its arity is part of its type) so the List methods do not apply to it. This is the explicit conversion, and it loses the per-position typing in exchange.
Destructuring is usually clearer when you know the shape: let (a, b) = pair.
Returns the elements, in order
Examples
(1, "a").items # => [1, "a"]
Iterating over a pair
("host", "port").items.each { |s| IO.printLine(s) }