> ## Documentation Index
> Fetch the complete documentation index at: https://docs.penbox.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Loops & arrays

> Iterating, transforming, filtering, and building collections in penscript

penscript provides a full set of operators for working with collections — iterating, transforming, filtering, querying, and building arrays. These are used to generate dynamic form fields, validate uploaded documents, compute aggregates, and build conditional structures based on variable-length data.

## Context Variables

When iterating over an array (with `:map`, `:every`, `:some`, `:find`, or `:filter`), penscript makes the following context variables available inside the loop body:

| Variable      | Description                             |
| ------------- | --------------------------------------- |
| `{@item}`     | The current item value                  |
| `{@index}`    | Current index (0, 1, 2...)              |
| `{@position}` | Current position (1, 2, 3...)           |
| `{@first}`    | `true` if the current item is the first |
| `{@last}`     | `true` if the current item is the last  |

Access nested properties of the current item with dot notation: `{@item.name}`, `{@item.role}`, `{@item.amount}`.

## Normalizing to Arrays: `:array`

Wraps a value in an array if it isn't one already. Useful for ensuring consistent array input when a value might be a single item or an array.

```json theme={null}
{ ":array": 1 }
// Result: [1]

{ ":array": "{data.tags}" }
// Scope: { "data": { "tags": [1, 2, 3] } }
// Result: [1, 2, 3]

{ ":array": "{data.tags}" }
// Scope: { "data": { "tags": "urgent" } }
// Result: ["urgent"]
```

### Generating Arrays with `:fill`

Combine `:array` with `:fill` to generate arrays of a specific length. Each element is created from the `:fill` template, with context variables available.

```json theme={null}
{
  ":array": 3,
  ":fill": { "id": "item_{@index}" }
}

// Result: [{ "id": "item_0" }, { "id": "item_1" }, { "id": "item_2" }]
```

## Mapping: `:map`

Transforms every item in an array. The transformation expression has access to `{@item}`, `{@index}`, `{@position}`, `{@first}`, and `{@last}`.

**Using `:to` syntax:**

```json theme={null}
{
  ":map": ["a", "b", "c"],
  ":to": "Value: {@item} (pos: {@position})"
}

// Result: ["Value: a (pos: 1)", "Value: b (pos: 2)", "Value: c (pos: 3)"]
```

**Using array syntax** — the second element is the transformation expression:

```json theme={null}
{
  ":map": [
    "{data.numbers}",
    { ":sum": ["@item", "@item"] }
  ]
}

// Scope: { "data": { "numbers": [1, 2, 3] } }
// Result: [2, 4, 6]
```

**Complex mapping with local variables using `:with`:**

This is the most powerful pattern — generating dynamic structures from data. The `:with` operator creates local variables for each iteration, keeping the template readable.

```json theme={null}
{
  ":map": [
    { ":range-array": [0, "{data.count}"] },
    {
      ":with": [
        { "pos": { ":sum": ["{@index}", 1] } },
        [
          { "type": "paragraph", "title": "Person {pos}" },
          {
            "key": "name_{@index}",
            "type": "text",
            "title": "Person {pos} name"
          }
        ]
      ]
    }
  ]
}

// Scope: { "data": { "count": 2 } }
// Result:
// [
//   { "type": "paragraph", "title": "Person 1" },
//   { "key": "name_0", "type": "text", "title": "Person 1 name" },
//   { "type": "paragraph", "title": "Person 2" },
//   { "key": "name_1", "type": "text", "title": "Person 2 name" }
// ]
```

## Filtering: `:filter`

Removes items from an array based on a condition. Works by mapping items to themselves (kept) or `false` (removed), then stripping the falsy values.

```json theme={null}
{
  ":filter": {
    ":map": [
      "{data.numbers}",
      {
        ":if": [
          { ":cmp": "@item", ":gt": 2 },
          "@item",
          false
        ]
      }
    ]
  }
}

// Scope: { "data": { "numbers": [1, 2, 3, 4, 5] } }
// Result: [3, 4, 5]
```

Filtering with inline expressions:

```json theme={null}
{
  ":filter": {
    ":map": [
      "{data.emails}",
      {
        ":if": [
          "{{ @item.endsWith('@example.com') }}",
          "@item",
          false
        ]
      }
    ]
  }
}

// Scope: { "data": { "emails": ["a@example.com", "b@other.com", "c@example.com"] } }
// Result: ["a@example.com", "c@example.com"]
```

## Finding Items: `:find`

Returns the first item in an array that matches a condition. If no match is found, returns `undefined`.

```json theme={null}
{
  ":find": ["{data.users}", { ":eq": ["@item.role", "admin"] }]
}

// Scope: {
//   "data": {
//     "users": [
//       { "id": 1, "role": "user" },
//       { "id": 2, "role": "admin" },
//       { "id": 3, "role": "admin" }
//     ]
//   }
// }
// Result: { "id": 2, "role": "admin" }   (first match only)
```

## Membership & Intersections

### `:includes`

Tests if an array contains a specific value.

```json theme={null}
{ ":includes": [[1, 2, 3], 2] }
// Result: true

{ ":includes": [[1, 2, 3], 4] }
// Result: false
```

Works with `:if` for access control patterns:

```json theme={null}
{
  ":if": { ":includes": ["{data.roles}", "admin"] },
  ":then": "Full access",
  ":else": "Limited access"
}

// Scope: { "data": { "roles": ["user", "admin"] } }
// Result: "Full access"
```

### `:intersects`

Tests if two arrays share any common elements.

```json theme={null}
{ ":intersects": ["{data.user_tags}", ["vip", "premium"]] }

// Scope: { "data": { "user_tags": ["new", "vip"] } }
// Result: true

// Scope: { "data": { "user_tags": ["basic"] } }
// Result: false
```

## Counting: `:count`

Returns the number of items in an array.

```json theme={null}
{ ":count": "{data.items}" }

// Scope: { "data": { "items": [1, 2, 3] } }
// Result: 3
```

### Conditional Counting with `:where`

Count only items that satisfy a condition:

```json theme={null}
{
  ":count": "{data.items}",
  ":where": { ":cmp": "@item", ":gt": 1 }
}

// Scope: { "data": { "items": [1, 2, 3, 4] } }
// Result: 3   (counts 2, 3, 4)
```

### Excluding Items with `:unless`

Combine `:where` and `:unless` for fine-grained filtering:

```json theme={null}
{
  ":count": "{data.items}",
  ":where": { ":cmp": "@item", ":gt": 1 },
  ":unless": { ":eq": ["@item", 3] }
}

// Scope: { "data": { "items": [1, 2, 3, 4] } }
// Result: 2   (counts 2 and 4 — excludes 3)
```

## Range Arrays: `:range-array`

Generates an array of sequential integers. The start is inclusive, the end is exclusive.

```json theme={null}
{ ":range-array": [0, 3] }
// Result: [0, 1, 2]

{ ":range-array": [2, 6] }
// Result: [2, 3, 4, 5]
```

Commonly used with `:map` to generate a dynamic number of form fields or UI elements based on a user-provided count.

## Flattening: `:flatten`

Deeply flattens nested arrays into a single flat array.

```json theme={null}
{ ":flatten": [[1, [2, [3]]]] }
// Result: [1, 2, 3]

{
  ":flatten": ["{data.items}", [null, 4, [[5], [[[[6]]]]]], false]
}

// Scope: { "data": { "items": [1, 2, 3] } }
// Result: [1, 2, 3, null, 4, 5, 6, false]
```

Useful after a `:map` that produces arrays of arrays — for example, when each mapped item generates multiple form fields and you need them in a single flat list.

## Sequence Check: `:increasing`

Returns `true` if all values in the array are in strictly increasing order.

```json theme={null}
{ ":increasing": "{data.daily_balances}" }

// Scope: { "data": { "daily_balances": [100, 150, 200] } }
// Result: true

// Scope: { "data": { "daily_balances": [100, 90, 120] } }
// Result: false
```

Useful for validating that sequential inputs are in the expected order (e.g., dates, amounts, installment schedules).

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Logic & comparisons" icon="code-branch" href="/penscript/logic">
    :every and :some for array conditions
  </Card>

  <Card title="Numbers & calculations" icon="calculator" href="/penscript/numbers">
    Aggregate calculations on arrays
  </Card>

  <Card title="Variables & scope" icon="brackets-curly" href="/penscript/variables">
    Context variables in loops
  </Card>

  <Card title="How penscript works" icon="gears" href="/penscript/how_it_works">
    Dynamic form generation with :map
  </Card>
</CardGroup>
