An index lets you find records instantly by key and iterate over ranges of keys in order. It supports multi-part keys: you can sort by a primary key and break ties with a secondary key. Name the key fields in square brackets after the record type, as in index<Elm[nr, -key]>: field sorts ascending, -field descending.
Two keyed collections over the SAME element type are two ROUTES to one set of records, not two collections. Give records to either one and both see them; write through one and the other reads the change; remove through one and it is gone from both. That is the point of declaring a second one — a second way in, by a different key — and loft says so if a literal fills both (linked-group-double-fill), because then one record set ends up holding everything they were given.
struct Elm {
nr: integer,
key: text,
value: integer
}
struct Db {
map: index < Elm[nr, -key] >
}
fn main() {
Adding and Looking Up Records
Fill the index just like a vector. Elements are automatically kept in key order. This index is sorted first by nr (ascending), then by key (descending) for ties.
db = Db {map: [
Elm {nr: 101, key: "One", value: 1 },
Elm {nr: 92, key: "Two", value: 2 },
Elm {nr: 83, key: "Three", value: 3 },
Elm {nr: 83, key: "Four", value: 4 },
Elm {nr: 83, key: "Five", value: 5 },
Elm {nr: 63, key: "Six", value: 6 }
] };
Provide all key fields together inside brackets to find a record. Supply fewer fields to match all records that share a prefix.
assert(db.map[101, "One"].value == 1, "Key lookup");
assert(!db.map[12, ""], "Missing key returns null");
assert(!db.map[83, "One"], "Wrong secondary key returns null");
Fewer fields than the key has: every record sharing that prefix.
prefix_sum = 0;
for r in db.map[83] {
prefix_sum += r.value
}
assert(prefix_sum == 12, "the three nr=83 records: {prefix_sum}");
Iterating in Order
A for loop visits all elements in key order.
total = 0;
for r in db.map {
total += r.value;
}
assert(total == 21, "Sum of all values: {total}");
Range Queries
Provide a range in the first key position to visit only the matching slice. [83..92, "Two"] means: nr from 83 up to (not including) 92, and key from "Two" down (since the key is sorted descending, "Two" is the start of that descending segment).
sum = 0;
for v in db.map[83..92, "Two"] {
sum = sum * 10 + v.value;
}
Elements matched: (83, Three, 3), (83, Four, 4), (83, Five, 5)
assert(sum == 345, "Range iteration result: {sum}");
Loop Helpers: #first and #count
r#first is true for the very first element visited. r#count is a running counter starting at 0. Note: #index is a compile error on index collections — use #count instead.
first_nr = 0;
count_total = 0;
for r in db.map {
if r#first {
first_nr = r.nr
}
count_total = r#count
}
assert(first_nr == 63, "First element by key: {first_nr}");
assert(count_total == 5, "Total elements: {count_total}");
Removing by Key
Assigning null to an index subscript removes the element with that exact key combination. Removing a key that is not present is a safe no-op.
db.map[92, "Two"] = null;
assert(!db.map[92, "Two"], "element (92, Two) was removed");
assert(db.map[101, "One"].value == 1, "element (101, One) still present");
db.map[999, "Z"] = null;
no-op; does not panic
total2 = 0;
for r in db.map {
total2 += r.value
}
assert(total2 == 19, "Sum after key removal: {total2}");
}
Run it yourself
This page is a loft program. It runs here, in your browser — press Run, then ask it something.
Stop on a line
// Copyright (c) 2024-2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// @NAME: Index
// @TITLE: Fast lookup and range queries over records
// An 'index' lets you find records instantly by key and iterate over ranges of keys in order.
// It supports multi-part keys: you can sort by a primary key and break ties with a secondary key.
// Name the key fields in square brackets after the record type, as in 'index<Elm[nr, -key]>':
// 'field' sorts ascending, '-field' descending.
//
// Two keyed collections over the SAME element type are two ROUTES to one set of records, not
// two collections. Give records to either one and both see them; write through one and the
// other reads the change; remove through one and it is gone from both. That is the point of
// declaring a second one — a second way in, by a different key — and loft says so if a
// literal fills both ('linked-group-double-fill'), because then one record set ends up
// holding everything they were given.
struct Elm {
nr: integer,
key: text,
value: integer
}
struct Db {
map: index < Elm[nr, -key] >
}
// @FTR-038 — an `index` on a multi-part key: lookup by key, ordered iteration, and a
// second key breaking ties in the other direction
fn main() {
// ## Adding and Looking Up Records
// Fill the index just like a vector. Elements are automatically kept in key order.
// This index is sorted first by 'nr' (ascending), then by 'key' (descending) for ties.
db = Db {map: [
Elm {nr: 101, key: "One", value: 1 },
Elm {nr: 92, key: "Two", value: 2 },
Elm {nr: 83, key: "Three", value: 3 },
Elm {nr: 83, key: "Four", value: 4 },
Elm {nr: 83, key: "Five", value: 5 },
Elm {nr: 63, key: "Six", value: 6 }
] };
// Provide all key fields together inside brackets to find a record.
// Supply fewer fields to match all records that share a prefix.
assert(db.map[101, "One"].value == 1, "Key lookup");
assert(!db.map[12, ""], "Missing key returns null");
assert(!db.map[83, "One"], "Wrong secondary key returns null");
// Fewer fields than the key has: every record sharing that prefix.
prefix_sum = 0;
for r in db.map[83] {
prefix_sum += r.value
}
assert(prefix_sum == 12, "the three nr=83 records: {prefix_sum}");
// ## Iterating in Order
// A 'for' loop visits all elements in key order.
total = 0;
for r in db.map {
total += r.value;
}
assert(total == 21, "Sum of all values: {total}");
// ## Range Queries
// Provide a range in the first key position to visit only the matching slice.
// '[83..92, "Two"]' means: nr from 83 up to (not including) 92, and key from "Two" down
// (since the key is sorted descending, "Two" is the start of that descending segment).
sum = 0;
for v in db.map[83..92, "Two"] {
sum = sum * 10 + v.value;
}
// Elements matched: (83, Three, 3), (83, Four, 4), (83, Five, 5)
assert(sum == 345, "Range iteration result: {sum}");
// ## Loop Helpers: #first and #count
// 'r#first' is true for the very first element visited.
// 'r#count' is a running counter starting at 0.
// Note: '#index' is a compile error on index collections — use '#count' instead.
first_nr = 0;
count_total = 0;
for r in db.map {
if r#first {
first_nr = r.nr
}
count_total = r#count
}
assert(first_nr == 63, "First element by key: {first_nr}");
assert(count_total == 5, "Total elements: {count_total}");
// ## Removing by Key
// Assigning null to an index subscript removes the element with that exact key combination.
// Removing a key that is not present is a safe no-op.
db.map[92, "Two"] = null;
assert(!db.map[92, "Two"], "element (92, Two) was removed");
assert(db.map[101, "One"].value == 1, "element (101, One) still present");
db.map[999, "Z"] = null;
// no-op; does not panic
total2 = 0;
for r in db.map {
total2 += r.value
}
assert(total2 == 19, "Sum after key removal: {total2}");
}