TransForm Programming Language Overview

Description

Introduction to the TransForm Programming Language, TPL, along with explanations of the data model, expressions, statements, etc.

Overview

The TransForm Programming Language (TPL) is a means by which a form designer can specify the behavior of a TransForm form as the user fills it in.

TPL is a traditional programming language turned to the needs of TransForm forms. While not identical to any other programming language, its syntax and semantics should be familiar and easily learned by developers familiar with languages such as Basic, JavaScript, Java, and C.

TPL code is used in TransForm in two main ways: Standalone expressions to calculate values for display or condition testing and multi-line functions to respond to events such as field value changes or to be called in expressions or other functions.

This documentation gives a simple introduction to TPL, followed by more detailed explanations of various aspects of the language. The different sections are:

Data Types

The types of data items and organization manipulated by TPL code.

Data Model

The particular data structures available to TPL code, including form data and local variables, and how it is accessed.

Expressions

The operators and operands that are used for computation.

Statements

The types of statements, including those for looping, testing conditionals, and flow-control.

Functions

Using built-in and user-defined functions in expressions.

Execution

The times when TPL code is executed and the restrictions sometimes imposed.

Built-in Functions

An overview of the functions built-in to TPL.

Why TPL?

Why an application-specific language was used instead of a popular, existing one.

Data Types

There are a few different types of data that are handled by TPL code. These include individual values, such as strings of text, as well as groupings of data, such as records with fields and datagroups with one or more items each with their own group of fields and/or other datagroups. There is also the type of data returned from database queries and REST calls organized in JSON format. JSON format has values (strings, numbers, and true/false/null), objects, and arrays. Form data is stored as JSON, with datagroups being arrays of objects, and most field values stored as strings. TPL provides means for working with these different data types.

Most data used by TPL code is either text data, including text representations of numeric and date/time values, or JSON-style objects (including arrays). Operators and built-in functions are provided for working with this data. For example "+" is a binary operator that results in a text representation of the numeric sum of two values that are, or can be converted to, numbers, and "&" is a binary operator that results in a text value that is the concatenation of two values that are accessed as text strings. JSON data that has other types of values (such as numeric, boolean, and null) are interpreted in an appropriate way by operators.

In expressions and statements, numeric constants are represented as integers or decimal values. Text constants are enclosed in double-quote (") characters. Within the double-quotes, a double-quote may be represented by a backslash ("\") followed by a double-quote. Other escapes are "\n" for embedded newline, "\t" for a tab character, and "\\" for a backslash.

Date/time values in TransForm are usually represented by text in the form of "YYYY-MM-DD hh:mm:ss.s". This is a locale-independent "year/month/day hours:minutes:seconds" form that when sorted alphabetically also sorts by chronological time. The full-time, seconds, or fractional seconds values are optionally present. The time zone is assumed to be that where the device is running. Built-in functions are provided to facilitate conversion to and from other forms, such as "seconds since a particular date".

True/False values in TransForm calculations are usually represented by a blank value ("") for false, and "1" or any other non-blank value for true. The IF statement, and the "!", "||" and "&&" operators, all treat "" as false and all other values as true. The expression "4 < 5" results in "1" and "4 > 5" results in "". Note that the values of some form fields may need to be interpreted differently and they may require explicit text value testing, such as checking for "Yes" for true and "No" for false, or setting to those values as appropriate for the results of a calculation.

An example of some data is the following (expressed as JSON):

{
   "field1": "Value A",
   "field2": "Value B",
   "array1":
      [
         {
            "x": 10,
            "y": 20
         },
         {
            "x": 15,
            "y": 45
         }
      ],
   "obj1":
      {
         "a": "Some text"
      }
}

TPL code can access that data using the following syntaxes:

name

A name accesses that name/value pair at top level. For example, "field1" refers to the name value pair with value "Value A" and "array1" refers to the name/value pair with a value of the array with two elements. Names should generally start with an alphabetic character or "_".

objectValue.name

A "." between an object value (such as the name of an object) and a name accesses the name/value pair within the object. For example, "obj1.a" would have a value of "Some text".

The objectValue must evaluate to a reference to an object. If the objectValue refers to an element in an array that is not already defined within an existing array, then that element will be defined with an empty object as its value when used with the "." syntax. If the objectValue refers to an object and the name refers to a name that is not already defined within the object, then that name will act as if it is undefined when used. If the objectValue refers to an undefined name/value pair within a parent object, that name will be defined in the parent object with an empty object. This means that intermediate objects and array items are automatically defined when part of the path to a name/value pair. For example:

a = obj()  ' new, empty object
b = a.x    ' b gets "", a.x stays undefined
c = a.x.y  ' c get "", a.x is empty object
arrayValue[index]

An integer value enclosed by brackets after an array value accesses the element of the array specified by the index (0-origined). If it is an array of objects, the value is an object. If it is an array of scalar values (strings, numbers), the value is that scalar value. For example, "array1[1]" would access the element with and object with x=15 and y=45. If the element does not exist (index is greater than the length of the array minus 1) then the value is "".

If the index is negative, then it specifies an element of the array counting from the last element (-1 is the last element, -2 is the second from the last, etc.). If the index specifies an element before the first element of the array, then the value is the first element.

objectValue[nameAsString]

A text value enclosed by brackets after an object value accesses the name/value pair within the object. For example, "obj1["a"]" would have a value of "Some text".

The "nameAsString" may be either a string constant (characters surrounded by double-quotes) or a value that results in text.

stringValue[index]

An integer value enclosed by brackets after a string (text) value results in the single character at that position in the string (0-origined). For example, "field1[3]" would be the letter "u".

If the index is negative, then it specifies the character in a position counting from the end of the string, with -1 being the last character. For example, "field1[-3]" would be the letter "e".

Indexes specifying characters past either end of the string will return "".

Other examples are:

  • Example 1

    Assigning variable a to the value of b.

    a = b
  • Example 2

    Assinging variable a to the value of the c property of the b object.

    a = b.c

Data Model

There are several classes of data that a TPL program needs to access. These include the data fields of the form being edited, information about the form (such as its status), and temporary data to be used as part of computing various results. TPL provides means for explicitly referring to each of the different classes of data.

Each of the different classes of data are organized as JSON-style data. That is, they are an object, with name/value pairs. The values may be text, objects, or arrays of objects.

To better understand the different classes, it helps to look at the "lifecycle" of filling out or updating form data and the different times in that lifecycle when TPL code is executed. We'll call this the Form Editing Lifecycle.

The Form Editing Lifecycle starts when a form (also known as a "form instance") is chosen from the Existing Forms List in the Form Filler and opened for editing by either double-tapping or using the Edit command in the Main Control Bar above the list. This will bring up the Form Details Screen which shows the contents of that form instance. The Form Details Screen shows some or all of the form's fields and their current values, optional descriptive and help information, and buttons for displaying "pages" with additional fields and performing certain operations such as uploading and submitting the form to the server.

Before the form is opened for editing, optional "ON *LOAD" TPL code may be executed.

Each time the Form Details Screen is displayed, as part of assembling that display TPL code may be executed to fill in placeholders within various blocks of text. A placeholder consist of a standalone TPL expression enclosed within brace characters ("{" and "}") within the text. For example, the text of a heading may include the placeholder "{field1}" which would be replaced by "Value A", or it may include "{len(field1)}" which would be replaced by "7".

As part of assembling the Form Details Screen, there may be IF commands as part of the form type definition to conditionally show and hide parts of the form. (Note: These are form IF commands in the form type's Commands List, not TPL IF statements in TPL code.) Those IF commands include a standalone IF TEST TPL expression that is evaluated each time the screen is reassembled. If that expression is True (any value other than "") then the commands up until an ELSE or ENDIF are executed to display fields, etc. Otherwise, those intermediate commands are skipped.

In addition to IF TEST expressions, some other form commands allow the use of Form Display Expressions as part of rendering their display and operation. The type of value needed and the way in which that value is used depends upon the command. For example, a List field may optionally obtain the list of items to display from a calculated array of objects instead of from a fixed list entered at the time the command was created by the form designer.

When certain editors are opened by the user to edit a field value, such as by tapping on a List Field, or by tapping the "Next" or "Previous" button in another editor to change to the editor, optional "ON *EDITOR" TPL code may be executed.

When a field's value is changed by the user, such as by typing in a new value and tapping the Confirm, Next Field, or Previous Field button, optional "ON *CHANGED" TPL code may be executed.

When certain buttons that are part of the form are tapped, optional ON *Button Execution TPL code associated with that button is executed.

Finally, when the form is finished being edited, and the display is being changed back to the Existing Forms List, optional "ON *FINISHED" TPL code may be executed.

See the Functions section of this Help for more specifics of the "ON *Events".

During the Form Editing Lifecycle, there are certain classes of data that relate to the particular form instance being edited. These classes of data are:

Form Data

Form Data is the JSON object that contains the current values of all of the form's fields. Within this object may also be arrays of objects for data groups that are part of the form, such as information about multiple photos or line items.

Group Data

Group Data is a JSON object that contains the current values of just a single item within a Data Group. Since there can be more than one item in a Data Group, this provides a simple way to refer to related fields within that item. The particular Data Group is the one that contains the field being edited, the text placeholder associated with the heading being evaluated, any IF commands being evaluated, etc. Group Data includes any sub-data groups within the item. For TPL code running for ON *LOAD and ON *FINISHED and when there are no enclosing data groups associated with the TPL code, Group Data is the same as Form Data; that is, it refers to all of the form's fields and arrays of data.

Metadata

Metadata is made up of some of the metadata fields for the form instance plus some other values. This includes the current status value for the form, the date/time created, the user ID (an email address) of the person assigned this form, and more.

In addition to the form instance data, there are other classes of data:

Local Data

Local Data is an object that may be used to hold temporary values needed during execution of a single TPL function. For example, the name/value pair named "i" could be used to hold the counter in a FOR loop and one named "result" could be used to hold the result object from an Ajax call (with "result.error" indicating any errors and "result.responseText" containing the server response as text). A different instance of the Local Data object is created for each function invocation or standalone expression. So, for example, a recursive function would have a new instance for each call, with the previous instance restored on return.

Global Data

Global Data is an object that is persistent throughout Form Editing Lifecycle. A different instance is created each time the lifecycle starts. The object may be used to hold temporary values that need to be shared among different TPL code functions and standalone expressions.

System Data

System Data is an object that exposes various values representing the current state of the Form Filler. This includes the current user ID and display name, account ID and display name, the "path" of data groups and objects starting at top-level Form Data and ending in Group Data, and more.

The first characters of a name specify which of the different classes of data is being referred to in TPL code:

LocalData

References to plain names (starting with an alphabetic character or "_") are assumed to refer the current function's temporary Local Data. Local Data values may be scalar values, like text, or arrays or objects. Local Data is an empty object when a function starts executing. New name/value pairs may be created by assigning to the name. See the description of assignment statements in the section on Statements and the sample code examples.

^GlobalData

References to names preceded by a single "^" character are assumed to refer the current Form Editing Lifecycle's Global Data. Global Data values may be scalar values, like text, or arrays or objects. Global Data is an empty object at the start of editing a form instance. New name/value pairs may be created by assigning to the name. See the description of assignment statements in the section on Statements and the sample code examples.

#FormData

References to names preceded by a single "#" character (e.g., "#field1") are assumed to refer the current Form Data. The entire form data object is available, e.g., "#array[1].y" would refer to the value of "y" in the second item in data group "array1".

##GroupData

References to names preceded by two "##" characters (e.g., "##x") are assumed to refer the current Group Data.

$#Metadata

References to names preceded by the two characters "$#" (e.g., "$#status") are assumed to refer the current form Metadata. The metadata is presented as an object with name/value pairs representing copies of form instance data. (This means that assigning to them does not change the actual value in the form.) The metadata items are:

$#accountid

The account ID associated with the form instance.

$#created

The date/time the form instance was created.

$#comments

The representation of the JSON for the comments associated with the form instance.

$#completed

The date/time when the status was last changed.

$#duedate

The value of the field in a form instance that may be used to specify a date. This value is used by the Form Filler Filtering functionality. It should either be blank or a date in the form of "yyyy-mm-dd", such as "2019-04-19". Like the other metadata fields, this is a copy. Assigning values to it does not update the actual form instance record. To update the value, use the metaDataSet() function. It may also be read using the metaDataGet() function.

$#formid

The Form ID of the form type being applied to the form data.

$#forminstanceid

The unique ID of the form instance.

$#formversion

The not currently used. If form IDs have versions, the version used for the data will be here to allow for modified versions to be handled.

$#haserrors

The flag that indicates whether or not an "error" has be set for the form instance. This is "Y" if true, "" if false.

$#missingrequired

The flag that indicates whether or not a "required" field has been detected that has no value. This is "Y" if true, "" if false.

$#nofiller

The value of the "no filler" setting for the form instance (set, for example, using the Management Console). The value is "Y" if true, and "N" or "" if false. This should usually be "N" or "" in the Form Filler.

$#person

The User ID of the person assigned this form instance, usually an email address and usually the logged in user. See the System Data $username and $userdisplayname values.

$#status

The current status of the form instance.

$#timestamp

The date/time of when this form instance was last inserted or updated in the server database. Blank if not yet saved.

$#user1 $#user2 $#user3 $#user4 $#user5 $#userlabel1 $#userlabel2 $#userlabel3 $#userlabel4 $#userlabel5

The value of the extra fields in a form instance reserved for use as determined by an account's administration. Like the other metadata fields, these are copies. Assigning values to them does not update the actual form instance record. To update the value, use the metaDataSet() function. They may also be read using the metaDataGet() function, which allows you to use a variable to specify the specific field.

$SystemData

References to names preceded a single "$" character (e.g., "$username") are assumed to refer the current form System Data. The data is presented as an object with name/value pairs representing copies of the various values. (This means that assigning to them does not change the original value in the system.) The System Data items are:

$accountdisplayname

The display name for the account the user is currently logged into.

$accountid

The account ID that the user is currently logged into.

$editfieldname

The name of the field variable when running as "ON *editor" and "ON *changed". Otherwise, "".

$formdata

The Form Data object. The same as what you get from "#" except that you can also use brackets after it to access top-level fields using a text value in a variable. The values within the object are read/write and changing them does change the form's data.

$formdisplayname

The display name for the current form's form type.

$formid

The form ID for the current form's form type.

$groupcount

The number of items in the current Group Data's parent array. See the related system data values $groupindex, $groupname, and $grouppath.

$groupdata

The Group Data object. The same as what you get from "##" except that you can also use brackets after it to access top-level fields within the data group using a text value in a variable. The values within the object are read/write and changing them does change the form's data.

$groupindex

The index (0-origined) of the current Group Data within its parent array. See the related system data values $groupcount, $groupname, and $grouppath.

$groupname

The Name command property of the data group containing the current Group Data in Form Display Expressions. The Array Name of the data group containing the current Group Data in most other cases.

$grouppath

A specification of the explicit "path" from Form Data to Group Data. This is an array of text and numeric values. The text values specify the names of the parent objects or arrays and the numeric values specify the element indexes (0-origined) within arrays. For example:

["array1",1]

would be the value of $grouppath if array1[1].x was being edited and array1[1] was the Group Data.

It is assumed that code using $grouppath is written knowing the general structure of the fields, objects, and arrays in the path for that form type. The $grouppath value is useful, for example, to access fields in a Group Data's parent or to make use of the index(es) of Group Data in its parent(s).

Related system data values are $groupcount, $groupindex, and $groupname.

$haserrors

The flag that indicates whether or not an "error" has be set for the form instance. This is "Y" if true, "" if false.

$language

The selection of which language to use when multi-languague support is available for an account's forms.

$languages

An object with information about the languages that may be supported by form types for this account, with name/value pairs specifying the short name and display name.

$lastdefsdownloadtime

The date/time when form type definitions were last downloaded from the server for this account. If none downloaded, this will be blank.

$lastrefresh

The date/time when all form instances were last downloaded (or refreshed) from the server for this account. If downloading was not done, this will be blank.

$lastsync

The date/time when any forms modified or created since last login were last uploaded to the server for this account. If no sync has occurred, this will be blank.

$missingrequired

The flag that indicates whether or not a "required" field has been detected that has no value. This is "Y" if true, "" if false.

$nowait

The flag that indicates whether execution show avoid situations that will require waiting for completion. This is "1" if true (do not wait), "" if false (waiting is permitted). See the description of "Execution" in this language documentation.

$userdisplayname

The display name for the currently logged in user.

$username

The user ID for the currently logged in user, usually an email address.

Expressions

Expressions in TPL are similar to most computer languages. They are a combination of explicit constant values, references to variable values, operators, and functions. An expression is evaluated by applying the operators and functions to the values and any intermediate values, resulting in the value of the expression itself. The order of execution of the operators is determined by certain rules which include the use of parenthesis to override the defaults.

An expression may consist of a single value, such as just the number "5" or a reference to a field like "field1", and not involve any operators or functions.

The value of an expressions may be a simple, scalar value, such as text, or it may be a reference to an JSON-style object or array.

Expressions are used in TransForm for a variety of purposes. They are used by IF commands in form type definitions to control hiding and showing parts of the form. They are used in placeholders within various blocks of text to, for example, provide text that varies depending upon the value of a field. They are used in statements that make up programs executed to respond to events such as field editing.

The operators are (in decreasing order of precedence):

+ - !

Unary plus, minus, and "not". Plus and minus attempt to convert the value to a number, or else 0. Plus results in the number as text. Minus results is the negative of the number as text. (minus of 1 is -1, minus of -1 is 1). "Not" treats the value as text and returns the text value "1" (true) if the value is blank and "" (false) otherwise.

* /

Multiply and divide. Both attempt to convert the value to their left and the value to their right to a number, or else 0. Multiply results in the product of the two. Divide results in the division of the left by the right. The results are text representations of the result values. For example:

2.00 * 3.00

will result in the text value "6". To control precision or formatting, use functions like round() or formatNumber().

+ - &

Add, subtract, and concatenate.

Add and subtract attempt to convert the value to their left and the value to their right to a number, or else 0. Add results in the sum of the two. Subtract results in subtracting the right from the left. The results are text representations of the result values. For example:

1.00 + 2.00

will result in the text value "3". To control precision or formatting, use functions like like round() or formatNumber().

Concatenate converts the value to its left and right to text. The result is the left value followed by the right. For example:

"abc" & "def"

will result in the text value "abcdef".

< <= >= >

Less than, less than or equal to, greater than or equal to, and greater than. These comparison operators attempt to convert the value to their left and the value to their right to a number ("" is treated as zero), or else they are left as text. If both values are numeric then the test is a numeric comparison. If either value is not convertible to a number then the test will be the text comparison of the two values converted to lower-case. The result is the text value "1" for true and "" for false. For example:

15 > 7

will result in the text value "1" (true), and:

"abc"> "ABC"

will result in the text value "" (false) because they are equal when converted to lowercase.

== != === !==

Equal, not equal, text equal, and text not equal.

The equal and not equal comparison operators attempt to convert the value to their left and the value to their right to a number ("" is treated as zero), or else they are left as text. If both values are numeric then the test is a numeric comparison. If either value is not convertible to a number then the test will be the text comparison of the two values converted to lower-case. The result is the text value "1" for true and "" for false. For example:

15 == "15.00"

will result in the text value "1" (true), and:

"abc" != "ABC"

will result in the text value "" (false) because they are equal when converted to lowercase.

Text equal and text not equal comparison operators treat both values as text and are case-sensitive. The result is the text value "1" for true and "" for false. For example:

15 === "15.00"

will result in the text value "" (false), and:

"abc" !== "ABC"

will result in the text value "1" (true).

&& ||

"And", and "or".

"And" and "or" attempt treat the values to their left and right as false ("") or true (non-blank). The results are either "1" (true) or "" (false). "And" is true if both values are true. "Or" is true if either value is true. For example:

2 < 3 && 4 < 3

will result in "" (false), while:

2 < 3 || 4 < 3

will result "1" (true).

Statements

A function written in TPL is made up of one or more statements. Each statement goes on its own line.

For example, this is an assignment statement followed by an IF statement, another assignment statement, and an ENDIF statement:

a = len(b)
IF a > 10
  c = "long"
ENDIF

To aid readability, statements may be extended to more than one line by ending a line with a "\" character. The newline character after the "\" (and the "\") will be ignored. Additionally, to provide for comments, any characters after an apostrophe (') up until the end of the line will be ignored.

This example uses both a continued line and a comment:

IF a >= 10 && \
   a < 20 ' See if a is within range

There are three types of statements: Expression Statements, Assignment Statements, and Keyword Statements.

An Expression Statement is just a plain expression on a line by itself. Normally, when used as part of multi-line code, this type of statement would be a function call for the purpose of the side-effects of the call. (Since normal operators like "+" and "-" have no side-effects, and the value of expression statements are not used once executed, statements like "2 + 2" are relatively meaningless.)

An example of an expression statement, which uses the built-in function "showWarning", is:

showWarning("Value \"" & #field1 & "\" is out of range. Please try again.")

An Assignment Statement is similar to assignment statements in many other computer languages. It consists of a left-side reference which will receive a value, as assignment operator, and a right-side expression. The right-side expression is evaluated and then the left-side reference is used by the assignment operator.

Some examples of assignment statements are:

' Set value of local name v1 to "4":
v1 = 2 + 2

' Set field z of the 1st item in data group array1 to "red":
#array1[0].z = "red"

The left-side must evaluate to a reference to a name in a name/value pair of an object or an item in an array. It may not be a plain value, such as a text or numeric constant, or an expression with operators. If it refers to a name that is not already defined within an object, then that name will be defined. If it refers to an element in an array that is not already defined within an existing array, that element will be defined.

There are four different assignment operators:

a = b

Assigns the value of the expression on the right to the reference on the left. Note that the expression on the right may be a scalar value or a reference to an object or array. If it's a reference, the reference to the object is used.

For example:

a = 2 + 2      ' Sets local name a to "4"
b = array1[0]  ' Sets b to an object with x=10 and y=20
b.x = 5        ' array1[0].x is now "5"
c = array1     ' Sets c to an array
c[0].y = 6     ' array1[0].y is now "6"
a += b

Adds the value of the expression on the right to the current value referenced on the left and then assigns that sum to the reference on the left.

For example:

a = 1   ' Sets local name a to "1"
a += 3  ' Sets a to "4"
a -= b

Subtracts the value of the expression on the right from the current value referenced on the left and then assigns that difference to the reference on the left.

For example:

a = 10  ' Sets local name a to "10"
a -= 3  ' Sets a to "7"
a &= b

Concatenates the current text value referenced on the left with the text value of the expression on the right and then assigns that combination to the reference on the left.

For example:

a = "test"  ' Sets local name a to "test"
a &= "ing"  ' Sets a to "testing"

A Keyword Statement is a mixture of one or more keywords and zero or more expressions. The keywords are case-insensitive text which are distinguished from the same letters used as field, variable, and function names by the syntax of the statement. Some keyword statements consist of a single, standalone keyword, such as "ENDIF" while others intersperse keywords and expressions, such as "RETURN 15". The style of this documentation is to show the keywords in all caps, but lower-case or mixed-case may be used if desired.

The statements for use within multi-line user code are:

IF condition ELSE ELSEIF condition ENDIF

An IF statement is used to execute or skip other statements depending upon the results of a conditional expression. It works in conjunction with an ENDIF statement and optional ELSE and ELSEIF statements which follow it.

The initial IF keyword is followed on the line by an expression. If the expression is true (not "") then the statements immediately following the IF statement are executed up until an ELSEIF or ELSE statement is encountered. Then all statements are skipped until an ENDIF is encountered.

If the IF condition expression is false ("") then the statements immediately following the IF statement are skipped until either an ELSEIF or ELSE statement is encountered. If an ELSEIF is encountered, then the conditional expression following that keyword on the line is executed and the process starts again. If an ELSE is encountered, then all statements following it are executed. If an ENDIF is encountered execution resumes.

An IF statement may be followed by any number of ELSEIF statements, but only one ELSE statement. All ELSEIF statements must precede the ELSE statement, if present. There must be a matching ENDIF statement at the end of the statements whose execution is controlled by the IF, ELSEIF, and ELSE statements.

IF statements may be nested. That is, the statements between an IF statement and the ENDIF may include additional IF/ENDIF sequences, with each ENDIF corresponding to the closest open IF.

For example:

msg = "There "
IF count==0
  msg &= "are no items"
ELSEIF count==1
  msg &= "is one item"
ELSE
  msg &= "are " & count & " items"
ENDIF
FOR v TO e FOR v TO e STEP s CONTINUE EXITFOR ENDFOR

The FOR and ENDFOR statements bracket a series of statements and control the repeated execution of those statements. (This process is commonly known as "looping" through those statements.) In addition, FOR and ENDFOR control the repeated incrementing of the value of a Looping Variable that may be used to differentiate each pass through those statements and determine when to stop executing the loop.

The FOR statement includes the keyword FOR, Looping Value Expression "v", the keyword TO, Ending Value Expression "e", and the optional combination of keyword STEP with Step Value Expression "s".

The Looping Value Expression is executed before the first time through the loop. A Looping Value Expression is either an assignment statement whose left side will then be used as the Looping Variable, or an expression that results in something that could be on the left side of an assignment statement which will be used as the Looping Variable and already has a value.

The Ending Value Expression is executed before each loop execution. It is compared numerically to the Looping Variable's value. If the Looping Variable's value is less than or equal to the Ending Value Expression the statements between the FOR and ENDFOR statements are executed. If the Looping Variable's value is greater than the Ending Value Expression the statements up to and including the matching ENDFOR statement are skipped and execution will continue with the statement following the ENDFOR statement.

When the ENDFOR statement is encountered in execution, the Step Value Expression is evaluated. That value, the Step Value, is used to increment the Looping Variable's value. (If there is no STEP keyword, or if the value is zero or non-numeric, the Step Value is assumed to be 1.) The Looping Value test is then performed to determine whether execution of the loop is to continue or be ended. If execution is to continue, it continues with the first statement after the FOR statement. If execution of the loop is to be ended, execution continues with the first statement after the ENDFOR statement.

Note that if the Step Value is negative, the Looping Value is appropriately decremented and the ending test will be inverted so that Looping Values less than the Ending Value will end looping and Looping Values greater than or equal to the Ending Value will continuing looping.

Executing a CONTINUE statement in a FOR loop will end that trip through the loop and act as if an ENDFOR statement was encountered, incrementing and testing the Looping Value.

Executing an EXITFOR statement in a FOR loop will end that execution of the loop and skip to the statement following the FOR loop's ENDFOR statement.

For example:

' Calculate the sum of array1's x values
sum = 0
FOR i=1 TO len(array1)
  sum += array1[i-1].x
ENDFOR

' Reverse characters in a text value
txt = "Testing"
reverse = ""
FOR i=len(txt)-1 TO 0 STEP -1
  reverse &= txt[i]
ENDFOR
' Result is reverse="gnitseT"

Functions

Like most computer languages, TPL expressions may make use of functions. Functions are included within an expression in most places where a value may appear. They consist of a name followed by zero or more expressions (called "arguments") within parenthesis, separated from each other by commas. The function returns a result that may be further used in an expression.

For example, here is an example using the math function "pow()":

a = 3
b = pow(2,1+a)+1 ' 2*2*2*2+1 = 17

The returned value may be an object or array. That value may be saved and the name/value pairs or elements may be used.

For example:

' Assume this User-Defined Function:
FUNCTION @example1
  x = obj()     ' Set x to an empty object
  x.a = 1       ' Set name a in x to 1 
  x.b = 2       ' Set name b in x to 2
  return x
ENDFUNCTION

a = @example1() ' a gets an object
c = a.a + a.b   ' c gets "3"

There are two main classes of functions: Built-in Functions and User-Defined Functions. Built-in Functions are part of the TPL implementation. Their names always start with an alphabetic character. For example, "len" is the name of a function that returns the number of elements in an array value or characters in a text value. User-defined functions are written by the user of TPL (the person who defines form types and uses the TransForm Designer in TransForm Central) as a series of TPL statements bracketed by FUNCTION and ENDFUNCTION statements. The names of user-defined functions always start with the "@" character. This way, there is no chance that a person will create a user-defined function whose name will conflict with that of a built-in function that they do not know about or that is added to the system at a later point.

Some functions have side-effects that are specific to the TransForm system. For example, a function may set a value in system data, access a server on the Internet, or display a message on the screen. Sometimes functions like these may appear by themselves as an Expression Statement and only return a meaningless value, such as "". Other times they may return an object with information (including any error information) which would then need to be assigned to a variable for further processing.

The value of arguments may be text or other simple values, or they may be JSON-style objects including arrays. The invoked function may read the value of all arguments. The invoked function may also reference and set name/value pairs within object arguments and elements within array arguments.

User Functions may access their arguments using the "args()" function. The value of "args(1)" is the first argument (if present), "args(2)" the second, etc. The "argslen()" function returns the number of arguments for a particular call. The value of "args(0)" is the name of the function that was invoked.

For example:

' Assume this User-Defined Function:
FUNCTION @example2
  x = args(1) ' First argument
  y = args(2) ' Second argument (an object)
  return x + y.x
ENDFUNCTION

a = @example2(array1[0].x, array1[1]) ' a gets "25"

References that may appear on the left side of an assignment statement, sometimes called "L-Values" in Computer Science (with the "L" standing for "Left"), may be used as arguments. Some functions may set new values for those passed as L-Values as needed.

For example:

' Assume this User-Defined Function:
FUNCTION @example3
  a1 = args(1)          ' Copy original value
  args(1) = "New value" ' Assign new value
  return a1             ' Return original
ENDFUNCTION

a = "Old value"
b = @example3(a) ' b gets "Old value", a is "New value"

A special class of functions are those that handle the different "ON *Events": the ON Functions. Unlike regular User-Defined Functions, which are bracketed by FUNCTION and ENDFUNCTION statements, the ON Functions are bracketed by ON and ENDON statements. The names all start with the "*" character to prevent naming conflicts.

There are several classes of ON *Events, each with their own naming convention for ON Functions and their own arguments:

ON *LOAD

When a form is opened for editing, the ON *LOAD function, if there is one in the code, is invoked. The only argument is arg(0), the name. Any changes made to the form data by the ON *LOAD function will affect the displayed form.

One use of an ON *LOAD function is to load global data that is used elsewhere in the form. For example, loading data for use in List Expressions for List fields. List Expressions use No Wait Execution, so may not access databases or perform Ajax requests. ON *LOAD does not require No Wait Execution, so may access those forms of data and then store the results as a global data value.

In this example, the ^lastLoaded global data item is given a new value each time a form instance is opened for edit:

ON *LOAD
  ^lastLoaded = now()
ENDON

The ^lastLoaded could then, for example, be used in a TPL Template in the title of a field, or used to initialize a value in an ON *editor handler.

ON *FINISHED

When the user switches from edit a form back to the list of forms, the ON *FINISHED function, if there is one in the code, is invoked. The only argument is arg(0), the name. Any changes made to the form data by the ON *FINISHED function will affect the saved form.

In this example, the #lastDone field is given a new value each time a form instance is finished:

ON *FINISHED
  #lastDone = now()
ENDON

In addition to when the DONE button above the Form Details is tapped, the ON *FINISHED event happens when the user duplicates a form and when a Change Status button is pressed or the Change Status menu item on the Form Details Menu is used.

The ON *FINISHED event is not guaranteed to be invoked in all situations. For example, a user could not use the DONE button, terminated the app from running, and then restart, starting with the Forms List. The form data would probably be intact but the *FINISHED would not have been invoked.

ON *editor

When the user brings up a Form Editor to enter or modify field data, an ON *editor event occurs. This happens in both the case of switching from the Form Details as well as when switching from one editor to another.

To handle an ON *editor event, the code is searched for a matching ON Function. There are three types of ON Function names that will match.

First, a search is performed for an ON Function with a name in the form of "*editor_fieldpath". "fieldpath" consists of the Data Group array names of containing Data Groups, if any, followed by the field name, all separated by "," characters. For example:

ON *editor_field1
ENDON

ON *editor_array1,x
ENDON

If no ON Function is found with that name, a search is performed for an ON Function with a name in the form of "*editor_fieldname", where fieldname is the name of the field. For example:

ON *editor_field1
ENDON

ON *editor_x
ENDON

Finally, if no ON Function is found with that name, a search is performed for an ON Function with a name in the form of "*editorCatchAll". For example:

ON *editorCatchAll
ENDON

If an appropriate ON Function is found, it is invoked.

The args(0) value will be the first name searched (the one with the fieldpath). This lets a single On Function handle a variety of fields and distinguish among them when needed.

The args(1) value is an object whose name/value pairs depend upon the type of field. For most fields there is a "valueToEdit" value that provides the value of the field at the start of editing. That value may be changed, which will change what appears in the editor. For a List field, there is a "qList" value which is an array of objects that make up the list choices, each object with both "value" and "text" name/value pairs.

In this example, the #commatest Text field takes the displayed value and removes comma characters before displaying for edit (there is a companion ON *changed example below):

ON *editor_commatest
  s = args(1)
  v = s.valueToEdit
  v = replaceMatch(v, ",", "")
  s.valueToEdit = v
ENDON

In this example, the #requestName list takes the list choices in the form type definition and appends the contents of the #custID and #custName fields to the value and text, respectively:

ON *editor_requestName
  s = args(1)
  for i=0 to len(s.qList)-1
    q = s.qList[i]
    name = q.value
    name = #custName & " (" & #custID & ") " & name
    q.text = name
    q.value = name
  endfor
ENDON

In this case, if #custID was "456" and #custName was "Acme Inc.", and the field definition had text/values of both "Red", "Green", and "Blue", the displayed list would use "Acme Inc. (456) Red", "Acme Inc. (456) Green", "Acme Inc. (456) Blue".

(Note that List fields may use the "List Expression" Form Display Expression to set the text/value pairs used for displaying the value on the Form Details, but that expression uses No Wait Execution and, unlike ON *editor, may not access databases nor do Ajax requests. The ON *edit may do those things. The results may be saved in global data for use by the List Expression.)

ON *changed

When the user finishes using a Form Editor to enter or modify field data, and the value has changed, an ON *changed event occurs. This happens in both the case of switching back to the Form Details as well as when switching from one editor to another.

To handle an ON *changed event, the code is searched for a matching ON Function. There are three types of ON Function names that will match.

First, a search is performed for an ON Function with a name in the form of "*changed_fieldpath". "fieldpath" consists of the Data Group array names of containing Data Groups, if any, followed by the field name, all separated by "," characters. For example:

ON *changed_field1
ENDON

ON *changed_array1,x
ENDON

If no ON Function is found with that name, a search is performed for an ON Function with a name in the form of "*changed_fieldname", where fieldname is the name of the field. For example:

ON *changed_field1
ENDON

ON *changed_x
ENDON

Finally, if no ON Function is found with that name, a search is performed for an ON Function with a name in the form of "*changedCatchAll". For example:

ON *changedCatchAll
ENDON

If an appropriate ON Function is found, it is invoked.

The args(0) value will be the first name searched (the one with the fieldpath). This lets a single On Function handle a variety of fields and distinguish among them when needed.

The new value of the field may be accessed from the field data itself, and updated by setting the field data.

In this example, the #commatest Text field takes the edited value, rounds to an integer, and adds comma characters (there is a companion ON *editor example above):

ON *changed_commatest
  v = #commatest
  v = formatNumber(v, "#,##0]")
  #commatest = v
ENDON
ON *button

When the user taps a button on the form, an ON *button event occurs. This happens in both the Form Details as well as a button shown as an editor when switching from one editor to another.

To handle an ON *button event, the code is searched for a matching ON Function. There are three types of ON Function names that will match.

First, if the button is contained within a Data Group, a search is performed for an ON Function with a name in the form of "*button_actionname_fieldpath". The "actionname" is the button Action Name, and "fieldpath" consists of the Data Group array names of containing Data Groups separated by "," characters. For example:

ON *button_doValidate_array1
ENDON

ON *button_computeSubTotal_array1,subarray
ENDON

If no ON Function is found with that name, or if the button is not within a Data Group, a search is performed for an ON Function with a name in the form of "*button_actionname", where actionname is the button's Action Name. For example:

ON *button_doValidate
ENDON

Finally, if no ON Function is found with that name, a search is performed for an ON Function with a name in the form of "*buttonCatchAll". For example:

ON *buttonCatchAll
ENDON

If an appropriate ON Function is found, it is invoked.

The args(0) value will be the first name searched (the one with the fieldpath). This lets a single On Function handle a variety of buttons and distinguish among them when needed.

In this example, the button will check that #partID is in the correct form (two digits, a "-", and another digit), and then display the appropriate message:

ON *button_checkPartID
  r = matchOne(#partID, "^\\d\\d-\\d$")
  if r
    msgShowBasic("Validation Succeeded","")
  else
    msgShowWarning(\
      "Validation Error", 
      #partID & " is not valid.")
  endif
ENDON

Execution

TransForm executes various TPL expressions and sequences of statements throughout the Form Editing Lifecycle. TPL code is executed synchronously. That is, statements and expressions are executed one at a time, one after another. A function executes from start to finish, relinquishing execution to other code only when it calls that code explicitly as a function. During the time that the code is executing the user interface does not respond to any user input. There is no response to button taps until execution completes.

TPL code execution is usually quite quick so the delay in response is not perceptible to the user. However, there are two cases where the user may notice a delay.

The first case is when the code executes a large number of statements. While normal execution on common devices can run at hundreds of thousands of statements per second, with typical placeholder, IF TEST, ON *changed, and other code finishing in milliseconds or less, the form designer may specify code that takes a longer time (perhaps inadvertently). Care should be taken to avoid such situations if possible, such as by performing long calculations in advance at a time when a delay is more acceptable, such as ON *LOAD.

The other case where the user may notice a delay is when a built-in function accesses services that have an inherent delay. For example, programmatic interactions over the Internet have communications and server delays due to latency, bandwidth, and load. On-device databases may have delays because of complex queries on large data sets.

In this second case, TransForm makes the delay visible to the user through a simple "Please wait..." display, such as covering the display with a transparent overlay and showing an animation.

In many situations, having a potential long inherent delay is not acceptable. Execution at these times is considered "No Wait Execution" and there is a read-only System Data value ("$nowait") that is true at those times. Built-in functions that may have such delays check before execution to see if delays are allowed. If delays are not allowed, the functions return immediately with an appropriate value reflecting that situation, such as an empty value or a specific error indication. User-defined functions can check the $nowait value and behave differently, such as using a default value instead of checking a database.

During the Form Editing Lifecycle, No Wait Execution is required when evaluating placeholders, IF TESTs, and List Expressions and other Form Display Expressions. Waits are allowed when executing ON *LOAD, ON *editor, ON *changed, Button Execution, and ON *FINISHED code.

Built-in Functions

TPL provides the following built-in functions:

abs(number)

Returns the absolute value of a number.

val = abs(-1) ' returns 1
val = abs(1)  ' returns 1
acos(number)

Returns the arc cosine value of argument in radians.

num = 0.5
result = acos(num) ' Returns 1.0471975511965979
ajaxGET(url, timeout)

Does an Ajax request to "url". Optional timeout of "timeout" milliseconds. Returns an object with attribute "error" set to text if an error or "" otherwise. If no error, then also includes XMLHttpRequest attributes: status, statusText, timeout, responseType, and responseText.

This function is not available during No Wait Execution.

ajaxSendRequest(type, url, timeout, body, headers)

The type must be one of "GET", "POST", "PUT", "DELETE", or "HEAD". Does an Ajax request of that type to "url". Optional timeout of "timeout" milliseconds (defaults to 10 seconds). Optional body is the text to use as the body of the request. Optional headers is a string made up of one or more lines. There are two parts to each line: a header field name and a header value, separated by a colon (":"). The parts are trimmed of space before and after. For example:

Content-Type: application/x-www-form-urlencoded

Returns an object with attribute "error" set to text if an error or "" otherwise. If no error, then also includes XMLHttpRequest attributes: status, statusText, timeout, responseType, and responseText.

This function is not available during No Wait Execution.

args(argnum)

Returns argument number "argnum" to the current function. args(0) returns the name of the function.

firstArgument = args(1) ' Returns first argument
functionName = args(0) ' Returns name of the current function
argslen()

Returns the number of arguments to the current function.

numArgs = argslen()
array()

Returns a new empty array object.

newArr = array() ' Create new array, newArr
array(val1, val2, ...)

Returns a new array object with all of the arguments as successive elements.

placesArr = array("London", "Kiev", "Sydney") ' creates array, placesArr, with 3 entries
arrayConcat(array1, value1, value2, ...)

Returns a new array, consisting of the elements in array1, followed by value1, value2, etc. If any of those values are an array, then the elements of that array are each added individually instead of the array itself.

placesNA = array("Canada","Mexico","United States")
placesEU = array("Greece","Italy","United Kingdom")

placesAll = arrayConcat(placesEU, placesNA)
arrayCopy(array)

Returns a new array, consisting of the elements in the argument, which must be an array.

plants = array("Basil","Chives","Rosemary","Thyme")

alsoPlants  = arrayCopy(plants)
arrayCopyMap(array, name1in, name1out, name2in,...)

Returns a new array of objects, consisting of one object for each object in the array argument. The name1in/name1out, name2in/name2out, etc., arguments are pairs of text values that specify the name of a name/value pair in the input object and what the name should be for the corresponding output object. If the array is viewed as records in a table, this has the effect of letting you subset and rename the fields.

Here is an example:

result = arrayCopyMap(#products, "author", "name", "id", "ISBN")

Instead of multiple values, a single text argument of input/output pairs separated by commas may be used (the names are trimmed of leading/trailing whitespace):

result = arrayCopyMap(#products, "author/name,id/ISBN")

A single array argument with pairs of elements representing input/output pairs may also be used:

result = arrayCopyMap(#products, array("author", "name", "id", "ISBN"))

If the name-in is blank, then the name-out value will be the index of the element in the array (0-origin):

result = arrayCopyMap(#products, "author", "name", "", "position")
arrayFilter(array, tests)

Tests all of the elements in array, each of which must be an object, returning a new array with all of those objects that met the tests. If there are no matches, it returns an array with zero elements. It uses the same testing and following arguments as the objTest() function. For example:

result = arrayFilter(#products, "price > 30")
arrayFilterIndex(array, tests)

Tests all of the elements in array, each of which must be an object, returning a new array of the indexes (a zero-origined number) of all of the elements in the array that met the tests. If there are no matches, it returns an array with zero elements. It uses the same testing and following arguments as the objTest() function. For example:

result = arrayFilterIndex(#products, "price > 30")
arrayFirst(array, tests)

Tests all of the elements in array, each of which must be an object, returning the first of those objects that meets the tests. If there are no matches, returns "". It uses the same testing and following arguments as the objTest() function. For example:

result = arrayFirst(#products, "price > 30")
arrayFirstIndex(array, tests)

Tests all of the elements in array, each of which must be an object, returning the index (a zero-origined number) of the first of those objects that meets the tests. If there are no matches, returns -1. It uses the same testing and following arguments as the objTest() function. For example:

result = arrayFirstIndex(#products, "price > 30")
arrayIndexOf(array, text, start)

Searches the elements of the array argument for the first occurrence of an element with the value of text (case-insensitive). Returns index within the array (0-origined) or "-1" if there is no occurrence. If text is "", looks for a blank value. Optional start is a number that specifies the index within array to start the search. If start is negative, then the start is calculated from the end of the array, with -1 being the last element in the array.

For example, this would set pos to 1:

colors = array("red","orange","yellow","green","blue")
pos = arrayIndexOf(colors, "orange") ' returns 1
pos = arrayIndexOf(colors, "green", -5) ' returns 3
arrayInsert(array, start, val1, val2,...)

Modifies the array by inserting as elements val1, val2, etc., at the position indicated by the numeric value start. If start is 0, the elements will be inserted at the very beginning of the array, if start is 1, they will be inserted right after the first. If start is negative, then it refers to a position counting back from the current end of the array's extent, with -1 being to insert as the new last item(s).

In addition to modifying the array itself, the function returns a reference to the updated array.

colors = array("red","orange","yellow","green","blue")

arrayInsert(colors,4,"teal") ' Inserts "teal" between "green" and "blue"

arrayInsert(colors,-6,"salmon") ' Inserts "salmon" between "red" and "orange"
arrayJoin(array, separator)

Returns a text value created by concatenating the text of each element in array, separated by the text of separator.

For example, this would set str to "a, b, c, d, e":

a = array("a","b","c","d","e")
str = arrayJoin(a, ", ")
arrayObjValue(array, name)

The array argument must be an array of objects and name must be a text value. Returns a new array with elements that are the values of the name/value pairs with that name in those objects.

If the array is viewed as records in a table, this has the effect of letting you get an array with the values of one field in all of the records. Unlike the arrayCopyMap() function, which returns an array of objects, this function returns an array of values. Note: The values may be any value in the name/value pairs, so are not restricted to text and may be objects or arrays themselves.

For example, this would assign a new array to aov, with aov[0] set to "x" and aov[1] to "y":

a = array()
a[0] = obj("one", "x", "two", "xx")
a[1] = obj("one", "y", "two", "yy")
aov = arrayObjValue(a, "one")
arrayPop(array)

Modifies the array by removing the last element of the array.

In addition to modifying the array itself, the function returns the popped value.

colors = array("red","orange","yellow","green","blue")
lastColor = arrayPop(colors) ' returns "blue", removes "blue" from colors
arrayPush(array, val1, val2, ...)

Modifies the array by appending the values as additional elements to the end of the array.

In addition to modifying the array itself, the function returns the resulting length of the array.

colors = array("red","orange","yellow","green","blue")
arrayPush(colors,"purple") ' Adds "purple" to end of array
arrayRemove(array, start, count)

Modifies the array by removing elements starting with the element position specified by start. If start is less than 0, then it specifies a position counting back from the last element of the array, with -1 being the last element. The optional count value specifies how many elements to remove. If not specified, then just one element will be removed.

In addition to modifying the array itself, the function returns a new array containing the deleted elements.

colors = array("red","orange","yellow","green","blue")
arrayRemove(colors,1,2) ' Removes "orange" and "yellow" from array
arrayShift(array)

Modifies the array by removing the first element of the array.

In addition to modifying the array itself, the function returns the shifted value.

colors = array("red","orange","yellow","green","blue")
result = arrayShift(colors) ' Returns "red", removes "red" from array
arraySlice(array, start, end)

Returns a new array consisting of elements of the array starting with the element position specified by start. If start is less than 0, then it specifies a position counting back from the last element of the array, with -1 being the last element. The optional end value specifies the element one past the elements to return. That is, the end value element will not be in the returned array. If not specified, all elements from start up to and including the end of the array will be returned. If end is less than 0, it specifies a position counting back from the last element of the array, with -1 being the last element. This means that a value of -1 will return all elements up to but not including the last element.

This function does not modify array, it just returns copies of elements as a new array.

colors = array("red","orange","yellow","green","blue")
warm = arraySlice(colors,0,3) ' Creates new array containing "red", "orange", and "yellow"
cool = arraySlice(colors,3,5) ' Creates new array containing "green" and "blue"
arraySort(array, direction)

Sorts array, modifying array. The direction may be one of: "up", "down", "upnumeric", or "downnumeric". For the upnumeric and downnumeric, if two values convert to a number then a numeric comparison is done. Otherwise, and for up and down, a locale-specific, non-case-sensitive text comparison is performed. Returns a reference to the newly modified array.

For example:

arraySort(#countries, "up") ' Sort #countries, ascending

colors = array("red","orange","yellow","green","blue")
arraySort(colors,"down") ' Sort colors, descending
arraySortObjValue(array, sortspec)

Sorts an array of objects, modifying array. The sortspec consists of one or more pairs of name/direction. The pairs may be sequential pairs of arguments, sequential pairs of elements in an array, or a single text value in the form of "name1/dir1,name2/dir2,...". The name specifies a name/value pair in the object that is the value of an element in the array. The direction may be one of: "up", "down", "upnumeric", or "downnumeric". For the upnumeric and downnumeric, if two values convert to a number then a numeric comparison is done. Otherwise, and for up and down, a locale-specific, non-case-sensitive text comparison is performed. Returns a reference to the newly modified array.

For example:

arraySortObjValue(#products, "price/upnumeric,name/up")
arraySortObjValue(#products, "price","upnumeric","name","up")
arrayUnshift(array, val1, val2, ...)

Modifies the array by inserting the values as additional elements at the start of the array. The first val becomes the first element of the array, etc.

In addition to modifying the array itself, the function returns the resulting length of the array.

colors = array("green","blue")

arrayUnshift(colors,"red","orange","yellow") ' adds "red", "orange", and "yellow" to the array
' New colors array is ["red","orange","yellow","green","blue"]
asin(number)

Returns the arc sine value of argument in radians.

num = 0.5
result = asin(num) ' Returns 0.5235987755982989
atan(number)

Returns the arc tangent value of argument in radians.

num = 0.5
result = atan(num) ' Returns 0.4636476090008061
atan2(x, y)

Returns a value between -PI and PI radians of the angle represented by the two values.

result = atan2(1,1) ' Returns 0.7853981633974483
ceil(number)

Returns the closest integer value greater than or equal to the argument.

num1 = ceil(1.4) ' returns 2
num2 = ceil(-4.5) ' returns -4
cos(number)

Returns the cosine value of argument. The number is the angle in radians.

num = PI() * 2
result = cos(num) ' Returns 1
dateDifference(scope, date1, date2)

Compares two dates (date1 and date2) and returns an object with information about the difference between the two. The date2 is optional, defaulting to the current date/time. The scope specifies which units of time are used. The allowed values are "years", "months", "weeks", "days", "hours", "minutes", and/or "seconds". If an array is passed in, the returned object will divide the difference between the scope units. A string with a "-" separating two units will output all units between and including the two specified, for example "weeks-hours" would output "weeks", "days" and "hours".

The returned object has name/value pairs for each of the units (years, months, etc.), as well as an array named "units" with the units used in the difference, and a value named "before" that is "1" if the first date value is before the second or "" if not.

For example, this would result in an object with before set to "1", days set to 2, weeks set to 4, and units an array with the two elements "weeks" and "days":

d = dateDifference("weeks-days", "2018-11-01", "2018-12-01")
dateGMTOffset()

Returns the difference between GMT and the device local timezone, in minutes.

Note that this is usually the negative of the offset used in text representation of dates. For example, executing dateGMTOffset() in the Eastern Standard Time timezone will result in 300 minutes (5 hours), while the difference usually displayed in text when including the timezone offset would be "-05:00".

gmtOffset = dateGMTOffset()
dateFromFormat(text1, format1)

Parses text1 as date and time using format1 as a guide. Returns the date value as a plain date value (e.g., "2018-11-01 14:22:08").

The following text in format1 may be used to specify the format:

"y" or "yy" for a two-digit year (e.g., 99 for 1999 and 03 for 2003). "yyyy" is a full year specification.

"M" is the month as a single digit. "MM" is the month as two digits (for instance "01" instead of "1" for January). "MON" is the month as a short name in upper-case. "mon" is the short month name in lower-case. "Mon" is the short month name in title-case. "MONTH" is the month as the full name in upper-case. "month" is the full month name in lower-case. "Month" is the full month name in title-case.

"d" is the day of the month in a single digit. "dd" is the day of the month in two digits. "x" is the day with "st", "rd" or "th". "X" is the day with "ST", "RD" or "TH".

"W" or "WD" is the weekday as a short name in upper-case. "w" or "wd" is the short weekday name in lower-case. "Wd" is the short weekday name in title-case. "WEEKDAY" is the weekday in the full name in upper-case. "weekday" is the full weekday name in lower-case. "Weekday" is the full weekday name in title-case.

"h" is the date time hours. "m" is the date time minutes. "s" is the date time seconds. "0" when placed before a "h", "m" or "s" is padding the value with zeros. "1" is the date time milliseconds to a tenth of a second. "2" is the date time milliseconds to a hundredth of a second. "3" is the date time milliseconds to a thousandth of a second. "hh", "mm" and "ss" can also be used when padding hours, minutes and seconds with zeros. "a" will convert the hours from 12 to 24 based on the meridian as a single character in lower-case. "A" will convert the hours from 12 to 24 based on the meridian as a single character in upper-case. "am" will convert the hours from 12 to 24 based on the meridian as two characters in lower-case. "AM" will convert the hours from 12 to 24 based on the meridian as two characters in upper-case.

"\" (represented by "\\" between double-quotes in text values) can be used in front of any of the above placeholders to escape them.

The conversion will use the format as a guide, so components expecting one character will often also work with two (e.g., a date of 12 with a format of just "d"), case may be ignored, etc.

Here are some examples with results (executed in 2018):

d = dateFromFormat("2018-11-01", "yyyy-MM-dd") ' 2018-11-01 00:00:00
d = dateFromFormat("Feb 15, 2018 6pm", "Mon d, yyyy ham") ' 2018-02-15 18:00:00
d = dateFromFormat("5/22/18 1:15:22", "M/d/yy h:m:s") '2018-05-22 01:15:22
d = dateFromFormat("5/22", "M/d") ' 2018-05-22 00:00:00
dateMilliseconds(date1)

Returns date1 as the number of milliseconds since midnight January 1, 1970 GMT. If the argument is missing the current date/time will be used.

milliseconds = dateMilliseconds("2020-05-07") ' Returns 1588824000000
dateParts(date1)

Returns an object with values for various parts of date1. If the argument is missing the current date/time will be used.

The name/value pairs will be for: dateMilliseconds (same as dateMilliseconds()), year (e.g., 2018), month (0-origined, so January is 0, February is 1, etc.), dayOfMonth (1-origined, so the 1st of February is 1), dayOfWeek (0-origined, so Sunday is 0, Monday is 1, etc.), dayOfYear (1-origined, so January 1 is 1), daysInMonth (so November is 30, February for "2016-02-15" is 29), weekOfYear (1-origined, starting on Mondays, based on the ISO 8601 specification, so January 1st is often 1, but sometimes 0 if in week 52 or 53 of the year before), hours (0-23), minutes (0-59), seconds (0-59), milliseconds (0-999), timeZoneOffset (same as dateGMTOffset() of local time).

dateMilliseconds is relative to GMT. All of the other values are relative to the local timezone.

For example, this will set p.year to 2018, p.month to 10, p.dayOfWeek to 4, p.hours to 0, etc.:

p = dateParts("2018-11-01")
datePlain(date1)

Returns date1 in the format "yyyy-MM-dd hh:mm:ss", the way it is normally stored in TPL. If the argument is missing the current date/time will be used.

today = datePlain() ' Returns today's date
dateSame(scope, date1, date2)

Checks whether one date is the same as another in a given time scope. For example, it can check whether or not two dates are in the same year or month. The result is "1" if they are the same within the scope or "" if not.

The scope may be one of: "year", "quarter", "month", "week", "day" (or "date"), "hour", "minute" or "second".

If the second date is not provided the current date/time will be used. Week is as returned by dateParts().

For example, the first results in "1" and the second in "":

a = dateSame("month", "2018-11-01", "2018-11-08") ' Returns 1
b = dateSame("day", "2018-11-01", "2018-11-08") ' Returns ""
dateToFriendly(date1, type, whenShowTime)

Returns a text representation of a date/time that is relative to the current date/time and more appropriate for many people in some contexts. This could be, for example, using day names for close dates, and only including parts of the date that are needed when further into the future or past and different than the current date and time.

There are three types of result: abbreviated, standard, and extended. (If the type is not present, then "standard" is assumed.) An "abbreviated" date is meant to be used to show the date in the shortest friendly format, such as "1st June". A "standard" date is meant to be used to show the date in a slightly more verbose format, such as "Mon 1st June". The "extended" date is meant to be used to show the date in a long format, such as "Monday the 1st of June".

The whenShowTime argument controls when the time is displayed. A value of day (the default value if the argument is missing) means that any calendar date that is within the range of relative days will show the time, such as "next Wednesday at 2:00pm" but not "1st Jun". A whenShowTime value of month will show the time for all dates within the month, a value of year will show the time for all dates within the year, and a value of always will always show the time. A value of never will never show the time. For all of these, only the first letter is checked, so "never" and "n" are the same.

In this example, assume that the current date/time is 1:15pm, November 14, 2018:

d = dateToFriendly("2018-12-01") ' Sat the 1st of Dec
d = dateToFriendly("2018-11-12 15:20:00", "e", "d") ' last Monday at 3:20pm
d = dateToFriendly("2018-10-12 15:20:00", "s", "d") ' Fri the 12th of Oct
d = dateToFriendly("2018-10-12 15:20:00", "a", "y") ' 12th of Oct at 3:20p
dateToFriendlyDifference(date1, detail, scope)

Returns a text representation of the amount of time date1 is compared to the current date/time that is more appropriate for many people than just showing the date/time itself. This could be, for example, using something like "3 days ago", and only including more detailed parts of that difference that are significant, such as "3 days ago" or "2 months ago" instead of "3 days 2 hours 5 minutes ago".

The detail argument is a number that specifies the level of detail, that is the maximum number of units of time shown. For example, a value of 1 would return "3 days ago" or "in 1 hour", while a value of 2 might show "3 days 2 hours ago" or "in 1 hour 5 minutes". If the argument is missing the value 1 will be used.

The scope argument is used to set the granularity of the units of time provided. For instance, setting it to a value of "years-days" will only show the difference in days, week, months, and years if needed, but never in hours, minutes or seconds. If the given scope results in a relative date that is the same, then the corresponding "same" text for the smallest unit is used. This means that if the smallest unit of comparison is "weeks", and the date is within a week of the current date, then the text returned would be "this week".

In this example, assume that the current date/time is 1:15pm, November 14, 2018:

d = dateToFriendlyDifference("2018-10-01") ' 1 month ago
d = dateToFriendlyDifference("2018-11-01") ' 1 week ago
d = dateToFriendlyDifference("2018-10-01", 2) ' 1 week 6 days ago
d = dateToFriendlyDifference("2018-11-01", 2, "year-month") ' this month
d = dateToFriendlyDifference("2018-12-16 09:00", 5, "days-hours") ' in 31 days 19 hours
dateToFormat(date1, format1)

Returns a text representation of date1 using format1 as a guide.

The following text in format1 may be used to specify the format (it is analogous to dateFromFormat()):

"y" or "yy" for a two-digit year (e.g., 99 for 1999 and 03 for 2003). "yyyy" is a full year specification.

"M" is the month as a single digit if possible. "MM" is the month as two digits (for instance "01" instead of "1" for January). "MON" is the month as a short name in upper-case. "mon" is the short month name in lower-case. "Mon" is the short month name in title-case. "MONTH" is the month as the full name in upper-case. "month" is the full month name in lower-case. "Month" is the full month name in title-case.

"d" is the day of the month in a single digit if possible. "dd" is the day of the month in two digits. "x" is the day with "st", "rd" or "th". "X" is the day with "ST", "RD" or "TH".

"W" or "WD" is the weekday as a short name in upper-case. "w" or "wd" is the short weekday name in lower-case. "Wd" is the short weekday name in title-case. "WEEKDAY" is the weekday in the full name in upper-case. "weekday" is the full weekday name in lower-case. "Weekday" is the full weekday name in title-case.

"h" is the date time hours. "m" is the date time minutes. "s" is the date time seconds. "0" when placed before a "h", "m" or "s" is padding the value with zeros. "1" is the date time milliseconds to a tenth of a second. "2" is the date time milliseconds to a hundredth of a second. "3" is the date time milliseconds to a thousandth of a second. "hh", "mm" and "ss" can also be used when padding hours, minutes and seconds with zeros. "a" will convert the hours from 24 to 12 based on the meridian and result in a single character in lower-case. "A" will convert the hours from 24 to 12 based on the meridian and result in a single character in upper-case. "am" will convert the hours from 24 to 12 based on the meridian and result in two characters in lower-case. "AM" will convert the hours from 24 to 12 based on the meridian and result in two characters in upper-case.

"\" (represented by "\\" between double-quotes in text values) can be used in front of any of the above placeholders to escape them.

Here are some examples with results (executed in 2018):

d = dateFromFormat("12/09/2019 10:30:00 am","MM/dd/yyyy h:m:s a")

d2 = dateToFormat(d, "yyyy-MM-dd") ' 2019-12-09
d2 = dateToFormat(d, "Mon d, yyyy ham") ' Dec 9, 2019 10am
d2 = dateToFormat(d, "M/d/yy h:m:s") '12/9/19 10:30:0
d2 = dateToFormat(d, "M/d") ' 12/9
dateTZ(date1)

Returns date1 in the format "yyyy-MM-ddThh:mm:ss-hh:mm", which is a format specified by ISO 8601 for a date/time that includes timezone offset. The "-hh:mm" part is replaced by the offset (with sign "+" or "-") from GMT for local time. If the argument is missing the current date/time will be used.

For example (if executed in a device set to Eastern Standard Time):

d = dateTZ("2018-11-01 10:00") ' 2018-11-01T10:00:00-0:500
decodeURI(text)

Turns URI-encoded text back into the pre-encoded text. This is the companion to the encoding done by encodeURI(). Returns the decoded value.

decodeURIComponent(text)

Turns URI Component-encoded text back into the pre-encoded text. This is the companion to the encoding done by encodeURIComponent(). Returns the decoded value.

degrees(number)

Takes a number in radians and returns a value in degrees.

num = PI() * 2
result = degrees(num) ' returns 360
E()

Returns the value of e (2.718281828459045).

result = E() 'Returns 2.718281828459045
encodeURI(text)

Encodes text by replacing certain characters with escape sequences that are part of the specification (RFC2396) for a URI (Uniform Resource Identifier). (URIs include URLs for Internet addresses as well as identifiers for access to files and other resources.) Returns the encoded value. Decoding the result may be done by decodeURI().

This function is useful, for example, for creating the URL for HTML or Ajax requests.

Certain characters needed to be unescaped for URIs (such as ":", "?", and "&") are not escaped.

To encode just a component of a URI, such as a query parameter value, the encodeURIComponent() function may be used which encodes a wider range of characters.

encodeURIComponent(text)

Encodes text by replacing certain characters with escape sequences that are part of the specification (RFC2396) for a URI (Uniform Resource Identifier). (URIs include URLs for Internet addresses as well as identifiers for access to files and other resources.) Returns the encoded value. Decoding the result may be done by decodeURIComponent().

This function is useful, for example, for assembling the body of Ajax POST requests.

Note that certain characters that need to be unescaped for URIs (such as ":", "?", and "&") are escaped by this function. To encode a URI without encoding those characters, use encodeURI().

exp(number)

Returns e raised to the power of the value of argument.

num = exp(4) ' Returns 54.598150033144236
floor(number)

Returns the closest integer value less than or equal to the argument.

num = floor(1.4) ' Returns 1
num = floor(-4.5) ' Returns -5
formatNumber(value1, format1)

Returns a text representation of numeric value1 using format1 as a guide.

The formatting is specified by characters in format1. Certain characters have special meaning and relate to the digits that make up the number. Other characters before and after those special characters are included in the results verbatim.

price = formatNumber(12.3, "$#,##0.00") ' Returns "$12.30"

The numeric value being formatted is assumed to be composed of two parts: The integer part and the fractional part (the part after a decimal). Likewise, the format specification for a value has two parts, one for the integer and one for the fraction. Each format part is made up characters ("#", "0", "_", and/or "*") that represent digits. Between the format for the integer part and the fraction there is a character that separates them and is put in the result as a decimal separator. It can be any character, but the decimal point character of the locale is usually used (e.g., "." in the USA, "," in some other countries).

For example:

numStr = formatNumber(1234.56, "#.##") ' 1234.56
numStr = formatNumber(1234.56, "#,##") ' 1234,56

The character "]" specifies the end of the integer portion and that the fraction should not be shown. The value will be rounded to an integer, and characters after the "]" will appear as themselves.

For example:

numStr = formatNumber(1234.56, "#]") ' 1235
numStr = formatNumber(1234.56, "#]##") ' 1235##

The character "[" in position to the left of the "]" specifies rounding the integer portion to end with that many zeroes.

For example:

numStr = formatNumber(1357.9, "#[##]") ' 1400

In the specification for the integer portion, the characters "#", "0", and "_" represent one digit of the value, starting from the decimal position and going left. If a "#" is present in the format and a digit is present in the corresponding position in the value, then the digit is put in the result. Otherwise that position is skipped. For example, the value "12" and a format of "####" would result in "12". If a "0" is present in the format and a digit is present in the corresponding position in the value, then the digit is put in the result. Unlike the "#", though, if there isn't a digit in that position then a zero is used. For example, "12" and "0000" would result in "0012". The "_" is similar to "0" except a space will be output if there isn't a digit in that position.

If there are more integer digits in the value than present in the format specification, the additional digits will be put in the result.

A character within the specification for the integer portion is treated as the grouping character. The number of digit position characters (2 or more) to the right of the grouping character determines the size of a "group". The number of digit positions to the left of the grouping character (of which there must be at least one) is not used to determine the size of the group. The grouping is repeated when producing the result if needed. This is usually used as the thousands separator. (Note: There must be a character specifying the end of the integer portion (e.g., "." or "]") for there to be a grouping character, otherwise the character would be treated as the decimal separator.)

For example:

numStr = formatNumber(1234.56, "#,###.##") ' 1,234.56
numStr = formatNumber(1234567.89, "#/##]") ' 1/23/45/68

The specification for the fraction portion is similar to the integer portion. The characters "#", "0", and "_" represent one digit of the value, starting from the decimal position, but going right instead of left. If a "#" is present in the format and a significant digit is present in the corresponding position in the value, then the digit is put in the result. Otherwise that position is skipped. (Trailing zeroes are assumed to not be significant.) For example, the value "1.2" and a format of "#.###" would result in "1.2". If a "0" is present in the format and a digit is present in the corresponding position in the value, then the digit is put in the result. Unlike the "#", though, if there isn't a digit in that position then a zero is used. For example, "1.2" and "#.000" would result in "1.200". The "_" is similar to "0" except a space will be output if there isn't a significant digit in that position.

If there are more significant fraction digits in the value than present in the format specification, the value will be rounded. To show all of the significant fraction digits and not round, use "*" as the fraction part.

For example:

numStr = formatNumber(1.2345, "#.##") ' 1.23
numStr = formatNumber(1.2345, "#.###") ' 1.235
numStr = formatNumber(1.2345, "#.*") ' 1.2345

The "<" character at the end of the fraction part will round the decimal up (like ceil()). The "<" character before the "]" in the integer part will round the integer up. The ">" character acts just like the "<" character, but rounds down (like floor()).

For example:

numStr = formatNumber(12.31, "#.#<") ' 12.4
numStr = formatNumber(12.39, "#.#>") ' 12.3
numStr = formatNumber(12341, "#<#]") ' 12350
numStr = formatNumber(12349, "#>#]") ' 12340

The characters "-/-" represent the fraction part and display it as a fraction.

For example:

numStr = formatNumber(1.4, "# -/-") ' 1 2/5
numStr = formatNumber(1.35, "# -/-") ' 1 7/20
numStr = formatNumber(1.35, "#.# -/-") ' 1 2/5

The ";" character separates multiple format specifications. If there are two parts, the second specifies how to display a negative value. (If there is only one part, negative values display a "-" at the beginning.) If there are three parts, the third specified how to display a zero value (original value must be zero, not a rounded one in the other formats).

For example:

numStr = formatNumber(-1.4, "$#.00;($#.00);- -") ' ($4.00)
numStr = formatNumber(1.4, "$#.00;($#.00);- -") ' $4.00
numStr = formatNumber(0, "$#.00;($#.00);- -") ' - -

The "\" character can be used to escape the following character and treat it as a normal format specification character.

For example:

numStr = formatNumber(1234, "Part\#00000") ' Part#01234
formatText(text1, format1, filler)

Returns the characters in text1 using format1 as a guide. Useful for adding extra characters and/or padding.

Starting with the first character, each character in format1 is in turn added to the result. Each "_" character in format1 is replaced by successive characters from text1. The "\" character before another character in format1 causes that character to be added to the result, even if it is "_" or "\". If all the characters in text1 are added to the result and format1 has additional "_" characters, the optional "filler" text is added instead of adding no characters in those positions.

For example:

phoneNum = formatText("6175551212", "(___)___-____") ' (617)555-1212
part = formatText("157", "Part A\\_______", "*") ' Part A_157***
goToTarget(targetname)

This function affects the display of the form in the Form Filler in a manner similar to the GoTo command in a form. It switches the display of the Form Filler app to display a specified part of the form. The Define Target command is used to specify the places that this function can "go to", distinguished by their target names.

Unlike the GoTo Target command, which looks for the first Define Target command with the specified name after the GoTo Target command, this function looks for the first Define Target command with the specified name of any targets in the whole Table of Contents. (The Table of Contents includes all commands, including repeated commands in data group items, but not commands skipped with IF commands.)

This function may be executed from a button, or an ON event handler. It should not be called in an expression, such as used to display a value.

ON *button_jumpToEnd
    goToTarget("Review")
ENDON
GPSPosition()

Returns an object with the current values for the GPS position as reported by the device. The position is only updated in the interim between the time when GPSStart() and GPSStop() are executed. Right after GPSStart() is executed there may take a short amount of time before the position is updated from its default "0.0,0.0" and this function returns a new value and a blank error status.

This function is available during No Wait Execution. It always returns a value immediately.

The result is an object with the following name/value pairs:

pos = GPSPosition()
pos.latitude ' Latitude in degrees
pos.longitude ' Longitude in degrees
pos.latlong ' Both as text with , to 6 decimal places
pos.accuracy ' Reported accuracy in meters
pos.altitude ' Altitude in meters if available
pos.altitudeAccuracy ' Reported accuracy
pos.timestamp 'Reported date/time obtained in ms.
pos.error ' Status (blank if OK)

The error status, if not blank, may be one of: "notstarted", "waiting", "permissiondenied", "unavailable", "timeout", or "error".

GPSStart(accuracy, periodseconds, watchmaxseconds)

Starts the acquisition of location position values by the device, such as by using GPS hardware. The most current position may then be obtained by using the GPSPosition() function.

GPSStart()

The position is only updated in the interim between the time when GPSStart() and GPSStop() are executed. Right after GPSStart() is executed there may take a short amount of time before the position is updated from its default "0.0,0.0". The GPS hardware will continue to be used at least until the time that GPSStop() is executed.

The arguments are optional. If they are all missing then changes in the position as reported by the device will be continually updated.

If they are provided, and the accuracy argument is not 0, they are used as follows: The position reported by the device will be updated until the accuracy is less than the accuracy argument (a number in meters), or the watchmaxseconds has passed. The use of the GPS hardware by the app will then be stopped. The tracking of changes will be restarted every periodseconds. (Periodseconds should be at least 2 seconds longer than watchmatchseconds.) This, for example, lets you use the GPS hardware for up to 20 seconds every 5 minutes to determine the position within 50 meters by setting accuracy to 50, periodseconds to 300, and watchmaxseconds to 20.

For many uses, the default with no arguments is appropriate. If the GPS usage drains the device battery too quickly, the arguments give some flexibility in controlling usage.

A common time to call GSPStart() is in an "ON *LOAD" event and GPSStop() in an "ON *FINISHED" event.

GPSStop()

Stops the acquisition of location position values by the device, such as by using GPS hardware. The position reported by the GPSPosition() function will then be "0.0,0.0" and the error status as "notstarted".

GPSStop()

The position is only updated in the interim between the time when GPSStart() and GPSStop() are executed. The GPS hardware will continue to be used at least until the time that GPSStop() is executed.

A common time to call GSPStart() is in an "ON *LOAD" event and GPSStop() in an "ON *FINISHED" event.

imageAsBase64(photoFieldValue)

The imageAsBase64() function returns a base64 data URL that may be needed for web services (such as OCR). The function takes one argument, the field that contains the photo. imageAsBase64() can be used with any photos, including new photos that have not been uploaded to the TransForm Cloud.

The function takes one argument, photoFieldValue, the field or variable that contains the photo.

The example below demonstrates using imageAsBase64() to encode a photo and send it to the https://ocr.space service:

ON *button_convertPhoto
    ^b64 = imageAsBase64(#photo)
    headers = "apikey:YOUR_API_KEY\nContent-Type:application/x-www-form-urlencoded"
    body = "base64Image=" & encodeURIComponent(^b64)

    r = ajaxSendRequest("POST", "https://api.ocr.space/Parse/Image", 60000, body, headers)

    rjson = JSONparse(r.responseText)

    if rjson.ParsedResults
        #ocrtext = rjson.ParsedResults[0].ParsedText
    else
        #ocrtext = ""
    endif

    #ocrresults = rjson
ENDON
indexOf(text1, text2, start)

Searches for the first occurrence of text2 within text1. Returns the position within text1 where text2 starts (0-origined) or "-1" if there is no occurrence. If text2 is "", returns "0". Optional start is a number that specifies where in text1 to start the search. If it is negative, then the start is calculated from the end of the text, with -1 being the last character.

isArray(value)

Returns the "1" if the argument is an array, "" otherwise.

isDefined(value)

Returns the "1" if the argument is defined, "" otherwise. Values like text and numbers are defined, as well as objects and arrays. Array elements that have not been assigned a value are undefined, as are object name/value pairs with undefined values or names that are not present. In many other functions, undefined values are treated like "" or result in values of "" or zero.

isNumber(value)

Returns the "1" if the argument can be viewed as a number, "" otherwise. In some other functions, "" is treated as a number (0), but not in this function.

isObj(value)

Returns the "1" if the argument is an an object, "" otherwise. Undefined, values like numbers and text, and arrays are not objects.

JSONparse(text)

Parses the text argument, looking for a JSON value. If the result is a plain text, a quoted string, or a numeric value, the result is that type of value. If the result is an object or array, then it will be a reference to a new instance of that type of value. Note that in the text representation of JSON, the names of name/value pairs must be quoted strings, and values of the plain text true, false, and null are accepted. (Most functions and operators treat the value of null similarly to "" or undefined.)

JSONstringify(obj, space)

Converts an object, array, or value to a JSON value and returns it as text. The optional space text value can be used to insert indenting white-space into the result. Space should be a string of no more than 10 blanks (e.g., "   ") or tabs (e.g., "\t").

latLongDistance(lat1, long1, lat2, long2)

Returns the approximate distance in meters between two latitude/longitude positions. (The approximation assumes a spherical Earth and may be off by 5%.) The positions are specified by four numbers, measured the traditional way in degrees.

latLongDistance(firstPosition, secondPosition)

Returns the approximate distance in meters between two latitude/longitude positions. (The approximation assumes a spherical Earth and may be off by 5%.) The positions are each specified by a single value, both either as a text value of "latitude,longitude" (e.g., "42.480268,-71.205987") or an object with values for the attributes "latitude" and "longitude" (such as that returned by the GPSPosition() function).

len(array)

Returns the number of elements in an array.

colors = array("red","orange","yellow","green","blue")
numColors = len(colors) ' Returns 5
len(text)

Returns the number of characters in a text value.

str = "Please enter a value."
strLen = len(str) ' Returns 21
ln(number)

Returns the natural log value of argument.

LN10()

Returns the natural log of 10.

result = LN10() ' Returns 2.302585092994046
LOG10E()

Returns the common log of e.

result = LOG10E() 'Returns 0.4342944819032518
LOG2E()

Returns the log base 2 of e.

result = LOG2E() 'Returns 1.4426950408889634
matchAll(text, regex, options)

Repeatedly applies regular expression regex to the text. Options is optional, and may be text with the letters "i" (case-insensitive test) and/or "m" (regex "^" and "$" match on newline; use "[\\s\\S]" to match any character including newline instead of "."). If no match, returns "". If any matches, returns a new array with each element being successive matches in text.

matchOne(text, regex, options)

Applies regular expression regex to the text. Options is optional, and may be text with the letters "i" (case-insensitive test) and/or "m" (regex "^" and "$" match on newline; use "[\\s\\S]" to match any character including newline instead of "."). If no match, returns "". If any matches, returns a new array with the first element (0-origin) being the entire matched text, and each additional element being successive parenthetical matches in text.

max(number1, number2, ...)

Returns the largest value of two or more numbers.

result = max(10, 1000) ' Returns 1000
result = max(-12, -200) ' Returns -12
metaDataGet(fieldname)

Returns the value of the specified form instance record metadata field name. If the value is unset, or the the fieldname is not "user1" through "user5", "userlablel1" through "userlabel5", nor "duedate", the value "" will be returned. Setting the value may be accomplished using the metaDataSet() function.

metaDataSet(fieldname, newvalue)

Sets the value of the specified form instance record metadata field name. The fieldname must be "user1" through "user5", "userlablel1" through "userlabel5", or "duedate". Getting the value may be accomplished using the metaDataGet() function or the $#user1, $#user2, etc., metaData values.

The "userlabel" values must be no longer than 30 characters in length. The "duedate" value must be either "" or in the form of "yyyy-mm-dd", such as "2019-04-19".

min(number1, number2, ...)

Returns the value of the smallest value of two or more numbers.

result = min(10, 1000) ' Returns 10
result = min(-12, -200) ' Returns -200
mod(a, b)

Rounds both values to integers, and then returns the integer remainder of the first divided by the second.

msgHideProgress()

Removes modal message box shown by msgShowProgress(), if any. Execution continues immediately. Returns "1".

msgShowBasic(title, body, confirm)

Displays a modal message box with a title and a body. The button to dismiss will be labeled "OK" if the optional confirm is missing. Execution pauses until the user responds. Returns "1".

This function is not available during No Wait Execution.

msgShowConfirm(title, body, confirm, cancel)

Displays a modal message box with a title and a body. The button to proceed will be labeled "OK" if the optional confirm is missing and the button to cancel will be labeled "Cancel" if the optional cancel is missing. Execution pauses until the user responds. Returns "1" for confirm and "" for cancel.

This function is not available during No Wait Execution.

msgShowConfirmWarning(title, body, confirm, cancel)

Displays a modal message box with a red title and a body. The button to proceed will be labeled "OK" if the optional confirm is missing and the button to cancel will be labeled "Cancel" if the optional cancel is missing. Execution pauses until the user responds. Returns "1" for confirm and "" for cancel.

This function is not available during No Wait Execution.

msgShowProgress(title, body)

Displays a modal message box with a title, a body, and a spinning icon. There is no button to dismiss. Execution continues immediately. Returns "1". Use msgHideProgress() to remove the modal message box.

This function is not available during No Wait Execution.

msgShowThreeButton(title, body, one, two, three)

Displays a modal message box with a title and a body. The first button to dismiss will be labeled "1" if the optional one is missing. The second button to dismiss will be labeled "2" if the optional two is missing. The third button to dismiss will be labeled "3" if the optional three is missing. Execution pauses until the user responds. Returns the label of the chosen button. This function is not available during No Wait Execution.

This function is not available during No Wait Execution.

msgShowWarning(title, body, confirm)

Displays a modal message box with a red title and a body. The button to dismiss will be labeled "OK" if the optional confirm is missing. Execution pauses until the user responds. Returns "1".

This function is not available during No Wait Execution.

nargs()

Deprecated. Use argslen().

now()

Returns the current date/time as yyyy-mm-dd hh:mm:ss.

current = now() ' Returns the current date and time
nowMilliseconds()

Returns the current date/time as the number of milliseconds since midnight January 1, 1970 GMT.

current = nowMilliseconds() ' Returns the current time in milliseconds
obj()

Returns a new empty object.

result = obj() ' Create a new object
obj(name1, value1, name2, value2, ...)

Returns a new object with name/value pairs specified by successive pairs of values. The keys are treated as strings and the values are simple values, arrays, or objects. The values may be references to values, objects, or arrays.

For example, an object with a "person" value of "Joe", age coming from #age, and children as an array of names, could be created by:

result = obj("person", "Joe", "age", #age, "children", array("Jane", "Nancy"))
objAssign(value1, value2)

Modifies the object referenced by the first argument, setting the name/value pairs in that first object to values from name/value pairs in the object referenced by the second argument. If the names are new, they are added; if they already had values, new values will be assigned from the second argument. If the values being assigned are arrays or objects, references are not assigned. Instead, copies are made and assigned, recursively. This is different than many other functions, such as arrayCopy() and arrayCopyMap(), which assign references.

Returns a reference to the updated value1.

For example, this would end up with o1 being an object with a "person" value of "Joe", age of 42, jobs set to 1, and children as a new array of names not changing c1:

c1 = array("Jane", "Nancy")
o1 = obj("person", "Joe", "age", 40, "children",  c1)
c2 = array("Maryjane", "Nancy", "Bob")
o2 = obj("age", 42, "jobs", 1, "children", c2)
objAssign(o1, o2)
objAssignNew(value1, value2)

Modifies the object referenced by the first argument, adding new name/value pairs to that first object from name/value pairs in the object referenced by the second argument. That is, if the names are new, they are added; if they already existed, new values will not be assigned from the second argument. (Contrast with the objAssign() function.) If the values being assigned are arrays or objects, references are not assigned. Instead, copies are made and assigned, recursively. This is different than many other functions, such as arrayCopy() and arrayCopyMap(), which assign references.

Returns a reference to the updated value1.

For example, this would end up with o1 being an object with a "person" value of "Joe", age of 40 (not 42), jobs set to 1, and children as a new array with only two names (Jane and Nancy) that does not change if c1's elements are changed:

c1 = array("Jane", "Nancy")
o1 = obj("person", "Joe", "age", 40, "children",  c1)
c2 = array("Maryjane", "Nancy", "Bob")
o2 = obj("age", 42, "jobs", 1, "children", c2)
objAssignNew(o1, o2)
objTest(obj, tests)

Returns true ("1") or false ("") depending upon whether or not the name/values in object obj pass the tests specified by the other argument(s). The tests are similar to those used to match records in a table in a SQL statement WHERE clause, with some minor but important differences.

The tests may be specified by separate successive arguments, each providing an additional component of the test specification, such as as names, operators, and values. They may also be specified by elements in a single array, with each successive element corresponding to an additional component of the test specification. They may also be specified by a single text value which is then broken up into the components similar to a normal program language expression with normal treatment of spacing.

The names (corresponding to field names in SQL) are case-sensitive. The operator names (such as AND, OR, and BETWEEN) that are text are not case-sensitive. The "NOT" operator is not the text word "NOT" but rather the single character "!".

These are examples of three different ways to specify the same tests on an object with name/value pairs "First", "Second", and "Third":

result = objTest(obj1, "First==\"a\" AND Second BETWEEN 1 AND 5")
result = objTest(obj1, "First", "==", "a", "AND", "Second", "between", "1", "and", "5")
tests = array("First", "==", "a", "AND", "Second", "between", "1", "and", "5")
result = objTest(obj1, tests)

The operators are: <, <=, ==, ===, !==, !=, >=, >, starts, ends, notStarts, notEnds, contains, notContains, matches, matchesCase, isDefined, isNotDefined, isNumber, isNotNumber, isObj, isNotObj, isArray, isNotArray, between, and in.

The non-alphabetic operators are very much like those operators in TPL expressions, with numeric comparisons if both values convert to numbers and are case-insensitive for text. The === and !== operators are case-sensitive and convert everything to text before comparison.

The STARTS, ENDS, and CONTAINS operators look for a case-insensitive exact match for the first characters in the value, the last characters in the value, and any place within the value. There are corresponding "NOT" versions of each.

The MATCHES operator uses a Regular Expression to test the value. It is case-insensitive (the "i" modifier), and assumes the "m" modifier. The MATCHESCASE operator is case-sensitive, assuming just the "m" modifier.

For example, to match values for "line" that start with "the" and have either of the words "fox" or "wolf" within the line, you could use:

result = objTest(obj2, "line", "MATCHES", "^The.*?\\s(fox|wolf)\\s")

The BETWEEN operator works like a combination of >= and <= and looks like this (the "and" word is required):

result = objTest(obj3, "quantity BETWEEN 1 AND 10")

The IN operator works like multiple "==" tests, separated by OR. The second part (after the IN) must be an array of text values:

result = objTest(obj4, "size", "IN", array("medium", "large", "x-large"))

The ISDEFINED operator tests whether or not a name/value pair with that name is present. The ISNUMBER operator tests whether the value looks like a numeric value. The ISOBJ and ISARRAY operators test whether or not the value is one of those types. All four of these have no second part, and have "isNot" variants.

For example:

result = objTest(obj5, "status isNotDefined OR status == \"completed\"")

For order of execution, the "!" has the highest precedence, the "AND" next, and "OR" least. Parentheses and the "!" operator may also be used:

result = objTest(obj5, "First==1 AND !(Second==2 OR Third==3)")
PI()

Returns the value of PI, "3.141592653589793"

result = PI() ' Returns 3.141592653589793
phonecall(phoneNumber)

The phonecall() function can be used to call a number on the local device. The function takes one argument, the phone number to dial. The phone number may only contain numbers, +, -, parentheses, spaces, commas, or semicolons.

The code below demonstrates two ON events for two buttons. The first shows passing a field that contains the phone number to the phonecall() function. The second shows passing a static string to the function:

ON *button_callSupport
    phonecall(#supportPhoneNumber)
ENDON

ON *button_callSales
    salesNumber = "617-555-1234"
    phonecall(salesNumber)
ENDON
phoneGapTFExecuteSQL(dbName, sql, [arg1, arg2, ..., argN])

This is an Experimental feature.

Argument
Description
dbName

The name of the SQLite database on the device

sql

The SQL statement to execute. One or more arguments can be included using the ? placeholder. See examples below for more information.

arg1, arg2, ... argN

One or more arguments listed in the order that they appear in the SQL statement. Arguments are required if the SQL statement to execute includes arguments.

The phoneGapTFExecuteSQL() executes a SQL query against an on-device SQLite database for the TransForm account. For example:

db = "SQLiteNorthwind.db"
sql = "SELECT * FROM Customers LIMIT 5"
sqlResults = phoneGapTFExecuteSQL(db,sql)

This selects the first five records in the Customers table of the on-device SQLiteNorthwind.db SQLite database.

You can use arguments in your SQL statements. Arguments are specified using the ? operator in the SQL statement. For example:

db = "SQLiteNorthwind.db"
sql = "SELECT * FROM Customers WHERE Country = ?"
arg1 = "Spain"

sqlResults = phoneGapTFExecuteSQL(db,sql,arg1)

This selects all Customers from the Country "Spain".

If a statement has more than one argument, they are passed to the phoneGapTFExecuteSQL() function after the SQL statement in the order that they appear. For example:

db = "SQLiteNorthwind.db"
sql = "SELECT * FROM Customers WHERE CustomerId LIKE ? AND Country = ?"
arg1 = "%a%"
arg2 = "Spain"

sqlResults = phoneGapTFExecuteSQL(db,sql,arg1,arg2)

This example selects all Customers from the Country "Spain" with a CustomerID that contains the letter "a".

Arguments can be read from fields in a form. This next example reads the Country from the form's location field. The results are displayed in their raw JSON format in another field call result. If the location field is blank, a message is shown in the result field asking the user to select a location.

db = "SQLiteNorthwind.db"
sql = "SELECT * FROM Customers WHERE Country = ?"
arg1 = #location

IF len(arg1) <= 0
    msgText = "Please select a Location"
ELSE
    sqlResults = phoneGapTFExecuteSQL(db,sql, arg1)
    msgText = JSONstringify(sqlResults,"  ")
ENDIF

#result = msgText

The records returned from phoneGapTFExecuteSQL() are returned as an Object. The complex example below shows how to iterate over the returned results to update the choices for a List field:

ON *editor_Customers
    ' Get an object for the field's list editor
    s = args(1)

    db = "SQLiteNorthwind.db"
    sql = "SELECT CustomerID, ContactName, City, Country FROM customers"
    sqlresults = phoneGapTFExecuteSQL(db, sql)

    data = array()

    if sqlresults.length<1
        data[0] = obj("value","No customers")
        s.qList = data
        return ' No query results
    endif

    FOR i=0 TO len(sqlresults)-1

        row = sqlresults[i]
        text = row.ContactName & ": " & row.City & ", " & row.Country
        value = row.CustomerID

        ' Add the Customer to the list of choices
        data[i] = obj("text",text,"value",value)

    ENDFOR

    s.qList = data
ENDON

Database field names are case sensitive and must match the case defined in the SQLite database.

For more information on how to write SQLite queries, visit https://sqlite.org/lang.html

pow(x, y)

Returns the x to the power of y.

result = pow(2,4) ' Returns 16
radians(number)

Takes a number in degrees and returns a value in radians.

result = radians(180) ' Returns 3.141592653589793
random()

Returns a random number greater than or equal to 0 and less than 1 as text.

result = random()
replace(text, match, replacement)

Returns a copy of text with all occurrences of match replaced with replacement. The searches are case-insensitive.

For example:

a = replace("This IS a test", "is", "x") ' Thxx xx a test
replaceCase(text, match, replacement)

Returns a copy of text with all occurrences of match replaced with replacement. The searches are case-sensitive.

For example:

a = replaceCase("This IS a test", "is", "x") ' Thxx IS a test
replaceMatch(text, regex, replacement, options)

Searches text for matches to regular expression regex. Returns a copy of text with all matches replaced with replacement. Options is optional, and may be text with the letters "i" (case-insensitive test) and/or "m" (regex "^" and "$" match on newline; use "[\\s\\S]" to match any character including newline instead of ".").

In replacement, the following patterns insert special text: $1, $2, etc., insert text that matched the 1st, 2nd, etc., parenthesized subexpression in regex, $& inserts the entire match to the regex, $` and $' are the text before and after the entire match (and are probably rarely of use), and $$ inserts a "$".

For example:

a = replaceMatch("This is a test", "(.)i", "[$1]I") ' T[h]Is[ ]Is a test
round(number, places)

For example, the first line sets a to 15, the second to 15.35, and the third to 20:

a = round(15.345)
a = round(15.345, 2)
a = round(15.345, -1)
sin(number)

Returns the sine value of argument. The number is the angle in radians.

split(text, separator)

Returns an array of "text" broken on character(s) "separator".

str = "a,b,c,d,e"
array = split(str,",") ' Returns an array: ["a","b","c","d","e"]
sqrt(number)

Returns the square root value of argument.

result = sqrt(100) ' returns 10
SQRT1_2()

Returns the reciprocal of the square root of 2.

result = SQRT1_2() 'Returns 0.7071067811865476
SQRT2()

Returns the value of square root of 2.

result = SQRT2() ' Returns 1.4142135623730951
substr(text, start, length)

Returns the length number of characters in text starting at character start (0-origin). If length is missing, then all of the rest of the characters in text will be returned. If start is negative, then the start is calculated from the end of text, with "-1" being the last character, "-2" the next to last, etc.

substring(text, start, end)

Returns the characters in text starting at character start (0-origin) up to but not including character end (0-origin). If end is missing, then all of the rest of the characters in text will be returned. If start or end is negative, then the respective values are calculated from the end of text, with "-1" being the last character, "-2" the next to last, etc.

tan(number)

Returns the tangent value of argument. The number is the angle in radians.

test(condition, truevalue, falsevalue)

Returns the value of truevalue or falsevalue depending upon whether condition is true (non-blank) or false (blank), respectively. The values may be scalar values like text strings or objects and array values.

For example, the first call returns "B", the second "B", and the third "1":

result = test(3==3, "A", "B")
result = test(3==4, "A", "B")
result = test("1", obj("a","1"), obj("a","2")).a
toLowerCase(text)

Returns the the text with all uppercase characters switched to lowercase.

str = "Alpha TransForm"
str = toLowerCase(str) ' Returns "alpha tranform"
toUpperCase(text)

Returns the the text with all lowercase characters switched to uppercase.

str = "Alpha TransForm"
str = toUpperCase(str) ' Returns "ALPHA TRANSFORM"
wait(duration)

Pauses execution for duration seconds. The value of duration should be a number between 1 and 60. It will be adjusted to conform to that range. Returns the adjusted value of duration.

This function is not available during No Wait Execution, when it will return "0".

wait(5) ' Wait 5 seconds

Render Functions

Another special class of functions are those that handle customized rendering of field values. These are Render Functions and Render Button Functions.

Many types of fields may have an associated Render Function specified. These include the following types of fields: text, number, phone, signed, currency, integer, scanner, list, button list, date, datetime, timenow, and timenowcountdown.

You set the Render Function for a field by choosing it from a list that is displayed when you click on the Formatting property of the field command. You may also specify the function by typing the name of the function (including the @ prefix) into the Render Function property when viewing the TPL Editing and Testing screen listing the form fields and their properties.

A Render Function takes two arguments: The string representation of the value that would normally be displayed (or in the case of a list, the value and not the display text) is args(1), and args(2) is an object with other values normally used to control the display of the field. As with other functions, the args(0) value is the name of the function being called.

A Render Function returns either a text value to be displayed, or an object. The text value will have special characters HTML escaped.

If an object is returned, the object must have a "type" attribute. If the type is "html", the value of the "html" property will be displayed for the field. This, for example, may be used to show values in different colors using CSS.

The second argument, the values that control the display of fields, among other values, contains the following: "qValue" (the value), "qType" (the type of field, such as "text" or "buttonlist"), "qSettings" (the settings value of the command as an object - for example, settings.multi is true if a button list is multi-valued, settings.extra has the Extra Settings text that may be set on the field Extra Settings property in the TPL Editing and Testing screen), and "qList" (an array of objects for lists and button lists). A common use of the settings.extra text is to hold the stringified JSON of additional properties. Alternatively, the text could be a comma-separated list. It is up to the author of the function reading the settings.extra text to specify the syntax.

For button lists, the object returned must have a "type" attribute of "bldata" (meaning "button list data"), and a "bldata" attribute with an object value. The bldata object must have the following attributes: An array named "buttons" with string values for "before" (optional HTML before the tappable area of the button), "buttonstyle" (optional contents of a style attribute for the div making up the button to be tapped including any style attributes to indicate selection if desired), "HTML" (required HTML to appear inside of the button div which may also inidicate selection), and "after" (optional HTML after the button, such as a line break to render the series of buttons on more than one line). In addition to the buttons attribute, there may be "bldata.containerstyle" (for example, used to optionally set CSS Flex or put a border around the whole field), "bldata.header" (optional HTML above the list of buttons), and "bldata.footer" (optional HTML below the list of buttons).

The list of Render Functions shown when the Formatting property for a field command is clicked is automatically populated by examining the first line of the TPL User-Defined Functions (the line starting with the FUNCTION @functionname). If the line has a comment (starting with a ' character) and in that comment there are the characters "#!render:" then the function is considered a Render Function and the text on the line after the ":" will be displayed in the Render Functions list.

A Render Button Function, optionally specified in the Render Button property on the TPL Editing and Testing screen listing the form fields and their properties, handles the processing of taps on the buttons. The first argument, like the Render Function, is the current value of the field. The second argument is an object with "settings" (a copy of the field command's "settings" property), "listinfo" (a copy of the listInfo calculated for the field), "buttonnumber" (the 0-origin button number), "x" and "y" (the position of the tap or click within the button), "title" (the title value), and "fieldname" (the name of the field either at top-level or within the data group). The function should return a text value for the new field value.

Why TPL?

A variety of factors went into the decision to create and use an application-specific language system for use in TransForm. This is both the language itself and the implementation of the language system.

Choosing an existing language opens the possibility of making use of an existing implementation, such as using the JavaScript engine built into mobile OS environments. A problem with an existing implementation is that most are not available in a language supported by the runtime environment of TransForm. If an existing implementation is not used, then there is the difficult challenge of creating and maintaining a compatible implementation that provides all of the features and behaviors (documented or not, needed for use working with forms or not) of other implementations. Without compatibility and full implementation, much of the advantages of an existing language are lost and the promise of "being standard" is not fulfilled. Hope that learning the language for use in TransForm will provide a strong basis for claiming skill for direct use elsewhere would then be suspect.

Another important factor was the need to have complete control of the implementation in order to help enforce security. The writers of the programs need to be able to be normal, line-of-business people, unvetted and perhaps unsupervised by an IT department. The language system needed to be able to be limited in functionality that could be abused, such as to leak data or operate maliciously while using the credentials of another user logged in with different roles.

Most common languages, including using JavaScript already part of the TransForm system, are designed to provide as much functionality as possible and would need to be cut back from their normal state. At that point, they would no longer be the same and would require special training to learn which features were changed or missing. Documentation elsewhere would be misleading. Code developed elsewhere could have anomalies when run in the restricted implementation. The promise of "write once, run elsewhere" would not apply without careful, knowledgeable checking in each case.

Another issue was the need to integrate the program execution with the form editing of TransForm. The normal execution engines of a language implementation would to need to be modified to meet those needs. The semantics of certain operations, and the definition of popular functions, would need to be changed.

An advantage of using a language system designed specifically for TransForm is that it can be tuned to the common operations performed when filling out forms. The library of functions can also be tuned to the data types being operated on. Features not required to meet the needs of TransForm users need not be implemented.

A desirable trait of a language used for TransForm is that it be easy for experienced programmers, familiar with other computer languages, to read and learn to use. That drove the use of explicit, word-based constructs such as "IF/ENDIF" and "FOR-TO-STEP/ENDFOR", and common expression and function-calling syntax.

Many of the form designers will not be directly using the language in any case. They will be using dialog-driven user interfaces to specify the operations to be performed, such as the construction of a conditional expression used to show or hide items on a form. For these users, the choice of language is not relevant at all, and hidden. However, if they do want to switch to using the language directly in a particular instance, the auto-generated code can be examined to provide a working baseline from which to start.

For all these reasons, and more, the decision to use TPL was made.

Videos

TransForm Programming Language: An Introduction to TPL

The first in a series about the TransForm Programming Language (TPL). In this webinar, we discuss what TPL is and how it can be used to enhance your TransForm Forms.

2021-02-23

TransForm Programming Language: Variables, Expressions, and Statements

Part two of a multi-part series on the TransForm Programming Language (TPL). In this webinar, we take a look at how to create variables in TPL and use them in expressions and statements.

2021-03-02

TransForm Programming Language: Event Handlers

Part three of a multi-part serires on the TransForm Programming Language (TPL). In this webinar, we take a look at how to use Event Handlers to add your own custom behaviors to your forms.

2021-03-09

TransForm Programming Language: Objects and Arrays

Part four of a multi-part serires on the TransForm Programming Language (TPL). In this webinar, we teach you about creating and using Object and Array variables in TPL.

2021-03-23

TransForm Programming Language: User Defined Functions

Part five of a multi-part serires on the TransForm Programming Language (TPL). In this webinar, you'll learn how to create and utilize your own TPL functions in your forms.

2021-04-06

TransForm Programming Language: Local Database Access with SQLite

Part six of a multi-part serires on the TransForm Programming Language (TPL). In this webinar, we teach you how to leverage SQLite for data storage and lookup in your forms using TPL.

2021-04-20

TransForm Programming Language: Capturing GPS Locations

Part seven of a multi-part serires on the TransForm Programming Language (TPL). In this webinar, you'll learn how to use TPL to capture the device location while a form is being used.

2021-05-11