Skip to main content

Comparison and truth

Everything a TDC config produces is text. A <gen type="number"> produces the text 42, a data pack produces the text Miller, and every built-in is text as well. Nothing carries a type alongside it, because nothing needs one: text is what lands in the CSV file at the end.

The only values that are not text are the ones you write yourself inside an expression — the 1 in if="N == 1", the admin in if="Role == admin".

That single fact decides every rule on this page.

Two questions, two operators​

Ask whether two things are equal and you are asking one of two different questions:

  • Are they the same number? 01 and 1 are.
  • Are they the same text? 01 and 1 are not.

Both questions are worth asking, so each has its own operator.

OperatorAsks"01" against 1
== !=the same number?true
=== !==the same text?false

This is the shape Perl settled on, and for the same reason: there, too, a value is text that may or may not read as a number, so one operator asks about the number and another asks about the characters.

<tdc>
<env count="4" seed="cmp" local="en">
<sequence name="Code"><gen type="text" value="1,01,1.0,+1" order="sequential"/></sequence>
</env>
<block>
<line><data>Code is "${{Code}}":</data><data if="Code == 1"> == 1</data><data if="Code === 1"> === 1</data></line>
</block>
</tdc>
./run code.tdc
Code is "1": == 1 === 1
Code is "01": == 1
Code is "1.0": == 1
Code is "+1": == 1

All four columns are the number one, so == 1 is true on all four. Only the first one prints as 1, so === 1 is true once.

== — the same number​

The rule, in order:

  1. If both sides are whole numbers, compare them as whole numbers. Exactly — a 19-digit id keeps every digit, because whole numbers never become doubles here. See Whole numbers.
  2. Otherwise, if one side is a number you wrote and the other is text that reads as a number, compare them as numbers.
  3. Otherwise, compare them as text.

Step 3 is what makes Role == admin work: neither side is a number, so both stay text.

Step 2 is what makes a money column work:

<tdc>
<env count="3" seed="money" local="en">
<sequence name="Total"><gen type="text" value="100.00,0.00,99.50" order="sequential"/></sequence>
</env>
<block>
<line><data>${{Total}}:</data><data if="Total == 100"> hundred</data><data if="Total == 0"> zero</data><data if="Total < 100"> under</data></line>
</block>
</tdc>
./run money.tdc
100.00: hundred
0.00: zero under
99.50: under
Two texts stay text

Step 2 needs a number you wrote. Two columns compared against each other never get pulled into arithmetic by it — but step 1 still applies, so two columns of digits do compare as whole numbers. "01" == "1" is true; "" == " " is false, even though both read as zero.

=== — the same text​

One rule, no steps: print both sides and compare the characters.

  • A column prints as whatever it holds.
  • A number you wrote prints the way it would in the output: 1 is 1, 1.5 is 1.5.
  • true and false print as those five and four letters.
  • Nothing at all — an empty column — prints as the empty text.
<tdc>
<env count="5" seed="http" local="en">
<sequence name="Status"><gen type="text" value="200,404,500,200,301" order="sequential"/></sequence>
</env>
<block>
<line><data>${{Status}}</data><data if="Status === 200"> OK</data><data if="Status >= 400"> failed</data></line>
</block>
</tdc>
./run status.tdc
200 OK
404 failed
500 failed
200 OK
301

Where the two disagree​

Only where the number and the characters genuinely differ. These are the shapes you will actually meet:

The column holds== 1=== 1Why
1truetruesame number, same characters
01truefalsea leading zero is a character
1.0truefalsea written-out decimal is characters too
+1truefalseso is a plus sign
1 (a space)truefalseso is a space
onefalsefalsenot a number and not those characters

Between two columns the same split holds, and there is no literal in sight:

<tdc>
<env count="4" seed="pair" local="en">
<sequence name="A"><gen type="text" value="01,7,x,0" order="sequential"/></sequence>
<sequence name="B"><gen type="text" value="1,7,x,00" order="sequential"/></sequence>
</env>
<block>
<line><data>${{A}} vs ${{B}}:</data><data if="A == B"> same number</data><data if="A === B"> same text</data></line>
</block>
</tdc>
./run pair.tdc
01 vs 1: same number
7 vs 7: same number same text
x vs x: same number same text
0 vs 00: same number

Reading text as a number is generous​

Step 2 of == uses the same reading the rest of the language uses, and it accepts more than you might expect: leading and trailing spaces are ignored, an empty value reads as zero, and 0x10 reads as sixteen.

<tdc>
<env count="4" seed="loose" local="en">
<sequence name="Field"><gen type="text" value="7,0x10,007,x" order="sequential"/></sequence>
</env>
<block>
<line><data>"${{Field}}":</data><data if="Field == 7"> == 7</data><data if="Field == 16"> == 16</data><data if="Field === '007'"> === '007'</data></line>
</block>
</tdc>
./run loose.tdc
"7": == 7
"0x10": == 16
"007": == 7 === '007'
"x":

None of that happens under ===, which reads nothing as anything. When the characters are the question — an id with a fixed width, a code with a leading zero, a hexadecimal field — === is the operator that answers it.

Ordering​

<, >, <= and >= always read both sides as numbers. There is no text ordering: two values that are not numbers are neither less than nor greater than each other, so both comparisons come out false.

<tdc>
<env count="6" seed="ord" local="en">
<sequence name="Age"><gen type="text" value="15,17,18,25,40,70" order="sequential"/></sequence>
</env>
<block>
<line><data>age ${{Age}}:</data><data if="Age < 18"> minor</data><data if="Age >= 18"> adult</data><data if="Age > 65"> senior</data></line>
</block>
</tdc>
./run ages.tdc
age 15: minor
age 17: minor
age 18: adult
age 25: adult
age 40: adult
age 70: adult senior

Two whole numbers are ordered exactly, past the point where a double would start rounding.

What counts as true​

A bare name is a condition on its own: if="Flag". So is anything !, && or || is handed. Two texts are false and every other text is true:

ValueTruthReason
`` (empty)falsethe column produced no value
falsefalsea flag column saying no
0truezero is a value, not an absence
00, 0.0truelikewise
(a space)truea space is a character
anything elsetrue
<tdc>
<env count="6" seed="truth" local="en">
<sequence name="V"><gen type="text" value="x,0,00,0.0,false,true" order="sequential"/></sequence>
</env>
<block>
<line><data>"${{V}}"</data><data if="V"> is true</data><data if="!V"> is false</data></line>
</block>
</tdc>
./run truth.tdc
"x" is true
"0" is true
"00" is true
"0.0" is true
"false" is false
"true" is true

This is Lua's and Ruby's rule — only "nothing" and "no" are false — in a language whose only carrier is text. TDC's two falsy texts are exactly those two things:

  • empty is how a column says it produced nothing: missing=, an if= that did not fire, a branch of parent= that is not this row's;
  • false is the boolean written the only way a column can write it.
0 is true, and that is the one to remember

if="Count" asks whether the column produced a value. A count of zero did. If the question is about the number, use the operator that means the number: if="Count != 0".

Why a flag column works​

_first, _last and every anomaly_flag column hold the literal text true or false. Because false is falsy, the flag reads as a condition directly:

<tdc>
<env count="6" seed="flag" local="en">
<sequence name="Amount"><gen type="number" value="10..99" anomaly="0.4" anomaly_factor="20" anomaly_flag="Spike"/></sequence>
</env>
<block>
<line><data>${{Amount}} Spike=${{Spike}}</data><data if="Spike"> OUTLIER</data><data if="Spike === 'true'"> ===true</data><data if="Spike == true"> ==true</data></line>
</block>
</tdc>
./run spike.tdc
89 Spike=false
30 Spike=false
76 Spike=false
47 Spike=false
1500 Spike=true OUTLIER ===true
1000 Spike=true OUTLIER ===true

Note the third marker: Spike == true never appears. == compares numbers, and neither the text true nor the boolean is one. Write the bare name, or === 'true'.

The same rule is what makes the comma-between-items idiom work:

<tdc>
<env count="4" seed="join" local="en">
<sequence name="Tag"><gen type="text" value="red,green,blue,gray" order="sequential"/></sequence>
</env>
<block>
<line><data>${{Tag}}</data><data if="!_last">, </data></line>
</block>
</tdc>
./run join.tdc
red, 
green, 
blue, 
gray

Two selectors that are always text​

parent="Code.1" and <case is="1"> are not comparisons — they are value selectors, asking which value the column produced on this row. They match the characters exactly, the way === does, and no reading as a number happens:

The column holdsparent="Code.1"<case is="1">== 1
1matchesmatchestrue
01nonotrue
1.0nonotrue

If a column of digits has to be matched by its number, do it with if= and ==. A case takes several keys at once — <case is="1|01|1.0"> — which is the shorter answer when the column holds a fixed set of spellings.

in​

in takes a list on its right and asks whether the left value is one of its members. It compares as loosely as ==, so a column of digits still matches a list of numbers.

<gen if="Country in [US, CA, MX]" type="text" value="NAFTA"/>

Bare words inside the list stay bare words, exactly as they do beside ==. A list is allowed nowhere else — see TDC259.

Which one to reach for​

The questionThe operator
Is this amount / age / count equal to N?==
Is this category / status word this one?== or ===
Is this id exactly these characters?===
Does this code still have its leading zero?===
Is this flag set?the bare name
Did this column produce anything at all?the bare name
Is this number greater / smaller?< > <= >=
Is it one of several?in

When both would work, == reads more naturally and is the everyday choice. Reach for === the moment the exact characters matter.

Precedence​

Highest first:

binding order
!   →   * / %   →   + -   →   < > <= >=   →   == != === !==   →   &&   →   ||

Parentheses override it. a ? b : c binds loosest of all.

The same five answers​

All five implementations — TypeScript, Python, Rust, C# and Java — answer every comparison on this page identically, and shared fixtures hold them to it. See Determinism.