Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
manipulating_tables_and_lookup_tables [2026/07/24 23:32]
hermann
manipulating_tables_and_lookup_tables [2026/07/25 04:29] (current)
hermann
Line 9: Line 9:
 A **Tuple** is a sequence of table cells, most often used to represent the key (or full set of keys) identifying a table row. Tuple elements are ordered by column index and have no names of their own — a Tuple by itself doesn'​t say which column each element belongs to; that mapping only exists in the context of the table it's being used against. A **Tuple** is a sequence of table cells, most often used to represent the key (or full set of keys) identifying a table row. Tuple elements are ordered by column index and have no names of their own — a Tuple by itself doesn'​t say which column each element belongs to; that mapping only exists in the context of the table it's being used against.
  
-A single Real or String value connects directly to any port expecting a Tuple — Dinamica EGO converts it automatically,​ which is why a plain key value can be wired straight into a functor like ''​GetTableValue'' ​without explicitly constructing a Tuple first. A multi-column key has to be built explicitly, one element at a time, by chaining ​''​AddTupleValue'' ​calls — each call appends one value to the Tuple produced by the previous call.+A single Real or String value connects directly to any port expecting a Tuple — Dinamica EGO converts it automatically,​ which is why a plain key value can be wired straight into a functor like [[Get Table Value]] ​without explicitly constructing a Tuple first. A multi-column key has to be built explicitly, one element at a time, by chaining ​[[Add Tuple Value]] ​calls — each call appends one value to the Tuple produced by the previous call
 + 
 +<​code>​ 
 +// A single value auto-converts to a one-element Tuple; no AddTupleValue needed. 
 +yearOnly := 2007; 
 + 
 +// A composite key needs one AddTupleValue call per extra element: the first 
 +// argument (2007) auto-converts to a one-element Tuple, and the call appends 
 +// 4534272 to it, producing a two-element Tuple. 
 +yearAndCity := AddTupleValue 2007 4534272; 
 +</​code>​
  
 Every functor below that identifies a row or sub-table by key takes that key as a Tuple. Every functor below that identifies a row or sub-table by key takes that key as a Tuple.
Line 15: Line 25:
 ===== Retrieving values and rows ===== ===== Retrieving values and rows =====
  
-^ Functor ^ Inputs ^ Output ^ Notes ^ +=== GetTableValue ​===
-| ''​GetTableValue''​ | ''​table'',​ ''​keys''​ (Tuple), ''​column''​ (name or index), optional ''​valueIfNotFound''​ | The value at that key/column | Reads a single cell. | +
-| ''​GetTableRow''​ | ''​keys''​ (Tuple), ''​table''​ | The full row, as a Tuple | Reads every value column for one key. | +
-| ''​GetLookupTableValue''​ | ''​table''​ (Lookup Table), ''​key'',​ optional ''​valueIfNotFound''​ | The value for that key | The Lookup Table equivalent of ''​GetTableValue''​ — no column argument, since a Lookup Table only ever has one. |+
  
-All three accept an optional fallback ​value returned when the key isn't present, instead of failing.+Reads a single cell. 
 + 
 +^ Parameter ^ Type ^ Required? ^ Description ^ 
 +| ''​table''​ | Table | Yes | The table to read from. | 
 +| ''​keys''​ | Tuple | Yes | The key identifying the row. | 
 +| ''​column''​ | name or index | Yes | Which value column to read. An index counts every column left to right, starting at 1, including key columns — so the first value column'​s index is one past the number of key columns, not 1 (see [[table_type|Table Type]]). | 
 +| ''​valueIfNotFound''​ | matches the column | No | Returned instead of failing if the key isn't present. | 
 + 
 +Output: the value at that key and column. 
 + 
 +=== GetTableRow === 
 + 
 +Reads every value column for one key at once. 
 + 
 +^ Parameter ^ Type ^ Required? ^ Description ^ 
 +| ''​keys''​ | Tuple | Yes | The key identifying the row. | 
 +| ''​table''​ | Table | Yes | The table to read from. | 
 + 
 +Output: the row's value columnsas a Tuple — not the key columns, since those are already known from the ''​keys''​ input. 
 + 
 +=== GetLookupTableValue === 
 + 
 +The Lookup Table equivalent of ''​GetTableValue''​ — no column argument, since a Lookup Table only ever has one. 
 + 
 +^ Parameter ^ Type ^ Required? ^ Description ^ 
 +| ''​table''​ | Lookup Table | Yes | The lookup table to read from. | 
 +| ''​key''​ | Real | Yes | The key to look up. | 
 +| ''​valueIfNotFound''​ | Real | No | Returned ​instead of failing ​if the key isn't present. | 
 + 
 +Output: the value for that key. 
 + 
 +**Example** — a small constant table and lookup table, each read with the functor above that matches its shape: 
 + 
 +<​code>​ 
 +myLookupTable := LookupTable [ 
 +    "​Key"​ "​Value",​ 
 +    1 10, 
 +    2 20, 
 +    3 30 
 +]; 
 +// lookedUpValue will be 20. 
 +lookedUpValue := GetLookupTableValue myLookupTable 2; 
 + 
 +myTable := Table [ 
 +    "​CityId*",​ "​Population",​ "​Area",​ 
 +    1, 667137, 125, 
 +    2, 39398, 6 
 +]; 
 +// population will be 667137. 
 +population := GetTableValue myTable 1 "​Population";​ 
 + 
 +// wholeRow will be the two-element Tuple (39398, 6) — Population then Area. 
 +wholeRow := GetTableRow 2 myTable; 
 +</​code>​
  
 ===== Iterating over Lookup Table values ===== ===== Iterating over Lookup Table values =====
  
-A [[For Each]] loop run over a connected Lookup Table executes once per key. Inside the loop, a ''​Step'' ​functor retrieves the current key — its input auto-binds to the container'​s current iteration value, so it needs no explicit wiring. From there, the key can be used directly, or passed to ''​GetLookupTableValue''​ to retrieve ​the corresponding ​value.+A [[For Each]] loop run over a connected Lookup Table executes once per key. Inside the loop, a [[Step]] functor retrieves the current key — its input auto-binds to the container'​s current iteration value, the same mechanism described in [[ego_script#​internal_output_ports|Internal output ports]], so it needs no explicit wiring. From there, the key can be used directly, or passed to [[Get Lookup Table Value]] to retrieve the corresponding value. 
 + 
 +A loop like this can often run its iterations in parallel — see [[basic_data_flow|Basic Data Flow]] for the conditions that allow it. 
 + 
 +**Example** — printing every entry of a Lookup Table. [[Print]] is itself a container — its ''​initialMessage'' ​is printed before whatever it contains runs, so an empty ''<​nowiki>​{{ }}</​nowiki>''​ block is enough when the only goal is to print something. [[Create String]] builds a message from a format string and the values connected inside it, referenced the same way a Code expression references a hook (see [[ego_script#​verbose_form|Verbose form]]) — by a numbered, type-prefixed tag (''<​v1>''​ for the first connected ​value, and so on)This example also uses ''​{ initialMessage = message }''​ [[ego_script#​nominal_syntax|nominal syntax]] for ''​Print'',​ and the ''​_''​ [[ego_script#​positional_syntax|discard convention]] for outputs that aren't needed: 
 + 
 +<​code>​ 
 +myLookupTable := LookupTable [ 
 +    "​Key"​ "​Value",​ 
 +    1 10, 
 +    2 20, 
 +    3 30 
 +]; 
 + 
 +_ := ForEach myLookupTable {{ 
 +    key := Step; 
 +    value := GetLookupTableValue myLookupTable key; 
 + 
 +    message := CreateString "(Key: <v1>, Value: <​v2>​)"​ {{ 
 +        NumberValue key 1; 
 +        NumberValue value 2; 
 +    }}; 
 + 
 +    _ := Print { initialMessage = message } {{ }}; 
 +}}; 
 +</​code>​
  
 ===== Iterating over Table rows ===== ===== Iterating over Table rows =====
  
-Table rows are iterated the same way, but need one extra functor: ​''​GetTableKeys'' ​returns a table mapping unique indices to the keys of the input table'​s first key column. When the key column is already Real-typed, those indices and the keys are the same numbers, so the mapping is trivial. When the key column is String-typed,​ the indices are distinct numeric placeholders,​ and a ''​GetTableValue''​ call inside the loop is needed to map the current index back to its actual String key — this indirection is //why// String-typed key columns are usually avoided when a table will be iterated (see [[table_type|Table Type]]).+Table rows are iterated the same way, but need one extra functor: ​[[Get Table Keys]] ​returns a table mapping unique indices to the keys of the input table'​s first key column. When the key column is already Real-typed, those indices and the keys are the same numbers, so the mapping is trivial. When the key column is String-typed,​ the indices are distinct numeric placeholders,​ and a ''​GetTableValue''​ call inside the loop is needed to map the current index back to its actual String key — this indirection is //why// String-typed key columns are usually avoided when a table will be iterated (see [[table_type|Table Type]]).
  
-A table with more than one key column can only be iterated one key column at a time this way. To examine every key column, nest loops — see Sub-Tables below.+A table with more than one key column can only be iterated one key column at a time this way. To examine every key column, nest loops — see [[#​sub_tables|Sub-Tables]] below. 
 + 
 +**Example** — printing every entry of a Table with a Real-typed key column. General tables are wrapped in [[Table]] rather than [[Lookup Table]]: 
 + 
 +<​code>​ 
 +myTable := Table [ 
 +    "​CityId*",​ "​Population",​ 
 +    1, 667137, 
 +    2, 39398 
 +]; 
 + 
 +allKeys := GetTableKeys myTable; 
 + 
 +_ := ForEach allKeys {{ 
 +    key := Step; 
 +    value := GetTableValue myTable key "​Population";​ 
 + 
 +    message := CreateString "(Key: <v1>, Value: <​v2>​)"​ {{ 
 +        NumberValue key 1; 
 +        NumberValue value 2; 
 +    }}; 
 + 
 +    _ := Print { initialMessage = message } {{ }}; 
 +}}; 
 +</​code>​ 
 + 
 +**Example** — the same, but with a String-typed key column. This is the extra-indirection case described above: ''​GetTableKeys''​ returns indices, not the original keys, so an extra ''​GetTableValue''​ inside the loop maps each index back to its String key before it can be used: 
 + 
 +<​code>​ 
 +// #string is redundant here — quoted values like "​Boston"​ already infer as String — 
 +// it's included only to make the key column'​s type explicit at a glance. 
 +myStringKeyedTable := Table [ 
 +    "​CityName*#​string",​ "​Population",​ 
 +    "​Boston",​ 667137, 
 +    "​Chelsea",​ 39398 
 +]; 
 + 
 +indexToKey := GetTableKeys myStringKeyedTable;​ 
 + 
 +_ := ForEach indexToKey {{ 
 +    index := Step; 
 +    key := GetTableValue indexToKey index 2; 
 +    value := GetTableValue myStringKeyedTable key "​Population";​ 
 + 
 +    message := CreateString "(Key: <s1>, Value: <​v1>​)"​ {{ 
 +        NumberString key 1; 
 +        NumberValue value 1; 
 +    }}; 
 + 
 +    _ := Print { initialMessage = message } {{ }}; 
 +}}; 
 +</​code>​ 
 + 
 +**Example** — a table with more than one value column. Nothing new is needed: call ''​GetTableValue''​ once per value column and combine the results: 
 + 
 +<​code>​ 
 +myTable := Table [ 
 +    "​CityId*",​ "​Population",​ "​Area",​ 
 +    1, 667137, 125, 
 +    2, 39398, 6 
 +]; 
 + 
 +allKeys := GetTableKeys myTable; 
 + 
 +_ := ForEach allKeys {{ 
 +    key := Step; 
 +    population := GetTableValue myTable key "​Population";​ 
 +    area := GetTableValue myTable key "​Area";​ 
 + 
 +    message := CreateString "​(CityId:​ <v1>, Population: <v2>, Area: <​v3>​)"​ {{ 
 +        NumberValue key 1; 
 +        NumberValue population 2; 
 +        NumberValue area 3; 
 +    }}; 
 + 
 +    _ := Print { initialMessage = message } {{ }}; 
 +}}; 
 +</​code>​
  
 ===== Sub-Tables ===== ===== Sub-Tables =====
Line 36: Line 198:
 A **sub-table** is a table containing only the rows matching one specific value of a key, with that key column removed from the result. Sub-tables are how Dinamica handles tables with more than one key column: peel off one key at a time, working with what remains. A **sub-table** is a table containing only the rows matching one specific value of a key, with that key column removed from the result. Sub-tables are how Dinamica handles tables with more than one key column: peel off one key at a time, working with what remains.
  
-^ Functor ^ Inputs ^ Output ^ Notes ^ +=== GetTableFromKey ​===
-| ''​GetTableFromKey''​ | ''​table'',​ ''​keys''​ (Tuple) | The matching sub-table | Retrieves rows for the given key(s), with those key columns dropped. | +
-| ''​SetTableByKey''​ | ''​table'',​ ''​keys''​ (Tuple), ''​subTable''​ | The updated table | Inserts or replaces the rows for the given key(s). The sub-table'​s column names and types must match the target table. |+
  
-Both functors ​only operate on the table'​s **leftmost** key column(s) — they can't reach into the middle of a composite key. If the key you need isn't leftmost, reorder the columns first with ''​ReorderTableColumn''​, which moves one column (key or value) to a new index; key and value columns can't be moved across each other.+Retrieves the rows matching the given key(s), with those key columns dropped from the result. 
 + 
 +^ Parameter ^ Type ^ Required? ^ Description ^ 
 +| ''​table''​ | Table | Yes | The table to read from. | 
 +| ''​keys''​ | Tuple | Yes | The leftmost key(s) identifying the sub-table. | 
 + 
 +Output: the matching sub-table. 
 + 
 +=== SetTableByKey === 
 + 
 +Inserts or replaces the rows matching the given key(s). 
 + 
 +^ Parameter ^ Type ^ Required? ^ Description ^ 
 +| ''​table''​ | Table | Yes | The table to update. | 
 +| ''​keys''​ | Tuple | Yes | The leftmost key(s) identifying where the sub-table goes. | 
 +| ''​subTable''​ | Table | Yes | The replacement rows. Column names and types must match ''​table''​. | 
 + 
 +Output: the updated table. 
 + 
 +Both [[Get Table From Key]] and [[Set Table By Key]] only operate on the table'​s **leftmost** key column(s) — they can't reach into the middle of a composite key. If the key you need isn't leftmost, reorder the columns first with [[Reorder Table Column]], which moves one column (key or value) to a new index; key and value columns can't be moved across each other.
  
 **Worked example.** Take a table of commodity prices, keyed by ''​Year''​ and ''​City'':​ **Worked example.** Take a table of commodity prices, keyed by ''​Year''​ and ''​City'':​
Line 56: Line 235:
 | 5483552 | 233 | | 5483552 | 233 |
  
-To instead retrieve by ''​City''​ first, ''​Year''​ would need to be reordered ahead of it, since ''​GetTableFromKey''​ can only key off the leftmost column(s). With three key columns, the same idea nests: peel off the leftmost key to get a sub-table, then peel off the next leftmost key of //that// sub-table, and so on — which is exactly how nested ''​For Each''​ loops iterate a multi-key table one column at a time.+To instead retrieve by ''​City''​ first, ''​Year''​ would need to be reordered ahead of it, since ''​GetTableFromKey''​ can only key off the leftmost column(s). With three key columns, the same idea nests: peel off the leftmost key to get a sub-table, then peel off the next leftmost key of //that// sub-table, and so on — which is exactly how nested ''​ForEach''​ loops iterate a multi-key table one column at a time; see [[ego_script#​container_functors|Container functors]] for how nesting containers this way affects execution order.
  
-===== Choosing between Tables ​and Lookup Tables =====+**Example** — building the table above, retrieving the 2007 sub-table, updating one of its prices with [[Set Table Cell Value]], writing it back, and finally reordering ''​City''​ ahead of ''​Year''​ to key by ''​City''​ instead:
  
-A Lookup ​Table's fixed shape — one Real keyone Real value — makes it faster to query and iterate than a general Tableand it supports proximity-based lookups (nearest keylinear interpolation) that Tables don't. Prefer a Lookup Table whenever the data actually fits that shapeincluding inside map/value expressionswhere Lookup Table operands add less evaluation overhead than Table operands.+<​code>​ 
 +priceTable := Table 
 +    "​Year*"​"​City*"​"​Price"​, 
 +    200445342721200, 
 +    2004, 5483552, 1453, 
 +    2007, 4534272, 4332, 
 +    2007, 5483552, 233 
 +];
  
-Reach for a general Table instead when a single value per key isn't enough — when a row needs more than one associated value, or a String value, or when rows need to be indexed by more than one key at once.+// Retrieve the City*/Price sub-table ​for Year 2007. 
 +pricesIn2007 := GetTableFromKey priceTable 2007;
  
-===== Examples =====+// Replace the price for city 5483552 within that sub-table. 
 +updatedPricesIn2007 :SetTableCellValue pricesIn2007 "​Price"​ 5483552 999;
  
-Both examples below use ''​Print'',​ ''​Step'',​ and ''​CreateString''​ to log each entry to the message console. ''​Print''​ is itself a container — its ''​initialMessage''​ is printed before whatever it contains runs, so an empty ''<​nowiki>​{{ }}</nowiki>''​ block is enough when the only goal is to print something. ''​CreateString''​ builds a message from a format string and the values connected inside it, referenced ​the same way a Code expression references a hook — by a numbered, type-prefixed tag (''<​v1>''​ for the first connected value, and so on).+// Write the modified sub-table back under the same key. 
 +priceTable2 := SetTableByKey priceTable 2007 updatedPricesIn2007;​
  
-**Printing ​every entry of a Lookup ​Table:**+// Move City (currently index 2) ahead of Year (index 1), so sub-tables can 
 +// instead be retrieved by City first. 
 +priceTableByCity := ReorderTableColumn priceTable2 "​City"​ 1; 
 +// citySubTable will be the Year*/Price pairs for city 4534272. 
 +citySubTable := GetTableFromKey priceTableByCity 4534272; 
 +</​code>​ 
 + 
 +**Example** — printing ​every entry of a table with more than one key column. [[Get Table Keys]] only ever iterates the //first// key column, so a table with several needs one nested loop per extra key: peel off a sub-table for each ''​Year'',​ then iterate the ''​City''​ key within it:
  
 <​code>​ <​code>​
-:= For Each myLookupTable {{ +priceTable ​:= Table [ 
-    ​key := Step; +    ​"​Year*",​ "​City*",​ "​Price",​ 
-    ​value := GetLookupTableValue myLookupTable key;+    ​2004, 4534272, 1200, 
 +    2004, 5483552, 1453, 
 +    2007, 4534272, 4332, 
 +    2007, 5483552, 233 
 +];
  
-    ​message := CreateString "(Key: <​v1>, ​Value: <​v2>​)"​ {{ +yearKeys := GetTableKeys priceTable;​ 
-        NumberValue ​key 1; + 
-        NumberValue ​value 2;+_ := ForEach yearKeys {{ 
 +    year := Step; 
 +    pricesInYear := GetTableFromKey priceTable year; 
 + 
 +    cityKeys := GetTableKeys pricesInYear;​ 
 + 
 +    _ := ForEach cityKeys {{ 
 +        city := Step; 
 +        price := GetTableValue pricesInYear city "​Price";​ 
 + 
 +        ​message := CreateString "(Year: <​v1>, ​City: <v2>, Price: <v3>)" {{ 
 +            NumberValue ​year 1; 
 +            NumberValue ​city 2
 +            NumberValue price 3; 
 +        }}; 
 + 
 +        _ := Print { initialMessage = message } {{ }};
     }};     }};
- 
-    _ := Print { initialMessage = message } {{ }}; 
 }}; }};
 </​code>​ </​code>​
  
-**Printing every entry of a Table** (with Real-typed key column and a value column named ''​Population''​):+===== Storing results across ​loop ===== 
 + 
 +A [[Mux Table]] or [[Mux Lookup Table]] can carry Table or Lookup Table across the iterations of loop, the same way [[Mux Value]] carries a single ​value — see [[ego_script#​carrying_and_selecting_values_across_iterations|Carrying and selecting values across iterations]]. Each iteration reads the mux's current output, adds a row to it with ''​AddTableRow'', and feeds the result back in as the mux's ''​feedback''​ input for the next iteration, building up a result table one row at a time. 
 + 
 +The accumulated table is read after the loop the same way any container'​s internal result is read from outside ita functor after the loop simply takes the accumulator'​s feedback variable as an input. There'​s nothing special about this — the loop is guaranteed to finish before anything depending on it runs, per [[basic_data_flow|Basic Data Flow]].
  
 <​code>​ <​code>​
-allKeys ​:= GetTableKeys myTable;+sourceLookup ​:= LookupTable [ 
 +    "​Key"​ "​Value",​ 
 +    1 667137, 
 +    2 39398, 
 +    3 181045 
 +];
  
-:= For Each allKeys {{ +emptyResults ​:= Table [ 
-    ​key := Step; +    "CityId*#​real", "​DoubledPopulation#​real"​ 
-    value := GetTableValue myTable key "Population";+];
  
-    message ​:= CreateString "(Key: <v1>, Value: <​v2>​)" ​{{ +:= ForEach sourceLookup ​{{ 
-        NumberValue key 1; +    ​accumulated := MuxTable emptyResults nextAccumulated;
-        NumberValue value 2; +
-    ​}};+
  
-    ​:= Print { initialMessage ​message } {{ }};+    ​key := Step; 
 +    population := GetLookupTableValue sourceLookup key; 
 +    doubled := $ [ $population * 2 ]; 
 + 
 +    newRow := AddTupleValue key doubled; 
 +    nextAccumulated :AddTableRow accumulated newRow;
 }}; }};
 +
 +// finalResults holds every row added across all iterations. Use nextAccumulated,​
 +// not accumulated (the mux's own output), which lags one iteration behind.
 +// The Table carrier passes it through, since := always binds a functor call.
 +finalResults := Table nextAccumulated;​
 </​code>​ </​code>​
  
-If ''​myTable'''​s ​key column were String-typed instead of Real, ''​allKeys'' ​would hold numeric placeholder indices rather than the real keysand an extra ''​GetTableValue'' ​call would be needed ​inside the loop to map each index back to its actual ​key — see Iterating over Table rows above.+> **Note:** This is also why ''​accumulated'' ​should never be read again after ''​AddTableRow''​ consumes it in the same iteration. When exactly one reference to a table'​s ​current version remainsDinamica can update it destructively — modifying the existing storage in place. A second live reference to ''​accumulated'' ​(reading it directly somewhere elseinstead of only through ​''​nextAccumulated''​) forces Dinamica to copy the table instead, since the old version now has to stay intact for that other reference. That copy cost repeats every iteration — see [[basic_data_flow|Basic Data Flow]] for this same rule stated generally, beyond just tables. 
 + 
 +**Why this isn't the best approach.** [[basic_data_flow|Basic Data Flow]] documents three conditions under which a loop's iterations can run simultaneously:​ no mux directly ​inside the loop, nothing produced inside the loop consumed outside it, and no loop member used as a submodel'​s output port. A mux-based accumulator like the one above breaks the first condition outright — the mux ties every iteration ​to the one before it, so the loop can never run as anything but sequential, even when the per-iteration work is otherwise completely independent,​ as it is above (each row's value depends only on that row's own key). 
 + 
 +When the transformation is this simple — one output row per input row, computed independently — the calculator shorthand already covered in [[calculate_functors#​5_lookup_table_operators|Lookup Table Operators]] produces the same result without a loop, a mux, or the sequential cost that comes with one: 
 + 
 +<​code>​ 
 +doubledResults := % [ %sourceLookup[line] * 2 ] "​CityId"​ "​DoubledPopulation"​ sourceLookup;​ 
 +</​code>​ 
 + 
 +===== Choosing between Tables and Lookup Tables ===== 
 + 
 +A Lookup Table'​s fixed shape — one Real key, one Real value — makes it faster ​to query and iterate than a general Table, and it supports proximity-based lookups (nearest ​key, linear interpolation) that Tables don't. Prefer a Lookup Table whenever the data actually fits that shape, including inside map/value expressions,​ where Lookup Table operands add less evaluation overhead than Table operands ​— see [[calculate_functors#​5_lookup_table_operators|Lookup ​Table Operators]] and [[calculate_functors#​6_multi-column_table_operators|Multi-Column Table Operators]] for how each is queried from within an expression. 
 + 
 +Reach for a general Table instead when a single value per key isn't enough — when a row needs more than one associated value, or a String value, or when rows need to be indexed by more than one key at once. 
 +