---
title: Collections
description: Arrays and maps in kScript, the reducer methods that iterate them, in-place sorting with comparators, and the two limits that keep them safe.
---

<div class="flex gap-3 mb-6">
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 text-blue-600 text-sm font-medium">
    Core Concept
  </span>
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-gray-100 text-gray-600 text-sm font-medium">
    5 min read
  </span>
</div>

## What you get

kScript gives you two collection types and a set of functional methods to work them:

- **Arrays** are ordered lists. Build them with `[...]` literals and work them with `push`, `get`, `set`, `length()`, reducers, and mutators.
- **Maps** are key/value stores. Create one with `{}` and use `set`, `get`, and `size()`.
- **Arrow lambdas** (`(x) => ...`) feed the array reducers (`map`, `filter`, `reduce`, `find`, `some`, `every`, `forEach`) and the optional comparator of `sort`.

Together these replace most hand-written loops. The reducers are the idiomatic way to transform a list, and they come with iteration guards built in, so a runaway transform stops loudly instead of hanging the chart. For a deeper tour of lambdas and the reducer family, see [Lambdas & Reducers](lambdas-and-reducers.md).

## Quick reference

```javascript
var xs = [1, 2, 3]          // array literal
xs.push(4)                  // append
xs.get(0)                   // read by index
xs.set(0, 99)               // write by index
xs.length()                 // count

var m = {}                  // map literal
m.set("k", 10)              // write
m.get("k")                  // read
m.size()                    // entry count

xs.map((v) => v * 2)        // transform each element
xs.filter((v) => v > 2)     // keep matching elements
xs.reduce((sum, v) => sum + v, 0)  // fold to one value
xs.find((v) => v > 2)       // first match, || na
xs.some((v) => v > 2)       // true if any match
xs.every((v) => v > 0)      // true if all match
xs.forEach((v) => { ... })  // run a side effect per element

xs.avg()                    // numeric reducers
xs.sum()
xs.median()
xs.stdev()                  // population standard deviation
xs.variance()               // population variance
xs.min()
xs.max()
xs.range()

xs.reverse()                // reverse in place and return the array
xs.sort()                   // numeric ascending sort, in place, returns the array
xs.sort((a, b) => b - a)    // custom order: comparator returns a number
xs.first()                  // first element
xs.last()                   // last element
xs.shift()                  // remove and return first element
xs.unshift(0)               // prepend
```
## A worked example

This script exercises the whole surface in one pass: it builds an array from the current bar's prices, mutates it, runs every reducer over it, stuffs results into a map, then sums everything into one plottable score. It is dense on purpose, as a map of what fits together.

```javascript title="Collections happy path"
//@version=2
define(title="Collections Happy Path", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

var prices = [trade.open, trade.high, trade.low]
prices.push(trade.close)
prices.set(0, prices.get(0) + barIndex / 10)

var transformed = prices.map((value, index) => value + index)
var filtered = transformed.filter((value) => value > trade.low)
var reduced = filtered.reduce((total, value) => total + value, 0)
var firstLarge = transformed.find((value) => value > trade.close)
var hasHigh = transformed.some((value) => value > trade.high)
var allPositive = transformed.every((value) => value > 0)
var sideEffectTotal = 0
transformed.forEach((value) => {
  sideEffectTotal += value / 1000
})

var levels = {}
levels.set("reduced", reduced)
levels.set("first", nz(firstLarge, trade.close))
levels.set("flags", (hasHigh ? 1 : 0) + (allPositive ? 1 : 0))

var score = levels.get("reduced") + levels.get("first") + levels.get("flags") + prices.length() + levels.size() + sideEffectTotal

plotLine(value=score, colors=["#2563eb"], width=2, label=["Collection score"], desc=["arrays maps lambdas and reducers"])
```

A few idioms worth lifting out of that wall:

- `prices.set(0, prices.get(0) + barIndex / 10)` reads, modifies, and writes one slot in place.
- `nz(firstLarge, trade.close)` matters because `find` returns `na` when nothing matches; `nz` supplies a fallback so the arithmetic stays finite. See [na and Color](na-and-scalar-types.md).
- The `(value, index)` lambda in `map` shows the optional index parameter; drop it when you don't need it.

What you'll see: one line whose value moves every bar, since `prices` is rebuilt from live OHLC and `barIndex` feeds into it.

## Numeric reducers and manipulators

Numeric arrays expose `avg`, `sum`, `median`, `stdev`, `variance`, `min`, `max`, and `range`. The statistics use population math: `variance()` divides by `n`, and `stdev()` is the square root of that population variance. Empty-array reducers return `na`. Arrays also expose `reverse`, `first`, `last`, `shift`, and `unshift` for common positional edits.

```javascript title="Array reducers and manipulators"
//@version=2
define(title="Array Reducers Manipulators", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

var values = [1, 2, 3, 4]
var reducerScore = values.avg() + values.sum() + values.median() + values.stdev() + values.variance() + values.min() + values.max() + values.range()

var reversed = values.reverse()
var shifted = reversed.shift()
reversed.unshift(trade.close)
var manipulatorScore = shifted + reversed.first() + reversed.last() + reversed.length()

var empty = []
var emptyScore = (isna(empty.avg()) ? 1 : 0) + (isna(empty.sum()) ? 1 : 0) + (isna(empty.median()) ? 1 : 0) + (isna(empty.stdev()) ? 1 : 0) + (isna(empty.variance()) ? 1 : 0) + (isna(empty.min()) ? 1 : 0) + (isna(empty.max()) ? 1 : 0) + (isna(empty.range()) ? 1 : 0)

plotLine(value=reducerScore + barIndex / 1000, colors=["#2563eb"], width=2, label=["Reducers"], desc=["array numeric reducers use population statistics"])
plotLine(value=manipulatorScore, colors=["#16a34a"], width=2, label=["Manipulators"], desc=["reverse first last shift and unshift mutate or read arrays"])
plotLine(value=emptyScore + barIndex / 1000, colors=["#dc2626"], width=2, label=["Empty"], desc=["empty array reducers return na"])
```

## Sorting

`sort()` orders an array **in place** and returns it. With no argument the order is **numeric ascending**: `[30, 4, 100, 25]` comes out `[4, 25, 30, 100]`. That is a true numeric sort, not JavaScript's default lexicographic one, which would put `100` first.

The examples in this section are self-checking: each counts failed expectations and detonates a deliberate runtime error if any assertion misses, so a script that runs at all is proof of the values in its comments.

```javascript title="Argless sort is numeric ascending"
//@version=3
define(title="Sort Argless Ascending", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

// Argless sort() is NUMERIC ascending (a JS-default lexicographic sort would
// yield [100, 25, 30, 4]; kScript must yield [4, 25, 30, 100]) and sorts in place.
var xs = [30, 4, 100, 25]
xs.sort()

var failures = 0
failures += xs.get(0) == 4 ? 0 : 1
failures += xs.get(1) == 25 ? 0 : 1
failures += xs.get(2) == 30 ? 0 : 1
failures += xs.get(3) == 100 ? 0 : 1
failures += xs.length() == 4 ? 0 : 1

if (failures > 0) {
  var bomb = [1]
  bomb.removeAt(9)
}

plotLine(value=xs.first() + barIndex / 1000, colors=["#2563eb"], width=2, label=["Sorted min"], desc=["first element after argless numeric ascending sort"])
```

Sorting is not v3-gated: the identical body under `//@version=2` compiles and runs with the identical result.

### Comparators

`sort` takes at most one argument, an arrow lambda `(a, b) => number` in the JavaScript convention: a **negative** return puts `a` first, a **positive** return puts `b` first. So `(a, b) => a - b` is ascending and `(a, b) => b - a` is descending. The comparator is an ordinary lambda (syntax and closures in [Lambdas & Reducers](lambdas-and-reducers.md)); what is special is the contract on its return value, enforced at compile time and again at run time (see the rejection list below).

```javascript title="Descending comparator, and sorting a copy"
//@version=3
define(title="Sort Comparator Descending", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

// The JS-idiom comparator: (a, b) => number. Negative = a first, positive = b first.
// (a, b) => b - a is descending.
var xs = [5, 1, 4, 2]
xs.sort((a, b) => b - a)

var failures = 0
failures += xs.get(0) == 5 ? 0 : 1
failures += xs.get(1) == 4 ? 0 : 1
failures += xs.get(2) == 2 ? 0 : 1
failures += xs.get(3) == 1 ? 0 : 1

// copy-then-sort idiom (sort is in-place; slice() makes the copy)
var ys = [3, 1, 2]
var sortedCopy = ys.slice(0, ys.length()).sort((a, b) => a - b)
failures += ys.get(0) == 3 ? 0 : 1
failures += sortedCopy.get(0) == 1 ? 0 : 1
failures += sortedCopy.get(2) == 3 ? 0 : 1

if (failures > 0) {
  var bomb = [1]
  bomb.removeAt(9)
}

plotLine(value=xs.first() + trade.close / 1000, colors=["#2563eb"], width=2, label=["Desc first"], desc=["largest element after descending comparator sort"])
```

Two idioms worth lifting out:

- `(a, b) => b - a` turns `[5, 1, 4, 2]` into `[5, 4, 2, 1]`.
- `sort` mutates the receiver, so sorting a **copy** is spelled `ys.slice(0, ys.length()).sort(...)`: the `slice` makes the snapshot, and the original `ys` still starts with `3` afterwards.

### Structs sort by any field, stably

To order a struct array, return the numeric difference of the field you care about. The sort is **stable**: elements that compare equal keep their original relative order. Below, the two `price=1` rows keep their original sequence (`seq` 2 then 4) and the two `price=2` rows keep 1 then 3.

```javascript title="Stable sort of structs by a field"
//@version=3
define(title="Sort Structs Stable", position="offchart", axis=true)

type Level {
  price: number
  seq: number
}

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

// Comparator sort over a struct array: order by any field.
// Duplicate keys prove stability (equal elements keep their original relative order).
var levels = [Level.new(price=2, seq=1), Level.new(price=1, seq=2), Level.new(price=2, seq=3), Level.new(price=1, seq=4)]
levels.sort((a, b) => a.price - b.price)

var failures = 0
failures += levels.get(0).price == 1 ? 0 : 1
failures += levels.get(1).price == 1 ? 0 : 1
failures += levels.get(2).price == 2 ? 0 : 1
failures += levels.get(3).price == 2 ? 0 : 1
// stability: within equal prices, seq order 2,4 and 1,3 is preserved
failures += levels.get(0).seq == 2 ? 0 : 1
failures += levels.get(1).seq == 4 ? 0 : 1
failures += levels.get(2).seq == 1 ? 0 : 1
failures += levels.get(3).seq == 3 ? 0 : 1

if (failures > 0) {
  var bomb = [1]
  bomb.removeAt(9)
}

plotLine(value=levels.get(0).seq + trade.close / 1000, colors=["#16a34a"], width=2, label=["Stable seq"], desc=["seq of first struct after stable sort by price"])
```

### In place, plus a chainable return

The receiver itself is reordered, and the return value is there so calls chain: `ys.sort().first()` is the minimum, and `zs.sort((a, b) => b - a).first()` is the maximum. The return is **not** an alias, though. kScript array assignment always copies (a plain `var alias = xs` followed by `alias.push(99)` leaves `xs` at its old length), so `var ret = xs.sort()` snapshots an independent copy: `ret.push(99)` grows `ret` to 4 while `xs` stays at 3.

```javascript title="In place, chaining, and the copied return"
//@version=3
define(title="Sort In Place Returns Array", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

var failures = 0

// sort() mutates the RECEIVER in place; the return value lets calls chain.
var xs = [3, 1, 2]
var ret = xs.sort()
failures += xs.get(0) == 1 ? 0 : 1
failures += xs.get(1) == 2 ? 0 : 1
failures += xs.get(2) == 3 ? 0 : 1

// kScript array ASSIGNMENT copies (true of any array assignment, not sort-specific):
// the assigned return is an independent snapshot, so mutating it leaves xs alone.
ret.push(99)
failures += ret.length() == 4 ? 0 : 1
failures += xs.length() == 3 ? 0 : 1

// returning the array is what makes chaining work
var ys = [9, 7, 8]
var lowest = ys.sort().first()
failures += lowest == 7 ? 0 : 1
failures += ys.get(0) == 7 ? 0 : 1

// comparator form is equally in-place and chainable
var zs = [1, 3, 2]
var top = zs.sort((a, b) => b - a).first()
failures += top == 3 ? 0 : 1
failures += zs.get(0) == 3 ? 0 : 1
failures += zs.get(2) == 1 ? 0 : 1

if (failures > 0) {
  var bomb = [1]
  bomb.removeAt(9)
}

plotLine(value=lowest + top + trade.close / 1000, colors=["#2563eb"], width=2, label=["Chain score"], desc=["chained sort().first() results plus close drift"])
```

### Strings have no built-in ordering

`<` and `>` compile on strings but are not an ordering: `"a" < "b"`, `"b" < "a"`, `"a" > "b"`, and `"b" > "a"` all evaluate `false`. A comparator like `(a, b) => a < b ? -1 : 1` therefore returns `1` for every pair and scrambles the array instead of sorting it: `["ccc", "a", "bb"]` comes back `["bb", "a", "ccc"]`. Order strings by deriving a **numeric** key instead, here by length:

```javascript title="Sort strings by a numeric key"
//@version=3
define(title="Sort Strings Comparator", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

// Non-numeric elements sort fine when the comparator DERIVES a number.
// (There is no lexicographic ordering: '<' on strings is not an ordering in
// kScript, so order strings by a numeric key such as length().)
var ss = ["ccc", "a", "bb"]
ss.sort((a, b) => a.length() - b.length())

var failures = 0
failures += ss.get(0) == "a" ? 0 : 1
failures += ss.get(1) == "bb" ? 0 : 1
failures += ss.get(2) == "ccc" ? 0 : 1

if (failures > 0) {
  var bomb = [1]
  bomb.removeAt(9)
}

plotLine(value=ss.get(0).length() + barIndex / 100, colors=["#2563eb"], width=2, label=["Shortest len"], desc=["length of shortest string after keyed sort"])
```

### na elements

Argless sort treats `na` as equal to everything: an array containing `na` does not throw, but how much of it actually gets sorted depends on where the `na` sits. `[3, na, 1]` comes back exactly `[3, na, 1]`, silently unsorted, yet the same values as `[3, 1, na]` come back `[1, 3, na]`, and `[5, 2, na, 4, 1]` comes back fully sorted as `[1, 2, 4, 5, na]`. Treat the result as **unreliable**, not merely unsorted.

```javascript title="Argless sort with na is position-dependent"
//@version=3
define(title="Sort Argless With Na", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

// Argless sort with an na element does NOT throw, but every comparison against
// na reports "equal", so the outcome depends on where the na sits:
// [3, na, 1] comes back UNCHANGED, yet [3, 1, na] comes back [1, 3, na].
var xs = [3, na, 1]
xs.sort()

var failures = 0
failures += xs.length() == 3 ? 0 : 1
failures += xs.get(0) == 3 ? 0 : 1
failures += isna(xs.get(1)) ? 0 : 1
failures += xs.get(2) == 1 ? 0 : 1
var naCount = xs.reduce((acc, v) => acc + (isna(v) ? 1 : 0), 0)
failures += naCount == 1 ? 0 : 1

var ys = [3, 1, na]
ys.sort()
failures += ys.get(0) == 1 ? 0 : 1
failures += ys.get(1) == 3 ? 0 : 1
failures += isna(ys.get(2)) ? 0 : 1

if (failures > 0) {
  var bomb = [1]
  bomb.removeAt(9)
}

plotLine(value=xs.get(0) + trade.close / 1000, colors=["#d97706"], width=2, label=["Unsorted head"], desc=["argless sort with na is position dependent not a no-op"])
```

{% hint style="warning" %}
**A comparator does not get the same pass.** With `[1, na, 2]`, the comparator `(a, b) => a - b` produces `na`, and the run stops with `Array method 'sort' comparator must return a finite number`. Strip `na` values (for example with `filter`) before a comparator sort.

{% endhint %}

### What sort rejects

The compile-time contract: the argument must be a lambda, at most one of them, returning a number. The classic JavaScript habit of returning a boolean is the first thing it catches:

```javascript title="Boolean comparator boundary"
//@version=3
define(title="Sort Reject Bool Comparator", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

// EXPECTED COMPILE FAILURE: the JS habit (a, b) => a > b returns a boolean.
// kScript rejects it at compile time; the comparator must return a number.
var xs = [3, 1, 2]
xs.sort((a, b) => a > b)

plotLine(value=trade.close, colors=["#dc2626"], width=2, label=["Close"], desc=["anchor for bool comparator rejection"])
```

This fails to compile with `Array method 'sort' comparator must return a number, got boolean at 9:1`. Return `a - b`, not `a > b`. The full rejection list, with observed errors:

- A comparator returning a string, `xs.sort((a, b) => "x")`: fails to compile with `Array method 'sort' comparator must return a number, got string at 8:1`.
- A non-lambda argument, `xs.sort(1)`: `Array method 'sort' expects a lambda callback at 8:9`.
- Two arguments, `xs.sort((a, b) => a - b, 1)`: `Array method 'sort' expects 0-1 arguments, got 2 at 8:1`.
- A comparator producing a non-finite number at run time, `xs.sort((a, b) => (b - a) / 0)`: compiles, then the run stops with `Array method 'sort' comparator must return a finite number`.
- A mixed return type that evades the static check, `xs.sort((a, b) => a < b ? -1 : "x")` (static type `number|string`): the same runtime guard fires, `Array method 'sort' comparator must return a finite number`.

## The two limits

Collections are bounded so a script can't exhaust memory or silently mis-type a list.

### Size ceiling: 100,000 elements

A single collection tops out at 100,000 elements. Push past it and the run stops with `Collection size limit exceeded at 9:5 (max 100000)`, pointing at the offending line. In practice you only hit this by accident, usually an unbounded `push` in a loop.

```javascript title="Collection size boundary"
//@version=2
define(title="Collection Size Boundary", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

if (isLastBar) {
  var values = []
  for (var n = 0; n < 100001; n += 1) {
    values.push(n)
  }
}

plotLine(value=trade.close, colors=["#dc2626"], width=2, label=["Close"], desc=["anchor for collection size boundary"])
```

### Arrays are typed

An array has one element type, inferred from how you first fill it. Mixing types is rejected at compile time. Here a numeric array gets a string pushed onto it:

```javascript title="Array type mismatch"
//@version=2
define(title="Array Type Mismatch Boundary", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
var values = [1, 2, 3]
values.push("not a number")

plotLine(value=trade.close, colors=["#dc2626"], width=2, label=["Close"], desc=["anchor for array type mismatch boundary"])
```

This fails to compile with `Array element type mismatch: expected number, got string at 6:13`. The takeaway: keep an array homogeneous. If you genuinely need to carry mixed data per row, reach for a [struct](user-defined-types.md) instead of forcing it into one array.
