Standard library
This page lists everything built into Tessel that isn’t a view or a
modifier: functions like print and readFile, conversions, math, random
numbers, files and folders, JSON and binary encoding, dates, the system,
networking, timers and background work, and the properties and methods of
Int, Float, String, Data, lists ([T]) and dictionaries ([K: V]).
The outputs shown in comments come from running the examples.
Calling built-in functions
Section titled “Calling built-in functions”Signatures use the same notation as the Views
page: arguments are passed with their labels, in order, except those marked
_. For example, writeFile(path: String, text: String) -> Bool is called
as writeFile(path: "notes.txt", text: "Hello").
When a function or method has exactly one parameter, you may leave out
its label: readFile("notes.txt") means readFile(path: "notes.txt"), and
list.remove(0) means list.remove(at: 0).
A last parameter of function type can be written as a block after the call:
after(seconds: 1) { print("a second later")}Printing and conversions
Section titled “Printing and conversions”print(_ value: Int, Float, Bool, String, Range or an enum, terminator: String = "\n")Writes a value to standard output, followed by terminator (a new line,
unless you say otherwise).
print("Hello") // Helloprint(42) // 42print(2.0) // 2.0print(true) // trueprint(Mode.grid) // gridprint(0..3) // 0..3print("Name: ", terminator: "") // no line break: the answer goes on the same line- A
Floatalways shows a decimal point (2.0), so it reads differently from anInt. - An enum value prints as its case name, and a range as
start..end. - Lists, dictionaries, sets and tuples print as they’re written in code:
[1, 2],["tea": 3],(1, "one"), with text inside them in quotes andnilfor a missing value. Their items must be printable this way (numbers, text,Bools, enums, and lists, dictionaries, sets and tuples of those). - Structs and optionals can’t be printed directly. Print a field instead,
or give an optional a default with
??. - In an app, the output goes to the terminal the app was started from (or to the console of the Tessel IDE).
printError
Section titled “printError”printError(_ value: …, terminator: String = "\n")Like print, but writes to standard error. Use it for error messages and
warnings from a command-line program, so they don’t mix with its real
output when that’s piped into a file or another program:
if let text = readFile(path) { print(text.uppercase())} else { printError("can't read {path}") exit(1)}readLine
Section titled “readLine”readLine() -> String?Reads the next line from standard input: what the user types, up to Enter,
or the next line of input piped into the program. The line break isn’t
included. At the end of the input, it returns nil.
fn main() { print("What's your name? ", terminator: "") let name = readLine() ?? "stranger" print("Hello, {name}!")}Read every line until the input ends with while let:
fn main() { var total = 0 while let line = readLine() { total += Int(line.trim()) ?? 0 } print("total: {total}")}printf '1\n2\n3\n' | tessel run sum.tsl # total: 6readInput
Section titled “readInput”readInput() -> StringReads all of standard input at once, until it ends; empty if there’s none.
It suits programs that process piped text, like
cat notes.txt | tessel run count.tsl:
fn main() { let text = readInput() print("{text.lines().count} lines, {text.words().count} words")}In an app (which has a window, not a terminal) there’s usually no input:
readLine() returns nil and readInput() an empty string.
Int(_ value: Int or Float) -> IntInt(_ value: String) -> Int?Converts a number or text to an Int.
print(Int(3.99)) // 3print(Int(-3.99)) // -3print(Int("42") ?? 0) // 42print(Int(" 42 ") ?? 0) // 42print(Int("4.2") ?? -1) // -1- From a
Float, the fraction is dropped (rounding toward zero). Values too big for anIntbecome the largest (or smallest)Int. - From a
String, the result is optional:nilif the text isn’t a whole number. Spaces around the number are ignored; a leading+or-is allowed.
Float()
Section titled “Float()”Float(_ value: Int or Float) -> FloatFloat(_ value: String) -> Float?Converts a number or text to a Float.
print(Float(3)) // 3.0print(Float("4.5") ?? 0.0) // 4.5print(Float("1e3") ?? 0.0) // 1000.0print(Float("abc") ?? 0.0) // 0.0From a String, the result is nil if the text isn’t a number. Spaces around
it are ignored, and exponents like 1e3 are understood.
String()
Section titled “String()”String(_ value: Int, Float, Bool, String or an enum) -> StringTurns a value into text, exactly as print would show it.
let label = String(3.0)print(label.count) // 3Usually string interpolation is simpler: "{value}" gives the same text.
print(sqrt(16.0)) // 4.0print(pow(2.0, 10.0)) // 1024.0print(sin(pi / 2.0)) // 1.0print(max(0, 2.5)) // 2.5print(abs(-4)) // 4Math functions take their arguments without labels, in the order shown.
| Function | Returns |
|---|---|
sqrt(x) | The square root of x. |
pow(x, power) | x raised to power: pow(2.0, 3.0) is 8.0. |
exp(x) | e raised to x. |
log(x) | The natural logarithm (base e) of x. |
log10(x) | The base-10 logarithm of x. |
sin(x), cos(x), tan(x) | The sine, cosine and tangent of the angle x. |
asin(x), acos(x), atan(x) | The angle whose sine, cosine or tangent is x. |
atan2(y, x) | The angle from the positive x axis to the point (x, y), from -pi to pi. Note that y comes first. |
floor(x) | x rounded down: floor(-2.7) is -3.0. |
ceil(x) | x rounded up: ceil(2.1) is 3.0. |
round(x) | x rounded to the nearest whole number; halves round away from zero, so round(2.5) is 3.0 and round(-2.5) is -3.0. |
abs(x) | x without its sign. |
min(a, b) | The smaller of a and b. |
max(a, b) | The larger of a and b. |
pi | The constant π, 3.141592653589793. |
- All of these take and return a
Float, exceptabs,minandmax: they work onInts orFloats, and return the same type. Both values ofminandmaxmust have the same type. A whole-number literal next to aFloatis read as aFloat, somax(0, x)works for aFloatx. To mix anIntvariable with aFloat, convert it withFloat(). - Whole-number literals are read as
Floats where aFloatis expected, sosqrt(16)works too. - Angles are in radians. To convert from degrees, multiply by
pi / 180.0:sin(30.0 * pi / 180.0). floor,ceilandroundreturn aFloat. Wrap them inInt()to get anInt:Int(round(2.6))is3.piis a value, not a function: writepi, notpi().- The results follow the usual floating-point rules and never stop the
program:
sqrt(-1.0)isNaN(“not a number”) andlog(0.0)is-inf.
Random numbers
Section titled “Random numbers”random
Section titled “random”random(_ range: Range) -> IntA random Int from the range’s start up to, but not including, its end.
let die = random(1..7) // 1, 2, 3, 4, 5 or 6let index = random(0..names.count)The range must have at least one number: an empty range (like 3..3, or
n..n when n is the same) stops the program with a runtime error.
randomFloat
Section titled “randomFloat”randomFloat() -> FloatA random Float from 0.0 up to, but not including, 1.0.
if randomFloat() < 0.1 { print("a one-in-ten chance")}let angle = randomFloat() * 2.0 * piTo pick or mix up list items, use randomElement
and shuffled.
Paths are ordinary strings. A relative path like "notes.txt" is relative to
the program’s current folder (see currentFolder). Use /
between folder names.
readFile
Section titled “readFile”readFile(path: String) -> String?The whole file as text, or nil if it doesn’t exist, can’t be read, or isn’t
valid UTF-8 text.
if let text = readFile("notes.txt") { print(text.lines().count)} else { print("no notes yet")}writeFile
Section titled “writeFile”writeFile(path: String, text: String) -> BoolWrites text to a file, replacing the file if it exists. Returns whether it
worked. The folder must already exist (see createFolder).
let ok = writeFile(path: "notes.txt", text: "Buy milk\n")print(ok) // truelistFolder
Section titled “listFolder”listFolder(path: String) -> [String]?The names of the files and folders in a folder, sorted, or nil if the folder
can’t be read.
for name in listFolder(".") ?? [] { print(name)}- The result has names only, not full paths: join them yourself, like
"{folder}/{name}". - Hidden files (names starting with
.) are included. - The names are sorted by character code, so
"B"comes before"a".
fileExists
Section titled “fileExists”fileExists(path: String) -> BoolWhether a file or folder exists at path.
print(fileExists("notes.txt"))isFolder
Section titled “isFolder”isFolder(path: String) -> BoolWhether path is an existing folder.
print(isFolder(".")) // truedeleteFile
Section titled “deleteFile”deleteFile(path: String) -> BoolDeletes a file. Returns whether it worked. It doesn’t delete folders (it
returns false for a folder).
if deleteFile("notes.txt") { print("deleted")}createFolder
Section titled “createFolder”createFolder(path: String) -> BoolCreates a folder, and any missing folders above it. Returns whether it worked; a folder that already exists counts as success.
print(createFolder("backups/2026")) // truerenameFile
Section titled “renameFile”renameFile(from: String, to: String) -> BoolRenames or moves a file or folder. Returns whether it worked. An existing
file at to may be replaced.
let moved = renameFile(from: "notes.txt", to: "backups/notes.txt")currentFolder
Section titled “currentFolder”currentFolder() -> StringThe full path of the program’s current folder: the folder it was started from. Relative paths are relative to this folder.
print(currentFolder()) // for example /Users/ada/Projects/notesappendFile
Section titled “appendFile”appendFile(path: String, text: String) -> BoolAdds text to the end of a file, creating the file if it doesn’t exist.
Returns whether it worked. Handy for logs.
writeFile(path: "log.txt", text: "started\n")appendFile(path: "log.txt", text: "saved\n")print((readFile("log.txt") ?? "").lines().count) // 2copyFile
Section titled “copyFile”copyFile(from: String, to: String) -> BoolCopies a file. Returns whether it worked. An existing file at to is
replaced. It doesn’t copy folders.
print(copyFile(from: "log.txt", to: "log-backup.txt")) // truefileSize
Section titled “fileSize”fileSize(_ path: String) -> Int?The size of a file in bytes, or nil if it doesn’t exist.
print(fileSize("log.txt") ?? 0) // 14print(fileSize("missing.txt") ?? -1) // -1modifiedTime
Section titled “modifiedTime”modifiedTime(_ path: String) -> Float?When the file last changed, as a time (seconds since
1970), or nil if it doesn’t exist.
if let time = modifiedTime("log.txt") { print("Last changed {formatDate(time)}")}readData
Section titled “readData”readData(_ path: String) -> Data?The whole file as raw bytes (Data), or nil if it can’t be read.
Use it for files that aren’t text, such as images or files written with
toBinary.
if let data = readData("header.bin") { print(data.count)}writeData
Section titled “writeData”writeData(path: String, data: Data) -> BoolWrites bytes to a file, replacing the file if it exists. Returns whether it worked.
let bytes = Data(bytes: [137, 80, 78, 71])print(writeData(path: "header.bin", data: bytes)) // trueFolders and paths
Section titled “Folders and paths”These functions give you the usual places to keep files, and build paths
that work on every system. To take a path apart, use the String properties
fileName, fileExtension and folder.
| Function | Returns |
|---|---|
homeFolder() | The user’s home folder, like /Users/ada (or C:\Users\ada on Windows). |
documentsFolder() | The user’s Documents folder. |
tempFolder() | A folder for temporary files. |
appDataFolder(_ name: String) | A folder for your app’s own files (settings, caches), created if needed. |
joinPath(_ folder: String, _ name: String) | folder and name joined with the system’s separator. |
appDataFolder
Section titled “appDataFolder”appDataFolder(_ name: String) -> StringThe folder where an app keeps its own files, like settings, with name
added. It’s created if it doesn’t exist yet.
- On macOS:
~/Library/Application Support/<name>. - On Windows:
%APPDATA%\<name>.
let settings = joinPath(appDataFolder("Notes"), "settings.json")Use your app’s name for name, so apps don’t mix up their files.
joinPath
Section titled “joinPath”joinPath(_ folder: String, _ name: String) -> StringJoins a folder and a name with / (\ on Windows). Extra separators where
they meet are removed. If folder is "", the result is just name.
print(joinPath("/Users/ada/", "notes.txt")) // /Users/ada/notes.txtlet notes = joinPath(documentsFolder(), "Notes")File dialogs
Section titled “File dialogs”openFile and saveFile show the system’s standard dialog for choosing a
file, and return the path the user picked. They’re meant for
apps: call them from a button’s action, a
shortcut or another block that runs when the user does
something. See Documents and custom file types
for a complete example.
openFile
Section titled “openFile”openFile(title: String = "", types: [String] = []) -> String?Asks the user to pick an existing file. Returns its full path, or nil if
the user cancelled.
Button("Open…") { if let path = openFile(title: "Open a note", types: ["note"]) { text = readFile(path) ?? "" }}titleis shown in the dialog’s title bar (not on every system).typeslimits the choice to files with these extensions. Write them without the dot:["note"], or["txt", "md"]for several. An empty list allows any file.
saveFile
Section titled “saveFile”saveFile(title: String = "", name: String = "", types: [String] = []) -> String?Asks the user where to save a file. Returns the chosen path, or nil if the
user cancelled. It doesn’t write anything: pass the path to
writeFile.
Button("Save…") { if let path = saveFile(title: "Save the note", name: "Untitled.note", types: ["note"]) { writeFile(path: path, text: text) }}nameis the file name the dialog suggests.typesworks as foropenFile. If the user types a name without an extension, the first extension intypesis added, so the example above always gives a path ending in.note.- The system dialog asks the user before replacing an existing file.
JSON and binary
Section titled “JSON and binary”These four functions turn your values into text or bytes, to save in a file
or send over the network, and read them back. They work with structs, enums,
lists, dictionaries, optionals, ranges, Data and the basic types, nested
as deeply as you like. See Saving data as JSON for a
complete example.
enum Priority { low, high }
struct Task { title: String done: Bool = false priority: Priority = Priority.low}
fn main() { let task = Task(title: "Write docs", priority: Priority.high) let text = toJson(task) print(text) // {"title":"Write docs","done":false,"priority":"high"}
let back: Task? = fromJson(text) print(back == task) // true}toJson
Section titled “toJson”toJson(_ value, pretty: Bool = false) -> StringThe value as JSON text. With pretty: true, the
text is spread over several lines and indented, which is easier to read:
print(toJson(task, pretty: true)){ "title": "Write docs", "done": false, "priority": "high"}fromJson
Section titled “fromJson”fromJson(_ text: String) -> T?Reads a value from JSON text. It gives nil if the text isn’t valid JSON or
doesn’t fit the type.
fromJson finds out which type to read from where its result goes, so give
the result a type:
let loaded: Task? = fromJson(text)let ages: [String: Int] = fromJson(text) ?? [:]if let numbers: [Int] = fromJson("[4, 8, 15]") { print(numbers.sum()) // 27}Without a type, tessel check asks for one:
error: can't tell what type `fromJson` should read --> main.tsl:2:16 |2 | let data = fromJson(text) | ^^^^^^^^^^^^^^ | = help: give the result a type, like `let project: Project? = fromJson(…)`toBinary
Section titled “toBinary”toBinary(_ value) -> DataThe value as compact bytes (MessagePack), smaller
and faster to read than JSON, but not readable by people. Save it with
writeData.
let data = toBinary(task)print(data.count) // 38fromBinary
Section titled “fromBinary”fromBinary(_ data: Data) -> T?Reads a value back from bytes made by toBinary, or nil if they don’t fit
the type. Like fromJson, it needs to know the type:
if let data = readData("task.bin") { let task: Task? = fromBinary(data)}How values are written
Section titled “How values are written”| Value | JSON | Example |
|---|---|---|
Int, Float, Bool, String | The same value | 42, 1.5, true, "hi" |
| A struct | An object with a key for each field | {"x":1.0,"y":2.0} |
| An enum case without values | Its name, as a string | "high" |
| An enum case with values | An object with the case’s name as key | {"custom":{"name":"feet","factor":0.3048}} |
| A list | An array | [1,2,3] |
| A dictionary | An object; Int and Bool keys are written as strings | {"ada":36} |
| An optional | The value, or null for nil | null |
| A range | An array with its start and end | [0,5] |
Data | A base64 string | "aGk=" |
toBinary writes the same structure in MessagePack, which can hold a few
things JSON can’t, so it keeps them as they are: Data as raw bytes (not
base64), Int and Bool dictionary keys as numbers and booleans, and NaN
and infinity as floats. That also makes binary files smaller.
When reading:
- Fields that are missing take their default value (or
nilfor an optional field); a missing field without a default makes the resultnil. This lets you add fields with defaults to a struct and still read files saved before. - Keys that the struct doesn’t have are ignored.
- A value of the wrong kind (like a number where a string belongs), an
unknown enum case, or text that isn’t valid JSON makes the whole result
nil. - An
Intalso accepts a whole number written with a decimal point, like2.0. Floatvalues that aren’t numbers (NaN, infinity) have no JSON form; they’re written asnull. Readingnullinto aFloatgivesNaN, and into aFloat?givesnil. (Binary keeps them exactly.)- A whole document that is just
nullreads asnil, whatever the type:let x: Float? = fromJson("null")isnil.
Functions and views can’t be saved. tessel check points out the field that
is the problem:
struct Counter { count: Int onChange: fn(Int)}error: `Counter` can't be saved with `toJson` --> main.tsl:8:23 |8 | let text = toJson(c) | ^ | = help: field `onChange`: functions can't be savedDictionary keys must be String, Int, Bool or an enum.
Data holds raw bytes: the contents of a file that isn’t text, the result of
toBinary, or a download.
let empty = Data()let bytes = Data(bytes: [72, 105, 33])let text = Data(text: "héllo")print(bytes.text ?? "not text") // Hi!print(text.count) // 6print(text[1]) // 195print(bytes == Data(text: "Hi!")) // true| Creating | |
|---|---|
Data() | No bytes. |
Data(bytes: [Int]) | The given bytes. Each Int should be from 0 to 255; other values are wrapped into that range. |
Data(text: String) | The text’s bytes in UTF-8. |
| Member | Returns | |
|---|---|---|
count | Int | Number of bytes. |
isEmpty | Bool | Whether there are no bytes. |
bytes | [Int] | The bytes, each from 0 to 255. |
text | String? | The bytes read as UTF-8 text, or nil if they aren’t valid UTF-8. |
base64 | String | The bytes as base64 text. |
data[i]is the byte at indexi(from 0), as anInt. An index outside the data stops the program with an error.Datacan’t be changed in place; make a new one instead.==and!=compare the bytes.- Read and write files of bytes with
readDataandwriteData.
base64
Section titled “base64”Base64 turns bytes into plain text, for places that only take text (like a JSON field or a URL).
data.base64 -> StringdecodeBase64(_ text: String) -> Data?print(Data(text: "Hi!").base64) // SGkhif let decoded = decodeBase64("SGkh") { print(decoded.text ?? "") // Hi!}decodeBase64 returns nil if the text isn’t base64. Spaces and line
breaks around it are ignored.
Dates and time
Section titled “Dates and time”A point in time is a Float: the number of seconds since 1 January 1970
(UTC). That makes times easy to store and compare, and to do arithmetic with:
add 60.0 for a minute later, or 86400.0 for a day.
let start = now()let deadline = date(year: 2026, month: 12, day: 31, hour: 17)print(formatDate(deadline, format: "EEEE, d MMMM yyyy")) // Thursday, 31 December 2026let days = Int((deadline - start) / 86400.0)Dates are shown and read in the computer’s time zone, unless you name another one (see Time zones).
now() -> FloatThe current time. Use it to time something, too:
let started = now()doWork()print("took {(now() - started).formatted(decimals: 2)} seconds")date(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0, timeZone: String = "") -> FloatThe time of a calendar date, in the computer’s time zone or the one named.
Months and days count from 1. A date that doesn’t exist, like 30 February,
gives NaN (see isNaN).
let t = date(year: 2026, month: 3, day: 7, hour: 9, minute: 5)print(formatDate(t)) // 2026-03-07 09:05let launch = date(year: 2026, month: 3, day: 7, hour: 9, timeZone: "Asia/Tokyo")A time the clocks skip (when they’re set forward for summer time) gives the moment an hour later; a time they show twice (when they’re set back) gives the first of the two.
formatDate
Section titled “formatDate”formatDate(_ time: Float, format: String = "yyyy-MM-dd HH:mm", timeZone: String = "") -> StringA time as text, in the computer’s time zone or the one named. format is a pattern where these letters are replaced;
anything else is copied as it is:
| Pattern | Means | Example |
|---|---|---|
yyyy | Year | 2026 |
yy | Year, two digits | 26 |
MMMM | Month name | March |
MMM | Short month name | Mar |
MM | Month, two digits | 03 |
M | Month | 3 |
dd | Day of the month, two digits | 07 |
d | Day of the month | 7 |
EEEE | Weekday | Saturday |
EEE | Short weekday | Sat |
HH | Hour (0–23), two digits | 09 |
H | Hour (0–23) | 9 |
hh | Hour (1–12), two digits | 09 |
h | Hour (1–12) | 9 |
mm | Minute, two digits | 05 |
m | Minute | 5 |
ss | Second, two digits | 30 |
s | Second | 30 |
a | AM or PM | AM |
SSS | Thousandths of a second | 250 |
Z | How far the zone is from UTC | +0100 |
ZZZZZ | The same, with a colon | +01:00 |
'text' | The text as it is | at |
'' | A single quote | ' |
let t = date(year: 2026, month: 3, day: 7, hour: 9, minute: 5, second: 30)print(formatDate(t, format: "yyyy-MM-dd HH:mm:ss")) // 2026-03-07 09:05:30print(formatDate(t, format: "d MMM yy")) // 7 Mar 26print(formatDate(t, format: "EEE, MMMM d")) // Sat, March 7print(formatDate(t, format: "h:mm a")) // 9:05 AMprint(formatDate(t, format: "dd/MM/yyyy")) // 07/03/2026print(formatDate(t, format: "d MMM 'at' HH:mm")) // 7 Mar at 09:05Month and weekday names are in English. Put words in single quotes, like
'at', so their letters aren’t replaced.
The format "iso" gives the form used on the internet and in JSON
(ISO 8601): the date, a T, the time, and how far the zone is from UTC.
print(formatDate(t, format: "iso", timeZone: "Europe/Paris")) // 2026-03-07T09:05:30+01:00print(formatDate(t, format: "iso", timeZone: "UTC")) // 2026-03-07T08:05:30ZparseDate
Section titled “parseDate”parseDate(_ text: String, format: String = "yyyy-MM-dd", timeZone: String = "") -> Float?Reads a time from text written in format (the same patterns as
formatDate), or nil if the text doesn’t match. Without an
hour, the time is midnight. The text is taken to be in the computer’s time
zone, or the one named, unless it says itself how far from UTC it is (with
Z or ZZZZZ in the format).
let day = parseDate("2026-03-07")let meeting = parseDate("07/03/2026 09:05", format: "dd/MM/yyyy HH:mm")print(parseDate("tomorrow") == nil) // trueThe whole text must match: parseDate("2026-03-07 09:05") is nil, because
the default format has no time.
The format "iso" reads the internet’s form, as servers and JSON usually
send it: with a distance from UTC (+01:00, or Z for none), with or
without fractions of a second. Without a distance, or with only a date, the
timeZone: counts.
let sent = parseDate("2026-03-07T09:05:30+01:00", format: "iso")let utc = parseDate("2026-03-07T08:05:30.250Z", format: "iso")let noon = parseDate("2026-03-07 12:00", format: "yyyy-MM-dd HH:mm", timeZone: "Asia/Tokyo")dateParts
Section titled “dateParts”dateParts(_ time: Float, timeZone: String = "") -> DatePartsWhat the calendar and the clock say at a moment: a DateParts struct.
| Field | Type | |
|---|---|---|
year | Int | |
month | Int | 1 for January to 12. |
day | Int | The day of the month, from 1. |
hour | Int | 0 to 23. |
minute | Int | |
second | Int | |
weekday | Int | 1 for Monday to 7 for Sunday. |
dayOfYear | Int | 1 for 1 January. |
let today = dateParts(now())if today.weekday >= 6 { print("It's the weekend")}print("Day {today.dayOfYear} of {today.year}")addToDate
Section titled “addToDate”addToDate(_ time: Float, years: Int = 0, months: Int = 0, days: Int = 0, hours: Int = 0, minutes: Int = 0, seconds: Int = 0, timeZone: String = "") -> FloatA time that’s some years, months or days later on the calendar (or earlier, with negative numbers), and then some hours, minutes and seconds later.
let due = addToDate(now(), days: 14)let renewal = addToDate(start, years: 1)let reminder = addToDate(meeting, minutes: -15)Adding 86400.0 seconds isn’t always the next day at the same time, and a
month isn’t a fixed number of days; addToDate follows the calendar:
- A day later is the same time of day, even when the clocks change in between (that day is 23 or 25 hours long).
- A month after 31 January is the last day of February: the day is brought back into a shorter month.
- Hours, minutes and seconds are simply that much time later.
daysBetween
Section titled “daysBetween”daysBetween(_ from: Float, _ to: Float, timeZone: String = "") -> IntHow many days later to is than from on the calendar: how many times the
date changes between them. It’s negative if to is earlier. Two minutes
that cross midnight are a day apart; 23 hours within one date are not.
let left = daysBetween(now(), deadline)print(if left == 0 { "Due today" } else { "{left} days left" })Time zones
Section titled “Time zones”Every date function has a timeZone: argument. Left out, it’s the
computer’s time zone. Otherwise it’s one of:
| Time zone | Example | |
|---|---|---|
| A name | "Europe/Paris", "America/New_York", "Asia/Tokyo" | A region and a city, from the time zone database that computers share. It knows that region’s summer time, now and in the past. |
"UTC" | The time the others are counted from. | |
| A distance from UTC | "+05:30", "-08:00", "+2" | Always that far, with no summer time. |
let t = now()print(formatDate(t, format: "HH:mm", timeZone: "Asia/Tokyo"))print(formatDate(t, format: "HH:mm", timeZone: "America/New_York"))A time itself has no time zone: it’s one moment everywhere. The zone only decides what the calendar and the clock say at that moment.
A time zone that doesn’t exist is treated like a date that doesn’t:
formatDate gives "", parseDate gives nil, date and addToDate
give NaN, and dateParts, daysBetween and timeZoneOffset give zeros.
Check a name that comes from outside the program with
timeZones.
timeZones
Section titled “timeZones”timeZones() -> [String]The names of all time zones, in alphabetical order: about 600 of them. Use it to offer a choice, or to check a name:
if timeZones().contains(name) { zone = name}localTimeZone
Section titled “localTimeZone”localTimeZone() -> StringThe name of the computer’s time zone, like "Europe/Paris", or "" if it
can’t be found out.
timeZoneOffset
Section titled “timeZoneOffset”timeZoneOffset(_ time: Float, timeZone: String = "") -> IntHow many seconds the zone’s clocks are ahead of UTC at that moment (negative if they’re behind). It depends on the moment, because of summer time.
let hours = timeZoneOffset(now(), timeZone: "America/New_York") / 3600 // -5, or -4 in summerThe system and other programs
Section titled “The system and other programs”environment
Section titled “environment”environment(name: String) -> String?The value of an environment variable, or nil if it isn’t set.
let home = environment("HOME") ?? "unknown"arguments
Section titled “arguments”arguments() -> [String]The arguments the program was started with, without the program’s own name.
fn main() { let args = arguments() if args.isEmpty { print("usage: greet <name>") exit(1) } print("Hello, {args[0]}!")}Run with tessel run greet.tsl Ada (or ./greet Ada once built), this
prints Hello, Ada!.
exit(_ code: Int = 0)Ends the program right away, with an exit code: 0 means success, anything
else a failure. Timers, downloads and other work still waiting are dropped.
platform
Section titled “platform”platform() -> StringThe system the program is running on: "macos" or "windows".
let shortcut = if platform() == "macos" { "Cmd+S" } else { "Ctrl+S" }isDarkMode
Section titled “isDarkMode”isDarkMode() -> BoolWhether the app is showing in its dark appearance: true when the system is
dark (or the app says appearance: .dark). The UI is built again when the
appearance changes, so a view can simply ask:
let card: Color = if isDarkMode() { .rgb(44, 44, 46) } else { .rgb(250, 246, 240) }In a program without an app, it’s false. See
Light and dark.
openURL
Section titled “openURL”openURL(_ url: String) -> BoolOpens a web page in the browser, or a file or folder with its usual app. Returns whether it worked.
Button("Help") { openURL("https://tessel-lang.netlify.app") }Button("Show folder") { openURL(appDataFolder("Notes")) }In a headless test run, nothing opens: it prints
(open …) instead.
copyToClipboard
Section titled “copyToClipboard”copyToClipboard(_ text: String) -> BoolPuts text on the system clipboard, as if the user had copied it. Returns whether it worked.
Button("Copy link") { copyToClipboard(link) }clipboardText
Section titled “clipboardText”clipboardText() -> String?The text on the clipboard, or nil if it has none (for example, when an
image was copied).
Button("Paste") { text += clipboardText() ?? "" }A headless test run uses a clipboard of its own, shared with its text fields’ copy and paste, so tests never touch yours.
runCommand
Section titled “runCommand”runCommand(path: String, args: [String] = [], onOutput: fn(String), onExit: fn(Int)) -> IntStarts another program and returns right away with an id for
stopCommand. The program runs in the background:
onOutputis called with each line the program prints, from both its standard output and standard error, without the line ending.onExitis called once, after all the output, with the exit code. The code is-1if the program couldn’t be started or was stopped by a signal. If it couldn’t be started,onOutputfirst receives a line starting witherror: can't start.
app Runner { state output: [String] = []
Button("List files") { output = [] runCommand(path: "ls", args: ["-l"], onOutput: { line in output.append(line) }) { code in output.append("finished with code {code}") } } for line in output { Text(line) }}pathis the program to run: a path like/bin/ls, or a name looked up in the folders of thePATHenvironment variable. It isn’t run through a shell, so each argument goes inargsseparately, and shell features like*or|don’t apply.- The program gets no input.
- Both blocks run on the UI thread, so they can change
statedirectly.
stopCommand
Section titled “stopCommand”stopCommand(id: Int)Stops a program started with runCommand. Its onExit block
still runs. Stopping a program that has already finished does nothing.
app Watcher { state running = -1
Button("Start") { running = runCommand(path: "sleep", args: ["60"], onOutput: { line in print(line) }) { code in running = -1 } } Button("Stop") { stopCommand(running) }}Callbacks
Section titled “Callbacks”The functions below finish their work later: the network, timers and
background work. They take a block (a callback) that Tessel runs when the
work is done. Callbacks run on the UI thread in an app, so they can change
state directly. They work in a program with only fn main() too: after
main returns, the program keeps running until no timers, requests,
WebSockets or background work are left, then exits. See
Files, commands and timers.
Networking
Section titled “Networking”fetch(url: String, done: fn(String?))Loads a URL in the background, then calls done with the text, or nil if
loading failed.
app Weather { state report = "Loading…"
Text(report).onAppear { fetch(url: "https://example.com/weather.txt") { text in report = text ?? "Could not load the weather" } }}http://andhttps://URLs are loaded with a GET request. An error status (like 404) counts as failure.file://URLs read a local file:file:///Users/ada/notes.txt.- The response is read as text.
For other methods (like POST), headers, or the status code, use
request.
request
Section titled “request”request(url: String, method: String = "GET", headers: [String: String] = [:], body: String = "", done: fn(HttpResponse)) -> IntSends an HTTP request in the background, then calls done with the
response. It returns an id, for cancelRequest; most
programs don’t need it.
struct Todo { title: String done: Bool = false}
app Todos { state status = ""
Button("Add") { request(url: "https://example.com/api/todos", method: "POST", headers: ["Content-Type": "application/json"], body: toJson(Todo(title: "Buy milk"))) { response in if response.ok { status = "Added" } else if response.status == 0 { status = "Couldn't connect: {response.error}" } else { status = "The server said {response.status}" } } } Text(status)}methodcan be any HTTP method:"GET","POST","PUT","PATCH","DELETE", and so on.headersare sent with the request.bodyis sent as it is; usetoJsonto send a struct as JSON.- A response with an error status (like 404 or 500) is still a response:
donegets it, with its status and body. Checkokorstatus. file://URLs read a local file, with status 200.
HttpResponse
Section titled “HttpResponse”What request gives its block. It’s a built-in struct with these fields:
| Field | Type | |
|---|---|---|
status | Int | The HTTP status code, like 200 or 404. 0 if there was no response at all. |
ok | Bool | Whether the status is from 200 to 299: the request worked. |
text | String | The body as text. |
data | Data | The body as bytes, for images and other files that aren’t text. |
headers | [String: String] | The response’s headers. The names are in lower case: response.headers["content-type"]. |
error | String | Why there was no response (the server couldn’t be reached, the URL is invalid, …), or "". |
To read a JSON answer, pass text to fromJson:
request(url: "https://example.com/api/todos") { response in if let todos: [Todo] = fromJson(response.text) { items = todos }}download
Section titled “download”download(url: String, to: String, progress: fn(Float)? = nil, done: fn(Bool)) -> IntDownloads a URL straight into a file, which suits large files. progress,
if given, is called now and then with how much has arrived, from 0.0 to
1.0. done is called at the end with whether it worked. It returns an id,
for cancelRequest.
app Downloader { state progress = 0.0 state status = ""
Button("Download") { let file = joinPath(tempFolder(), "big.zip") download(url: "https://example.com/big.zip", to: file, progress: { fraction in progress = fraction }) { ok in status = if ok { "Done" } else { "Download failed" } } } Text("{Int(progress * 100.0)}% {status}")}- An error status (like 404) counts as failure.
- While downloading, the data goes to a file with
.partadded to the name; it’s renamed totoonly once everything has arrived. So a failed download leaves no half-written file behind. An existing file attois replaced. progressgets its fractions only if the size is known (the server says how big the file is, or it’s a local file). Either way, it’s called with1.0exactly once, when everything has arrived.http://,https://andfile://URLs can be downloaded;file://copies a local file.
cancelRequest
Section titled “cancelRequest”cancelRequest(_ id: Int)Gives up a request or a download that’s under
way. Its done block is never called (nor progress), and a cancelled
download leaves no file behind, not even a partial one.
app Downloader { state loading: Int? = nil state status = ""
if let id = loading { Button("Cancel") { cancelRequest(id) loading = nil status = "Cancelled" } } else { Button("Download") { loading = download(url: "https://example.com/big.zip", to: "big.zip") { ok in loading = nil status = if ok { "Done" } else { "Download failed" } } } } Text(status)}- Since
doneisn’t called, do your own tidying up where you cancel, as the example does. - Cancelling something that has finished already, or an id that was never given out, does nothing.
- A request that’s still waiting for the server to answer is dropped by your program at once, though the connection itself is only closed when the server answers or gives up.
openWebSocket
Section titled “openWebSocket”openWebSocket(url: String, onMessage: fn(String), onData: fn(Data)? = nil, onClose: fn(String)) -> IntConnects to a WebSocket server (ws:// or wss://) and returns an id for
sendWebSocket and closeWebSocket.
The connection stays open, and messages can go both ways:
onMessageis called with each text message the server sends.onData, if given, is called with each binary message, asData. Without it, binary messages go toonMessage, as text.onCloseis called once, when the connection ends, with the reason:"closed"if you closed it, the server’s reason (or"closed by the server"), or what went wrong, like"couldn't connect: …".
app Chat { state socket = -1 state messages: [String] = [] state draft = ""
VStack { for message in messages { Text(message) } TextField("Message", text: draft).onSubmit { sendWebSocket(socket, text: draft) draft = "" } } .onAppear { socket = openWebSocket(url: "wss://example.com/chat", onMessage: { text in messages.append(text) }) { reason in messages.append("Disconnected: {reason}") socket = -1 } }}A server that sends binary messages (bytes rather than text) needs onData:
socket = openWebSocket(url: "wss://example.com/feed", onMessage: { text in log.append(text)}, onData: { data in received += data.count}) { reason in socket = -1}sendWebSocket
Section titled “sendWebSocket”sendWebSocket(_ id: Int, text: String) -> BoolSends a text message. Returns false if the WebSocket isn’t open (it was
closed, or id is wrong). Messages sent before the connection is ready are
sent once it is.
sendWebSocketData
Section titled “sendWebSocketData”sendWebSocketData(_ id: Int, data: Data) -> BoolSends a binary message: the bytes of a Data. Like
sendWebSocket, it returns false if the WebSocket isn’t
open.
sendWebSocketData(socket, data: toBinary(move))sendWebSocketData(socket, data: Data(bytes: [1, 2, 255]))closeWebSocket
Section titled “closeWebSocket”closeWebSocket(_ id: Int)Closes a WebSocket. Its onClose block still runs, with "closed". Closing
one that is already closed does nothing.
Timers
Section titled “Timers”after(seconds: Float, action: fn())Runs action once, after a delay, on the UI thread.
app Reminder { state showHint = false
Text("Welcome!").onAppear { after(seconds: 2) { showHint = true } } Text("Tip: press Cmd+S to save").hidden(!showHint)}A delay of 0 (or less) runs action as soon as possible, after the current
code finishes. An after timer can’t be cancelled; to repeat, use
every.
every(seconds: Float, action: fn()) -> IntRuns action again and again, every seconds, until it’s stopped with
stopTimer. The first run is one interval from now. Returns an
id for stopTimer.
app Clock { state time = ""
Text(time).font(size: 32).onAppear { every(seconds: 1) { time = formatDate(now(), format: "HH:mm:ss") } }}stopTimer
Section titled “stopTimer”stopTimer(_ id: Int)Stops a timer started with every. The timer can stop itself:
fn main() { var ticks = 0 var id = 0 id = every(seconds: 0.5) { ticks += 1 print("tick {ticks}") if ticks == 3 { stopTimer(id) } }}This prints tick 1, tick 2 and tick 3, then the program ends, since
nothing is left to wait for. Stopping a timer that has already stopped does
nothing.
Background work
Section titled “Background work”background
Section titled “background”background(work: fn() -> T, done: fn(T))Runs work on another thread, so a long computation doesn’t freeze the app,
then calls done with its result on the UI thread. It’s usually written with
done as a trailing block:
let n = limitbackground(work: { primesBelow(limit: n) }) { primes in status = "{primes.count} primes"}The two threads share nothing: work gets its own copies of the values it
uses, and done gets a copy of the result. That’s what makes it safe, and
it leads to a few rules, which tessel check enforces:
workcan use local values, and call top-level functions (fns outside any app, view or struct) and methods of the values it has.- It can’t use
state, bindings,selfor its fields, or the functions of the app or view: they belong to the UI thread. Copy what it needs into aletfirst, as withnabove. - The values it uses and its result must be values
toBinarycan save: not functions or views. workmust end with a value.returnworks inside it too.
Values are copied when background is called, so changes made afterwards
don’t reach work. See Heavy work in the background
for a complete app.
A runtime error in work (like an index out of range) stops the program,
as it would anywhere else.
An Int is a whole number. The arithmetic operators work on it, and
abs, min and max take Ints too.
| Member | Returns | |
|---|---|---|
isEven | Bool | Whether the number is even. |
isOdd | Bool | Whether the number is odd. |
clamped(min: Int, max: Int) | Int | The number, moved into the range from min to max. |
print((4).isEven) // trueprint((-3).isOdd) // trueprint((15).clamped(min: 0, max: 10)) // 10let row = 7let shade = if row.isEven { Color.lightGray } else { Color.white }Write a number literal in parentheses to use a member on it: (4).isEven.
A Float is a decimal number. The arithmetic operators work on it, and the
math functions take and return Floats.
| Member | Returns | |
|---|---|---|
formatted(decimals: Int) | String | The number as text, with exactly decimals digits after the point. |
clamped(min: Float, max: Float) | Float | The number, moved into the range from min to max. |
isNaN | Bool | Whether the value is NaN, “not a number” (like 0.0 / 0.0 or sqrt(-1.0)). |
isFinite | Bool | Whether the value is an ordinary number: not infinite and not NaN. |
let volume = 1.4print(volume.clamped(min: 0.0, max: 1.0)) // 1.0print(sqrt(-1.0).isNaN) // trueprint((1.0 / 0.0).isFinite) // falseNaN is never equal to anything, not even itself, so x == x is false
for a NaN: use isNaN to check for it.
formatted
Section titled “formatted”let third = 1.0 / 3.0print(third) // 0.3333333333333333print(third.formatted(decimals: 2)) // 0.33print((2.0).formatted(decimals: 3)) // 2.000print((0.666).formatted(decimals: 0)) // 1let area = pi * 0.5 * 0.5print("Area: {area.formatted(decimals: 4)} m²") // Area: 0.7854 m²- The number is rounded to
decimalsplaces. An exact half goes to the even digit, so(2.5).formatted(decimals: 0)is"2"and(3.5).formatted(decimals: 0)is"4". Because most decimal fractions can’t be stored exactly, a value like1.005may round down. decimalscan be from 0 to 15; values outside that range are treated as 0 or 15. With0, there’s no decimal point.- A result that would be a negative zero, like
-0.00, is shown as0.00. formattedis aFloatmethod only. For anInt, convert first:Float(count).formatted(decimals: 1).
String
Section titled “String”A String is text. Strings can’t be changed in place; methods return new
strings. Join strings with + or interpolation ("{a}{b}"); += appends to
a String variable. ==, !=, <, <=, > and >= compare strings;
< compares character codes, so "B" < "a".
“Character” below means a Unicode character (code point): "é".count is 1.
| Member | Returns | |
|---|---|---|
count | Int | Number of characters. |
isEmpty | Bool | Whether the text is "". |
contains(_ text: String) | Bool | Whether text appears in it. |
hasPrefix(_ text: String) | Bool | Whether it starts with text. |
hasSuffix(_ text: String) | Bool | Whether it ends with text. |
split(_ separator: String) | [String] | The parts between separators. |
lines() | [String] | The lines, without line endings. |
trim() | String | Without spaces and line breaks at both ends. |
uppercase() | String | In capital letters. |
lowercase() | String | In small letters. |
replace(_ text: String, with: String) | String | With every text replaced. |
substring(from: Int, to: Int) | String | The characters from index from up to to. |
find(_ text: String) | Int? | The index where text first appears. |
findLast(_ text: String) | Int? | The index where text last appears. |
character(at: Int) | String? | The character at an index, or nil. |
characters | [String] | Each character, as a list. |
words() | [String] | The words: the parts between spaces, tabs and line breaks. |
occurrences(of: String) | Int | How many times a text appears. |
trimStart() | String | Without spaces and line breaks at the start. |
trimEnd() | String | Without spaces and line breaks at the end. |
capitalized() | String | With the first letter of each word in capitals. |
reversed() | String | The characters in reverse order. |
repeated(_ times: Int) | String | The text repeated times times. |
padStart(_ length: Int, with: String = " ") | String | Filled at the start up to length characters. |
padEnd(_ length: Int, with: String = " ") | String | Filled at the end up to length characters. |
urlEncoded | String | Made safe to put in a URL. |
urlDecoded | String? | A urlEncoded text turned back. |
fileName | String | For a path: the last part. |
fileExtension | String | For a path: the file’s extension, without the dot. |
folder | String | For a path: the folder it’s in. |
Strings can’t be indexed with […]; use character(at:), substring,
split or find.
count and isEmpty
Section titled “count and isEmpty”let word = "héllo"print(word.count) // 5print(word.isEmpty) // falseprint("".isEmpty) // truecontains, hasPrefix and hasSuffix
Section titled “contains, hasPrefix and hasSuffix”let file = "main.tsl"print(file.contains("in")) // trueprint(file.hasPrefix("main")) // trueprint(file.hasSuffix(".tsl")) // trueprint(file.contains("Main")) // falseAll three are case-sensitive. Every string contains, starts and ends with
"".
let parts = "a,,b,".split(",")print(parts.count) // 4print(parts.joined(separator: "|")) // a||b|print("abc".split("").joined(separator: " ")) // a b c- Empty parts are kept: two separators in a row give an empty string between
them, and a separator at the start or end gives an empty first or last
part. Splitting
""gives[""]. - An empty separator splits the text into single characters.
let text = "one\ntwo\r\nthree\n"print(text.lines().count) // 3print(text.lines()[1]) // twoSplits at \n and at \r\n. A line break at the very end doesn’t start
another line, and "".lines() is empty.
print("[" + " hi \n".trim() + "]") // [hi]Removes spaces, tabs and line breaks from both ends, not from the middle.
uppercase and lowercase
Section titled “uppercase and lowercase”print("Hello".uppercase()) // HELLOprint("Hello".lowercase()) // helloprint("straße".uppercase()) // STRASSEThey follow Unicode rules, so a result can have a different length.
replace
Section titled “replace”print("a-b-a".replace("a", with: "x")) // x-b-xReplaces every occurrence, left to right. Replacing "" returns the text
unchanged.
substring
Section titled “substring”let word = "héllo"print(word.substring(from: 1, to: 3)) // élprint(word.substring(from: 2, to: 100)) // llo- Indexes count characters from 0.
fromis included,tois not. - Out-of-range indexes are clamped to the text, so
substringnever stops the program. Iftoisn’t afterfrom, the result is"".
let line = "error: missing value"print(line.find(":") ?? -1) // 5print(line.find("warning") ?? -1) // -1if let i = line.find(": ") { print(line.substring(from: i + 2, to: line.count)) // missing value}The character index (from 0) of the first place text appears, or nil if
it doesn’t. The index works with substring.
findLast
Section titled “findLast”let path = "docs/guide/intro.md"print(path.findLast("/") ?? -1) // 10print("a-b-a".findLast("a") ?? -1) // 4Like find, but the index of the last place text appears.
character(at:) and characters
Section titled “character(at:) and characters”let word = "héllo"print(word.character(at: 1) ?? "?") // éprint(word.character(at: 9) ?? "none") // noneprint(word.characters.count) // 5for letter in "abc".characters { print(letter)}character(at:) is nil for an index outside the text. characters is a
property (no parentheses): a list with each character as a one-character
string.
let words = " one two\tthree\n".words()print(words.count) // 3print(words.joined(separator: ",")) // one,two,threeSplits at runs of spaces, tabs and line breaks, and leaves out empty parts,
unlike split.
occurrences
Section titled “occurrences”print("a,b,,c".occurrences(of: ",")) // 3print("banana".occurrences(of: "ana")) // 1Counts from left to right without overlapping, so "ana" is found once in
"banana". Counting "" gives 0.
trimStart and trimEnd
Section titled “trimStart and trimEnd”print("[" + " hi ".trimStart() + "]") // [hi ]print("[" + " hi ".trimEnd() + "]") // [ hi]Like trim, but at one end only.
capitalized
Section titled “capitalized”print("hello big world".capitalized()) // Hello Big WorldMakes the first letter of each word a capital. The other letters are left as they are.
reversed and repeated
Section titled “reversed and repeated”print("héllo".reversed()) // olléhprint("ab".repeated(3)) // abababprint("=".repeated(10)) // ==========repeated with 0 or less gives "".
padStart and padEnd
Section titled “padStart and padEnd”print("7".padStart(3, with: "0")) // 007print("[" + "ab".padEnd(5) + "]") // [ab ]print("12345".padStart(3)) // 12345They add with (a space unless you say otherwise) until the text is
length characters long, which lines up columns of text and numbers. Text
that is already long enough is left as it is.
urlEncoded and urlDecoded
Section titled “urlEncoded and urlDecoded”let query = "fish & chips"let url = "https://example.com/search?q={query.urlEncoded}"print(url) // https://example.com/search?q=fish%20%26%20chipsprint("fish%20%26%20chips".urlDecoded ?? "") // fish & chipsprint("%zz".urlDecoded ?? "invalid") // invalidurlEncodedkeeps letters, digits and- _ . ~, and writes every other character as%and a code. Use it for a single part of a URL, like a query value or a path segment, not a whole URL.urlDecodedalso reads+as a space. It’snilif the text has a%that isn’t followed by a valid code.
Three properties take a file path apart:
let path = "/Users/ada/notes/todo.txt"print(path.fileName) // todo.txtprint(path.fileExtension) // txtprint(path.folder) // /Users/ada/notesfileNameis the part after the last/(or\), the whole text if there’s none.fileExtensionis the part of the file name after its last dot:"archive.tar.gz"gives"gz". It’s""if there’s no dot, and for names that start with a dot, like".profile".folderis the part before the last/(or\), or""if there’s none.
To put paths together, use joinPath.
A list [T] holds values of one type in order. Write one as [1, 2, 3]; an
empty list needs its type from context: var names: [String] = [].
| Member | Returns | |
|---|---|---|
count | Int | Number of items. |
isEmpty | Bool | Whether there are no items. |
first | T? | The first item, or nil if empty. |
last | T? | The last item, or nil if empty. |
append(_ item: T) | Adds item at the end. Changes the list. | |
insert(_ item: T, at: Int) | Inserts item before index at. Changes the list. | |
remove(at: Int) | T | Removes and returns the item at at. Changes the list. |
removeAll(where: fn(T) -> Bool) | Removes every item for which the block is true. Changes the list. | |
filter(_ keep: fn(T) -> Bool) | [T] | The items for which the block is true. |
first(where: fn(T) -> Bool) | T? | The first item for which the block is true. |
last(where: fn(T) -> Bool) | T? | The last item for which the block is true. |
contains(_ item: T) | Bool | Whether an item equals item. |
reversed() | [T] | The items in reverse order. |
map(_ transform: fn(T) -> U) | [U] | Each item transformed by the block. |
sorted(by: fn(T, T) -> Bool = …) | [T] | The items in order. |
joined(separator: String = "") | String | Only for [String]: the items joined into one string. |
indices | Range | The valid indexes, 0..count. |
index(of: T) | Int? | The index of the first item equal to the value. |
lastIndex(of: T) | Int? | The index of the last item equal to the value. |
firstIndex(where: fn(T) -> Bool) | Int? | The index of the first item for which the block is true. |
count(where: fn(T) -> Bool) | Int | How many items the block is true for. |
any(where: fn(T) -> Bool) | Bool | Whether the block is true for at least one item. |
all(where: fn(T) -> Bool) | Bool | Whether the block is true for every item. |
sum() | T | Only for [Int] and [Float]: the items added up. |
min() | T? | Only for [Int], [Float] and [String]: the smallest item. |
max() | T? | Only for [Int], [Float] and [String]: the largest item. |
prefix(_ count: Int) | [T] | The first count items. |
suffix(_ count: Int) | [T] | The last count items. |
dropFirst(_ count: Int = 1) | [T] | Without the first count items. |
dropLast(_ count: Int = 1) | [T] | Without the last count items. |
slice(from: Int, to: Int) | [T] | The items from index from up to to. |
unique() | [T] | Without repeated items. |
shuffled() | [T] | The items in random order. |
randomElement() | T? | A random item, or nil if empty. |
enumerated() | [(Int, T)] | Each item with its index: for (i, x) in list.enumerated(). |
appendAll(_ items: [T]) | Adds all of items at the end. Changes the list. | |
removeFirst() | T | Removes and returns the first item. Changes the list. |
removeLast() | T | Removes and returns the last item. Changes the list. |
Other list operations:
list[i]reads the item at indexi(from 0), andlist[i] = valuereplaces it. An index outside the list stops the program with an error.a + bis a new list with the items ofathenb;list += [x]appends.==and!=compare two lists item by item.for item in list { … }loops over the items. See Collections.
Methods that change the list (append, appendAll, insert, remove,
removeFirst, removeLast, removeAll) need a list you can change: a var, a state, or a bind parameter, or a
field of one. The others return a new list and leave the original alone.
first and last
Section titled “first and last”let scores = [40, 75, 90]print(scores.first ?? 0) // 40print(scores.last ?? 0) // 90print(scores.first(where: { s in s > 50 }) ?? 0) // 75print(scores.last(where: { s in s < 50 }) ?? 0) // 40Without parentheses, first and last are properties; with a where:
block, they search.
append, insert and remove
Section titled “append, insert and remove”var items = ["b", "c"]items.append("d")items.insert("a", at: 0)print(items.joined(separator: ",")) // a,b,c,dlet removed = items.remove(at: 1)print(removed) // bprint(items.joined(separator: ",")) // a,c,dinsert(at:) accepts indexes from 0 to count (the end). remove(at:)
needs an index of an existing item. Other indexes stop the program with an
error.
removeAll
Section titled “removeAll”var numbers = [1, 5, 2, 8, 3]numbers.removeAll(where: { n in n > 2 })print(numbers.count) // 2filter
Section titled “filter”let numbers = [1, 5, 2, 8, 3]let big = numbers.filter { n in n > 2 }print(big.count) // 3contains(_:)
Section titled “contains(_:)”let tags = ["red", "green"]print(tags.contains("green")) // trueprint(tags.contains("blue")) // falsereversed
Section titled “reversed”let letters = ["a", "b", "c"]print(letters.reversed().joined()) // cbalet numbers = [1, 2, 3]let labels = numbers.map { n in "#{n}" }print(labels.joined(separator: " ")) // #1 #2 #3The block’s result type decides the new list’s type; the block must return a value.
sorted
Section titled “sorted”let names = ["Linus", "ada", "Grace"]print(names.sorted().joined(separator: ",")) // Grace,Linus,adalet byLength = names.sorted(by: { a, b in a.count < b.count })print(byLength.joined(separator: ",")) // ada,Linus,Grace- Without
by:, lists ofInt,FloatandStringare sorted smallest first. Strings are compared by character code, so capital letters come before small ones. - Other lists (like a list of structs) need
by:: a block that returnstruewhen its first value should come before its second. - The sort is stable: items that are equal keep their order.
joined
Section titled “joined”let words = ["one", "two", "three"]print(words.joined(separator: ", ")) // one, two, threeprint(words.joined()) // onetwothreeOnly lists of String have joined. To join other values, map them to
strings first.
indices
Section titled “indices”let names = ["Ada", "Grace", "Linus"]print(names.indices) // 0..3for i in names.indices { print("{i + 1}. {names[i]}")}A range of every valid index, for when you need the index as well as the item.
index(of:) and lastIndex(of:)
Section titled “index(of:) and lastIndex(of:)”let nums = [4, 8, 15, 8]print(nums.index(of: 8) ?? -1) // 1print(nums.lastIndex(of: 8) ?? -1) // 3print(nums.index(of: 99) ?? -1) // -1nil if no item is equal to the value. They work for any list whose items
can be compared with ==, structs included.
firstIndex(where:)
Section titled “firstIndex(where:)”let nums = [4, 8, 15, 16]print(nums.firstIndex(where: { n in n > 10 }) ?? -1) // 2count(where:), any(where:) and all(where:)
Section titled “count(where:), any(where:) and all(where:)”let nums = [4, 8, 15, 16, 23]print(nums.count(where: { n in n.isEven })) // 3print(nums.any(where: { n in n > 20 })) // trueprint(nums.all(where: { n in n > 5 })) // falseFor an empty list, any is false and all is true.
sum, min and max
Section titled “sum, min and max”let nums = [4, 8, 15]print(nums.sum()) // 27print(nums.min() ?? 0) // 4print(nums.max() ?? 0) // 15print(["pear", "apple"].min() ?? "") // applesumis for lists ofIntorFloat; it’s0for an empty list.minandmaxare for lists ofInt,FloatorString, and arenilfor an empty list. Strings are compared as with<.- For other lists,
mapto numbers first, or usesorted(by:).
prefix, suffix, dropFirst and dropLast
Section titled “prefix, suffix, dropFirst and dropLast”let nums = [1, 2, 3, 4, 5]print(nums.prefix(2).sum()) // 3 (1 + 2)print(nums.suffix(2).sum()) // 9 (4 + 5)print(nums.dropFirst().count) // 4print(nums.dropLast(2).sum()) // 6 (1 + 2 + 3)print(nums.prefix(100).count) // 5A count bigger than the list is fine: you get the whole list (or an empty one). None of them stop the program.
let letters = ["a", "b", "c", "d", "e"]print(letters.slice(from: 1, to: 3).joined()) // bcprint(letters.slice(from: 3, to: 99).joined()) // deThe items from index from (included) up to to (not included), like
substring for strings. Indexes outside the list are clamped
to it, and if to isn’t after from, the result is empty.
unique
Section titled “unique”let tags = ["red", "blue", "red", "green", "blue"]print(tags.unique().joined(separator: ",")) // red,blue,greenKeeps the first of each group of equal items, in their order.
randomElement and shuffled
Section titled “randomElement and shuffled”let names = ["Ada", "Grace", "Linus"]let winner = names.randomElement() ?? "nobody"let order = names.shuffled()randomElement is nil for an empty list. See also
Random numbers.
appendAll, removeFirst and removeLast
Section titled “appendAll, removeFirst and removeLast”var queue = ["a", "b"]queue.appendAll(["c", "d"])print(queue.removeFirst()) // aprint(queue.removeLast()) // dprint(queue.joined(separator: ",")) // b,cqueue.appendAll(other) does the same as queue += other. removeFirst
and removeLast need a list with at least one item; on an empty list they
stop the program with an error, like remove(at:).
Dictionary
Section titled “Dictionary”A dictionary [K: V] maps keys to values. Write one as ["a": 1, "b": 2];
an empty one needs its type from context: var ages: [String: Int] = [:].
Keys can be Int, String, Bool, an enum whose cases have no values, or
a tuple or struct made of those.
| Member | Returns | |
|---|---|---|
count | Int | Number of entries. |
isEmpty | Bool | Whether there are no entries. |
keys | [K] | The keys. |
values | [V] | The values. |
contains(key: K) | Bool | Whether there is an entry for the key. |
removeValue(forKey: K) | V? | Removes the entry and returns its value, or nil if there was none. Changes the dictionary. |
Reading and writing entries:
dict[key]is the value forkey, as an optional (V?):nilif there’s no entry.dict[key] = valueadds or replaces an entry;dict[key] = nilremoves it.==and!=compare two dictionaries’ entries, in any order.
var ages = ["Ada": 36, "Linus": 21]ages["Grace"] = 85ages["Linus"] = 22print(ages["Linus"] ?? 0) // 22print(ages["Bob"] ?? 0) // 0print(ages.contains(key: "Ada")) // trueprint(ages.removeValue(forKey: "Ada") ?? 0) // 36ages["Grace"] = nilprint(ages.keys.joined(separator: ",")) // Linuskeys and values are in the order the entries were first added.
Replacing a value keeps its place; removing an entry and adding it again
moves it to the end.
A for loop over a dictionary gives each entry as a (key, value) tuple,
in order:
let stock = ["apples": 3, "pears": 0]for (fruit, count) in stock { print("{fruit}: {count}")}zip(_ a: [A], _ b: [B]) -> [(A, B)]Pairs up the items at the same positions of two lists, as long as the shorter list:
let names = ["Ada", "Alan"]let scores = [90, 85, 70]for (name, score) in zip(names, scores) { print("{name}: {score}") // Ada: 90, then Alan: 85}A set Set<T> holds values without order and without repeats. Write one
as a list where a set is expected (let seen: Set<Int> = [1, 2]), make one
from a list with Set(list), or an empty one with Set<Int>(). Items can
be the same types as dictionary keys. for x in set goes through the items
in the order they were first added. Two sets are == when they have the
same items. JSON writes a set as an array. See
Collections.
| Member | Returns | |
|---|---|---|
count | Int | Number of items. |
isEmpty | Bool | Whether there are no items. |
values | [T] | The items, in the order they were first added. |
contains(_ item: T) | Bool | Whether the item is in the set. |
insert(_ item: T) | Adds the item (nothing happens if it’s there). Changes the set. | |
remove(_ item: T) | Removes the item (nothing happens if it isn’t there). Changes the set. | |
union(_ other: Set<T>) | Set<T> | The items in either set. |
intersection(_ other: Set<T>) | Set<T> | The items in both sets. |
subtracting(_ other: Set<T>) | Set<T> | The items that aren’t in other. |
isSubset(of: Set<T>) | Bool | Whether every item is also in the other set. |
isSuperset(of: Set<T>) | Bool | Whether every item of the other set is in this one. |
isDisjoint(with: Set<T>) | Bool | Whether no item is in both. |
filter(_ keep: fn(T) -> Bool) | Set<T> | The items for which keep is true. |
sorted(by: fn(T, T) -> Bool = …) | [T] | The items as a sorted list; without by:, only for Int, Float and String items. |
Tuples
Section titled “Tuples”A tuple groups 2 to 8 values: its type is written (Int, String), a value
(1, "one"), and its items are .0, .1 and so on. let (a, b) = pair
takes one apart. Tuples compare with ==, can be dictionary keys and set
items when their items can, and JSON writes them as arrays. See
Collections.
Reasons for failures
Section titled “Reasons for failures”The functions in this section return a Result or an
Error? instead of an optional, so a failure comes with a message that says
why. They’re built in; you don’t import anything.
Result and Error
Section titled “Result and Error”struct Error { message: String }
enum Result<T> { ok(value: T) failure(error: Error)}Error("reason") makes an error; the label is optional. Result has these
helpers:
| Method | Returns |
|---|---|
value() | T?: the value, or nil on failure |
error() | Error?: the error, or nil on success |
isOk() | Bool |
valueOr(_ fallback: T) | the value, or fallback on failure |
try unwraps a Result (or checks an Error?) and passes a failure on to
the caller. See Errors.
readText
Section titled “readText”readText(_ path: String) -> Result<String>Like readFile, but a failure says why, for example
can't read notes.txt: there's no such file or folder.
writeText
Section titled “writeText”writeText(path: String, text: String) -> Error?Like writeFile; nil means it worked.
appendText
Section titled “appendText”appendText(path: String, text: String) -> Error?Like appendFile; nil means it worked.
readBytes
Section titled “readBytes”readBytes(_ path: String) -> Result<Data>Like readData, with a reason on failure.
writeBytes
Section titled “writeBytes”writeBytes(path: String, data: Data) -> Error?Like writeData; nil means it worked.
parseInt
Section titled “parseInt”parseInt(_ text: String) -> Result<Int>Parses a whole number, ignoring spaces around it. On failure the message
is "abc" isn't a whole number.
parseFloat
Section titled “parseFloat”parseFloat(_ text: String) -> Result<Float>Parses a number. On failure the message is "abc" isn't a number.
decodeJson
Section titled “decodeJson”decodeJson<T>(_ text: String) -> Result<T>Like fromJson, with the type given by the result you assign
to. The reason is either this isn't valid JSON: … or the JSON doesn't have the shape of this type.
struct Point { x: Int, y: Int }
let r: Result<Point> = decodeJson("\{\"x\": 1}")if let e = r.error() { print(e.message) // the JSON doesn't have the shape of this type}decodeBinary
Section titled “decodeBinary”decodeBinary<T>(_ data: Data) -> Result<T>Like fromBinary, with a reason on failure.
Runtime errors
Section titled “Runtime errors”A few mistakes can only be found while the program runs. They stop the program with a message and the source location:
error: index 5 is out of range for a list of 3 items --> main.tsl:4:11| Error | Caused by |
|---|---|
index … is out of range | list[i], remove(at:) or insert(at:) with an index outside the list, removeFirst() or removeLast() on an empty list, or data[i] outside the data. |
integer overflow | Int arithmetic whose result is too big for an Int. |
division by zero | Int division or remainder (/, %) by 0. |
Float arithmetic never stops the program: dividing by 0.0 gives an
infinite value (printed as inf).