Create: Computing AgesBrass Docs
Libraries

string

Text: cut, search, change case, format numbers, split and trim. Every text also has these as methods: s:upper().

All computers

The string library works on text: item names, lines typed by a player, lines of a file, messages from another computer. Every function returns a new text: a text itself never changes.

Method syntax. Every text value also has these functions as methods: name:upper() is string.upper(name), and line:sub(1, 3) is string.sub(line, 1, 3). It works on any expression that gives a text, read():trim() included. A text written directly in the code needs parentheses: ("iron"):upper(), because "iron":upper() does not compile.

Brass
local item = "minecraft:iron_ingot"
local colon = item:find(":")
print(item:sub(colon + 1))
print(#item, item:upper())
print(("brass"):rep(2, "+"))
Screen
iron_ingot
20  MINECRAFT:IRON_INGOT
brass+brass

Positions start at 1 for the first character. A negative position counts from the end: -1 is the last character, -2 the one before. Every character counts as one, accented letters included. A text cannot be indexed like a table: s[1] stops with attempt to index a string value with a number key, use s:sub(1, 1).

Operators. .. joins two texts and turns numbers into text by itself ("x" .. 3 is "x3"), but nil, true and tables are refused (attempt to concatenate a nil value): use tostring. #s is the length. == compares the characters, and < compares texts by character codes: "Zinc" < "apple" (capitals come first) and "10" < "9" (digit by digit). Text is never turned into a number by arithmetic: "10" + 1 is an error, use tonumber.

Writing texts. Between "..." or '...', with the escapes \n (new line), \t (tab), \\, \", \', \r, \0 and \xNN (a character by its hexadecimal code). A long text can span several lines between [[ and ]], where nothing is escaped.

Limits

A text holds at most 65536 characters. Going over, with .., string.rep, string.format or table.concat, stops the program with string too long. Every function of this page that takes a text also accepts a number, converted like tostring does it: string.len(1234) is 4.

Functions
string.len(s)The number of characters of a text.
string.upper(s)Converts the text to capitals.
string.lower(s)Converts the text to small letters.
string.sub(s, i [, j])The part of the text from position i to position j, both included.
string.find(s, text [, init])Position of a text inside another, or nil.
string.starts(s, prefix)True if the text starts with prefix.
string.ends(s, suffix)True if the text ends with suffix.
string.rep(s, n [, sep])The text repeated n times, with an optional separator between the copies.
string.reverse(s)The text backwards.
string.format(fmt, ...)Builds a text from a model and values: "%d items", "%.2f", "%5s"...
string.split(s [, sep])Cuts the text into a list of pieces, at each separator.
string.trim(s)Removes the spaces, tabs and line breaks at both ends of the text.
string.byte(s [, i])The code of one character of the text (the first by default).
string.char(...)Text made of character codes.

Length and case

#

string.len(s)

→ number

The number of characters of a text.

Parameters
s string
a text
Returns
number
its number of characters

#s gives the same and is shorter to write. A common use is centring a title on the screen:

Brass
print(string.len("Andesite"), #"Andesite", #"")
local title = "IRON FARM"
local width = term.get_size().w
print(string.rep(" ", math.floor((width - #title) / 2)) .. title)
Screen
8   8   0
                     IRON FARM

See also string.sub()

#

string.upper(s)

→ string

Converts the text to capitals.

Parameters
s string
a text
Returns
string
the same text in capitals

Letters with accents follow: é becomes É. Other characters stay as they are.

Brass
local name = "brass casing"
print(name:upper())
print(string.upper("Gate 3: open"))
Screen
BRASS CASING
GATE 3: OPEN

See also string.lower()

#

string.lower(s)

→ string

Converts the text to small letters.

Parameters
s string
a text
Returns
string
the same text in small letters

Comparing texts is exact: "Yes" ~= "yes". Bring what a player typed to small letters before comparing it, so that YES, Yes and yes all work:

Brass
local function is_yes(answer)
  local a = answer:trim():lower()
  return a == "yes" or a == "y"
end
print(is_yes("YES"), is_yes("  y "), is_yes("no"))
Screen
true    true    false

In a program: write("Open the gate? ") if is_yes(read()) then ... end.

See also string.upper() string.trim()

Cutting and searching

#

string.sub(s, i [, j])

→ string

The part of the text from position i to position j, both included.

Parameters
s string
a text
i number
the first position kept (negative counts from the end)
j number optional
the last position kept, included (-1, the end, when left out)
Returns
string
the characters from i to j
Brass
local s = "Mechanical Press"
print(s:sub(1, 10))
print(s:sub(12))
print(s:sub(-5))
print(s:sub(-5, -3))
Screen
Mechanical
Press
Press
Pre

It never fails on positions out of the text: a start before the first character starts at the first one, an end past the last character stops at the last one, and a start after the end gives "". Positions must be whole numbers (bad argument #2 to 'sub' (number has no integer representation)).

Brass
local s = "Saw"
print("[" .. s:sub(0, 2) .. "]", "[" .. s:sub(2, 99) .. "]", "[" .. s:sub(5) .. "]")
Screen
[Sa]    [aw]    []

To fit a name in a column, cut it and mark the cut:

Brass
local function fit(name, width)
  if #name > width then
    return name:sub(1, width - 1) .. "~"
  end
  return name
end
print(fit("Andesite Alloy", 10))
print(fit("Zinc", 10))
Screen
Andesite ~
Zinc

See also string.find() string.split()

#

string.find(s, text [, init])

→ number|nil⚙ cost 1 per 16 characters

Position of a text inside another, or nil.

Parameters
s string
the text to search in
text string
the text to look for, taken as it is
init number optional
the position where the search starts (negative counts from the end)
Returns
number|nil
the position where text first appears, or nil

The search is plain: every character means itself. Lua's patterns do not exist in Brass, so ., %, [ or ( have no special meaning (in Lua, ("v1.2"):find(".") finds any character, here it finds the dot). There is no string.match, gmatch or gsub either: find, sub and split do the same jobs. A fourth argument (the "plain" flag of Lua) is accepted and ignored.

Only the start position comes back. The match ends at start + #text - 1. The search is case sensitive: lower both texts first to ignore case.

Brass
print(("v1.2"):find("."))
print(("Iron Ingot"):find("Ingot"), ("Iron Ingot"):find("ingot"))
local id = "create:brass_ingot"
local colon = id:find(":")
print(id:sub(1, colon - 1), id:sub(colon + 1))
Screen
3
6   nil
create  brass_ingot

init starts the search further on, which lets you find every occurrence. A start past the end gives nil:

Brass
local log = "jam at press; ok; jam at saw; ok"
local count, from = 0, 1
while true do
  local at = log:find("jam", from)
  if at == nil then
    break
  end
  count = count + 1
  from = at + 1
end
print("jams: " .. count)
Screen
jams: 2

See also string.sub() string.starts() string.split()

#

string.starts(s, prefix)

→ boolean

True if the text starts with prefix.

Parameters
s string
a text
prefix string
the beginning to look for
Returns
boolean
true if s starts with prefix

A Brass extension (Lua has none). Exact, case sensitive, and an empty prefix is always found. Handy to sort item ids by mod, or commands by their first word:

Brass
local items = {"create:brass_ingot", "minecraft:iron_ingot", "create:zinc_ingot"}
for _, id in ipairs(items) do
  if id:starts("create:") then
    print(id)
  end
end
Screen
create:brass_ingot
create:zinc_ingot

See also string.ends() string.find()

#

string.ends(s, suffix)

→ boolean

True if the text ends with suffix.

Parameters
s string
a text
suffix string
the end to look for
Returns
boolean
true if s ends with suffix

A Brass extension, like string.starts. Use it for families of items or for file names:

Brass
local ids = {"minecraft:iron_ingot", "minecraft:iron_nugget", "create:zinc_ingot"}
local ingots = 0
for _, id in ipairs(ids) do
  if id:ends("_ingot") then
    ingots = ingots + 1
  end
end
print(ingots .. " kinds of ingot")
print(("report.log"):ends(".log"), ("report.log"):ends(""))
Screen
2 kinds of ingot
true    true

See also string.starts()

Building text

#

string.rep(s, n [, sep])

→ string⚙ cost 1 per 8 copies

The text repeated n times, with an optional separator between the copies.

Parameters
s string
the text to repeat
n number
how many times (0 or less gives "")
sep string optional
a text put between the copies
Returns
string
the copies, joined
Brass
print(string.rep("=", 20))
print(("ab"):rep(3, ", "))
print("[" .. ("x"):rep(0) .. "]")
Screen
====================
ab, ab, ab
[]

n must be a whole number. A result over 65536 characters stops with string too long.

A progress bar made of characters: the filled part, then the empty part, always the same width.

Brass
local function bar(value, max, width)
  local filled = math.min(width, math.round(value / max * width))
  return "[" .. string.rep("#", filled) .. string.rep("-", width - filled) .. "]"
end
print(bar(750, 1000, 20) .. " 75%")
print(bar(0, 1000, 20))
print(bar(1200, 1000, 20) .. " full")
Screen
[###############-----] 75%
[--------------------]
[####################] full

See also string.format()

#

string.reverse(s)

→ string

The text backwards.

Parameters
s string
a text
Returns
string
the same characters, last first

A train line written as one letter per station is visited backwards on the return trip:

Brass
local route = "MSFD"   -- Mine, Smeltery, Factory, Depot
print("Outward: " .. route)
print("Return:  " .. route:reverse())
Screen
Outward: MSFD
Return:  DFSM

See also string.sub()

#

string.format(fmt, ...)

→ string

Builds a text from a model and values: "%d items", "%.2f", "%5s"...

Parameters
fmt string
the model, with a % code where each value goes
values any optional
one value for each code, in order
Returns
string
the model with the values written in

Each % code of the model is replaced by the next value, written the way the code says:

codetakeswritesexample
%d or %ia whole numberthe number42
%fa numberwith 6 decimals, or .N decimals3.141593, %.2f → 3.14
%sany valuethe value, like tostringIron, true, nil
%x, %Xa whole numberin hexadecimalff, FF
%%nothinga % sign%

Between the % and the letter, in this order, you can add:

  • flags: - pads on the right (the value goes left), 0 pads numbers with zeros, + always shows the sign of a number, a space puts a space where a positive number has no sign.
  • a width, one or two digits: the least number of characters, padded with spaces on the left. %5d writes 42 as 42.
  • a precision, a dot and digits: the number of decimals for %f, the most characters kept for %s (%.3s cuts Andesite to And). %d and %x ignore it.
Brass
print(string.format("%d items in %d chests", 1250, 20))
print(string.format("Speed: %.1f RPM", 63.27))
print(string.format("[%5d] [%-5d] [%05d]", 42, 42, 42))
print(string.format("%+d / %+d", 5, -5))
print(string.format("%x %X 100%%", 255, 3054))
print(("%.3s"):format("Andesite"))
Screen
1250 items in 20 chests
Speed: 63.3 RPM
[   42] [42   ] [00042]
+5 / -5
ff BEE 100%
And

Columns. A width per column, - for the texts so they line up on the left, nothing for the numbers so they line up on the right:

Brass
local stock = {
  {name = "Iron Ingot", count = 1250},
  {name = "Copper Ingot", count = 87},
  {name = "Zinc Nugget", count = 4},
  {name = "Brass Ingot", count = 320},
}
print(string.format("%-14s %6s", "Item", "Count"))
print(string.rep("-", 21))
for _, row in ipairs(stock) do
  print(string.format("%-14s %6d", row.name, row.count))
end
Screen
Item            Count
---------------------
Iron Ingot       1250
Copper Ingot       87
Zinc Nugget         4
Brass Ingot       320

A clock. os.day_time() counts the ticks of the Minecraft day, 0 being 6:00 in the morning. %02d writes two digits, with a leading zero:

Brass
local function clock(ticks)
  local t = (ticks + 6000) % 24000
  local h = math.floor(t / 1000)
  local m = math.floor(t % 1000 * 60 / 1000)
  return string.format("%02d:%02d", h, m)
end
print(clock(os.day_time()))
print(clock(0), clock(6500), clock(18000))
local seconds = 754
print(string.format("%d:%02d left", seconds // 60, seconds % 60))
Screen
12:00
06:00   12:30   00:00
12:34 left

When it fails. string.format is strict, and the program stops:

messagecause
bad argument #2 to 'format' (number has no integer representation)%d or %x with a number that has decimals: round it first (math.floor, math.round)
bad argument #2 to 'format' (number expected, got string)%d, %f or %x with a text, even "42": convert it with tonumber
bad argument #3 to 'format' (no value)fewer values than codes
invalid conversion '%e' to 'format'a code that does not exist in Brass (%c, %e, %g, %o, %q...), a width of three digits, a precision over 99, a % alone at the end (write %%), or flags that do not fit the code: - or 0 without a width, + with %s or %x

Extra values are ignored. With %f, infinity and "not a number" are written Infinity and NaN (%s writes inf and nan), and with %x a negative number comes out as 16 hexadecimal digits (-1 gives ffffffffffffffff).

See also tostring() string.rep()

Splitting and cleaning

#

string.split(s [, sep])

→ table⚙ cost 1 per 16 characters

Cuts the text into a list of pieces, at each separator.

Parameters
s string
the text to cut
sep string optional
the separator, taken as it is (a single space " " when left out)
Returns
table
the pieces, a list of texts

A Brass extension that replaces Lua's patterns in most jobs. The separator can be several characters long (", "). The pieces keep everything between two separators, so:

  • two separators in a row give an empty piece, and a separator at the start or the end gives an empty piece there;
  • a text without the separator gives a list with the whole text, and an empty text gives {""};
  • an empty separator "" cuts between every character.
Brass
local parts = ("iron,gold,,copper"):split(",")
print(#parts, parts[3] == "", parts[4])
local letters = ("ABC"):split("")
print(#letters, letters[1], letters[3])
print(#("a b"):split(), #(""):split(","))
Screen
4   true    copper
3   A   C
2   1

With table.concat, it replaces one piece of text by another everywhere:

Brass
print(table.concat(("iron_ingot"):split("_"), " "))
Screen
iron ingot

Reading commands typed by the player. Without a separator, split cuts at every single space, so two spaces in a row give an empty word: trim the line and skip the empty words.

Brass
local function words(line)
  local out = {}
  for _, w in ipairs(line:trim():split(" ")) do
    if w ~= "" then
      table.insert(out, w)
    end
  end
  return out
end

local function run(line)
  local w = words(line)
  local command = w[1]
  if command == nil then
    return
  end
  if command == "speed" then
    local rpm = tonumber(w[2])
    if rpm == nil then
      print("usage: speed <rpm>")
    else
      print("speed set to " .. rpm .. " RPM")
    end
  elseif command == "open" or command == "close" then
    print(command .. " gate " .. (w[2] or "1"))
  else
    print("unknown command: " .. command)
  end
end

run("speed   128")
run("  open 3 ")
run("speed fast")
run("")
run("jump")
Screen
speed set to 128 RPM
open gate 3
usage: speed <rpm>
unknown command: jump

In a real program the lines come from the keyboard: while true do write("> ") run(read()) end.

See also string.trim() table.concat()

#

string.trim(s)

→ string

Removes the spaces, tabs and line breaks at both ends of the text.

Parameters
s string
a text
Returns
string
the text without spaces at its ends

The spaces inside stay. Trim what a player typed and the lines of a file (a file written on another system may end its lines with an invisible \r).

Brass
local typed = "   Iron Ingot  \n"
print("[" .. typed:trim() .. "]")
Screen
[Iron Ingot]

See also string.split()

Character codes

Every character has a number, its code: A is 65, a is 97, 0 is 48, é is 233 (the Unicode code). Codes help to turn letters into numbers and back.

#

string.byte(s [, i])

→ number|nil

The code of one character of the text (the first by default).

Parameters
s string
a text
i number optional
the position of the character (1 when left out, negative counts from the end)
Returns
number|nil
the code of that character, nil past the end

Lua can return several codes at once. Brass returns one: call it once per position.

Brass
print(string.byte("A"), ("abc"):byte(2), ("abc"):byte(-1))
print(("abc"):byte(10))
Screen
65  98  99
nil

Shelves of a storage wall named A1, B4, C12: the letter gives the column.

Brass
local function cell(code)
  local column = code:upper():byte(1) - string.byte("A") + 1
  local row = tonumber(code:sub(2))
  return {column = column, row = row}
end
local c = cell("c12")
print(c.column, c.row)
Screen
3   12

See also string.char()

#

string.char(...)

→ string

Text made of character codes.

Parameters
codes number
one or more character codes, from 0 to 65535
Returns
string
the text made of these characters

string.char() with no code gives "". A code outside 0 to 65535 stops with bad argument #1 to 'char' (value out of range), a code with decimals with bad argument #1 to 'char' (number has no integer representation) (the number is the position of the bad code). Codes below 32 are control characters: the screen shows them as ?.

Brass
print(string.char(72, 105))
for i = 1, 5 do
  write(string.char(64 + i) .. " ")
end
print()
print(string.char(("A"):byte() + 1))
Screen
Hi
A B C D E
B

See also string.byte()

Common patterns

A stock panel. string.format for the columns, string.rep for the bars, colours from term:

Brass
local vaults = {
  {name = "Iron Ingot", count = 3120, capacity = 4096},
  {name = "Copper Ingot", count = 860, capacity = 4096},
  {name = "Andesite Alloy", count = 3950, capacity = 4096},
  {name = "Brass Ingot", count = 140, capacity = 2048},
}
term.clear()
term.set_cursor(1, 1)
term.set_fg(term.colors.yellow)
print(string.format("%-15s %6s  %s", "ITEM", "COUNT", "LEVEL"))
for _, v in ipairs(vaults) do
  local ratio = v.count / v.capacity
  local filled = math.round(ratio * 20)
  term.set_fg(term.colors.white)
  write(string.format("%-15s %6d  ", v.name, v.count))
  if ratio >= 0.9 then
    term.set_fg(term.colors.red)      -- nearly full: the input will stop
  elseif ratio < 0.1 then
    term.set_fg(term.colors.orange)   -- nearly empty
  else
    term.set_fg(term.colors.lime)
  end
  print(string.rep("#", filled) .. string.rep(".", 20 - filled) .. string.format(" %3d%%", math.round(ratio * 100)))
end
Screen
Screen

**Reading key=value lines.** split cuts the file into lines, find and sub cut each line at the =, trim cleans both sides, tonumber turns numbers back into numbers. The pcall example of Global functions builds a complete settings loader on this.

Brass
local text = "speed = 64\nside = back\n# a comment\n"
local config = {}
for _, line in ipairs(text:split("\n")) do
  local eq = line:find("=")
  if eq and not line:starts("#") then
    config[line:sub(1, eq - 1):trim()] = line:sub(eq + 1):trim()
  end
end
print(config.speed, config.side, tonumber(config.speed) * 2)
Screen
64  back    128