Data Feed Formula Reference
The formula language used in data feed columns — how a column addresses its source (including XPath for JSON and XML), how vectors work, and the full function catalogue with the places it deliberately departs from Excel.
Every column of a data feed has a formula: an
expression that produces that column's values from the feed's raw data. The
simplest formula is a bare reference to one source field; anything Excel-like
can be built on top of it.
{ "name": "Amount", "type": "NUMERIC", "formula": "__source__@C:C;" }
{ "name": "Category", "type": "TEXT", "formula": "IF(__source__@C:C; > 1000, \"Large\", \"Small\")" }The language looks like Excel and shares most of its function names, but it is
not Excel. It operates on whole columns rather than cells, it has no & or
^ operators, and several familiar names (REPLACE, TRIM, DATE) do
something different from their Excel counterparts. Those differences are called
out throughout, and collected in Traps.
Writing a formula is a normal
PATCH /data_feeds/{id}on thecolumnsarray.
A write replaces the whole array, so send every column you want to keep — see
Data feeds.
The shape of a formula
- A leading
=is optional.=SUM(...)andSUM(...)parse identically. - Function names are case-insensitive.
if(...),If(...)andIF(...)
are the same function. This guide uppercases them by convention. - Arguments are separated by commas.
- Whitespace and newlines between tokens are ignored, so a long nested formula
can be laid out over several lines. (Remember to escape the newlines when you
put the formula in JSON.) //starts a line comment;/* ... */is a block comment.
IF(__source__@F:F; < 10, "A: 1-9",
IF(BETWEEN(__source__@F:F;, 10, 49), "B: 10-49",
IF(BETWEEN(__source__@F:F;, 50, 249), "C: 50-249",
BLANK())))
Everything is a vector
This is the one idea that makes the rest of the language make sense, and it is
where an Excel mental model goes wrong first.
A formula is evaluated once for the whole column, not once per row. Every
value in the language is a vector — an ordered list of values. A reference
like __source__@C:C; is the entire source field as one vector. A literal like
10 is a vector of length one, which the language calls a scalar.
Operators and most functions work element-wise:
| Expression | Result |
|---|---|
__source__@A:A; + __source__@B:B; | a vector, each A added to the corresponding B |
__source__@A:A; * 2 | a vector, every A doubled — the scalar is broadcast |
__source__@A:A; > 100 | a vector of TRUE/FALSE, one per row |
SUM(__source__@A:A;) | a scalar — an aggregate collapses the vector |
When two vectors of different lengths meet in an arithmetic operator, the result
has the length of the longer one and the missing elements are treated as 0.
A scalar on either side is applied to every element of the other.
The practical consequence: a column formula should produce a vector of the same
length as the feed's other columns. An aggregate like SUM or COUNT returns a
single value, and a column whose formula is an aggregate will have one row.
To keep a per-row result while aggregating, use GROUPBY (below), not SUM.
Referring to data
There are two kinds of reference, and they use different punctuation. Both end
in a semicolon. Forgetting it is the single most common syntax error: the
lexer reads everything from the @ or # to the next ; as the address, so an
unterminated reference swallows the rest of the formula.
The feed's own source: __source__@<address>;
__source__@<address>;__source__ is a placeholder the public API substitutes for the feed's internal
datasource id, in both directions — read a feed and you get __source__, write
__source__ back and it is resolved for you. Write the placeholder literally;
never try to guess the real id.
What goes after the @ depends on the format of the source (source.format
on the feed), not on the connector:
source.format | Address form | Example |
|---|---|---|
csv | Excel-style column/row/range | __source__@C:C; |
xls | worksheet name, comma, then Excel-style | __source__@Sheet1,C:C; |
json, xml | XPath expression into the document | __source__@/player/first_name; |
The Excel-style forms are:
| Form | Means |
|---|---|
A:A | all of column A |
A1 | a single cell |
A1:A9 | a range down one column |
B1:D1 | a range across one row |
1:1 | all of row 1 |
The json and xml form is large enough to have its own section.
An address may contain commas (the Sheet1,C:C form always does). They do
not split function arguments — the address runs to the ;, so
IF(__source__@Sheet1,C:C; > 8, "yes", "no") is a three-argument IF, not a
four-argument one.
Another column: <feedId>#<columnId>;
<feedId>#<columnId>;A reference of the form <feed id>#<column id>; reads an already-computed
column, either of this feed or of another one. Both ids are the public ids
returned by GET /data_feeds.
// this feed's own "Rating" column, by id
2b7f911633132df974e331a8f7e9fb9c#44da6dd61efae626819f61f0a171999b;
// a column of a different feed this one is joined to
733715a57c24cf19eb7a90fe4dc114fc#f210d320aaeecb09cf40eafa733161ff;
Use this to build a column on top of another column's result instead of
repeating its formula. Referencing another feed only resolves if the two feeds
are joined — see joined_to_ids on the feed.
Unlike __source__, these ids are not rewritten by the API. Write the real
feed id and column id.
Addressing JSON and XML
When source.format is json or xml, the address after the @ is an XPath
expression rather than a cell range. JSON is treated as the same kind of
hierarchical document as XML: objects become elements named by their keys,
arrays become repeated sibling elements. So a JSON document like
{ "player": [
{ "first_name": "Anders", "last_name": "Hedberg", "team": { "name": "Winnipeg", "wha": "1974-1978" } },
{ "first_name": "Maurice", "last_name": "Richard", "nickname": "Rocket", "team": { "name": "Montreal", "nhl": "1942-1960" } }
] }is addressed as /player/first_name, /player/team/wha, and so on.
Two things to keep straight before anything else:
- XPath indexes are 1-based.
/player[1]is the first player. (The formula
language's ownSUBSTRINGandSLICEare 0-based — they are unrelated.) @inside the path is not the datasource separator. Some sources use@
as the first character of a field name, and in XML@prefixes an attribute.
Only the first@in the formula separates the datasource from its address.
Choosing how much to select
The same element can be addressed several ways, and the choice decides how many
values the column gets. These are the forms the modeller's Selection Options
generate, and the ones worth knowing by hand:
| Address | Selects |
|---|---|
/player/first_name | every first_name at that level — the normal case, one column of data |
/player[1]/first_name[1] | one specific value, like a single cell |
/player[1]/first_name | every first_name inside the first player only |
//first_name | every first_name anywhere in the document, at any depth |
/player/* | every immediate child value of every player |
/player[2]/* | every immediate child value of the second player |
/player[3]/*/* | every second-level descendant value of the third player |
kf:names(/player) | the field names in player, not the values |
kf:fill_elements(/player,'nickname') | every nickname, with blanks for players that have none |
Predicates
A predicate in square brackets filters the nodes at that step.
// last names of players on team Winnipeg
/player[team/name='Winnipeg']/last_name
// every value whose field NAME contains "name"
/player/*[contains(name(),'name')]
// first names of players that have a nickname at all
/player[nickname]/first_name
Standard XPath functions are available inside a predicate; the ones that earn
their keep here are name(), contains(), substring(), position(),
last() and count().
Axes
Axes select relative to the current node — child::, descendant::,
following-sibling::, preceding-sibling:::
// first names of players that have a nickname sibling
/player/first_name[following-sibling::nickname]
// last names of players who played in the WHA
/player/last_name[following-sibling::team/wha]
One JSON-specific catch: sibling order in JSON is alphabetical by field name,
not document order. (In XML it is document order.) So whether a field is a
preceding-sibling or a following-sibling of another follows from their names,
not from how the JSON was written — first_name precedes last_name because
f sorts before l, regardless of the order the service emitted them in.
The alignment problem
This is the failure mode that actually bites, and it follows directly from
everything being a vector: a feed's columns are
combined by position, so every column must produce the same number of values
in the same order. A field that is missing from some records silently breaks
that. Given the example above, where only the second player has a nickname:
/player/first_name -> Anders, Maurice (2 values)
/player/nickname -> Rocket (1 value)
The feed lines Rocket up with Anders. Nothing errors; the data is just
wrong. kf:fill_elements(path, 'fieldname') fixes it by filling the gap
wherever the field is absent:
kf:fill_elements(/player,'nickname') -> "", Rocket
Reach for it whenever a JSON column comes from an optional field. Two caveats:
- The filler is an empty string, not a null, so a row it padded is caught by
= ""and not by= BLANK(). See
Blanks. - It does not work for nested arrays.
The kf: functions
kf: functions| Function | What it does |
|---|---|
kf:names(object, [sorted]) | Returns the field names in an element rather than their values. sorted defaults to true — see the warning below. |
kf:fill_elements(path, 'fieldname') | Returns fieldname from every element under path, filling with "" where it is missing. Keeps columns aligned. |
kf:fill_elements(path, 'fieldname', target) | The same, but reaches one level deeper: target is a child name inside fieldname, or a 1-based index if fieldname holds an array. |
kf:element_at(object, index) | Returns the element at 1-based position index. The only way to reach a field that has no name — an unnamed array position. |
kf:names is the common one, because APIs routinely return data keyed by the
thing you want on an axis — a date, a country, a demographic bucket:
kf:names(/player[1]) -> first_name, last_name, team
kf:names(/player/team) -> name, wha, name, nhl
kf:element_at(/rows,1) -> the 1st value of each row, where rows are unnamed
Watch the sort when pairing names with values. kf:names sorts its result
alphabetically unless you pass false, while the matching values come back in
source order. So the obvious-looking pair
__source__@kf:names(/data/values/value); // the categories — SORTED
__source__@/data/values/value/*; // the numbers — source order
only lines up if the keys happened to already be in alphabetical order. Pass
false to keep both sides in the same order:
__source__@kf:names(/data/values/value, false);
__source__@/data/values/value/*;
Literals
| Kind | Syntax | Notes |
|---|---|---|
| Number | 10, 1.5 | No exponent notation. A leading - is negation. |
| String | "hello" | Double quotes only. \" escapes a quote. |
| Character | 'A' | Single quotes, exactly one character. |
| Boolean | TRUE, FALSE | Any case. |
| Null | null | See Blanks. |
| Variable | $name | Bound by SET and MAP. |
Operators
From loosest to tightest binding:
| Precedence | Operators |
|---|---|
| 1 (loosest) | = != < <= > >= |
| 2 | + - |
| 3 | * / |
| 4 (tightest) | unary -, unary + |
Parentheses group as usual.
Note what is missing, because Excel habits will reach for all three:
- No
&for string concatenation — useCONCAT(a, b). - No
^for exponentiation — usePOWER(x, n). - No
%for modulo — useMOD(x, n). (%is also not a percent suffix.) - No
<>for inequality — it is spelled!=.
= is equality. A leading = at the very start of the formula is the optional
prefix, not a comparison.
Blanks, nulls and empty strings
Three things look similar and are not:
BLANK()returns a vector of one null value.BLANK(n)returnsnof
them. It is the normal way to say "no value" in a result: theFALSEbranch
of anIFthat should leave the cell empty isBLANK().nullis the literal null value.x = BLANK()andx = nullboth test
for emptiness.""is an empty string, which is a value. It is not equal toBLANK().
Blanks propagate: most functions return a blank for a blank input rather than an
error. The counting functions differ deliberately: COUNT skips both blanks and
empty strings, COUNTBLANK counts only those, COUNTNUMERIC counts only the
elements that are numbers, and COUNTALL counts every element whatever it is.
Types and coercion
There are three value types — number, string and logical — plus an error type.
A vector has one type for all of its elements.
Functions convert their arguments to the type they need, so an explicit cast is
rarely necessary — "10" + 5 is 15. Comparison between a string and a number
compares them numerically where both parse as numbers, and as text otherwise.
Note that a column's declared type (TEXT, NUMERIC, DATE) is applied to
the formula's result, and a DATE column additionally needs fmtArgs saying
how to read and render its values. The formula language itself has no date
type — see Dates.
Function catalogue
Names are case-insensitive. [opt] marks an optional argument.
This is the complete set of supported functions — the same list the app's
formula bar offers. The evaluator also resolves a number of internal names that
exist for the product's own use; they are not supported, not documented, and may
change or disappear without notice. If a function is not listed below, do not
use it.
Logic and conditionals
| Function | Notes |
|---|---|
IF(test, then, else) | Element-wise over test. |
AND(a, b, ...), OR(a, b, ...), NOT(a) | |
SWITCH(value, case1, result1, case2, result2, ...) | Use the literal case "_default_" for the fallback: SWITCH(x, 1, "one", "_default_", "many"). |
IN(values, set) | Is each value a member of set? |
BETWEEN(values, start, end) | Inclusive at both ends. |
Selecting and reshaping vectors
| Function | Notes |
|---|---|
SELECT(values, tests) | Keeps the elements of values where tests is TRUE. The workhorse filter: SELECT(__source__@A:A;, __source__@B:B; = "green"). |
ARRAY(a, b, ...) | Concatenates everything into one vector, coerced to the type of the first argument. Also splits a single comma-separated string. |
SLICE(v, [start], [end]) | 0-based, end-exclusive; negative indices count from the end. start defaults to 1, not 0 — a bare SLICE(v) drops the first element. |
FIRST(v, [n]), LAST(v, [n]) | n defaults to 1. |
SPLICE(v, start, deleteCount, [insert]) | Like JavaScript's splice. |
REVERSE(v) | |
SORT(v, [direction], [by]) | direction is "asc", "desc", "ascnumeric" or "descnumeric" (default "asc", which sorts as text). by sorts v by a parallel vector. |
GROUP(v) | Unique values, in alphabetical order. |
COUNTDISTINCT(v) | Count of each unique value, aligned with GROUP(v). |
PADVALUES(v, n, [value]) | Appends n blanks, or n copies of value. |
REPEAT(v, n) | Repeats each value n times. |
JOIN(v, [glue]) | Collapses a vector into one string. glue defaults to ",". |
BLANK([n]) |
Aggregation
All of these collapse a vector to a scalar.
SUM, SUMIF, AVERAGE, AVERAGEIF, MEDIAN, MODE, MIN, MAX,
COUNT, COUNTALL, COUNTNUMERIC, COUNTBLANK, COUNTIF, STDEV,
STDEVP, VARIANCE, VARIANCEP, RANK, SLOPE, STANDARDIZE,
NORMSDIST, ZTEST, ERF, ERFC.
COUNTIF and SUMIF take a condition vector rather than Excel's criteria
string: COUNTIF(__source__@A:A; <= 30).
GROUPBY(keys, values, [aggregation]) is the one that keeps a row per
group. It buckets values by the matching element of keys and aggregates each
bucket. The third argument is a formula string in which the variable
values is bound to the bucket — it defaults to "SUM(values)", and anything
is allowed there:
GROUPBY(__source__@A:A;, __source__@B:B;) // sum per key
GROUPBY(__source__@A:A;, __source__@B:B;, "AVERAGE(values)")
GROUPBY(__source__@A:A;, __source__@B:B;, "COUNTDISTINCT(values)")
GROUPBY(__source__@A:A;, __source__@B:B;, "JOIN(values)")
Results are in alphabetical order of keys, so they align with GROUP(keys) —
which is how you build the label column beside an aggregated one.
Running totals and trends
| Function | Notes |
|---|---|
CUMULATIVE(v) | Running total. |
CUMULATIVE_DIFFERENCE(v, [first]) | Difference from the previous element. first is "zero" or "use_first_value". |
MA_SIMPLE(v, n) | Simple moving average. |
MA_CUMULATIVE(v) | Cumulative moving average. |
MA_EXPONENTIAL(v, n) | Exponential moving average. |
Text
| Function | Notes |
|---|---|
CONCAT(a, b, ...) | Element-wise across vectors; this is the & replacement. |
LEFT(v, [n]), RIGHT(v, [n]) | n defaults to 1. |
SUBSTRING(v, from, [to]) | 0-based, end-exclusive; negative indices count from the end. |
LENGTH(v) | |
UPPER(v), LOWER(v), CAPITALIZE(v) | CAPITALIZE uppercases the first letter of each word and leaves the rest alone ("aBCd" → "ABCd"). |
TRIM_WHITESPACE(v) | Strips leading and trailing whitespace. This is Excel's TRIM. |
SUBSTITUTE(text, old, new, [occurrence]) | Substring replacement. old and new may be vectors, applied in turn. |
SUBSTITUTE_REGEX(text, pattern, new) | pattern is a regular expression. |
INDEXOF(v, search, [occurrence]), LASTINDEXOF(v, search, [occurrence]) | 0-based position; blank when not found, not -1. occurrence counts from 1 and must be positive. |
CONTAINS(haystack, needle) | TRUE per element where haystack contains needle. Case-sensitive. |
TRUNCATE(v, length, [position], [style]) | position is "end" (default), "start" or "split"; style is "ellipsis", "period" or "blank". |
TEXT_REVERSE(v) | Reverses the characters of each string. (REVERSE reverses the vector.) |
REMOVE_EMOJI(v) | |
NUMBERFORMAT(v, decimals, [separator]) | Formats a number as text. |
URLENCODE(v), URLDECODE(v) | |
COUNTRY_CLEAN(v, [2|3]) | Normalizes country names; optionally to ISO alpha-2 or alpha-3. |
Two names do not mean what Excel means:
REPLACE(v, old, [new])replaces whole values that equalold, not
substrings at a position. For substrings useSUBSTITUTE.TRIM(v, [match])removes elements from the vector — those equal to
match, or all blanks ifmatchis omitted. It does not touch whitespace;
that isTRIM_WHITESPACE.
Math
ABS, ROUND(x, digits), CEILING(x, [significance]),
FLOOR(x, [significance]), MOD(x, n), POWER(x, n).
CEILING and FLOOR round to a multiple (CEILING(0.234, 0.01) is 0.24),
where ROUND's second argument is a number of decimal places.
Lookup
LOOKUP(values, keys, results) — for each element of values, find it in
keys and return the matching element of results. keys and results are
typically columns of a joined feed:
LOOKUP(__source__@A:A;,
733715a57c24cf19eb7a90fe4dc114fc#641b58722863c003fefeb7861177b9b6;,
733715a57c24cf19eb7a90fe4dc114fc#f210d320aaeecb09cf40eafa733161ff;)
DATASOURCE(datasourceId, pointer) reads a field of another data source by
literal id — DATASOURCE("0123…def", "A:A"). The public API does not expose
datasource ids (that is the whole point of __source__), so prefer
<feedId>#<columnId>; for cross-feed reads; this is here because you will see it
in formulas written in the app.
Variables and mapping
SET(names, values, expression) binds variables for one expression:
SET(ARRAY("$low", "$high"), ARRAY(10, 100),
SELECT(__source__@A:A;, BETWEEN(__source__@A:A;, $low, $high)))
MAP(values, varName, expression) evaluates expression once per element
of values, with varName bound to that element:
MAP(__source__@A:A;, "$it", $it / 10)
MAPFLAT is the same but flattens a per-element vector result into one vector.
Variable names start with $, and the name is passed as a string in the
second argument while the expression uses it as a bare $name.
Dates
A date in this language is a number: epoch seconds. There is no date type.
So a date pipeline is always: parse text into epoch seconds, do arithmetic, then
format back to text.
DATE(__source__@B:B;, "yyyy-MM-dd") // text -> epoch seconds
DATE_ADD(<epoch>, "3", -1) // epoch -> epoch (one month earlier)
DATEVALUE(<epoch>, "MMM yyyy") // epoch -> text
| Function | Notes |
|---|---|
DATE(text, [format], [timezone]) | Parse. format defaults to "MM/dd/yyyy". Also accepts the literals "TODAY" and "YESTERDAY". Nothing like Excel's DATE(y, m, d). |
DATEVALUE(epoch, [format]) | Format back to text. Nothing like Excel's DATEVALUE. |
DATE_CONVERT(text, formatIn, formatOut) | Parse and reformat in one step. |
TODAY([tz]), YESTERDAY([tz]), NOW() | |
DATE_ADD(epoch, unit, amount, [tz]) | Negative amount subtracts. |
DATE_STARTOF(epoch, unit, [relative], [first], [tz], [endOfDay]) | |
DATE_ENDOF(epoch, unit, [relative], [first], [tz], [endOfDay]) | |
DATE_IN(epoch, unit, [relative], [first], [tz]) | TRUE if the date falls in that period. relative shifts the period: -1 is the previous one. |
DATE_UNITVALUE(epoch, unit, [first], [tz]) | Extracts one component. |
DATE_SET(epoch, unit, value, [tz]) | Sets one component. |
DATE_CLOSEST(epoch, dayOfWeek, direction, [tz]) | direction is "forward" or "backward". |
DATERANGE(start, end, format) | Generates a vector of dates. |
COUNT_DAYS(start, end, [excludeDOW], [holidays], [tz]) | Whole days between, excluding the given days of week and dates. |
TIME(values, format) | Parses a duration into seconds — TIME("1:47:67:800", "d:h:m:s"). Not Excel's TIME. |
Unit codes
Units are passed as numbers (usually quoted), not names. This table is not
guessable, and getting it wrong is silent — DATE_ADD(d, "5", 1) adds a day,
DATE_ADD(d, "3", 1) adds a month.
| Code | Unit | Code | Unit | |
|---|---|---|---|---|
"1" | year | "8" | second | |
"2" | quarter | "15" | day of year | |
"3" | month | "34" | week of month | |
"4" | week | "45" | day of week | |
"5" | day | |||
"6" | hour | |||
"7" | minute |
DATE_ADD, DATE_STARTOF/DATE_ENDOF and DATE_IN accept the calendar units
(1–8); DATE_UNITVALUE additionally accepts 15, 34 and 45.
The first argument
first argumentSeveral date functions take a first argument naming where a period starts: a
day of week for weeks ("sun", "mon", "tue", "wed", "thu", "fri",
"sat") or a month for quarters and fiscal years ("jan" … "dec"). Omit it —
or pass null or "" — for the account default.
Formats and time zones
format is a Java SimpleDateFormat pattern (yyyy-MM-dd, dd-MMM-yy,
MM/dd/yyyy HH:mm:ss). Two special values are recognised: "EPOCH" and
"EPOCH-MS", for input that is already epoch seconds or milliseconds.
timezone is an IANA zone id ("America/Toronto", "UTC", "GMT+8"). Omit it
to use the account's time zone. Where a function has both a per-date time zone
(on DATE) and a calculation time zone (the trailing argument on
COUNT_DAYS, DATE_ADD and friends), set both to the same value unless you
specifically want them to differ.
Traps
The shortlist of things that are wrong-by-default if you assume Excel or SQL:
- The trailing
;. Every@and#reference ends with one. Without it,
the parser reads to the end of the formula. __source__is literal. Do not substitute a datasource id; the API does
that.- A formula runs once for the column, not per row.
SUM(...)yields a
one-row column. REPLACEis not substring replacement — that'sSUBSTITUTE.TRIMis not whitespace trimming — that'sTRIM_WHITESPACE.DATEandDATEVALUEare not Excel's. They parse and format text
against epoch seconds.- Date units are numbers, and
"5"is day while"3"is month. SUBSTRINGis 0-based and end-exclusive, unlike Excel'sMID.SLICE(v)with no start drops the first element, becausestart
defaults to 1.- No
&,^,%or<>— useCONCAT,POWER,MODand!=. ""is notBLANK(). Test for emptiness with= BLANK().- Commas inside an address do not separate arguments.
- A JSON field missing from some records misaligns the whole column —
wrap it inkf:fill_elements(see the alignment problem). - XPath indexes are 1-based, while
SUBSTRINGandSLICEare 0-based. - JSON siblings are ordered alphabetically by field name, so
following-sibling::follows the names, not the document. kf:namessorts by default. Pair it with a values column using
kf:names(x, false), or the two columns may not line up.kf:fill_elementspads with"", not a null, so padded rows do not
match= BLANK().
Worked examples
Bucket a numeric column.
IF(__source__@F:F; < 10, "A: 1-9",
IF(BETWEEN(__source__@F:F;, 10, 49), "B: 10-49",
IF(BETWEEN(__source__@F:F;, 50, 249), "C: 50-249",
IF(__source__@F:F; > 999, "E: 1K+",
BLANK()))))
Fall back to another column when one is empty.
IF(__source__@I:I; = BLANK(), CAPITALIZE(__source__@C:C;), __source__@I:I;)
Normalize a date column to yyyy-MM.
DATE_CONVERT(__source__@B:B;, "d-MMM-yyyy", "yyyy-MM")
Flag rows in the last 30 days.
IF(DATE(__source__@B:B;, "yyyy-MM-dd") >= DATE_ADD(TODAY(), "5", -30), "recent", "older")
Revenue per account, one row per account.
GROUPBY(__source__@A:A;, __source__@G:G;)
Two aligned columns out of a keyed JSON object.
// column 1 — the keys
kf:names(/data/values/value, false)
// column 2 — the values beside them
/data/values/value/*
A JSON field that only some records have.
kf:fill_elements(/player,'nickname')
Enrich from a joined feed.
LOOKUP(__source__@A:A;,
733715a57c24cf19eb7a90fe4dc114fc#641b58722863c003fefeb7861177b9b6;,
733715a57c24cf19eb7a90fe4dc114fc#f210d320aaeecb09cf40eafa733161ff;)Updated about 2 hours ago