each

each, keyword

The each keyword offers an iteration mechanism against an observation table. Iterations are not ordered. The table named after each is always the iteration table.

table Obs = with
  [| as N |]
  [| 1 |]
  [| 2 |]
  [| 3 |]

Obs.Cpy = each Obs
  cpy = Obs.N + 1
  return cpy

show table "" with Obs.N, Obs.Cpy

each .. scan , keyword

The each .. scan keywords offer an iteration mechanism against an observation table. Iterations are ordered based on the argument passed to scan. The scan expression must depend only on data available before the loop starts. Expressions computed inside the loop are rejected to avoid rebuilding indices per iteration.

table Obs = with
  [| date(2021, 1, 1) as Date, 13 as Quantity |]
  [| date(2021, 2, 1)       , 11              |]
  [| date(2021, 3, 1)       , 17              |]
  [| date(2021, 4, 1)       , 18              |]
  [| date(2021, 5, 1)       , 16              |]

Best = 0

Obs.BestSoFar = each Obs scan Obs.Date
  keep Best
  NewBest = max(Best, Obs.Quantity)
  Best = NewBest
  return NewBest

show table "" with Obs.Date, Obs.BestSoFar

Table diagram

each uses the table diagram to decide how tables are accessed in the loop:

Reads from upstream tables return the scalar value matching the current iteration line. Writes to the iteration table or upstream tables are forbidden; use keep Upstream.X as X to update upstream values through a scalar alias. Writing to upstream-cross tables is forbidden. Broadcasting into the iteration table or upstream tables is forbidden.

The keep statements must appear first in the block and refer to variables defined before the loop. each .. scan requires at least one keep. desc reverses the scan order.

Upstream vectors must be kept as scalars and updated through an alias:

table Items[id] = with
  [| as Id, as X, as Cat |]
  [| 1, 1, "A" |]
  [| 2, 2, "B" |]

table Categories[cat] = by Items.Cat
Categories.X = 0

Items.B = each Items scan Items.Cat
  keep Categories.X as X
  X = Items.X + X
  return X

Cross broadcasts

Inside each, a vector from an upstream-cross table may be broadcast onto the full-table component of that cross table.

table Items[id] = with
  [| as id, as X, as Cat |]
  [| "1", 1, "A" |]
  [| "2", 2, "B" |]
  [| "3", 3, "A" |]
  [| "4", 4, "B" |]

table T = with
  [| as Y |]
  [| 1 |]
  [| 2 |]
  [| 3 |]
  [| 4 |]

expect table T max 4
table Cat[c] max 2 = by Items.Cat
table TCat = cross(T, Cat)

TCat.Z = match TCat.c with
  "A" -> T.Y
  "B" -> (1 + T.Y) ^ 2

Items.B = each Items
  x = Items.X
  T.Z = TCat.Z
  y = sum(T.Z) when (T.Y < x)
  return y

show table "Items" with
  Items.id
  Items.Cat
  Items.B

If the projected table is the right component of the cross table, then that cross table must be small.

Assignments to full tables

Inside each, a process may assign its result to a small full table or to a grouped table derived from that full table. The produced vectors can then be read later in the same iteration.

table Items[id] = with
  [| as id, as X |]
  [| "1", 1 |]
  [| "2", 2 |]
  [| "3", 3 |]
  [| "4", 4 |]

table T = with
  [| as Y, as Cat |]
  [| 1, "A" |]
  [| 3, "B" |]
  [| 4, "B" |]
  [| 2, "A" |]

expect table T max 4
table C[cat] max 4 = by T.Cat

Items.B = each Items
  x = Items.X
  C.X = sum(T.Y) when (T.Y > x)
  return join(text(C.X); "-") sort cat

As usual, a table that is aggregated as a whole inside the loop must be small.

Nested blocks

Nested each blocks are allowed. The inner block gets its own iteration table, while keep aliases from the outer block remain available as scalars. The usual table-diagram restrictions still apply inside the inner block.

table Items[id] = with
  [| as id, as X |]
  [| "1", 1 |]
  [| "2", 2 |]

table T = with
  [| as Y |]
  [| 1 |]
  [| 2 |]

expect table T max 2

Items.Trace = each Items
  X = Items.X
  T.Step = each T scan T.Y
    keep X
    X = X + T.Y
    return X
  return join(text(T.Step); "-") sort T.Y

show table "Nested each" with
  Items.id
  Items.Trace

The inner block does not relax the usual restrictions: table statements are still forbidden, and assigning a vector from a non-scalar full table is still rejected.

Cross-table returns

An each block may return both a vector on the iteration table and a vector in a cross table built from the iteration table and another table.

table Items[id] = with
  [| as id, as X |]
  [| "1", 1 |]
  [| "2", 2 |]
  [| "3", 3 |]

table T = with
  [| as Y |]
  [| 1 |]
  [| 2 |]
  [| 3 |]

expect table T max 3
table ItemsT = cross(Items, T)

Items.Y, ItemsT.Pair = each Items
  x = Items.X
  return (x, "\{x}:\{T.Y}")

show table "Items" with
  Items.id
  Items.Y

show table "Pairs" with
  ItemsT.id
  T.Y
  ItemsT.Pair

The same rule applies inside nested loops: return always targets the current loop, and one of its outputs may live in a cross table tied to that loop.

Return-less blocks

If only the final keep values matter, return can be omitted:

table Currencies = with
  [| as Code |]
  [| "EUR" |]
  [| "JPY" |]
  [| "USD" |]

Sep = ""
List = ""
each Currencies scan Currencies.Code
  keep Sep
  keep List
  List = "\{List}\{Sep}\{Currencies.Code}"
  Sep = ", "

show scalar "" with List

auto ordering

scan auto follows the primary dimension ordering:

table T = extend.range(3)
x = 0
T.X = each T scan auto
  keep x
  x = T.N - x
  return x

Any-order blocks

scan T.* allows arbitrary ordering:

table Obs = with
  [| as X |]
  [| 42 |]
  [| 41 |]
  [| 45 |]

myMin = 1B
myMax = -(1B)
each Obs scan Obs.*
  keep myMin
  keep myMax
  myMin = min(myMin, Obs.X)
  myMax = max(myMax, Obs.X)

each .. when

when filters iterations and is only allowed on return-less blocks:

table T = extend.range(5)
s = 0
each T scan auto when T.N mod 2 == 1
  keep s
  s = s + T.N

Comparison with for

each and for overlap, but they are not identical. Prefer each when the loop body is naturally row-oriented.

User Contributed Notes
0 notes + add a note