Lines and joining
Text rarely arrives one value at a time. A file, a pasted list or a reply from a server is one run of bytes with line breaks in it, and a program that wants the entries has to cut it into lines. Going the other way, a program that has a list of names and wants to show them needs them in one text, with a comma between each two.
The Text package does both, and changes the case of whole texts too:
import Text::{ FoldCase, Join, StringView, TextError, ToLower, ToUpper };
Walking the lines
Lines is a method of StringView. It returns an iterator, so a for loop walks it:
for line in text.Lines() {
count += 1;
PrintLine("line {} [{}]", count, line);
}
Each line is a StringView of the original bytes, without its line break, so the walk copies nothing and allocates nothing. The text must therefore outlive the loop, as with every view. Three rules decide where the lines are:
| Text | Lines |
|---|---|
"a\nb" | a, b — the last line needs no break |
"a\r\nb\n" | a, b — \r\n is one break, and a final break adds no line |
"a\n\nb" | a, an empty line, b — a blank line is kept |
"" | none at all |
So the program's guest list, with five lines, one of them blank and one ending in \r\n as Windows writes it, prints five lines with no break characters left in them.
Joining pieces
Join takes a slice of views and a separator, and makes one String with the separator between each two parts:
let joined = Join(allocator, names[..kept], StringView::FromValidated(", "))?;
flowchart LR
p1["`Ada Lovelace`"] --> j["`Join` with `, `"]
p2["`grace hopper`"] --> j
p3["`Alan Turing`"] --> j
p4["`Katherine Johnson`"] --> j
j --> s["one `String`:<br/>three separators"]There is always one separator fewer than there are parts: one part comes back on its own, and no parts give empty text. That is the job usually done with a loop and a flag that remembers whether a comma is due, and getting it wrong leaves a stray comma at one end. Join works out the final length first and allocates once. Like every call that allocates text, it returns String ! TextError, so ? passes a failure on.
Changing case
ToUpper, ToLower and FoldCase make a new String from a view. They apply Unicode's full mappings — the ones Unicode case wrote out a scalar at a time with ToUpperFull — to the whole text in one call:
let upper = ToUpper(allocator, StringView::FromValidated("Straße"))?;
let lower = ToLower(allocator, StringView::FromValidated("ÉCOLE"))?;
ß has no single capital letter, so Straße, six characters, becomes STRASSE, seven. What the three leave out are the mappings that depend on the language or on the letters around one: a Turkish dotless i, or a Greek capital sigma that should become the final ς at the end of a word. ToLower always gives σ.
| Function | Straße | STRASSE | Use it for |
|---|---|---|---|
ToUpper | STRASSE | STRASSE | showing text in capitals |
ToLower | straße | strasse | showing text in small letters |
FoldCase | strasse | strasse | comparing text, ignoring case |
Comparing without case
The last column is the point of FoldCase. Two texts that differ only in case fold to the same bytes, so folding both sides and comparing the folds finds a name however it was typed:
let query = FoldCase(allocator, StringView::FromValidated("GRACE HOPPER"))?;
for i in 0..kept {
let folded = FoldCase(allocator, names[i])?;
if folded.Equals(query) {
PrintLine("found {} at position {}", names[i], i + 1);
}
}
The comparison is Equals. A String carries the allocator it came from, which has no ==, so folded == query does not compile.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Text read from a file or typed by a person arrives as one run of bytes with line breaks in it.
// `Lines` walks that run one line at a time. Each line is a view of the original bytes, without
// its break, so the walk copies nothing and allocates nothing.
//
// `Join` goes the other way: it takes several pieces and makes one String, with a separator
// between each two. It is the tidy answer to "a comma after every item but the last".
//
// `ToUpper`, `ToLower` and `FoldCase` change the case of a whole text and hand back a new String.
// They apply Unicode's full mappings, so a letter may become two and the length may change.
// `FoldCase` is the one for comparing: two texts that differ only in case fold to the same bytes.
import Allocator::{ Allocator, SystemAllocator };
import Io::PrintLine;
import Text::{ FoldCase, Join, StringView, TextError, ToLower, ToUpper };
func Main() -> ! TextError {
var system = SystemAllocator();
let allocator: Allocator = system;
// A guest list as a file might hold it: one line ends in `\r\n`, as Windows writes it, one is
// blank, and the text ends with a break.
let text = StringView::FromValidated(
"Ada Lovelace\r\ngrace hopper\n\n Alan Turing \nKatherine Johnson\n");
// A line ends at `\n` or `\r\n`, and neither is part of the line. The blank line is kept as an
// empty line, but the final break does not start another one: this text has five lines.
var count = 0;
for line in text.Lines() {
count += 1;
PrintLine("line {} [{}]", count, line);
}
// Keep the names, trimmed, in an array of views; the blank line is skipped.
var names: StringView[8] = [StringView(); 8];
var kept: uint = 0;
for line in text.Lines() {
let name = line.Trim();
if !name.IsEmpty() {
names[kept] = name;
kept += 1;
}
}
// Four names, three separators. `Join` allocates the result once, at its final length.
let joined = Join(allocator, names[..kept], StringView::FromValidated(", "))?;
PrintLine("joined {}", joined);
// The parts and the separator are views, not bare literals. Passing ", " as it is fails:
//
// error: argument 3 to 'Join' has type 'char8[..]', but parameter 'separator' requires
// 'StringView'
// Case changes make new text. `ß` has no single capital letter, so in upper case it becomes
// "SS": six characters in, seven out.
let upper = ToUpper(allocator, StringView::FromValidated("Straße"))?;
let lower = ToLower(allocator, StringView::FromValidated("ÉCOLE"))?;
PrintLine("upper {} ({} characters)", upper, upper.View().ScalarCount());
PrintLine("lower {}", lower);
// To compare without regard to case, fold both sides and compare the folds with `Equals`.
// Folding the query once and each name once finds "grace hopper" however it was typed.
// `folded == query` is refused, since a String carries its allocator, which has no `==`:
//
// error: structural equality for 'String' is unavailable because element type
// 'Allocator' has no '==' operator
let query = FoldCase(allocator, StringView::FromValidated("GRACE HOPPER"))?;
for i in 0..kept {
let folded = FoldCase(allocator, names[i])?;
if folded.Equals(query) {
PrintLine("found {} at position {}", names[i], i + 1);
}
}
// `Straße` and `STRASSE` differ in case and in length, and still fold to the same text.
let street = FoldCase(allocator, StringView::FromValidated("Straße"))?;
let shouted = FoldCase(allocator, StringView::FromValidated("STRASSE"))?;
PrintLine("same {}", street.Equals(shouted));
}
Besides Io, its Rux.toml lists Allocator and Text under [Dependencies].
Run it
cd Examples/Text/JoinLines
rux run
line 1 [Ada Lovelace]
line 2 [grace hopper]
line 3 []
line 4 [ Alan Turing ]
line 5 [Katherine Johnson]
joined Ada Lovelace, grace hopper, Alan Turing, Katherine Johnson
upper STRASSE (7 characters)
lower école
found grace hopper at position 2
same true
Common mistakes
Try it yourself
- Join the names with
"\n"instead of", ", then walk the result withLines. Do you get the same four names back? - Number the non-blank lines only, so that the blank line does not use up a number.
- Search for
"KATHERINE JOHNSON"and"katherine". Which one is found, and why? - Print
ToUpperof"fix", written with the single ligature characterfi, and count its characters withScalarCount.
Learn more
- String view —
Trim,Splitand the other borrowing operations - Unicode case — the per-character mappings these functions apply
- String builder — building text a piece at a time when
Joinis not enough - Regular expressions — splitting and searching by pattern rather than by fixed text
- Text transforms and
StringViewin the API reference - String literals in the Rux Reference — escapes such as
\rand\n
14.13 Input
Read lines from standard input with ReadLine, where reaching the end of the input is an IoError whose kind is EndOfStream.
14.15 Regular expressions
Compile a regular expression with Regex::New, which returns Regex ! RegexError, test text with IsMatch, find where a match is with Find and FindAt, and turn plain text into a pattern with Escape.