# Arithmetic and Comparison Operators

Source: https://rungs.dev/docs/structured-text/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.

> **INFO**
>
> **Modeled on Studio 5000 Logix Designer**
>
> [rungs.dev](https://studio.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 [#arithmetic-operators]

| Operator | Description      | Example                  |
| -------- | ---------------- | ------------------------ |
| +        | Addition         | Total := A + B;          |
| -        | Subtraction      | Delta := A - B;          |
| \*       | Multiplication   | Product := A \* 5;       |
| /        | Division         | Average := Sum / Count;  |
| MOD      | Remainder        | Remainder := A MOD B;    |
| \*\*     | Raise to a power | Squared := Value \*\* 2; |

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

```iecst
Result := (Value1 + Value2) * 3;
```

### The power runs before a minus sign, and two powers run left to right [#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:

```iecst
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]

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

| Operator | Description        | Example                 |
| -------- | ------------------ | ----------------------- |
| `=`      | Equal to           | `IF Mode = 1 THEN`      |
| `<>`     | Not equal to       | `IF State <> 0 THEN`    |
| `<`      | Less than          | `IF Temp < 100 THEN`    |
| `>`      | Greater than       | `IF Speed > Limit THEN` |
| `<=`     | Less than or equal | `IF Level <= 50 THEN`   |
| `>=`     | Greater or equal   | `IF Count >= Max THEN`  |

### Comparing BOOLs [#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`:

```iecst
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-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:

```iecst
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 operand | Right operand | Destination | What you get                         |
| ------------ | ------------- | ----------- | ------------------------------------ |
| `DINT`       | `DINT`        | `DINT`      | Integer division (truncates to zero) |
| `DINT`       | `DINT`        | `REAL`      | Float division (fraction preserved)  |
| any `REAL`   | any           | `DINT`      | Float division, then banker rounding |
| any `REAL`   | any           | `REAL`      | Float 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 [#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:

```iecst
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:

```iecst
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:

```iecst
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 [#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:

```iecst
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) [#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:

| Value  | School rounding | Banker rounding |
| ------ | --------------- | --------------- |
| `0.5`  | `1`             | **`0`**         |
| `1.5`  | `2`             | `2`             |
| `2.5`  | `3`             | **`2`**         |
| `3.5`  | `4`             | `4`             |
| `-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.

```iecst
DintSetpoint := RealSetpoint;   // banker rounding applied automatically
```

## Division by zero [#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 wrote            | What 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 [#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:

```iecst
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:

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

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

```iecst
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 [#built-in-math-functions]

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

| Function      | What 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.                                                                           |

```iecst
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:

```iecst
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`:

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

## Numeric literals [#numeric-literals]

Numbers you type directly into your code are called **literals**.

| Form      | Type   | Example              |
| --------- | ------ | -------------------- |
| `42`      | `DINT` | `Counter := 42;`     |
| `-42`     | `DINT` | `Offset := -42;`     |
| `3.14`    | `REAL` | `Pi := 3.14;`        |
| `1.0e3`   | `REAL` | `Scale := 1.0e3;`    |
| `2.5E-2`  | `REAL` | `Gain := 2.5E-2;`    |
| `16#00FF` | `DINT` | `Mask := 16#00FF;`   |
| `2#1010`  | `DINT` | `Pattern := 2#1010;` |
| `8#377`   | `DINT` | `Legacy := 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](https://rungs.dev/docs/tags/data-types#writing-dint-literals) for the details.

## Putting it together [#putting-it-together]

```iecst
// 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;
```

> **IDEA**
>
> **Practice**
>
> Try the arithmetic in the [Scale exercise](https://rungs.dev/exercises/scale) — convert a 4–20 mA reading into 0–100 °C — then the comparisons in the [Sign Check exercise](https://rungs.dev/exercises/sign-check) — flag a number as negative, zero, or positive.