# Integration Toolkit Solutions Wiki

**URL:** <https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080>\
**Category:** 3rd Party Modules\
**Tags:** scripting, ignition81\
**Created:** [April 14, 2025, 9:02pm UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080 "2025-04-14T21:02:36Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [April 14, 2025, 9:02pm UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/1 "2025-04-14T21:02:36Z")

</div>

By popular request, I've created this topic to be used in wiki mode for collaborative generation of solutions to particular problems. To keep this neat, please follow these guidelines:

- New problems seeking solutions should be posted as replies, and the discussion of that problem kept **within** that comment, wiki-style. A one-line summary should be added to this topic starter, with an embedded link (not on its own line). After posting, be sure to convert the comment to operate in wiki mode.  

- Use the "Hide Details" tool to keep the one comment neat. In particular, code blocks belong inside the details.

- Create a "uses" details section at the end of a problem/solution comment where you deep link into the public module documentation for each function or feature used in the solution. Consider also deep linking into the IA manual, too.

[Public Integration Toolkit Overview](https://www.automation-pros.com/toolkit/doc/)

[Ignition Native Scripting Functions, By Scope](https://www.docs.inductiveautomation.com/docs/8.1/appendix/scripting-functions#functions-by-scope)

[Ignition Native Expression Functions](https://www.docs.inductiveautomation.com/docs/8.1/appendix/expression-functions)

Use the [Integration Toolkit wiki - setup discussion](https://forum.inductiveautomation.com/t/integration-toolkit-wiki-setup-discussion/105078) thread for discussion _about_ this wiki.

{ Please add, within reason, to these best practices. }

## Problem Index

#### Link your comments below here, as bullets, with a one-line description as the link text.

- [Creating a custom property dataset using a temporary binding](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/3)
- [Merging lists into a single list of objects](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/3)
- [Filtering datasets and emulating the IN operator](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/6)
- [Filter table data using where()](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/9#p-392839-filter-table-data-using-where-1)
- [Incremental readings from totalising meter asMap() and lag()](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/10#p-392846-incremental-readings-from-totalising-meter-1)
- [Merging lists into a single list of objects where each list is represented by its own key in each object](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/8)
- [Simple orderBy() example](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/10#p-392932-simple-orderby-example-1)
- [Get the row index of a dataset/list matching criteria - Matches `lookup` function but returns row index](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/12)
- [timeMe() expression execution timer](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/13#p-393854-timeme-expression-execution-timer-1)
- [alias()](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/14#p-393864-alias-1)
- [jsonDecode() and jsonEncode](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/15#p-393866-jsondecode-1)
- [Mimicking Python join to concatenate a list of dicts into a string](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/20)
- [Accepting a single tagPath or list of tagPaths into `tags` (for backward compatibility)](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/21)
- [Storing IPv4 addresses as integers rather than strings](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/21#p-401538-storing-ipv4-addresses-as-integers-rather-than-strings-1https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/21#p-401538-storing-ipv4-addresses-as-integers-rather-than-strings-1)
- [Iterating through an array-tag to determine if value is present](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/23)
- [Creating a list of Months in format `YYYY-MM` between a start and end date](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/24#p-415352-creating-a-list-of-months-in-format-yyyy-mm-between-a-start-and-end-date-1)
- [Adding Columns to a Dataset where the Column Data is derived from expressions](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/25#p-418639-adding-columns-to-a-dataset-where-the-column-data-is-derived-from-expressions-1)
- [Joining elements of an array into a string with delimiter with slicing](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/28)

### Perspective Tables

- [Perspective Table Data and Columns Config](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/2)
- [Add Conditional Styling to Perspective Table Rows](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/7)
- [Adding styling to a table row where a cell equals a value](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/29)
- [Dataset Edit Differencing](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/31)

### Tags

- [Converting a `tags()` output into a dict](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/30)

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [April 14, 2025, 9:28pm UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/2 "2025-04-14T21:28:20Z")

</div>

## Perspective Table Data and Columns Config from Datasets

When using the JSON return format in a Named Query, the column order and original column datatypes are lost. You can move a Named Query binding to a custom property of the component or view, then use this simple iteration expression to deliver the jsonified content to the table:

> **Binding on props.data**
>
> ```javascript
> forEach(
> {path.to.source.dataset.prop},
> asMap(it())
> )
> 
> ```

> **Binding on props.columns (optional)**
>
> ```javascript
> forEach(
> columnsOf({path.to.source.dataset.prop}),
> if(
> it()[0] = 'special_column_name',
> asMap(
> 'field', it()[0],
> 'sortable', true,
> 'editable', true,
> 'filter', asMap('enabled', true, 'visible', 'on-hover'),
> 'resizable', true //,
> // 'header', asMap('title', replace(runScript(concat("'",it()[0],"'",'.title()')),'_',' ')), // don't do this
> // 'render', it()[1] // not necessary (ds column types are implicit)
> // add additional properties as needed
> ),
> asMap('field', it()[0])
> )
> )
> 
> ```
> 
> @pturmel - can we get a `title()` function?

> **Features employed**
>
> - [Iterables](https://www.automation-pros.com/toolkit/doc/iteration.html#iterables) General behavior of all iterators.
> - [`forEach()`](https://www.automation-pros.com/toolkit/doc/iteration.html#foreach) Loops through the dataset, calling the nested expression with one row at a time.
> - [`it()`](https://www.automation-pros.com/toolkit/doc/iteration.html#it) Delivers the one row in dataset format (same column names and types as the source).
> - [`asMap()`](https://www.automation-pros.com/toolkit/doc/iteration.html#asmap) Converts the first row of the dataset it is given into a mapping (dictionary) of key-value pairs.
> - [`columnsOf()`](https://www.automation-pros.com/toolkit/doc/datasets.html#columnsof "columnsOf()"): gets column names (and types)

---

<div class="post-metadata">

**Author:** ![Transistor](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/transistor/32/3380_2.png) [@Transistor](https://forum.inductiveautomation.com/u/Transistor)\
**Post date:** [April 14, 2025, 11:02pm UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/3 "2025-04-14T23:02:16Z")

</div>

## Creating a custom property dataset using a temporary binding

There isn't a built-in method to create a dataset in a Perspective custom property. Creating an Expression Binding with this script will generate one. The binding can then be removed leaving the dataset.

> **Expression binding on custom property**
>
> ```python
> unionAll(
> asMap(
> "someDate", "date",
> "someBool", "B",
> "someInt", "I",
> "someString", "str"
> ),
> asList() // No row data.
> )
> /* Returns
> "#NAMES"
> "someDate","someBool","someInt","someString"
> "#TYPES"
> "date","B","I","str"
> "#ROWS","0"
> */
> 
> ```

> **Features employed**
>
> - [unionAll()](https://www.automation-pros.com/toolkit/doc/datasets.html#unionall) Assembles an output dataset from scratch, using the given column names and types (internally via a `DatasetBuilder`), performing a UNION ALL with each row source.
> - [asList()](https://www.automation-pros.com/toolkit/doc/iteration.html#aslist) unconditionally assembles all of its arguments into a List.

You really only need to create one column with the binding and then modify the result with the Dataset Editor. (Click the icon to the right of the `Dataset [...]`.)

---

<div class="post-metadata">

**Author:** ![nminchin](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/nminchin/32/6911_2.png) [@nminchin](https://forum.inductiveautomation.com/u/nminchin)\
**Post date:** [April 15, 2025, 11:58am UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/4 "2025-04-15T11:58:50Z")

</div>

## Merging lists into a single list of objects

How would I merge multiple lists into a single list of objects/dicts?  
e.g.

```python
a = [1,2,3,4]
b = ['a','b','c','d']
c = {'a': 1, 'b': 2, ...} # create this one

```

Iterate over one while picking from the other. But use transform() to snapshot for use within the iterator.

> **Emulation of dict(zip(b, a))**
>
> ```python
> transform(
> {path.to.list.a},
> asMap(
> forEach(
> {path.to.list.b},
> it(),
> value()[idx()]
> )
> )
> )
> 
> ```

Note that the toolkit's `asPairs()` function works like python's `.extend()`, not `zip()`.

You can shuffle the order of operations to get various output types.

> **Features Employed**
>
> - [transform()](https://www.automation-pros.com/toolkit/doc/utility.html#transform) Makes a snapshot of list `a`.
> - [Iterables](https://www.automation-pros.com/toolkit/doc/iteration.html#iterables) General behavior of all iterators.
> - [forEach()](https://www.automation-pros.com/toolkit/doc/iteration.html#foreach) Loops through the list of keys, calling the nested expressions with one key at a time, yielding a list of pairs.
> - [it()](https://www.automation-pros.com/toolkit/doc/iteration.html#it) Delivers the key from list `b`.
> - [value()](https://www.automation-pros.com/toolkit/doc/utility.html#value) Retrieves the snapshot of list `a`.
> - [idx()](https://www.automation-pros.com/toolkit/doc/iteration.html#idx) Retrieves the zero-based loop index to use as a subscript into list `a`.
> - [asMap()](https://www.automation-pros.com/toolkit/doc/iteration.html#asmap) Creates a map of the list of key-value pairs.

{ Someone forgot to make their question a wiki.... }

---

<div class="post-metadata">

**Author:** ![lrose](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/lrose/32/62724_2.png) [@lrose](https://forum.inductiveautomation.com/u/lrose)\
**Post date:** [April 15, 2025, 5:32pm UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/6 "2025-04-15T17:32:21Z")

</div>

## Filtering datasets and emulating the IN operator

Many times you want to filter a dataset where a columns value is equal to one of any item in a list. In SQL this is possible by utilizing the 'IN' operator. However, outside of using a QueryString parameter, due to the way that Named Query parameters work you can not supply a List of values.

If you create a custom property to hold the raw data set from the named query, then you can use a simple expression binding to filter the dataset further in a similar manner to that seen when using the SQL `IN` operator.

> **Filtering by a List of Values**
>
> ```python
> unionAll(
> columnsOf({path.to.raw.data}),
> forEach(
> {path.to.List},
> where(
> {path.to.raw.data},
> it()['ColumnToFilter'] = it(1)
> )
> )
> )
> 
> ```

Note that for filtering by a single value, only the `where()` expression is needed.

> **Filtering by a Single Value**
>
> ```python
> where(
> {path.to.raw.data},
> it()['ColumnToFilter'] = {path.to.filter.value}
> )
> 
> ```

> **Features employed**
>
> - [Iterables](https://www.automation-pros.com/toolkit/doc/iteration.html#iterables) General behavior of all iterators.
> - [unionAll()](https://www.automation-pros.com/toolkit/doc/datasets.html#unionall) Assembles the list of filtered rows sources into a new filtered dataset.
> - [columnsOf()](https://www.automation-pros.com/toolkit/doc//datasets.html#columnsof) Returns an ordered map of the column names versus column type class names (as strings).
> - [forEach()](https://www.automation-pros.com/toolkit/doc/iteration.html#foreach) Loops through the list of items, for comparison in the conditional argument of the `where()` expression.
> - [where()](https://www.automation-pros.com/toolkit/doc/iteration.html#where) Prunes the source data according to supplied conditional expressions.
> - [it([depth])](https://www.automation-pros.com/toolkit/doc/iteration.html#it) Delivers the current iterations value based on the depth argument supplied.

Note that for static lists, the [asList()](https://www.automation-pros.com/toolkit/doc/iteration.html#asList) expression can be used in place of `{path.to.list}`

---

<div class="post-metadata">

**Author:** ![dkhayes117](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/dkhayes117/32/68344_2.png) [@dkhayes117](https://forum.inductiveautomation.com/u/dkhayes117)\
**Post date:** [April 16, 2025, 9:13pm UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/7 "2025-04-16T21:13:21Z")

</div>

## Add Conditional Styling to Perspective Table Data Rows

Add conditional styling, such as font color, background color, etc. based on a cell value

> **Conditional Row Background Color**
>
> ```python
> forEach(
> {this.custom.data},
> asMap(
> 'value', it(), // use asMap(it()) for dataset sources
> 'style', asMap('backgroundColor', if(it()['population'] > 1000000, 'red', ''))
> )
> )
> 
> ```

> **Features employed**
>
> - [forEach()](https://www.automation-pros.com/toolkit/doc/iteration.html#foreach) Loops through each row..
> - [asMap()](https://www.automation-pros.com/toolkit/doc/iteration.html#asmap) Creates a map of the list of key-value pairs.
> - [it()['columnName']](https://www.automation-pros.com/toolkit/doc/iteration.html#it) Delivers a cell value based on the column name.

Note that for dataset sources, each row needs to use `asMap()` for the `value` key (see expression comments).

---

<div class="post-metadata">

**Author:** ![nminchin](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/nminchin/32/6911_2.png) [@nminchin](https://forum.inductiveautomation.com/u/nminchin)\
**Post date:** [April 16, 2025, 10:06pm UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/8 "2025-04-16T22:06:22Z")

</div>

## Merging lists into a single list of objects where each list is represented by its own key in each object

How would I merge multiple lists into a single list of objects/dicts, where each list is given its own key name?  
e.g.

```python
BobList = [1,2,3,4]
PeterList = ['a','b','c','d']
MergedList = [{'Bob': 1, 'Peter': 'a':}, {'Bob': 2, 'Peter': 'b'} ...] # create this one

```

I want to be able to name the keys explicitly, they won't match the list name exactly

* * *

**Solutions**

Requirements:

- The number of supplied keys is equal to the number of lists to combine
- The lists being combined are the same length
- The keys should all be unique, otherwise the last lists values associated with that key will be used.

### Simple solution by @Transistor

> **Expression binding**
>
> ```python
> forEach(
> asList(1, 2, 3, 4),
> asMap(
> 'Bob', it(),
> 'Peter', asList('a','b','c','d')[idx()] 
> )
> )
> or
> 
> forEach(
> {this.custom.BobList},
> asMap(
> 'Bob', it(),
> 'Peter', {this.custom.PeterList}[idx()] 
> )
> )
> 
> ```

Note: If length of BobList \< length of PeterList the expression will return null.

### Advanced solution by @lrose

Use `transform()` to snapshot a list of lists (Lists to be combined), then use `forEach()` in it's range form to "enumerate" the lists. Finally, use `asMap()` and `forEach()` to loop over the list of keys, and return a list of lists where each nested list is a key, value pair for the map.

> **Expression binding**
>
> ```python
> transform(
> asList(
> asList(1,2,3,4),
> asList('a','b','c','d'),
> ),
> forEach(
> len(value()[0]), //acts like pythons range()
> asMap(
> forEach(
> asList('Bob','Peter'),
> asList(
> it(),value()[idx()][idx(1)]
> )
> )
> )
> )
> )
> 
> ```
> 
> or
> 
> ```python
> transform(
> {path.to.list.of.lists},
> forEach(
> len(value()[0]),
> asMap(
> forEach(
> {path.to.list.of.keys},
> asList(
> it(),value()[idx()][idx(1)]
> )
> )
> )
> )
> )
> 
> ```

> **Advantages**
>
> The main advantage of this approach over the one provided by @Transistor is it allows for any arbitrary number of List/Key combinations, with out further editing of the expression. This means that the expression will work even if the expression parameters are produced dynamically, so long as the requirements for the function to work are met.
> 
> Using `transform()` allows for a single reference to the List of Lists, meaning it can be referenced multiple times throughout the expression with out needing to do the work of dereferencing the property path to get the value. Making the expression more performant, especially as the number of list/key combinations grows.

Some clarification on the inner most `forEach()` loop.

In this expression `it(), value()[idx()][idx(1)]`, `it()` is the current key, `value()` is the list of lists, `[idx()]` is the current index of the key used to retrieve the list associated with that key from the lists of lists, `[idx(1)]` is the value in the associated list at the "enumerated" index.

So the first time through the loop, it would return [['Bob',1],['Peter','a']], which when handed to `asMap()` results in the map `{'Bob':1,'Peter':'a'}`.

> **Functions used**
>
> - [transform()](https://www.automation-pros.com/toolkit/doc/utility.html#transform) Makes a snapshot of the list of lists.
> - [forEach()](https://www.automation-pros.com/toolkit/doc/iteration.html#foreach) loops through the items in the source.
> - [asMap()](https://www.automation-pros.com/toolkit/doc/iteration.html#asmap) produce a Java Map with string keys.
> - [asList()](https://www.automation-pros.com/toolkit/doc/iteration.html#aslist) unconditionally assembles all of its arguments into a List.
> - [len()](https://www.docs.inductiveautomation.com/docs/8.1/appendix/expression-functions/logic/len) returns the length of its argument.
> - [it()](https://www.automation-pros.com/toolkit/doc/iteration.html#it) Delivers the key from the lists of keys
> - [idx([depth])](https://www.automation-pros.com/toolkit/doc/iteration.html#idx) retrieves the loop index for an index for the forEach iterator based on the value of its depth argument.

---

<div class="post-metadata">

**Author:** ![Transistor](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/transistor/32/3380_2.png) [@Transistor](https://forum.inductiveautomation.com/u/Transistor)\
**Post date:** [April 16, 2025, 10:50pm UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/9 "2025-04-16T22:50:46Z")

</div>

## Filter table data using where()

A very simple example of using the where() filter on the Perspective table component's default data.

> **Expression binding on custom property**
>
> ```python
> where(
> {this.props.data},
> it()['country'] = 'China' // case sensitive.
> )
> 
> ```

> **Features employed**
>
> - [where()](https://www.automation-pros.com/toolkit/doc/iteration.html#where) prunes the source data according to supplied conditional expressions.
> - [it()](https://www.automation-pros.com/toolkit/doc/iteration.html#it) retrieves the loop value for an iterator from inside the nested expression.

---

<div class="post-metadata">

**Author:** ![Transistor](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/transistor/32/3380_2.png) [@Transistor](https://forum.inductiveautomation.com/u/Transistor)\
**Post date:** [April 16, 2025, 11:58pm UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/10 "2025-04-16T23:58:29Z")

</div>

## Incremental readings from totalising meter using asMap() and lag()

Useful for extracting the difference in periodic meter readings.

> **Sample dataset**
>
> ```python
> "#NAMES"
> "t_stamp","reading"
> "#TYPES"
> "date","I"
> "#ROWS","6"
> "2025-04-17 00:00:00.000","5"
> "2025-04-17 01:00:00.000","7"
> "2025-04-17 02:00:00.000","11"
> "2025-04-17 03:00:00.000","17"
> "2025-04-17 04:00:00.000","25"
> "2025-04-17 05:00:00.000","35"
> 
> ```
> 
> or paste the JSON below onto a component's `custom.data`.
> 
> ```python
> [
> {"t_stamp": 1744844400000, "reading": 5 },
> {"t_stamp": 1744848000000, "reading": 7},
> {"t_stamp": 1744851600000, "reading": 11},
> {"t_stamp": 1744855200000, "reading": 17},
> {"t_stamp": 1744858800000, "reading": 25},
> {"t_stamp": 1744862400000, "reading": 35}
> ]
> 
> ```

> **Output**
>
> ```python
> [
> {"t_stamp":1744844400000,"reading": null},
> {"t_stamp":1744848000000,"reading": 2},
> {"t_stamp":1744851600000,"reading": 4},
> {"t_stamp":1744855200000,"reading": 6},
> {"t_stamp":1744858800000,"reading": 8},
> {"t_stamp":1744862400000,"reading": 10}
> ]
> 
> ```

> **Expression binding**
>
> e.g., on Perspective custom property:
> 
> ```python
> forEach(
> {this.custom.data},
> asMap(
> 't_stamp', it()['t_stamp'], 
> 'Increment', it()['reading'] - lag()['reading']
> )
> )
> 
> ```

> **Functions employed**
>
> - [forEach()](https://www.automation-pros.com/toolkit/doc/iteration.html#foreach) loops through the items in the source.
> - [asMap()](https://www.automation-pros.com/toolkit/doc/iteration.html#asmap) produce a Java Map with string keys.
> - [it()](https://www.automation-pros.com/toolkit/doc/iteration.html#it) retrieves the loop value for the **it** erator.
> - [lag()](https://www.automation-pros.com/toolkit/doc/iteration.html#lag) retrieves the **previous** loop value for the iterator. It returns _null_ for the first pass and so the incremental reading will be null for the first interval. (The _null_ in calculation seems to be neatly handled by the asMap() function which returns a _null_.)

---

<div class="post-metadata">

**Author:** ![Transistor](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/transistor/32/3380_2.png) [@Transistor](https://forum.inductiveautomation.com/u/Transistor)\
**Post date:** [April 17, 2025, 3:44pm UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/11 "2025-04-17T15:44:14Z")

</div>

## Simple orderBy() example

Reorder the given source data by key.

> **Source data**
>
> Paste this into a custom property, `unsorted`.
> 
> ```python
> [
> {"fruit": "apples", "count": 10},
> {"fruit": "bananas", "count": 8},
> {"fruit": "citrus", "count": 11},
> {"fruit": "dates", "count": 7}
> ]
> 
> ```

> **Expression binding**
>
> Create this binding on another custom property. e.g., `sorted`.
> 
> ```python
> orderBy(
> {this.custom.unsorted},
> it()['count']
> )
> 
> ```

> **Result**
>
> ```python
> [
> {"fruit": "dates", "count": 7},
> {"fruit": "bananas", "count": 8},
> {"fruit": "apples", "count": 10},
> {"fruit": "citrus", "count": 11}
> ]
> 
> ```

## descending() example

> **Expression binding**
>
> ```python
> orderBy(
> {this.custom.unsorted},
> descending(
> it()['count']
> )
> )
> 
> ```

> **Result**
>
> ```python
> [
> {"fruit": "citrus", "count": 11},
> {"fruit": "apples", "count": 10},
> {"fruit": "bananas", "count": 8},
> {"fruit": "dates", "count": 7}
> ]
> 
> ```

## naturalOrder() example

Case-insensitive sort.

> **Expression binding**
>
> ```python
> orderBy(
> {this.custom.unsorted},
> naturalOrder(
> it()['code']
> )
> )
> 
> ```

> **Source data**
>
> ```python
> [
> {"fruit": "apples", "code": "a10"},
> {"fruit": "bananas", "code": "8b"},
> {"fruit": "citrus", "code": "A10"},
> {"fruit": "dates", "code": 7}
> ]
> 
> ```

> **Result**
>
> ```python
> [
> {"code": 7, "fruit": "dates"},
> {"code": "8b", "fruit": "bananas"},
> {"code": "a10", "fruit": "apples"},
> {"code": "A10", "fruit": "citrus"}
> ]
> 
> ```
> 
> Note that `a10` is output _before_ `A10`.

## naturalCasedOrder() example

Case-sensitive sort.

> **Expression binding**
>
> ```python
> orderBy(
> {this.custom.unsorted},
> naturalCasedOrder(
> it()['code']
> )
> )
> 
> ```

> **Result**
>
> ```python
> [
> {"code": 7, "fruit": "dates"},
> {"code": "8b", "fruit": "bananas"},
> {"code": "A10", "fruit": "citrus"}.
> {"code": "a10", "fruit": "apples"}
> ]
> 
> ```
> 
> Note that `A10` is output _before_ `a10`.

* * *

> **Functions used**
>
> - [orderBy()](https://www.automation-pros.com/toolkit/doc/iteration.html#orderby) reorders the given source data per the given key expressions.
> - [it()](https://www.automation-pros.com/toolkit/doc/iteration.html#it) retrieves the loop value for an iterator from inside the nested expression.
> - [descending()](https://www.automation-pros.com/toolkit/doc/iteration.html#descending) reverses the comparison order of any comparable handed to it.
> - [naturalOrder()](https://www.automation-pros.com/toolkit/doc/iteration.html#naturalorder)
> - [naturalCasedOrder()](https://www.automation-pros.com/toolkit/doc/iteration.html#naturalcasedorder) orders in case-sensitive alphanumeric.

---

<div class="post-metadata">

**Author:** ![nminchin](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/nminchin/32/6911_2.png) [@nminchin](https://forum.inductiveautomation.com/u/nminchin)\
**Post date:** [April 23, 2025, 6:07am UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/12 "2025-04-23T06:07:40Z")

</div>

## Get the row index of a dataset/list matching criteria - Matches `lookup` function but returns row index

I want to replicate the functionality of `lookup(...)` except instead of returning a cell value, I want to return the index of the found row.

Wrap a `forEach()` inside `where()` to include the original index in the output.

> **Index Lookup**
>
> ```python
> where(
> forEach( // Expand the source into pairs of original index and content there
> {path.to.source},
> idx(),
> it()
> ),
> it()[1]['someColumn'] = 'SomeValue' // original content is the 2nd element of the pair
> )[0][0] // Extract first match, then original index is the 1st element of the pair
> 
> ```

Note that most applications that need the index need it to retrieve multiple parts of the found row. It is better to simply use `where()` to return the entire first matching result. { I want to use this for the `scrollIntoView` JS function which needs the row index }

If the source is sure to be a dataset, consider using `selectStar()` to add a new column holding the original index instead of making pairs. Something like this:

> **Dataset Row Lookup**
>
> ```python
> where(
> where(
> selectStar( // Expand the source to include an original index column
> {path.to.source.dataset},
> asMap('_original_idx', I),
> idx()
> ),
> it()['someColumn'] = 'SomeValue' // any ordinary condition
> ),
> idx() < 1 // Extract first row
> )
> 
> ```

> **Functions Used**
>
> - [where()](https://www.automation-pros.com/toolkit/doc/iteration.html#where)
> - [forEach()](https://www.automation-pros.com/toolkit/doc/iteration.html#foreach)
> - [idx()](https://www.automation-pros.com/toolkit/doc/iteration.html#idx)
> - [it()](https://www.automation-pros.com/toolkit/doc/iteration.html#it)
> - [selectStar()](https://www.automation-pros.com/toolkit/doc/datasets.html#selectstar)
> - [asMap()](https://www.automation-pros.com/toolkit/doc/iteration.html#asmap)

---

<div class="post-metadata">

**Author:** ![Transistor](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/transistor/32/3380_2.png) [@Transistor](https://forum.inductiveautomation.com/u/Transistor)\
**Post date:** [April 24, 2025, 12:44am UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/13 "2025-04-24T00:44:43Z")

</div>

## timeMe() expression execution timer

_[I've reached the limit of two consecutive posts again so posting this here temporarily until someone else posts something and then I can post it seperately.]_

This function injects an MDC1 filter key into the logging system during expression execution, allowing a specific expression’s activity to be easily located in the logs.  
1 Mapped Diagnostic Context.

> **Expression**
>
> ```python
> timeMe(
> 'timeMeLog', // string to be returned with the MDC.
> forEach( // sample expression to be timed.
> asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
> asMap(
> 'Number', it(),
> 'Square', it() ^ 2
> )
> )
> )
> 
> ```

 ![timeMe log](https://us1.discourse-cdn.com/inductiveautomation/original/3X/b/b/bb63c2097c06bfc4fdc7ec97393cbc93128f1ffb.png)

_Results available in gateway web page → Status → Logs.  
Note the MDC string from the script._

> **Functions used**
>
> - [timeMe()](https://www.automation-pros.com/toolkit/doc/debugging.html#timeme).

---

<div class="post-metadata">

**Author:** ![Transistor](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/transistor/32/3380_2.png) [@Transistor](https://forum.inductiveautomation.com/u/Transistor)\
**Post date:** [April 24, 2025, 1:23am UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/14 "2025-04-24T01:23:46Z")

</div>

## alias()

 ![Dataset](https://us1.discourse-cdn.com/inductiveautomation/original/3X/9/b/9b794c19335b9a7fc4522f07546ebda860d8f36c.png)  
_custom.dataset._

`alias({this.custom.myDataset}, 'new_')`  
results in this:

 ![alias](https://us1.discourse-cdn.com/inductiveautomation/original/3X/f/4/f422a09a9d4ff61b8dda6f21784797952b79338a.png)

It just works! "Ideal for use with the various `JOIN` operations [below](https://www.automation-pros.com/toolkit/doc/datasets.html#alias) to avoid column name clashes."

[alias()](https://www.automation-pros.com/toolkit/doc/datasets.html#alias).

## columnsOf()

`columnsOf({this.custom.myDataset})` returns

```python
{
  "someBool": "B",
  "someDate": "date",
  "someString": "str",
  "someInt": "I"
}

```

- [columnsOf()](https://www.automation-pros.com/toolkit/doc/datasets.html#columnsof)

```python
`
```

---

<div class="post-metadata">

**Author:** ![Transistor](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/transistor/32/3380_2.png) [@Transistor](https://forum.inductiveautomation.com/u/Transistor)\
**Post date:** [April 24, 2025, 1:29am UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/15 "2025-04-24T01:29:08Z")

</div>

## toTransient() and nonTransient()

Can someone edit this to give examples of what these functions are supposed to do. I seem to get back the original dataset.

- [toTransient()](https://www.automation-pros.com/toolkit/doc/datasets.html#totransient)
- [nonTransient()](https://www.automation-pros.com/toolkit/doc/datasets.html#nontransient)

* * *

That is essentially what they do, from the surface. A little deeper, they change whether the content of the dataset, is serializable or not. The content in `TransientDatasets` is not serializable, meaning they are not persistent. I would need to rely on Phil or perhaps someone from IA to say what expressions return a `TransientDataset`. The exception being the `recorder()` expression from the Tool Kit, which is noted as returning one.

I personally have not come up with a need for these two expressions, but I am sure that Phil saw a need for them and so they exist.

@lrose, thanks. What exactly does 'serializable' mean in this context?

@Transistor It means that the class implements the `Serializable` interface. Which tells the JVM that the state of an object can be serialized into and consequently deserialized from a byte stream. Which means the state of the object can persisted for later use.

---

<div class="post-metadata">

**Author:** ![Transistor](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/transistor/32/3380_2.png) [@Transistor](https://forum.inductiveautomation.com/u/Transistor)\
**Post date:** [April 24, 2025, 1:29am UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/16 "2025-04-24T01:29:17Z")

</div>

## jsonDecode()

Takes a JSON string and converts it into a Python object such as a list or a dictionary. If the input is not valid JSON, a string is returned.

```python
jsonDecode("{'item': [10, 20, 30, 40, 50]}")
// returns {"item": [10, 20, 30, 40, 50]}

jsonDecode("{'item': [10, 20, 30, 40, 50]}")['item']
// returns [10, 20, 30, 40, 50]

jsonDecode("{'item': [10, 20, 30, 40, 50]}")['item'][1]
// returns 20

```

- [jsonDecode()](https://www.automation-pros.com/toolkit/doc/utility.html#jsondecode)

## jsonEncode()

Can someone edit this to give a working example and explain why this (example from system.util.jsonEncode()) doesn't work with the Integration Toolkit?

> **jsonEncode({view.custom.employees})**
>
> ```python
> [
> {
> "value": {
> "firstName": {
> "value": "John",
> "quality": {
> "code": 0
> },
> "timestamp": "Apr 26, 2025, 12:01:14 PM"
> },
> "lastName": {
> "value": "Doe",
> "quality": {
> "code": 0
> },
> "timestamp": "Apr 26, 2025, 12:01:14 PM"
> }
> },
> "quality": {
> "code": 0
> },
> "timestamp": "Apr 26, 2025, 12:01:14 PM"
> },
> {
> "value": {
> "firstName": {
> "value": "Anna",
> "quality": {
> "code": 0
> },
> "timestamp": "Apr 26, 2025, 12:01:14 PM"
> },
> "lastName": {
> "value": "Smith",
> "quality": {
> "code": 0
> },
> "timestamp": "Apr 26, 2025, 12:01:14 PM"
> }
> },
> "quality": {
> "code": 0
> },
> "timestamp": "Apr 26, 2025, 12:01:14 PM"
> },
> {
> "value": {
> "firstName": {
> "value": "Peter",
> "quality": {
> "code": 0
> },
> "timestamp": "Apr 26, 2025, 12:01:14 PM"
> },
> "lastName": {
> "value": "Jones",
> "quality": {
> "code": 0
> },
> "timestamp": "Apr 26, 2025, 12:01:14 PM"
> }
> },
> "quality": {
> "code": 0
> },
> "timestamp": "Apr 26, 2025, 12:01:14 PM"
> }
> ]
> 
> ```

- [jsonEncode()](https://www.automation-pros.com/toolkit/doc/utility.html#jsonencode)

The `custom.employees` is a simple array of firstname, lastname dictionaries. Why does jsonEncode add quality and timestamp keys and values?

```python
jsonEncode(
	{"employees": [{"firstName": "John", "lastName": "Doe"}, {"firstName": "Anna", "lastName": "Smith"}, {"firstName": "Peter", "lastName": "Jones"}]}
)

```

Why does this give a "Nested paths not allowed (Line 2, Char. 17)" error?  
Perspective makes everything a qualified value when assigning to its properties. Including values in arrays and dictionaries. Try `jsonEncode(unQualify(...))`.

---

<div class="post-metadata">

**Author:** ![hunterdg](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/hunterdg/32/67774_2.png) [@hunterdg](https://forum.inductiveautomation.com/u/hunterdg)\
**Post date:** [April 28, 2025, 9:19am UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/17 "2025-04-28T09:19:58Z")

</div>

## parsePath().toString() equivalent? 😬

How do i

> [@Automation Professionals' Integration Toolkit Module](https://forum.inductiveautomation.com/t/automation-professionals-integration-toolkit-module/74934/153):
>
> produce proper historical paths

from `parsePath()`?

Forgive my lack of familiarity if there’s an easy way - I try to avoid datasets unless absolutely necessary. (e.g. Table `props.columns` derivation).

Perhaps a "ds vs object" blurb might be useful if my ignorance is showing.

> **here's my uuugly method (first try)**
>
> ```json
> {
> "type": "expr",
> "config": {
> "expression": "parsePath(\r\n\tconcat(\r\n\t\t{view.custom.node.meta_node.Tag_Path}, // concat(\u0027[\u0027, {session.custom.project.tag.provider}, \u0027]\u0027), \u0027\u0027),\r\n\t\t\"/Analog Tags/\",\r\n\t\tif(\r\n\t\t\t{view.custom.Type_ID.meta} \u003d \u0027Wl\u0027,\r\n\t\t\t\u0027TEMPERATURE\u0027,\r\n\t\t\t\u0027METER_TEMP\u0027\r\n\t\t)\r\n\t)\r\n)"
> },
> "transforms": [
> {
> "expression": "flatten(forEach(\r\n\t{value},\r\n\tasPairs(it())\r\n))",
> "type": "expression"
> },
> {
> "expression": "where(\r\n\t{value},\r\n\t!isNull(it()[1])\r\n)",
> "type": "expression"
> },
> {
> "expression": "forEach(\r\n\t{value},\r\n\tlower(groupConcat(it(), \u0027:\u0027))\r\n)",
> "type": "expression"
> },
> {
> "expression": "groupConcat({value}, \u0027:/\u0027)",
> "type": "expression"
> }
> ]
> }
> 
> ```

TL/DR: Use `parsePath()` in conjunction with `tags()` reading the storage provider property of the tags. Tags will return a fully de-relativized path, that can then be combined with the value returned from `tags` and a separately provided system name to construct the historical path.

> **Expression**
>
> ```python
> transform(
> {path.to.gateway.name},
> forEach(
> tags(
> forEach(
> {path.to.list.of.tag.paths},
> concat(it(),'.historyProvider')
> )
> ),
> transform(
> parsePath(it()[0]),
> concat(
> 'histprov:',it()[1], //gets value returned from tags.
> ':/drv:',coalesce(value()['drv'],value(1)), //if tags is supplied a path with a driver then use that, otherwise use the provided value
> ':',value()['prov'],
> ':/tag:',value()['tag']
> )
> )
> )	
> )
> 
> ```

To produce a proper Historical Path given a simple tag path, you need to know a few things.

1. The Historical Provider that is configured for the given tag path.
2. The name of the gateway the Historical Provider is on. (If a single gateway system, then it's just the gateway name. For remote providers you need the gateway name and the tag provider on the remote gateway.

A historical tag path looks like this:

```python
histprov:hist_prov_name:/drv:gateway_name:tag_provider_name:/tag:tag_path

```

This then of course begs the question of how you can dynamically retrieve that information.

In a single gateway system it's pretty simple. There is a System Tag `SystemName` that will give you the gateway name, and you can actually read the `historyProvider` property from the tag, given the tag path. So the first is a simple tag binding, and the second is a simple Indirect Tag Binding.

For a multiple gateway system it is not that simple. For reference see: [Remote Tag Provider History Queries](https://forum.inductiveautomation.com/t/remote-tag-provider-history-queries/93632)

Once you have those pieces of information then it's just a matter of parsing the given tag path into it's parts and reconstructing them into the historical tag path.

So, given the following path `[default]_Simulator_/Ramp/Ramp0`, in a single gateway system where the System Name is "Ignition\_Gateway", and the historical provider is "History\_Provider". This expression will return the correct historical tag path.

> **Expression for Single Tag Path**
>
> ```python
> transform(
> parsePath({this.custom.tagPath}),
> lower(
> concat(
> 'histprov:', coalesce(value()['histprov'],{this.custom.histProv}),':/',
> 'drv:',coalesce(value()['drv'],{this.custom.gatewayName}),':',coalesce(value()['prov'],'default'),':/',
> 'tag:',value()['tag']
> )
> )
> )
> 
> ```
> 
> NOTE: I don't believe that paths are case sensitive, but if you want this to match the output from say the Power Chart, then the `lower()` does that.

> **Output**
>
> ```python
> histprov:history_provider:/drv:ignition_gateway:default:/tag:_Simulator_/Ramp/Ramp0
> 
> ```

If you provide `parsePath` with multiple strings, then it returns a row for each string. Modifying the above expression with `forEach` will allow you to return a list of multiple historical tag paths.

> **Expression for Multiple Tag Paths**
>
> ```python
> transform(
> parsePath({this.custom.tagPath},{this.custom.tagPath1}),
> forEach(
> value(),
> lower(
> concat(
> 'histprov:', coalesce(it()['histprov'],{this.custom.histProv}),':/',
> 'drv:',coalesce(it()['drv'],{this.custom.gatewayName}),':',coalesce(it()['prov'],'default'),':/',
> 'tag:',it()['tag']
> )
> )
> )
> )
> 
> ```

> **Features employed**
>
> - [Iterables](https://www.automation-pros.com/toolkit/doc/iteration.html#iterables) General behavior of all iterators.
> - [transform](https://www.automation-pros.com/toolkit/doc/utility.html#transform): Makes a snapshot of the dataset returned from parsePath.
> - [parsePath](https://www.automation-pros.com/toolkit/doc/datasets.html#parsepath "parsePath()"): returns `QualifiedPath` parts as a dataset
> - see also [QualifiedPathUtils](https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.47/com/inductiveautomation/ignition/common/QualifiedPathUtils.html#parsePossibleTagpathNew(java.lang.String)), [QualifiedPath](https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.47/com/inductiveautomation/ignition/common/QualifiedPath.html#parse(java.lang.String))
> 
> - [coalesce](https://www.docs.inductiveautomation.com/docs/8.1/appendix/expression-functions/logic/coalesce): returns the first non-null argument
> - [concat](https://www.docs.inductiveautomation.com/docs/8.1/appendix/expression-functions/string/concat): returns the concatenation of all provided arguments
> - [lower](https://www.docs.inductiveautomation.com/docs/8.1/appendix/expression-functions/string/lower): returns the provided string in all lower case
> - [forEach](https://www.automation-pros.com/toolkit/doc/iteration.html#foreach): loops through each row in the dataset.
> - [it](https://www.automation-pros.com/toolkit/doc/iteration.html#it): Delivers the row from this iteration through the dataset.

@lrose - surely the intent of parsePath() was to make this process purely dynamic (as opposed to hardcoding the path part names as your examples do)?

@hunterdg - My examples are dynamic. Given a tag path, this will output a historical tag path. There are a couple of other bindings involved as explained, but it will change dynamically with the path.

The intent of `parsePath` is to break a string path down into its constituent parts. A simple tag path by itself does not contain the needed information to build a corresponding historical tag path. Information must be retrieved and inserted. `parsePath` on its own does not do any extra look up.

@hunterdg I suppose, maybe this is what you're looking for, although, this will not provide a valid historical tag path, unless it is handed a valid historical tag path.

> **Expression**
>
> ```python
> transform(
> parsePath({this.custom.tagPath},{this.custom.tagPath1}),
> forEach(
> value(),
> groupConcat(
> flatten(
> forEach(
> columnsOf(it()), // columns of dataset returned from parsePath
> forEach(
> where(
> it(1), // rows of dataset returned from parsePath
> !isNull(it()[it(1)[0]])
> ), // all non-null values in this row
> concat(it(1)[0],':',it()[it(1)[0]]) // columnName + : + columnValue
> )
> )
> ),
> ':/'
> )
> )
> )
> 
> ```

@lrose - This does indeed work for a _non-historical_ (but PowerChart-compatible) QualifiedPath as you point out, but I’m somewhat disappointed as it seems to be basically the consolidated version of my original method (which is still very much appreciated). I assumed there must be an easier way to concat dataset rows interpolated with their column names. ~~Fairly certain `QualifiedPath.toString()` performs the equivalent and is much more approachable, despite the overhead (and regardless, I believe a scripting context is needed to build a “proper” historical QualifiedPath object anyway).~~

~~EDIT - @pturmel’s original post mentions “(I've been frustrated with the need for scripts to produce proper _ **historical** _ paths from simple tag paths.)”…~~

I now see that `parsePath()` requires passing a _string_ (with `histprov` + `drv`) to return a _historical_ path - guess I missed that critical (and obvious in hindsight) piece - so i might as well just pass the historical paths strings straight to PowerChart. It is still useful to derive _non-historical_ stringified qualified paths from a QualifiedPath object though.

Feel free to cleanup this post. I consider my question “mostly” answered - but I’m still uncertain as to the rationale for `parsePath()` - as it most certainly does not seem to "produce proper historical paths from simple tag paths” as the original post suggests.

@hunterdg `parsePath` is best used when using `tags()` to read the Storage Provider property of tags. If you use parsepath on the returned full tag path, you can use its parts with the returned value, and separately provided system name, to construct the history qualified path from a basic tag path. A big `concat()` operation.

@pturmel 🙏

---

<div class="post-metadata">

**Author:** ![hunterdg](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/hunterdg/32/67774_2.png) [@hunterdg](https://forum.inductiveautomation.com/u/hunterdg)\
**Post date:** [April 28, 2025, 11:54pm UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/18 "2025-04-28T23:54:30Z")

</div>

## merging lists of dicts

given identical-length lists of dicts, how to merge nested dicts of each (into a single list)

> **Merge corresponding dictionaries in lists**
>
> ```python
> transform(
> {path.to.list.a},
> forEach(
> {path.to.list.b},
> asMap( // Convert lists of pairs to a dictionary
> asPairs( // convert dictionaries to lists of pairs
> it(),
> value()[idx()],
> asMap( // bonus
> 'additionalKey1', 'additionalValue1',
> 'additionalKey2', 'additionalValue2'
> )
> )
> )
> )
> )
> 
> ```

bonus: how to add additional key/value pairs that don’t exist  
bonus2 (my actual use-case): insert dicts from one to another as _nested_ dicts

> **Inject dictionaries from list A into a key of dictionaries from list B**
>
> ```python
> transform(
> {path.to.list.a},
> forEach(
> {path.to.list.b},
> asMap( // Convert lists of pairs to a dictionary
> asPairs( // convert dictionaries to lists of pairs
> it(),
> asMap( //bonus2
> 'nestedKey', value()[idx()]
> )
> )
> )
> )
> )
> 
> ```

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [May 7, 2025, 12:34am UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/19 "2025-05-07T00:34:06Z")

</div>

Reformatting a list of dictionaries, like the default data of the Perspective table, back into a dataset. Extracting nested `.value` elements where necessary.

> **Dataset from List of Mappings**
>
> ```python
> unionAll(
> asMap(
> 'city', 'str',
> 'country', 'str',
> 'population', 'I'
> ),
> forEach(
> {/root/Table.props.data},
> asList(
> unMap(it()['city'], 'value'),
> unMap(it()['country'], 'value'),
> unMap(it()['population'], 'value')
> )
> )
> )
> 
> ```

---

<div class="post-metadata">

**Author:** ![nminchin](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/nminchin/32/6911_2.png) [@nminchin](https://forum.inductiveautomation.com/u/nminchin)\
**Post date:** [May 21, 2025, 2:49am UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/20 "2025-05-21T02:49:32Z")

</div>

## Mimicking Python join to concatenate a list of dicts into a string

How would I do this?

e.g.  
Data:

```python
[
  {
    "datetime": 1747795200000,
    "price": 108.64
  },
  {
    "datetime": 1747795500000,
    "price": 119.71819
  },
  {
    "datetime": 1747795800000,
    "price": 119.71819
  },
  {
    "datetime": 1747796100000,
    "price": 114.23557
  }
]

```

Output:

```python
"2025-05-21 10:30 - $108.64
2025-05-21 11:00 - $119.72
2025-05-21 11:30 - $119.72
2025-05-21 12:00 - $114.24"

```

Python equivalent:

```python
"\r\n".join(
		"{} - ${:.2f}".format(
			system.date.format(system.date.fromMillis(row['datetime']), 'YYYY-MM-dd HH:mm'),
			row['price'])
		for row in value
	)

```

[PS. this wiki was such a good idea]

> **List concat**
>
> ```python
> groupConcat(
> forEach(
> {path.to.source.list},
> stringFormat(
> "%s - $%.f",
> dateFormat(
> fromMillis(it()['datetime']),
> 'yyyy-MM-dd HH:mm'
> ),
> it()['price']
> )
> ),
> "\r\n"
> )
> 
> ```

---

<div class="post-metadata">

**Author:** ![nminchin](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/nminchin/32/6911_2.png) [@nminchin](https://forum.inductiveautomation.com/u/nminchin)\
**Post date:** [June 23, 2025, 1:05am UTC](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080/21 "2025-06-23T01:05:03Z")

</div>

## Accepting a single tagPath or list of tagPaths into `tags` (for backward compatibility)

I recently needed to add support for reading a list of tagPaths rather than just a single tagPath in a template, but needed to keep backwards compatibility, and compatibility for existing view template instances that don't have the new list param at all.

This expression essentially creates a single list of tagPaths by combining a single tagPath string and the new _list_ of tagPaths into one list and then subscribing to their values with `tags`.

_For this template, the tagPaths are actually paths to a UDT instance and I needed to read a specific tag from them, so in the expression I also append the relative path to the tag as well._

> **Expression**
>
> ```python
> tags(
> forEach(
> asPairs(
> if(
> {view.params.decorations.alarms.alarmSummaryAreaPath} != '',
> asList({view.params.decorations.alarms.alarmSummaryAreaPath}),
> asList()
> ), // if the param is '', present as an empty list, otherwise wrap it into a list
> coalesce(
> {view.params.decorations.alarms.alarmSummaryAreaPaths},
> asList()
> ) // if the param is Null, present as an empty list
> ),
> it() + '/Alarm Summary/Status'
> )
> )
> 
> ```

> **Example of input values**
>
> alarmSummaryAreaPath:  
> `"[tagProv]Path/To/Folder"`
> 
> alarmsummaryAreaPaths:
> 
> ```python
> [
> "[tagProv]Path/To/Other Folder",
> "[tagProv]Path/To/Other Other Folder"
> ]
> 
> ```
> 
> The expression will subscribe to these tags:
> 
> ```python
> [
> "[tagProv]Path/To/Folder/Alarm Summary/Status",
> "[tagProv]Path/To/Other Folder/Alarm Summary/Status",
> "[tagProv]Path/To/Other Other Folder/Alarm Summary/Status"
> ]
> 
> ```

[Next page](https://forum.inductiveautomation.com/t/integration-toolkit-solutions-wiki/105080.md?page=2)
