# Syntax and Operators

{% hint style="info" %}
Critical Moments uses the excellent [expr library](https://expr.medv.io) for conditional evaluation. Portions of this document are from their documentation, and those portions are under a MIT licence. Their original documentation is available [here](https://github.com/antonmedv/expr/blob/master/docs/Language-Definition.md), and licensed [here](https://github.com/antonmedv/expr/blob/master/LICENSE).

Portions of this document are extensions specific to Critical Moments, all rights reserved.
{% endhint %}

### Literals

| Comment | `/* */` or `//`      |
| ------- | -------------------- |
| Boolean | `true`, `false`      |
| Integer | `42`, `0x2A`         |
| Float   | `0.5`, `.5`          |
| String  | `"foo"`, `'bar'`     |
| Array   | `[1, 2, 3]`          |
| Map     | `{a: 1, b: 2, c: 3}` |
| Nil     | `nil`                |

### Operators

| Arithmetic  | `+`, `-`, `*`, `/`, `%` (modulus), `^` or `**` (exponent) |
| ----------- | --------------------------------------------------------- |
| Comparison  | `==`, `!=`, `<`, `>`, `<=`, `>=`                          |
| Logical     | `not` or `!`, `and` or `&&`, `or` or `\|\|`               |
| Conditional | `?:` (ternary), `??` (nil coalescing)                     |
| Membership  | `[]`, `.`, `?.`, `in`                                     |
| String      | `+` (concatenation), `contains`, `startsWith`, `endsWith` |
| Regex       | `matches`                                                 |
| Range       | `..`                                                      |
| Slice       | `[:]`                                                     |
| Pipe        | `\|`                                                      |

Examples:

```expr
user.Age in 18..45 and user.Name not in ["admin", "root"]
```

```expr
foo matches "^[A-Z].*"
```

```expr
a_list | filter(.Size < 280) | map(.Content) | join(" -- ")
```

```expr
filter(posts, {now() - .CreatedAt >= 7 * duration("24h")})
```

#### Membership Operator

Fields of structs and items of maps can be accessed with `.` operator or `[]` operator. Elements of arrays and slices can be accessed with `[]` operator. Negative indices are supported with `-1` being the last element.

The `in` operator can be used to check if an item is in an array or a map.

```expr
user.Name in ["admin", "owner"]
```

**Optional chaining**

The `?.` operator can be used to access a field of a struct or an item of a map without checking if the struct or the map is `nil`. If the struct or the map is `nil`, the result of the expression is `nil`.

```expr
author?.User?.Name
```

**Nil coalescing**

The `??` operator can be used to return the left-hand side if it is not `nil`, otherwise the right-hand side is returned.

```expr
author?.User?.Name ?? "Anonymous"
```

#### Slice Operator

The slice operator `[:]` can be used to access a slice of an array.

For example, if `array` is `[1, 2, 3, 4, 5]`:

```expr
array[1:4] == [2, 3, 4]
array[1:-1] == [2, 3, 4]
array[:3] == [1, 2, 3]
array[3:] == [4, 5]
array[:] == array
```

#### Pipe Operator

The pipe operator `|` can be used to pass the result of the left-hand side expression as the first argument of the right-hand side expression.

For example, expression `split(lower(user.Name), " ")` can be written as:

```expr
user.Name | lower() | split(" ")
```

### String Functions

#### trim(str\[, chars])

Removes white spaces from both ends of a string `str`. If the optional `chars` argument is given, it is a string specifying the set of characters to be removed.

```expr
trim("  Hello  ") == "Hello"
trim("__Hello__", "_") == "Hello"
```

#### trimPrefix(str, prefix)

Removes the specified prefix from the string `str` if it starts with that prefix.

```expr
trimPrefix("HelloWorld", "Hello") == "World"
```

#### trimSuffix(str, suffix)

Removes the specified suffix from the string `str` if it ends with that suffix.

```expr
trimSuffix("HelloWorld", "World") == "Hello"
```

#### upper(str)

Converts all the characters in string `str` to uppercase.

```expr
upper("hello") == "HELLO"
```

#### lower(str)

Converts all the characters in string `str` to lowercase.

```expr
lower("HELLO") == "hello"
```

#### split(str, delimiter\[, n])

Splits the string `str` at each instance of the delimiter and returns an array of substrings.

```expr
split("apple,orange,grape", ",") == ["apple", "orange", "grape"]
split("apple,orange,grape", ",", 2) == ["apple", "orange,grape"]
```

#### splitAfter(str, delimiter\[, n])

Splits the string `str` after each instance of the delimiter.

```expr
splitAfter("apple,orange,grape", ",") == ["apple,", "orange,", "grape"]
splitAfter("apple,orange,grape", ",", 2) == ["apple,", "orange,grape"]
```

#### replace(str, old, new)

Replaces all occurrences of `old` in string `str` with `new`.

```expr
replace("Hello World", "World", "Universe") == "Hello Universe"
```

#### repeat(str, n)

Repeats the string `str` `n` times.

```expr
repeat("Hi", 3) == "HiHiHi"
```

#### indexOf(str, substring)

Returns the index of the first occurrence of the substring in string `str` or -1 if not found.

```expr
indexOf("apple pie", "pie") == 6
```

#### lastIndexOf(str, substring)

Returns the index of the last occurrence of the substring in string `str` or -1 if not found.

```expr
lastIndexOf("apple pie apple", "apple") == 10
```

#### hasPrefix(str, prefix)

Returns `true` if string `str` starts with the given prefix.

```expr
hasPrefix("HelloWorld", "Hello") == true
```

#### hasSuffix(str, suffix)

Returns `true` if string `str` ends with the given suffix.

```expr
hasSuffix("HelloWorld", "World") == true
```

### Date Functions

The following operators can be used to manipulate dates. See more examples in [Working with Dates](https://docs.criticalmoments.io/conditional-targeting/conditional-guides/working-with-dates).

```expr
date("2023-08-14") + duration("1h")
date("2023-08-14") - duration("1h")
date("2023-08-14") - date("2023-08-13") == duration("24h")
```

#### now()

Returns the current date and time.

```expr
createdAt > now() - duration(1h)
```

#### duration(str)

The duration function takes a string such as `1.5h`, `24h`, or `-30s`. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".

Durations can be added to datetimes like so: `now() - duration('12h')`

#### date(str\[, format\[, timezone]])

Converts the given string `str` into a date.

* str \[required, string]: required, the date string to parse
* format \[optional, string]: the format of the date string. If omitted we'll try a variety of formats to try to parse the given string. Example: `date('2024-01-15T22:00:00-08:00', RFC3339)`. The following variables can be provided to parse for common standards.
  * date\_format: `YYYY-MM-DD` format
  * date\_and\_time\_format: `YYYY-MM-DD HH:MM:SS.SSSSSS` format
  * date\_with\_tz\_format: A `YYYY-MM-DDZ` date with a timezone. Example `2021-01-01Z` for UTC or `2021-01-01+05:00`for a 5 hour offset
  * RFC3339 (allowing fractional seconds)
  * RFC822
  * RFC850
  * RFC1123
  * RFC822Z: RFC822 with numeric zone like `-700`
  * RFC1123Z: RFC1123 with numeric zone like `-700`
  * Custom strings: supported using the golang time package time format [described here](https://pkg.go.dev/time)
* timezone \[optional, string]: The timezone to parse in. Possible values include:
  * "UTC", empty string, or not specified - UTC
  * "Local": the user's local timezone
  * A location name corresponding to a file in the IANA Time Zone database, such as "America/New\_York"

See examples in [Working with Dates](https://docs.criticalmoments.io/conditional-targeting/conditional-guides/working-with-dates).

#### formatTime(timestamp, format, \[timezone])&#x20;

This function can be used to format timestamps, and extract components of a date. It returns the provided timestamp, formatted in the requested format. It returns an integer when possible (hour of day, month of year, etc) and a string when not possible (name of the month of the year).

Example: `formatTime(now(), 'year') == 2024`

* Timestamp: a timestamp such as `now()`, the result of a call to `date(...)`, or the result of a call to a `unixTime*(...)`call
* Format: a string format. One of the following strings:
  * "dow": the int day of week. Sunday is 0, Saturday is 6.
  * "dow\_short": the string short form day of week. Mon, Tue, etc
  * "dow\_long": the string full form day of week. Monday, Tuesday, etc
  * "dom": the int day of month (1-31)
  * "hod": the int hour of the day (0-23)
  * "moh": the int minut of the hour (0-59)
  * "ampm": "AM" or "PM"
  * "month": the int month. Jan is 1, Dec is 12.
  * "month\_short": the short form name of month. Example: Jan
  * "month\_long": the full form name of month. Example: January.
  * "year": the int year. Example, 1999
  * [A golang formatted time layout string](https://pkg.go.dev/time#pkg-constants)
* Timezone (optional): the timezone to apply to the timestamp before formatting. If not provided, defaults to the user's local timezone. Can be "Local" (user local), "UTC" or a location name corresponding to a file in the IANA Time Zone database, such as "America/Toronto".

#### unixTimeSeconds(int), unixTimeMilliseconds(int), unixTimeNanoseconds(int) Functions

These three functions take in a unix timestamp (time since January 1, 1970 UTC) and return a datetime for that point in time. Negative values are supported for times before 1970.

{% hint style="info" %}
Be sure you've call the appropriate function. Different libraries provide unix time in seconds, milliseconds or nanoseconds.&#x20;
{% endhint %}

### Number Functions

#### max(n1, n2)

Returns the maximum of the two numbers `n1` and `n2`.

```expr
max(5, 7) == 7
```

#### min(n1, n2)

Returns the minimum of the two numbers `n1` and `n2`.

```expr
min(5, 7) == 5
```

#### abs(n)

Returns the absolute value of a number.

#### ceil(n)

Returns the least integer value greater than or equal to x.

```expr
ceil(1.5) == 2.0
```

#### floor(n)

Returns the greatest integer value less than or equal to x.

```expr
floor(1.5) == 1.0
```

#### round(n)

Returns the nearest integer, rounding half away from zero.

```expr
round(1.5) == 2.0
```

### Array Functions

#### all(array, predicate)

Returns **true** if all elements satisfies the predicate. If the array is empty, returns **true**.

```expr
all(a_list, {.Size < 280})
```

#### any(array, predicate)

Returns **true** if any elements satisfies the predicate. If the array is empty, returns **false**.

#### one(array, predicate)

Returns **true** if *exactly one* element satisfies the predicate. If the array is empty, returns **false**.

```expr
one(participants, {.Winner})
```

#### none(array, predicate)

Returns **true** if *all elements does not* satisfy the predicate. If the array is empty, returns **true**.

#### map(array, predicate)

Returns new array by applying the predicate to each element of the array.

```expr
map(a_list, {.Size})
```

#### filter(array, predicate)

Returns new array by filtering elements of the array by predicate.

```expr
filter(users, .Name startsWith "J")
```

#### find(array, predicate)

Finds the first element in an array that satisfies the predicate.

```expr
find([1, 2, 3, 4], # > 2) == 3
```

#### findIndex(array, predicate)

Finds the index of the first element in an array that satisfies the predicate.

```expr
findIndex([1, 2, 3, 4], # > 2) == 2
```

#### findLast(array, predicate)

Finds the last element in an array that satisfies the predicate.

```expr
findLast([1, 2, 3, 4], # > 2) == 4
```

#### findLastIndex(array, predicate)

Finds the index of the last element in an array that satisfies the predicate.

```expr
findLastIndex([1, 2, 3, 4], # > 2) == 3
```

#### groupBy(array, predicate)

Groups the elements of an array by the result of the predicate.

```expr
groupBy(users, .Age)
```

#### count(array, predicate)

Returns the number of elements what satisfies the predicate.

Equivalent to:

```expr
len(filter(array, predicate))
```

#### join(array\[, delimiter])

Joins an array of strings into a single string with the given delimiter. If no delimiter is given, an empty string is used.

```expr
join(["apple", "orange", "grape"], ",") == "apple,orange,grape"
join(["apple", "orange", "grape"]) == "appleorangegrape"
```

#### reduce(array, predicate\[, initialValue])

Applies a predicate to each element in the array, reducing the array to a single value. Optional `initialValue` argument can be used to specify the initial value of the accumulator. If `initialValue` is not given, the first element of the array is used as the initial value.

Following variables are available in the predicate:

* `#` - the current element
* `#acc` - the accumulator
* `#index` - the index of the current element

```expr
reduce(1..9, #acc + #)
reduce(1..9, #acc + #, 0)
```

#### sum(array)

Returns the sum of all numbers in the array.

```expr
sum([1, 2, 3]) == 6
```

#### mean(array)

Returns the average of all numbers in the array.

```expr
mean([1, 2, 3]) == 2.0
```

#### median(array)

Returns the median of all numbers in the array.

```expr
median([1, 2, 3]) == 2.0
```

#### first(array)

Returns the first element from an array. If the array is empty, returns `nil`.

```expr
first([1, 2, 3]) == 1
```

#### last(array)

Returns the last element from an array. If the array is empty, returns `nil`.

```expr
last([1, 2, 3]) == 3
```

#### take(array, n)

Returns the first `n` elements from an array. If the array has fewer than `n` elements, returns the whole array.

```expr
take([1, 2, 3, 4], 2) == [1, 2]
```

#### sort(array\[, order])

Sorts an array in ascending order. Optional `order` argument can be used to specify the order of sorting: `asc` or `desc`.

```expr
sort([3, 1, 4]) == [1, 3, 4]
sort([3, 1, 4], "desc") == [4, 3, 1]
```

#### sortBy(array, key\[, order])

Sorts an array of maps by a specific key in ascending order. Optional `order` argument can be used to specify the order of sorting: `asc` or `desc`.

```expr
sortBy(users, "Age")
sortBy(users, "Age", "desc")
```

### Map Functions

#### keys(map)

Returns an array containing the keys of the map.

```expr
keys({"name": "John", "age": 30}) == ["name", "age"]
```

#### values(map)

Returns an array containing the values of the map.

```expr
values({"name": "John", "age": 30}) == ["John", 30]
```

### Type Conversion Functions

#### type(v)

Returns the type of the given value `v`. Returns on of the following types: `nil`, `bool`, `int`, `uint`, `float`, `string`, `array`, `map`. For named types and structs, the type name is returned.

```expr
type(42) == "int"
type("hello") == "string"
type(now()) == "time.Time"
```

#### int(v)

Returns the integer value of a number or a string.

```expr
int("123") == 123
```

#### float(v)

Returns the float value of a number or a string.

#### string(v)

Converts the given value `v` into a string representation.

```expr
string(123) == "123"
```

#### toJSON(v)

Converts the given value `v` to its JSON string representation.

```expr
toJSON({"name": "John", "age": 30})
```

#### fromJSON(v)

Parses the given JSON string `v` and returns the corresponding value.

```expr
fromJSON('{"name": "John", "age": 30}')
```

#### toBase64(v)

Encodes the string `v` into Base64 format.

```expr
toBase64("Hello World") == "SGVsbG8gV29ybGQ="
```

#### fromBase64(v)

Decodes the Base64 encoded string `v` back to its original form.

```expr
fromBase64("SGVsbG8gV29ybGQ=") == "Hello World"
```

#### toPairs(map)

Converts a map to an array of key-value pairs.

```expr
toPairs({"name": "John", "age": 30}) == [["name", "John"], ["age", 30]]
```

#### fromPairs(array)

Converts an array of key-value pairs to a map.

```expr
fromPairs([["name", "John"], ["age", 30]]) == {"name": "John", "age": 30}
```

### Miscellaneous Functions

#### len(v)

Returns the length of an array, a map or a string.

#### get(v, index)

Retrieves the element at the specified index from an array or map `v`. If the index is out of range, returns `nil`. Or the key does not exist, returns `nil`.

```expr
get([1, 2, 3], 1) == 2
get({"name": "John", "age": 30}, "name") == "John"
```

### Random Number Generation

Critical moments provides 4 methods for random number generation.

All 4 return a 63-bit non-negative integer. Typically you want to use modulo operator (%) on the result of these functions. Example condition which returns true 5% of the time `rand() % 100 < 5`

#### rand()

rand() returns a random number. It returns a new random number every time it is invoked.

#### sessionRand()

sessionRand() returns a random number which is stable for the duration of this session (held in memory until app restarted).

#### stableRand()

stableRand() returns a random number that is stable for the lifetime of this app install. It is generated on first use, and stored to disk. It's useful if you want to have a random number consistently return the same value for a user (example: opting into an AB test).

#### randForKey(stringSeed, intSeed)

randForKey returns pseudo random number given a key seed, and int seed. It returns the same number if provided the same inputs. It's useful in a number of cases:

* For generating a stable random number across many devices using an input string or int: `randForKey('', user_id)` or `randForKey(user_email, 0)`
* For opting into AB tests, but shuffling for different experiments. For example, `randForKey('experiment1', stableRand()) % 100 == 0` and `randForKey('experiment2', stableRand()) % 100 == 0` will each return a consistent random int over time, but a different one than the other experiment.
* A combination of use cases (stable across device, and shuffling per use case): `randForKey(user_email + 'experiment1', 1)`

### Version Number Operators

Critical moments has a built in condition functions to help you compare version numbers.&#x20;

{% hint style="info" %}
Typically you don't want to use string comparisons on version numbers except for exact match checks. The check `"11.0" > "2.0"`will evaluate to false with string comparisons since "1" < "2".
{% endhint %}

#### Version Number Format

Version numbers are represented as strings of semantic version numbers:

* The simpliest format is `1.2.3`. This is our prefered style, and is used for built-in properties. There's no limit to the number of sub-versions you include.
* You can optionally prefix them with a v: `v1.2.3`&#x20;
* You can optionally postfix them with a dash followed by text `v1.2.3-beta`

#### versionGreaterThan , versionLessThan , versionEqual

These three functions can be used to compare 2 version number strings, and return a boolean result.

Version numbers with a postfix are considered less than otherwise equal version numbers without a postfix (v2.1-beta < v2.1), but we don't compare the values of the postfix strings (v2.1-alpha == v2.1-beta == v2.1-helloworld).

{% hint style="warning" %}
All three functions return false if passed an invalid version number. Be sure to pick the right operator to fail as intended if your version number strings might be nil or invalid.
{% endhint %}

<details>

<summary>API Reference: <code>versionGreaterThan(a,b)</code> , <code>versionLessThan(a,b)</code> , <code>versionEqual(a,b)</code></summary>

#### Parameters

* a **\[string]**: a version number string
* b **\[string]**: a second version number string

#### Return Value

Boolean result of comparing a to b. For example `versionGreaterThan('v1.1', 'v1.0')` returns true. False if either string is not a valid version number.

</details>

<details>

<summary>Examples</summary>

* `versionGreaterThan(os_version, '16')` returns true if they are running OS version > 16.0, excluding 16.0 exactly
* `!versionLessThan(os_version, '16')` returns true if they are running OS version >= 16.0, including 16.0 exactly (note the not/!)
* `versionLessThan(app_version, '2.0')` the app version is less than 2.0
* `versionEqual(os_version, '16.4.1')` they are running exactly OS 16.4.1
* `versionGreaterThan('2.0+invalid', '1.0')` returns false since passed an invalid version number

</details>

#### function versionNumberComponent(versionString, componentInt)&#x20;

This function can be used in condition strings to extract individual components of a version number as integers. For example `versionNumberComponent("1.2.3", 1)` returns `2`.

<details>

<summary>API Reference: versionNumberComponent(versionString, index)</summary>

#### Parameters

* versionString **\[string]**: a version number string
* index **\[int]**: the index of the component to extract

#### Return Value

This function returns an integer if the versionString is valid, and the index exists in the version string. Otherwise it returns nil.

</details>

<details>

<summary>Examples of versionNumberComponent</summary>

* `versionNumberComponent("1.2.3", 0)` returns `1`
* `versionNumberComponent("1.2.3", 2)` returns `3`
* `versionNumberComponent("1.2.3", 4)` returns `nil`
* `versionNumberComponent("1.2.3.invalid_version", 0)` returns `nil`
* `versionNumberComponent(os_version, 0) >= 15` - true if the operating system version is v15 or over, regardless of sub-versions
* `versionNumberComponent(os_version, 0) >= 16 || (versionNumberComponent(os_version, 0) == 15 && versionNumberComponent(os_version, 1) ?? 0 >= 2)` - true if the operating system version is v15.2 or greater. Uses the nil coalescing operator `??` in case the version string is simply "15".

</details>

#### Exact version checks

In some cases, like checking for a known buggy version number, exact string comparisons may  be more succinct than `versionEqual(a,b)`. Examples:

* `os_version == "16.4.1"` returns true if the os version is exactly 16.4.1 (string comparison)
* `app_version in ["1.2.3", "2.3.1"]` returns true if the app version is in a set of exact strings

### Query Event History

#### eventCount(eventName)

Fetches the number of times an event has occurred from the events database.

#### eventCountWithLimit(eventName, limitInt)

Fetches the number of times an event has occurred from the events database, but the max count retuned will be limitInt. This provides a small performance improvement if checking a minimum count on a high volume event.

#### latestEventTime(eventName)

Returns the latest datetime when this event occurred.&#x20;

Function will return nil if that event was never fired. Example nil handling:

* Nil coalescing, using now as default value `(lastEventTime('event_name') ?? now())`&#x20;
* Ternary operator: `lastEventTime('event_name') == nil ? false : lastEventTime('event_name') < now() - duration('1h')`

#### propertyEver(propNameString, value)

Returns a boolean indicating if a property has ever had a given value, using a local database tracking prior observed values.&#x20;

The value can be a string, int, float, bool or date. The value must match the named property's type or it will return false.

For performance reasons, not all built in properties are tracked in the history, and they may sample at different times (app startup for some, lazily on access for others). Check this works for your use case locally before deploying.

### Checking Installed Apps

#### canOpenUrl(urlString)

This function returns a boolean indicating if this app can open the provided url, which is useful for checking if other apps are installed. For example `canOpenUrl('spotify:')` returns true if Spotify is installed.&#x20;

{% hint style="warning" %}
Non-standard URL schemes [need to be included in your app's info.plist](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/plist/info/LSApplicationQueriesSchemes). For URLs schemes not included, this function will return false.&#x20;
{% endhint %}

### Predicate

The predicate is an expression. It takes one or more arguments and returns a boolean value. To access the arguments, the `#` symbol is used.

```expr
map(0..9, {# / 2})
```

If items of the array is a struct or a map, it is possible to access fields with omitted `#` symbol (`#.Value` becomes `.Value`).

```expr
filter(a_list, {len(.Value) > 280})
```

Braces `{` `}` can be omitted:

```expr
filter(a_list, len(.Value) > 280)
```
