Arithmetic and Comparison Operators

Reference for arithmetic and comparison operators in Structured Text on rungs.dev, including built-in math functions and DINT/REAL rounding rules.

Most logic eventually comes down to math: counting, scaling, comparing. This page covers the operators you'll use, the built-in math functions, and the rounding rules you need to know when integers and decimals meet.

Modeled on Studio 5000 Logix Designer

rungs.dev models its math behavior on Studio 5000 Logix Designer® — division, rounding, and type coercion follow the same conventions. Rare edge cases may differ.

Arithmetic Operators

OperatorDescriptionExample
+AdditionTotal := A + B;
-SubtractionDelta := A - B;
*MultiplicationProduct := A * 5;
/DivisionAverage := Sum / Count;
MODRemainderRemainder := A MOD B;
**Raise to a powerSquared := Value ** 2;

Use parentheses to control the order, just like in regular math:

Result := (Value1 + Value2) * 3;

The power runs before a minus sign, and two powers run left to right

Both are worth committing to memory, because brackets are the only way to get the other reading:

Result := -2 ** 2;    // -4, because it means -(2 ** 2)
Result := (-2) ** 2;  // 4  — brackets make the sign part of the number
Result := 2 ** 3 ** 2;  // 64, because it means (2 ** 3) ** 2
Result := 2 ** (3 ** 2);  // 512 — what maths would give you

Comparison Operators

Comparison operators ask a true/false question. They show up most often inside an IF.

OperatorDescriptionExample
=Equal toIF Mode = 1 THEN
<>Not equal toIF State <> 0 THEN
<Less thanIF Temp < 100 THEN
>Greater thanIF Speed > Limit THEN
<=Less than or equalIF Level <= 50 THEN
>=Greater or equalIF Count >= Max THEN

Comparing BOOLs

Look closely at the table — every example compares numbers. That's the rule: comparison operators only work on numeric values. Comparing two BOOL tags with = or <> is a compile error.

For bits, the question "are these two different?" already has an operator — XOR:

IF In_A <> In_B THEN        // ✗ compile error — BOOLs can't use comparison operators
IF In_A XOR In_B THEN       // ✓ true when the two bits differ
IF NOT (In_A XOR In_B) THEN // ✓ true when they match

Division: integers vs decimals

Division is the one operator whose result depends on what you're dividing into. The destination tag's type decides whether you keep the fraction or lose it.

Try this in your head: 7 / 2. The exact answer is 3.5. What you get in the tag depends on where you're putting it:

DintResult := 7 / 2;       // DintResult = 3   — integer truncation
RealResult := 7 / 2;       // RealResult = 3.5 — fraction preserved
DintResult := 7.0 / 2;     // DintResult = 4   — see "Banker rounding" below

The full rule:

Left operandRight operandDestinationWhat you get
DINTDINTDINTInteger division (truncates to zero)
DINTDINTREALFloat division (fraction preserved)
any REALanyDINTFloat division, then banker rounding
any REALanyREALFloat division

A small detail worth knowing: integer division always truncates toward zero, not down. So -7 / 2 is -3, not -4.

One decimal changes the whole expression

The choice between whole-number and decimal maths is made once, for the whole expression, before anything is worked out — not step by step. If a decimal appears anywhere in it, every step uses decimals, even the ones written before it:

DintResult := 7 / 2 * 1.5;   // 5 — the 1.5 makes 7 / 2 a decimal divide: 3.5 * 1.5 = 5.25
DintResult := 10 / 4 * 4;    // 8 — no decimal anywhere: 10 / 4 is 2, then × 4

That first line is the one to remember. The 1.5 is at the far end of the expression and it still changes how 7 / 2 at the start is worked out.

It holds even where the decimal is hidden inside something that answers a whole number. The bitwise operators always answer a whole number, but a decimal inside one still switches the rest of the expression to decimals:

DintResult := 7 / 2 + (1.5 AND 1);  // 4

Working that through: the 1.5 becomes 2 before AND can use it, and 2 AND 1 is 0. But its presence made 7 / 2 a decimal divide, so the sum is 3.5 + 0 = 3.5, which rounds to 4. Without the 1.5 in there, the same shape would give 3.

The three things that switch an expression to decimals, then, are: a REAL value anywhere in it, a call to SIN, COS, TAN, ASIN, ACOS, ATAN, LN, LOG, DEG or RAD, or a REAL destination.

That list of functions is exact. SQRT, TRUNC and ABS are not on it, even though the first two hand back a decimal:

DintResult := 7 / 2 + SIN(0.0);   // 4 — SIN switches the whole thing to decimals: 3.5 + 0.0
DintResult := 7 / 2 + SQRT(4.0);  // 6 — the 4.0 did that, not SQRT
DintResult := 7 / 2 + SQRT(4);    // 5 — nothing decimal anywhere: 3 + 2

One exception: AND, OR and XOR between two comparisons

Everything above holds right across an expression — through brackets, through a comparison, even inside a function's brackets. The single exception is a logical AND, OR or XOR joining two comparisons. Those split the expression in two, and each side decides for itself:

IF (Ratio > 1.0) AND (Count / Divisor > 3) THEN  // Count / Divisor stays whole

The Ratio on the left does not reach the division on the right. Note this is the logical use of AND — joining two comparisons. The bitwise use, on two numbers, splits nothing, which is the (1.5 AND 1) example above.

Banker rounding (REAL into DINT)

When you put a decimal value into a DINT, Studio uses banker rounding — also called round half to even.

You probably learned in school: round halves up (0.5 → 1, 1.5 → 2, 2.5 → 3, 3.5 → 4). Banker rounding does something slightly different — when the value is exactly halfway, it rounds to the nearest even number:

ValueSchool roundingBanker rounding
0.510
1.522
2.532
3.544
-2.5-2-2
-3.5-3-4

For values that aren't exactly halfway, banker rounding works the same as normal rounding: 1.4 → 1, 1.6 → 2.

Why use it? Because rounding all halves up adds a small bias: if you round many measurements over and over, the total drifts a little high. Banker rounding splits halves evenly between rounding up and rounding down, so the bias cancels out. Industrial controllers and financial systems use it for the same reason.

DintSetpoint := RealSetpoint;   // banker rounding applied automatically

Division by zero

Dividing by zero is normally an error. Studio handles it without crashing the program. The result depends on the operand types and the destination type:

What you wroteWhat you get
DintDest := 5 / 0;DintDest = 5 — Source A copied (DINT/DINT)
DintDest := 0 / 0;DintDest = 0 — Source A copied
DintDest := 5.0 / 0.0;DintDest = 2147483647 — +Infinity clamps to MAX_DINT
DintDest := -5.0 / 0.0;DintDest = -2147483648 — -Infinity clamps to MIN_DINT
DintDest := 0.0 / 0.0;DintDest = 0 — NaN becomes 0
RealDest := 5.0 / 0.0;RealDest = +Infinity
RealDest := 0.0 / 0.0;RealDest = NaN ("Not a Number")
RealDest := 5 / 0;RealDest = +Infinity — see the note below

For a DINT destination, all-DINT division copies the left operand (Source A); when any operand is REAL, the float result is clamped into the DINT range so you never see Infinity in an integer tag. A REAL destination keeps the raw +Infinity, -Infinity, or NaN value. The program keeps running in every case in this table.

Watch the last row. 5 / 0 looks like whole-number division, but the destination type decides the kind of maths, not just the operands. Store into a DINT and you get integer division, which copies Source A and gives you 5. Store the very same expression into a REAL and it becomes float division, which gives you +Infinity. The text you wrote is identical; only the tag on the left changed.

One place it does stop the program

Everything above is about division in ordinary maths. Divide by zero inside an array index and the controller stops the scan instead:

Buffer[Count / 0] := 1;   // the scan stops here

There is no sensible element to reach, so nothing is written and the rest of the routine never runs. Check the divisor before using it as an index:

IF Divisor <> 0 THEN
    Buffer[Count / Divisor] := 1;
END_IF;

If you want to handle a possible zero divisor explicitly, check for it first:

IF Divisor <> 0 THEN
    Result := Numerator / Divisor;
ELSE
    Result := 0;   // or whatever fallback makes sense
END_IF;

MOD mostly follows the same rules — the result's sign follows the left operand (-7 MOD 2 is -1) and REAL operands keep the fraction (7.5 MOD 2.0 is 1.5) — but MOD by zero differs from division by zero: integer x MOD 0 writes 0 (division copies the left operand), and REAL x MOD 0.0 gives Infinity with the sign of x.

Built-in math functions

These are the math functions you can call from any expression:

FunctionWhat it does
ABS(x)Absolute value (drops the minus sign).
SQRT(x)Square root. Negative inputs become SQRT(ABS(x)).
SIN(x)Sine of x, where x is in radians.
COS(x)Cosine of x, in radians.
TAN(x)Tangent of x, in radians.
ASIN(x)The angle in radians whose sine is x.
ACOS(x)The angle in radians whose cosine is x.
ATAN(x)The angle in radians whose tangent is x.
ATAN2(y, x)The angle in radians from the X axis to the point (x, y). Both signs are used, so it can tell one quadrant from another.
LN(x)Natural logarithm (base e).
LOG(x)Logarithm base 10.
DEG(x)Converts radians to degrees.
RAD(x)Converts degrees to radians.
TRUNC(x)Drops the fractional part, rounding toward zero.
DintDest := ABS(-7);     // 7
RealDest := SQRT(-1.0);  // 1.0 — never NaN, never an error
RealDest := SIN(0.0);    // 0.0
RealDest := DEG(3.14159274);  // 180.0
DintDest := TRUNC(-2.7); // -2, not -3: it rounds toward zero

TRUNC is not the same as storing a REAL in a DINT. Storing rounds to the nearest whole number; TRUNC always heads toward zero:

DintDest := 2.7;         // 3  — rounded
DintDest := TRUNC(2.7);  // 2  — truncated
DintDest := -2.7;        // -3 — rounded
DintDest := TRUNC(-2.7); // -2 — truncated

Logarithms and the trigonometric functions answer a REAL, which keeps about seven digits, so an answer you would expect to be exact often is not. LN(2.718281828) gives 0.99999994, not 1.0. Never compare one for exact equality — compare against a range instead.

There is no MIN or MAX function. Use IF/ELSE:

// "Smaller" = MIN(A, B)
IF A < B THEN
    Smaller := A;
ELSE
    Smaller := B;
END_IF;

Numeric literals

Numbers you type directly into your code are called literals.

FormTypeExample
42DINTCounter := 42;
-42DINTOffset := -42;
3.14REALPi := 3.14;
1.0e3REALScale := 1.0e3;
2.5E-2REALGain := 2.5E-2;
16#00FFDINTMask := 16#00FF;
2#1010DINTPattern := 2#1010;
8#377DINTLegacy := 8#377;

Scientific notation must include a decimal point: write 1.0e3, not 1e3. The decimal point is what tells Studio you mean a REAL value.

The 16# (hexadecimal), 2# (binary), and 8# (octal) forms are radix literals — they spell out a DINT's bit pattern instead of its decimal value. Underscores may group digits for readability: 2#1010_1010 is the same value as 170. See Writing DINT literals for the details.

Putting it together

// Add two values and scale the result
Result := (Value1 + Value2) * 3;

// Trigger an alarm if temperature exceeds 100
IF Temperature > 100 THEN
    Alarm := 1;
END_IF;

// Convert raw ADC count (0-4095) into a percentage (0-100)
// REAL destination keeps the fraction — RawCount = 2047 gives ~49.988
ScaledReal := (RawCount * 100) / 4095;

// Same expression into a DINT — RawCount = 2047 gives 49 (truncated, not rounded)
DintScaled := (RawCount * 100) / 4095;

// Round a REAL setpoint to the nearest DINT (banker rounding applies)
DintSetpoint := RealSetpoint;

Practice

Try the arithmetic in the Scale exercise — convert a 4–20 mA reading into 0–100 °C — then the comparisons in the Sign Check exercise — flag a number as negative, zero, or positive.

On this page