Arrays, lists and maps

Store many values in arrays, lists and maps, walk them with FOR and FOREACH, and read constant tables with DATA and READ.

In one line#

aukimi has three containers for many values, plus a way to write constant tables in the program itself:

ArrayListMap
DeclareDIM a(10) or a AS integer[]NewList l.i()NewMap m.i()
Find a value byits position: a(3)the current element: l()a text key: m("gold")
Grow.insert(v), .length = nAddElement(l())write a new key
WalkFOR i = 0 TO a.lengthFOREACH l()FOREACH m()

Assigning a container to another variable does not copy it. After b = a, both names point to the same array, list or map, and a change through one shows through the other. Copy with CopyArray, CopyList or CopyMap.

Records, which group named fields, are on their own page: Records and methods.

DIM#

DIM <name>(<last index> [, <last index>…]) [AS <type>] [= [<value>, …]]
DIM <name>[<last index> [, <last index>…]] [AS <type>] [= [<value>, …]]
DIM scores(4)
scores(0) = 10
scores(4) = 50
Console(Str(scores.length))   ' 4, the last index
FOR i = 0 TO scores.length
  Console(Str(scores(i)))   ' 10, 0, 0, 0, 50
NEXT

The number is the last index, not the count. DIM scores(4) makes five cells, numbered 0 to 4. This is the AGK, DarkBASIC and PureBasic rule, and not the C or JavaScript one.

  • Every cell starts at 0, or at "" when the name ends in $.
  • A bound can be any expression: DIM grid(width, height).
  • With AS <type>, every write is converted to that type. AS integer drops decimals, AS uint8 wraps at 256. With AS a record type, every cell is a record. See Variables and types.
  • DIM on a name that already exists makes a new array: the old values are lost. To resize and keep the values, assign .length (below).
  • A DIM at the top of the program is visible inside every FUNCTION. A DIM inside a FUNCTION belongs to that function.
DIM names$(2)
names$(1) = "Ada"
DIM ages(2) AS integer
ages(0) = 3.7
DIM bytes(1) AS uint8
bytes(0) = 300
Console(names$(1) + " " + Str(ages(0)) + " " + Str(bytes(0)))   ' Ada 3 44

⚠️ GLOBAL DIM and LOCAL DIM do not work reliably. With round brackets, the next use of the array is refused as an unknown command. Inside a FUNCTION, GLOBAL DIM does not make the array visible outside it. Write a plain DIM at the top of the program instead.

Several dimensions, three spellings#

DIM grid(2, 3)
grid(1, 2) = 7
Console(Str(grid[1, 2]))   ' 7
Console(Str(grid[1][2]))   ' 7
Console(Str(grid.length))   ' 2, last index of the first dimension
Console(Str(grid[0].length))   ' 3, last index of the second dimension

a(i, j), a[i, j] and a[i][j] read and write the same cell. Round brackets are the classic spelling, square brackets the AGK one.

⚠️ There is no bounds check. Reading past the last index gives no value at all (Str shows undefined). Writing past it silently grows the array, and the cells in between have no value either. Check your index against .length.

Typed arrays: AS <type>[]#

<name> AS <type>[]
<name> AS <type>[<last index> [, <last index>…]]
values AS integer[] = [1, 2, 3]
Console(Str(values.length))   ' 2
more AS integer[5] = [9]
Console(Str(more[0]) + " " + Str(more[5]))   ' 9 0
empty AS string[]
Console(Str(empty.length))   ' -1, no cell at all

This is the same array as DIM … AS, written the AGK way. With [] and no bound, the array starts empty. FUNCTION parameters and results use this form too, see Functions.

Initialisers and list assignment#

= [ … ] fills the cells from index 0. It works on one-dimensional arrays only. A list longer than the array grows it.

Later in the program, assigning a list writes from index 0 and keeps the other cells. It grows the array when needed and never shrinks it:

DIM a(4)
a = [7, 8]
Console(Str(a(0)) + Str(a(1)) + " " + Str(a.length))   ' 78 4

Array members#

DIM a(2) = [30, 10, 20]
a.insert(5)
a.insert(99, 0)
Console(Str(a.length))   ' 4
a.remove()
a.remove(0)
a.sort()
Console(Str(a(0)) + " " + Str(a(1)) + " " + Str(a(2)))   ' 10 20 30
Console(Str(a.find(20)))   ' 1
Console(Str(a.find(42)))   ' -1
MemberWhat it does
a.lengththe last index: the number of cells minus one, -1 when empty
a.length = nresizes so that n is the last index; kept cells keep their values, new ones get the default
a.insert(v)adds v at the end
a.insert(v, i)inserts v at index i and shifts the rest
a.remove()removes the last cell
a.remove(i)removes cell i and shifts the rest
a.find(v)the index of the first cell equal to v, or -1
a.sort()sorts in place, smallest first
UNDIM a[]empties the array (.length becomes -1)

.length is the last index. A loop over every cell is FOR i = 0 TO a.length, and the number of cells is a.length + 1. This is the AGK rule. Coming from JavaScript, it is the trap.

  • .insert, .remove and .length = n are statements, on their own line. They do not return a value.
  • .find uses strict equality: a.find("2") does not find the number 2. It scans the whole array, which does not need to be sorted.
  • .sort() compares strings character by character, by code: every capital letter comes before every small one ("Zebra" before "apple"). An array of records is sorted on its first field.
  • The members work on arrays inside records: bag.names.insert("Key").
  • UNDIM takes square brackets. UNDIM a() compiles into a broken program:
DIM a(2)
UNDIM a()

Catalogue commands that take an array. SortArray(a, options) sorts with options (#AK_ORDER_HIGH_TO_LOW for largest first), RandomizeArray(a) shuffles, CopyArray(source, destination) copies, CompareArray(a, b) returns 1 when the two hold the same values, and ArraySize(a) returns the last index like .length. See the command reference.

DIM a(2) = [1, 2, 3]
DIM b(0)
CopyArray(a, b)
b(0) = 99
Console(Str(a(0)) + " " + Str(b.length))   ' 1 2, a is untouched, b was resized

Lists#

NewList <name>.<type>()
NewList <name>()

A list holds values in order and has one current element. Most list work moves that cursor, then reads or writes the element under it.

NewList scores.i()
AddElement(scores())
scores() = 10
AddElement(scores())
scores() = 30
FirstElement(scores())
AddElement(scores())
scores() = 20
FOREACH scores()
  Console(Str(ListIndex(scores())) + ": " + Str(scores()))   ' 0: 10, 1: 20, 2: 30
NEXT scores()
Console(Str(ListSize(scores())))   ' 3
  • scores() is the current element. Read it, or assign it: scores() = 10. With no current element, the program stops with "BASIC list has no current element".
  • AddElement inserts after the current element and makes the new one current. That is why 20 lands between 10 and 30 above.
  • The type follows the dot: a short suffix (.i, .f, .d, .s, .q…), a full name (.integer, .string, .float64), or a record type (NewList foes.Foe(), see below). Without a type, the list holds integers.
  • Values are converted to the list's type. .f is a 32-bit float, so 0.1 reads back as 0.10000000149011612. Use .d for full precision.
  • A record list gives its fields through the current element: foes().hp = 30.
  • A name cannot be both a list and a map.

⚠️ The $ sigil does not declare a list. NewList names$() is refused. Write the type after a dot:

NewList names$()
NewList names.s()
AddElement(names())
names() = "Ada"
Console(names())   ' Ada

⚠️ += on the current element fails when the program runs, and so does INC scores(). Write scores() = scores() + 5.

FOREACH#

FOREACH <list>()
  <statements>
NEXT [<list>()]

FOREACH walks a list from the first element to the last. Inside the loop, list() is the element of this pass, and you can change it.

NewList a.i()
FOR k = 1 TO 5
  AddElement(a())
  a() = k
NEXT
FOREACH a()
  IF Mod(a(), 2) = 0 THEN DeleteElement(a())
NEXT
FOREACH a()
  Console(Str(a()))   ' 1, 3, 5
NEXT
  • Deleting the current element inside FOREACH is safe: the loop goes on with the next one.
  • EXIT and CONTINUE work as in any loop.
  • The parentheses are required: FOREACH a and NEXT a are compile errors. NEXT alone is fine.
  • FOREACH works on maps (below) and on list or map fields of a record: FOREACH bag.items().
  • FOREACH does not work on arrays: use FOR i = 0 TO a.length.
  • FOREACH moves the list's one cursor. A FOREACH nested over the same list, or a call that moves the cursor, changes where the outer loop is.
  • PureBasic's ForEach … Next is the same thing: keywords ignore case.

The 25 list calls#

Every call takes the list with its parentheses, scores(). Positions count from 0.

CallWhat it doesReturns
AddElement(l())adds an element after the current one (first place when there is none) and makes it current1
InsertElement(l())adds an element before the current one and makes it current1
DeleteElement(l() [, flags])deletes the current element; the one before becomes current. With flags = 1, deleting the first element makes the new first one current1 if an element is current afterwards, else 0
ClearList(l())removes every elementnothing
FreeList(l())removes every element and frees the list; ListSize then returns -1nothing
ListSize(l())the number of elementsa count, -1 after FreeList
ListIndex(l())the position of the current elementa position, -1 when none
ResetList(l())moves the cursor before the first elementnothing
NextElement(l())moves to the next element1, or 0 past the end
PreviousElement(l())moves to the previous element1, or 0 before the start
FirstElement(l())moves to the first element1, or 0 when empty
LastElement(l())moves to the last element1, or 0 when empty
SelectElement(l(), i)moves to position i1, or 0 when out of range
ChangeCurrentElement(l(), i)the same as SelectElement1 or 0
CopyList(source(), destination())replaces the destination with a copy of the source1, or 0 when the types differ
CompareList(a(), b() [, flags])compares two lists element by element; #AK_MATCH_FOLD_CASE ignores case1 if equal, else 0
MergeLists(source(), destination() [, where])moves every element of the source into the destination; where is #AK_LIST_LAST (default), #AK_LIST_FIRST, or #AK_LIST_BEFORE / #AK_LIST_AFTER the destination's current elementnothing
SplitList(source(), destination() [, keep])moves the current element and everything after it into the destination; with keep = 1, the current element staysnothing
SortList(l(), options [, start, end])sorts a list of numbers or strings; options #AK_ORDER_LOW_TO_HIGH, #AK_ORDER_HIGH_TO_LOW, #AK_ORDER_FOLD_CASEnothing
SortStructuredList(l(), "field", options [, start, end])sorts a record list on one field, named as textnothing
RandomizeList(l() [, start, end])shuffles the list, or the rangenothing
PushListPosition(l())remembers the current elementnothing
PopListPosition(l())returns to the remembered elementnothing
MoveElement(l(), where [, i])moves the current element: #AK_LIST_FIRST, #AK_LIST_LAST, or #AK_LIST_BEFORE / #AK_LIST_AFTER the element at position inothing
SwapElements(l(), i, j)swaps the elements at positions i and jnothing

A wrong number of arguments is a compile error, for example "SortList(...) expects 2-4 argument(s)". The sort options are required: SortList(l()) alone is refused.

TYPE Foe
  name AS string
  hp AS integer
ENDTYPE
NewList foes.Foe()
AddElement(foes())
foes().name = "orc"
foes().hp = 30
AddElement(foes())
foes().name = "elf"
foes().hp = 10
SortStructuredList(foes(), "hp", #AK_ORDER_LOW_TO_HIGH)
FOREACH foes()
  Console(foes().name + " " + Str(foes().hp))   ' elf 10, orc 30
NEXT

Coming from PureBasic. The names and most arguments match. Three calls take a position where PureBasic takes an element pointer, because aukimi has no element pointers: SwapElements, ChangeCurrentElement and the last argument of MoveElement. SortStructuredList takes the field name as text, not an offset and a type. AddElement returns 1, not the address of the element. foes()\hp is written foes().hp.

Maps#

NewMap <name>.<type>([<slots>])

A map stores values under text keys. It also has a current element.

NewMap stock.i()
stock("apple") = 3
stock("pear") = 5
stock("apple") += 2
Console(Str(stock("apple")))   ' 5
Console(Str(MapSize(stock())))   ' 2
IF FindMapElement(stock(), "plum") = 0 THEN Console("no plum")
FOREACH stock()
  Console(MapKey(stock()) + " = " + Str(stock()))   ' apple = 5, pear = 5
NEXT
  • m("key") = v creates the entry or replaces its value. += and the other compound assignments work on an entry.
  • Reading m("key") creates the entry when it is missing, with the default value. To test without creating, use FindMapElement.
  • Every access makes that entry the current one. m() is the current entry: read it, or assign it.
  • Keys are text. A number is turned into text: m(1) and m("1") are the same entry. Keys are case-sensitive: "A" and "a" are two entries.
  • FOREACH and NextMapElement visit entries in the order they were created.
  • <slots> is a size hint kept for PureBasic programs. It must be a positive whole number and changes nothing else.
  • A record map gives its fields through the entry: bag("sword").qty = 1.

The 13 map calls#

CallWhat it doesReturns
AddMapElement(m(), key [, flags])creates the entry, or resets an existing one to the default value, and makes it current. flags must be 01
FindMapElement(m(), key)makes the entry current if it exists. Creates nothing1 if found, else 0
DeleteMapElement(m() [, key])deletes the entry, or the current one1 if something was deleted, else 0
ClearMap(m())removes every entrynothing
FreeMap(m())removes every entry and frees the map; MapSize then returns -1nothing
MapSize(m())the number of entriesa count, -1 after FreeMap
MapKey(m())the key of the current entrytext, "" when none
ResetMap(m())moves the cursor before the first entrynothing
NextMapElement(m())moves to the next entry1, or 0 past the end
CopyMap(source(), destination())replaces the destination with a copy of the source1, or 0 when the types differ
CompareMap(a(), b() [, flags])compares keys and values; #AK_MATCH_FOLD_CASE ignores case1 if equal, else 0
PushMapPosition(m())remembers the current entrynothing
PopMapPosition(m())returns to the remembered entrynothing
NewMap m.s()
m("x") = "1"
m("y") = "2"
ResetMap(m())
WHILE NextMapElement(m())
  Console(MapKey(m()) + "=" + m())   ' x=1, y=2
WEND

INC and DEC#

INC <target> [, <amount>]
DEC <target> [, <amount>]
n = 5
INC n
INC n, 10
DEC n, 3
Console(Str(n))   ' 13
DIM a(2)
INC a[1], 4
Console(Str(a[1]))   ' 4
b AS uint8 = 255
INC b
Console(Str(b))   ' 0, a uint8 wraps
  • INC adds 1, or the amount. DEC subtracts.
  • The target is a variable, an array cell in square brackets a[i], a field p.x, or a mix: ps[i].x.
  • On a typed variable, the result wraps like any write to that type.
  • Under OPTION EXPLICIT, the target must be declared first.

⚠️ Use square brackets for an array cell. INC a(1) compiles into a broken program, and so do INC ps(1).x, INC l() and INC m("k"). Write INC a[1], INC ps[1].x, l() = l() + 1, m("k") += 1.

DIM a(2)
INC a(1)

Swapping two values#

There is no SWAP a, b statement. The line is refused:

a = 1
b = 2
SWAP a, b

Swap() with parentheses exists, but it does something else: it ends a frame, like Sync(). See The game loop and tasks.

Swap two variables or two array cells through a third one. Swap two list elements with SwapElements(l(), i, j).

a = 1
b = 2
temp = a
a = b
b = temp
Console(Str(a) + " " + Str(b))   ' 2 1

DATA, READ and RESTORE#

DATASECTION
  [<label>:]
  DATA[.<type>] <literal> [, <literal>…]
ENDDATASECTION

READ[.<type>] <target>
RESTORE [<label>]

A DATASECTION holds constant values written in the program. READ takes them one by one, in order. RESTORE moves the reading position.

RESTORE Levels
READ count
FOR i = 1 TO count
  READ name$
  READ.f speed#
  Console(name$ + " " + Str(speed#))   ' Forest 1.5, Cave 2.25
NEXT
DATASECTION
  Levels:
  DATA 2
  DATA.s "Forest"
  DATA.f 1.5
  DATA.s "Cave"
  DATA.f 2.25
ENDDATASECTION
SpellingAccepted forms
OpeningDATASECTION or DATA SECTION
ClosingENDDATASECTION, END DATASECTION or END DATA SECTION

DATA types. Each DATA line has one type, written after a dot:

SuffixHolds
none, .i (also .l, .q, .w, .b, .a, .u, .c)numbers
.f, .dnumbers
.sstrings in double quotes
  • DATA is only allowed inside a DATASECTION. The section is not run: it can sit at the top or at the bottom of the program.
  • Values must be literals. Numbers can be negative, decimal, %1010, 0c12, 0x10 or 1.25e2. TRUE and FALSE count as 1 and 0. DATA 1 + 2 is a compile error, and so is a string in a DATA line without .s.
  • Values are read in the order written, across lines and labels.

READ. The kind to read comes from READ.<type>, or else from the target's sigil: $ reads a string, # a decimal number, no sigil a whole number.

  • The target can be a variable, an array cell (a(1) or a[1]), or a field (p.x).
  • Reading a whole number from a decimal value drops the decimals: 2.9 gives 2.
  • Reading a string where the next value is a number, or the reverse, stops the program: "BASIC READ type does not match DATA".
  • Reading past the last value stops the program: "BASIC READ reached the end of DATA".

RESTORE. RESTORE alone goes back to the first value. RESTORE Label goes to the first value after that label. Labels ignore case, and an unknown label is a compile error. A data label is not a GOSUB label: you cannot call it.

Coming from another BASIC. AGK has no DATA. DarkBASIC reads untyped DATA lines with no section. aukimi follows PureBasic's DataSection, with a typed DATA line, and adds two shortcuts: an untyped DATA holds numbers, and READ can take its type from the sigil.