APL DataBinding Syntax (APL 2023.3)
(This is not the most recent version of APL. Use the Other Versions option to see the documentation for the most recent version of APL)
You use databinding syntax to write expressions that Alexa evaluates when displaying your Alexa Presentation Language (APL) document. You use databinding expressions to bind component properties to your data source, and to write conditional logic to hide and show components depending on criteria such as the viewport characteristics.
About data binding expressions
You use data binding expressions inside JSON strings. A data binding expression has the form "${expression}
". You can use any number of expressions inside a string, for example: "${2}+${2} = ${2+2}"
.
Expressions are evaluated within the current databinding context. The databinding context is a global dictionary that supports boolean values, numbers, strings, arrays, objects, null, and references to defined resources.
Supported value types
Identifier
An identifier is a name used to identify a databinding variable. Identifiers must follow the C identifier naming convention: [azAZ_][azAZ09_]*
. This means that the identifier must start with an upper or lowercase ASCII letter or underscore, followed by zero or more ASCII letters, numbers, or the underscore:
${data}
${_myWord23}
${__AnUgly26_letter__examplE}
String literals
Strings are defined using either single or double quotes. The starting and ending quote must match. Escape quotes, carriage returns, and linefeed characters.
${"Doublequoted string"}
${'Singlequoted string'}
${"Inner quote: \" or '"}
Expressions can be nested inside of a string.
${"Two plus two is ${2+2}"}
Numbers
Positive, negative, and floating point numbers are supported. Scientific notation isn't supported. All numbers are internally stored as doubles:
${1}
${34.75}
${64000000000}
Boolean values
Boolean values of true
and false
are supported.
${true}
${false}
null
The null
constant is supported.
${null}
Array
Define arrays inside of a databinding expression by providing a commaseparated list of expressions within square brackets:
${["A", "B", "C"].length} // Evaluates to 3
${["on","off"][0]} // Evaluates to "on"
${[true,"true",1][0]} // Evaluates to boolean true
Maps
Define a map object inside of a databinding expression by providing a string key and value pairs within curly brackets.
${{"A": 1, "B": 2}["A"]} // Evaluates to 1
Resources
Resources defined in the databinding context use the reserved character @
. For example:
${@myBlue}
${@isLandscape ? @myWideValue : @myNarrowValue}
APL packages define resources exposed in databinding.
Absolute dimensions
A dimensional suffix converts a number into an absolute viewport dimension. The valid dimensional suffixes are dp
, px
, vh
, and vw
:
${23 dp} // 23 displayindependent pixels
${10 px} // 10 pixels
${50 vw} // 50% of the width of the viewport
${100vh} // 100% of the height of the viewport
Dimensional suffixes must be attached to a number, not a more complex expression. For example ${(20*20) dp}
isn't valid, but ${20*20dp}
is valid.
Truthy and coercion
Databinding expressions involve different data types. These types can convert into other types. The following table gives an example of the different conversions. The examples shown in the table assume a viewport width of 512dp
and a dpi of 320
.
Object  Example  As Boolean  As Number  As String  As Color  As Dimension 

Null  null 
false  0  ""  transparent  0dp 
Boolean  true 
true  1  "true"  transparent  0dp 
Boolean  false 
false  0  "false"  transparent  0dp 
Number  23 
true  23  "23"  #00000017  23dp 
Number  0 
false  0  "0"  transparent  0dp 
String  "My dog" 
true  0  "My dog"  transparent  0dp 
String  "" 
false  0  ""  transparent  0dp 
String  "2.3" 
true  2.3 
"2.3"  transparent  2.3dp 
String  "red" 
true  0  "red"  #ff0000ff  0dp 
String  "50vw" 
true  50  "50vw"  transparent  256dp 
Array  [] 
true  0  ""  transparent  0dp 
Map  {} 
true  0  ""  transparent  0dp 
Color  red 
true  0  "#ff0000ff"  #ff0000ff  0dp 
Dimension  32px 
true  16  "16dp"  transparent  16dp 
Dimension  0vh 
false  0  "0dp"  transparent  0dp 
Dimension  23% 
true  0.23  "23%"  transparent  23% 
Dimension  0% 
false  0  "0%"  transparent  0% 
Dimension  auto 
true  0  "auto"  transparent  auto 
Anything else  ... 
true  0  ""  transparent  0dp 
Boolean coercion
A truthy value is a value that's considered true
when evaluated in a boolean context. All values are truthy except for false
, 0
, ""
, a zero absolute or relative dimension, and null
.
Number coercion
The Boolean true
value converts to the number 1
. String values are converted using the C++ std::stod
method, which is influenced by the locale. Absolute dimensions convert to the number of displayindependent pixels in the absolute dimension. Relative dimensions convert to the percentage value (for example, 32%
converts to 0.32
). Everything else converts to 0.
String coercion
Internal types convert to strings according to the rules shown in the following table:
Object  Example  Result  Description 

Null  null 
'' 
The null value isn't displayed. 
Boolean  true 
'true' 
Boolean true and false display as strings. 
false 
'false' 

Number  23 
'23' 
Integers have no decimal places. 
1/3 
'0.333333' 
Nonintegers have decimal places.  
String  "My \"dog\" " 
'My "dog" ' 
String values. 
Array  [...] 
'' 
Arrays don't display. 
Map  {...} 
'' 
Maps don't display. 
Color  red 
'#ff0000ff' 
Colors display in #rrggbbaa format. 
Dimension  23 dp 
'20dp' 
Absolute dimensions display with the suffix dp 
Dimension  20 % 
'20%' 
Percentage dimensions display with the suffix % 
Dimension  auto 
'auto' 
The auto dimension displays as 'auto' 
Anything else  ${Math.min} 
'' 
Math functions don't display. 
The specific format of noninteger numbers isn't defined, but should follow the C++ standard for sprintf(buf, "%f", value)
. The format might change based on the locale.
Color coercion
Color values are stored internally as 32bit RedGreenBlueAlpha (RGBA) values. Numeric values are treated as unsigned 32bit integers and converted directly. String values are parsed according to the rules in Data Types  Color.
Absolute dimension coercion
Numeric values are assumed to be measurements in dp
and are converted to absolute dimensions. String values are parsed according to the rules in Data Types  Dimension. All other values are 0.
Relative dimension coercion
Numeric values are assumed to be percentages and are converted directly. For example, 0.5 converts to 50%. Strings are parsed according to the rules in Data Types  Dimension. All other values are 0.
Operators
APL supports the following types of operators: arithmetic, logical, comparison, null coalescing, and ternary.
Arithmetic operators
APL supports the standard arithmetic operations for addition, subtraction, multiplication, division, and remainder.
${1+2} // 3
${12} // 1
${1*2} // 2
${1/2} // 0.5
${1%2} // 1.
Addition and subtraction work for pairs of numbers, absolute dimensions, and relative dimensions. When a number is combined with either an absolute or relative dimension, the number is coerced into the appropriate dimension.
The addition operator also acts as a stringconcatenation operator if either the left or right operand is a string.
${27+''} // '27'
${1+' dog'} // '1 dog'
${'have '+3} // 'have 3'
Multiplication, division, and the remainder operator work for pairs of numbers. Multiplication also works if the one of the operands is a relative or absolute dimension and the other is a number. This calculation results in a dimension. Division works if the first operand is a relative or absolute dimension and the second is a number. This calculation results in a dimension.
The remainder operator behaves as in JavaScript.
${10 % 3} // 1
${1 % 2} // 1
${3 % 6} // 3
${6.5 % 2} // 0.5
Logical operators
APL supports the standard logical and/or/not operators.
${true  false} // true
${true && false} // false
${!true} // false
The &&
returns the first operand if it isn't truthy and the second otherwise. The 
operator returns the first operand if it's truthy and the second otherwise.
${7 && 2} // 2
${null && 3} // null
${7  2} // 7
${0  16} // 16
Comparison operators
Comparison operators return boolean values.
${1 < 2}
${75 <= 100}
${3 > 1}
${4 >= 4}
${myNullValue == null}
${(2>1) == true}
${1 != 2}
The comparison operators don't apply to arrays and objects.
Comparison operators don't perform type coercion. For example, the expression ${1=='1'}
returns false
. The following table lists the valid comparisons. All other comparisons return false
.
Comparison types  <, >, <=, >=  ==, !=  Notes 

Number to Number 
Valid 
Valid  
Number to Absolute Dimension 
Valid 
Valid 
The number is treated as a displayindependent pixel dimension 
Number to Relative Dimension 
Valid 
Valid 
The number is treated as a percentage. For example, 0.4 equals 40%. 
Relative Dimension to Relative Dimension 
Valid 
Valid 
The dimensions are compared as percentages. 
Absolute Dimension to Absolute Dimension 
Valid 
Valid 
The dimensions are converted to displayindependent pixels. 
String to String 
Valid 
Valid  
Boolean to Boolean 
False 
Valid  
Color to Color 
False 
Valid  
Null to Null 
False 
Valid 

Auto to Auto Dimension 
False 
Valid 
Two auto dimensions are equal (even when the final size isn't equal) 
All other combinations 
False 
False 
The ==
operator in APL is similar to the ===
operator in JavaScript.
Null coalescing
The ??
operator is the nullcoalescing operator. It returns the lefthand operand if the operand isn't null
; otherwise it returns the righthand operand. You can chain the nullcoalescing operator.
${person.name ?? person.surname ?? 'Hey, you!'}
The nullcoalescing operator returns the lefthand operand if it's anything but null:
${1==2 ?? 'Dog'} // returns false
${1==2  'Dog'} // returns 'Dog'
Ternary operator
The ternary conditional operator ${a ? b : c}
evaluates the lefthand operand. If it evaluates to true or a truthy value, the middle operand is returned. Otherwise the righthand operand is returned.
${person.rank > 8 ? 'General' : 'Private'}
Array and object access
Array
Array access uses the []
operator, where the operand must be an integer. Arrays also support the .length
operator to return the length of the array. Accessing an element outside of the array bounds returns null
.
${myArray[4]} // 5th element in the array (0indexed)
${myArray.length} // Length of the array
${myArray[1])} // Last element in the array
${myArray[myArray.length]} // Returns null (out of bounds)
Passing a negative index counts backwards through the array.
${a[1] == a[a.length  1]} // True
Object
Objects support the .
operator and the []
array access operator with string values.
${myObject.name} // The 'name' property of myObject
${myObject['name']} // The 'name' property of myObject
If the property isn't defined, the expression returns null
.
Calling the .
or []
operator on null
returns null
.
${myNullObject.address.zipcode} // Returns null
The rightside operand of the dot operator must be a valid identifier.
Function calls
Databinding supports a limited number of builtin functions. Functions use the following form.
functionName( arg1, arg2, … )
Functions don't require arguments. A function returns a single value. The following examples show a variety of function expressions.
${Math.floor(1.1)} // 1
${Math.ceil(1.2)} // 2
${Math.round(1.2)} // 1
${Math.min(1,2,3,4)} // 1
${Math.max(1,2,3,4)} // 4
${String.toUpperCase('Hello')} // HELLO
${String.toLowerCase('Hello')} // hello
${String.slice('Hello', 1, 1)} // ell
The available functions are grouped by toplevel property:
Array functions
The toplevel Array
property is a collection of functions and constants for manipulating arrays.
The examples in the Example column use the following array: a = [101,102,103,104,105,106]
.
Function  Description  Example 

Array.indexOf(x,y) 
The index of element y in array x . Returns 1 if the array x doesn't contain y . 
${Array.indexOf(a,102)} == 1 
Array.range(start, end, [step]) 
Return an array of integer elements that starts from


Array.slice(array,start[,end]) 
Return the subset of

${Array.slice(a,3)} == [104,105,106] ${Array.slice(a,1,3)} == [102,103] ${Array.slice(a,2)} == [105,106] 
Math functions
The toplevel Math
property is a collection of functions and constants for numerical calculations.
Function  Description  Example 


The absolute value of x 


The arccosine of x 


The hyperbolic arccosine of x 


The arcsine of x 


The hyperbolic arcsine of x 


The arctangent of x 


The hyperbolic arctangent of x 


The arc tangent of y/x 


The cube root of x 


The smallest integer greater than or equal to x. 


Return x if y<x, z if y>z and otherwise y. 


The cosine of x 


The hyperbolic cosine of x 


e raised to the x, where e is Euler's constant. 


2 raised to the x 


e raised to the x minus 1. 


Converts x into a floatingpoint number. A trailing '%' character specifies a percentage. 


The largest integer less than or equal to x. 


The square root of the sum of the squares of the arguments 


Convert x into an integer. b is the optional base to use for string conversion, where 0 autodetects or 2–36 define the base. 


Returns true when x is finite. Returns false when x is infinite or is NotANumber (NaN) 


Returns true when x is infinite 


Returns 


The natural logarithm of x 


The natural logarithm of 1+x 


The base10 logarithm of x 


The base2 logarithm of x 


The largest argument 


The smallest argument 


Raises x to the y power 


A random number between 0 and 1 


Return the nearest integer to x 


The sign of x: 


The sine of x 


The hyperbolic sine of x 


The square root of x 


The tangent of x 


The hyperbolic tangent of x 


The integer portion of x 

Constant  Description  Value 

Math.E 
Euler's constant (e)  2.718281828459045 
Math.LN2 
Natural logarithm of 2  0.6931471805599453 
Math.LN10 
Natural logarithm of 10  2.302585092994046 
Math.LOG2E 
Base2 logarithm of e  1.4426950408889634 
Math.LOG10E 
Base10 logarithm of e  0.4342944819032518 
Math.PI 
Ratio of the circumference of a circle to the diameter (π)  3.141592653589793 
Math.SQRT1_2 
Square root of 0.5  0.7071067811865476 
Math.SQRT2 
Square root of 2  1.4142135623730951 
String functions
The toplevel String
property is a collection of functions for manipulating strings.
Function  Description  Example 

String.charAt(x,y) 
Return the character at index y. If y is a negative number, select from the end of the string.  ${String.charAt('école', 0)} == 'é' ${String.charAt('école', 2)} == 'l' 
String.length(x) 
Return the length of the string  ${String.length('schön')} == 5 
String.slice(x,y[,z]) 
Return the subset of x starting at index y and extending to but not including index z. If z is omitted, the remainder of the string is returned. If y is a negative number, select from the end of the string.  ${String.slice('berry', 2, 4)} == 'rr' ${String.slice('berry', 2)} == 'ry' 
String.toLowerCase(x) 
Lowercase the string  ${String.toLowerCase('bEn')} == 'ben' 
String.toUpperCase(x) 
Uppercase the string  ${String.toUpperCase('bEn')} == 'BEN' 
The string length
and slice
functions work on Unicode code points, not bytes.
The following example shows all the available String
functions. This example also uses deferred evaluation. The data source stores the function examples as databinding expressions with the #{...}
placeholder. This lets the document display the text of the function, and then also use eval()
to evaluate the function to display the result.
Time functions
The toplevel Time
property is a collection of functions that convert from time values in milliseconds into years, months, days, hours, minutes, and seconds. The examples in the table assume a bound time value T=1567786974710, which in humanreadable terms is Friday September 6, 2019 at 16:22:54 and 710 milliseconds.
Function  Description  Example 

Time.year(x) 
The year  ${Time.year(T)} == 2019 
Time.month(x) 
The month (from 0 through 11)  ${Time.month(T)} == 8 (September) 
Time.date(x) 
The date of the month (131)  ${Time.date(T)} == 6 
Time.weekDay(x) 
The day of the week (06)  ${Time.weekDay(T)} == 5 (Friday) 
Time.hours(x) 
The hour of the day (023)  ${Time.hours(T)} == 16 
Time.minutes(x) 
The minutes of the hour (059)  ${Time.minutes(T)} == 22 
Time.seconds(x) 
The seconds in the minute (059)  ${Time.seconds(T)} == 54 
Time.milliseconds(x) 
The milliseconds (0999)  ${Time.milliseconds(T)} == 710 
Time.format(f, x) 
A formatted text string  ${Time.format('H:mm', T)} == "16:22" 
To construct a basic digital clock, you could use the Time functions to build a 24hour clock out of a Text
component and the localTime
property:
{
"type": "Text",
"bind": {
"name": "T",
"value": "${localTime}"
},
"text": "${Time.hours(T)}:${Time.minutes(T)}"
}
This example binds the value of localTime
into a local variable T
to make the text expression shorter. This clock renders a time like 4:04 PM as 16:4
, which might not be what you want. You can use conditional statements to determine the time and include leading/trailing zeros if necessary.
You can also use conditional statement to display a 12hour clock as shown in this example. This binds both the hours and minutes to local variables, and then uses conditional statements to output the time in a 12hour format with am
or pm
.
{
"type": "Text",
"bind": [
{
"name": "h",
"value": "${Time.hours(localTime)}"
},
{
"name": "m",
"value": "${Time.minutes(localTime)}"
}
],
"text": "${h >= 12 ? h  12 : h}:${m < 10 ? '0' : ''}${m} ${h >= 12 ? 'pm' : 'am'}"
}
You can also use the Time.format
function to simplify the code. This example also uses a conditional statement based on hours to add the "am" or "pm" at the end.
{
"type": "Text",
"bind": [
{
"name": "h",
"value": "${Time.hours(localTime)}"
}
],
"text": "${Time.format('h:mm', localTime) + (h >= 12 ? ' pm' : ' am')}"
}
Time.format
The format
function takes a string argument containing the formatting codes and a time value. The following formatting codes are supported:
Value  Range  Description 

YY 
00..99  Year, two digits 
YYYY 
1970..XXXX  Year, four digits 
M 
1..12  Month (1=January) 
MM 
01..12  Month, two digits (1=January) 
D 
1..31  Day of the month 
DD 
01..31  Day of the month, two digits 
DDD 
0..N  Days, any number of digits 
H 
0..23  24h hour 
HH 
00..23  24h hour, two digits 
HHH 
0..N  Hours, any number of digits 
h 
1..12  12h hour 
hh 
01..12  12h hour, two digits 
m 
0..59  Minutes 
mm 
00..59  Minutes, two digits 
mmm 
0..N  Minutes, any number of digits 
s 
0..59  Seconds 
ss 
00..59  Seconds, two digits 
sss 
0..N  Seconds, any number of digits 
S 
0..9  Deciseconds 
SS 
00..99  Centiseconds 
SSS 
000..999  Milliseconds 
All the formatting codes return digits only.
The following table shows examples of the formatting functions, using the time value September 6, 2019 at 16:22:54 and 710 milliseconds.
Format  Value 

DDMMYYYY 
06092019 
M/D/YY 
9/6/19 
DDD days 
18145 days (after the epoch) 
H:mm 
16:22 
hh:mm 
04:22 
H:mm:ss 
16:22:54 
Time formatting also works for relative times from timers. The following table shows examples that use the value 7523194
, which corresponds to two hours, five minutes, and 23.194 seconds.
Format  Value 

mmm:ss.S 
125:23.1 
HHH:mm:ss.SS 
2:05:23.19 
sss.SSS 
7523.194 
The following example uses elapsedTime
to show the time after the document has loaded. To restart the timer in the sandbox, refresh the page.
Databinding string conversion
Because APL is serialized in JSON, all databound expressions are defined inside of a JSON string:
{
"MY_EXPRESSION": "${....}"
}
When there are no spaces between the quotation marks and the databinding expression, the result of the expression is the result of the databinding evaluation. For example:
"${true}" > Boolean true
"${2+4}" > Number 6
"${0 <= 1 && 'three'}" > String 'three'
When extra spaces are in the string outside of the databinding expression or when two databinding expressions are juxtaposed, the result is a string concatenation:
" ${true}" > String ' true'
"${2+4} " > String '6 '
"${2+1}${1+2}" > String '33'
Deferred evaluation
When APL evaluates a property or bound variable, expressions written with the ${...}
syntax are evaluated and resolved. Sometimes, you might want to defer this evaluation to later and control when to resolve an expression. With APL version 2023.2 and later, you can use the #{...}
placeholder and the eval()
builtin function to defer expression evaluation. You can also use eval()
to evaluate expressions that aren't evaluated automatically, such as expressions within a data source.
version
property for the document must be "2023.2" or later. Documents with an earlier version
ignore the new syntax, even when run on a device that has a supported version.Deferred placeholder syntax
Databinding evaluation replaces the #{...}
placeholder with a standard databinding expression, but doesn't evaluate that expression. This means that the ${...}
syntax remains. The following example shows a Text
component that uses the #{...}
syntax in the text
property.
{
"type": "Text",
"text": "The equation #{1+2} evaluates to ${1+2}"
}
Normal evaluation on the string results in the string "The equation ${1+2} evaluates to 3"
. The ${1+2}
expression remains in the string.
The expression inside of the #{...}
placeholder must be a valid APL expression or the replacement fails. The following example shows a bind
array with two items. The variable X
resolves to ${1+a}
. The variable Y
fails to evaluate because 1+
isn't a valid expression. Therefore, the Y
contains #{1+}
.
"bind": [
{
"name": "X",
"value": "#{1+a}"
},
{
"name": "Y",
"value": "#{1+}"
}
]
eval()
function
The eval()
builtin function takes a single argument and runs the databinding Databinding algorithm on the argument. The argument is either a string that was created with the #{...}
placeholder or a string provided in a data source.
The following example shows three bind
items. Format
uses the deferred data binding syntax, and Greeting
calls the eval()
function.
"bind": [
{
"name": "Format",
"value": "Hello, #{Name}"
},
{
"name": "Name",
"value": "Chris"
},
{
"name": "Greeting",
"value": "${eval(Format)}"
}
]
The bindings resolve to the following values:
Format = "Hello, ${Name}"
Name = "Chris"
Greeting = "Hello, Chris"
You can nest the eval()
calls, as shown in the following example.
"bind": [
{
"name": "A",
"value": "#{B}"
},
{
"name": "B",
"value": "#{C}"
},
{
"name": "C",
"value": "Hello"
},
{
"name": "D",
"value": "${eval(A)}"
},
{
"name": "E",
"value": "${eval(eval(A))}"
}
]
The bindings in this example resolve to the following values:
A = "${B}"
B = "${C}"
C = "Hello"
D = "${C}"
E = "Hello"
Note that calling eval(A)
results in "${C}"
, not "Hello"
.
Use deferred evaluation for resource localization
With deferred evaluation, you can embed dynamic data within items defined as resources. This technique is useful for formatting languagespecific strings.
The following example defines a TEMPERATURE_FORMAT
resource that uses different formatting for different locales. Change the locale in the data source to see the screen update.
The example adapts to the language and converts the temperature to a regionappropriate format. For the enUS
environment, APL sets the resources to the following values:
CELSIUS = "${TEMP} °C"
FAHRENHEIT = "${TEMP * 9 / 5 + 32} °F"
TEMPERATURE_FORMAT = "The temperature is ${TEMP * 9 / + 32} °F" // "enUS" clause selected
text = "The temperature is 77 °F"
The eval()
call in the text
property evaluates the TEMPERATURE_FORMAT
string using TEMP=25.0
, which results in "The temperature is 77 °F"
. If you change the value of TEMP
, such as with the SetValue, the text
property reevaluates the expression and updates the component.
Deferred evaluation and data sources
You can use the eval()
function to evaluate databinding expressions passed to your document in a data source.
By default, data binding evaluation doesn't process expressions within a data source. For example, assume you set a data source property to the expression ${1+2}
and then bind that data source property to a component in your document. When the component inflates, the component property contains the text "${1+2}"
and not the evaluated result of "3"
.
To process data binding expressions within data source properties, pass the property to the eval()
function.
The following example shows how you can use the eval()
function with a data source.
How eval() processes arrays and objects
An eval()
function call on an array or object evaluates each array entry and object property separately. This feature can be useful to process a set of formatting strings.
For example, assume X
is an object with two properties that contain databinding expressions.
X = {
"formal_greeting": "Hello, ${NAME}",
"casual_greeting": "Hi ${NAME}!"
}
NAME = "Raj"
If you pass X
to eval()
, the function returns an object in which each property is now resolved.
eval(X) = {
"formal_greeting": "Hello, Raj",
"casual_greeting": "Hi Raj!"
}
The following example shows an APL document that invokes eval()
on an object.
Deferred evaluation guidelines
The eval() function is expensive
The eval()
function parses and evaluates the expression each time you call the function. This means that the eval()
function is an expensive operation. Before you use eval()
, consider whether you could use a different approach.
Avoid recursive calls to eval()
Be careful not to create recursive calls to eval()
that never end.
The following example creates a bind
variable A
that calls eval()
on itself.
Example of a recursive call
{
"type": "Text",
"bind": {
"name": "A",
"value": "HA! #{eval(A)}"
},
"text": "I'm invincible! ${eval(A)}!"
}
APL limits the depth of recursive evaluation calls. Therefore, this example might display text such as "I'm invincible! HA! HA! HA! ${eval(A)}" instead of an infinitely long string. The limit is guaranteed to be at least 3, but might be higher in a specific runtime.
Related topics
Last updated: Nov 30, 2023