Chapter 6 / 32 · Part 2 · Decisions

Comparisons and thresholds

Compare at the right boundary, add hysteresis and dead bands, and use LIMIT, MIN, MAX and SEL instead of long IF chains.

Six operators that return a BOOL

Most decisions in a machine program come down to a number against a limit. Is the pressure above 6 bar? Has the batch reached 25 cartons? Is the oven within 2 °C of its setpoint? Structured Text has six comparison operators, and every one of them produces a BOOL:

OperatorMeaningExample
=equal toStepNo = 3
<>not equal toRecipe <> 0
<less thanLevel < 20.0
<=less than or equalCount <= 99
>greater thanTemp > 85.0
>=greater than or equalElapsed >= T#5s

Because the result is a BOOL, a comparison can sit anywhere a BOOL can: in an IF condition, combined with AND and OR, or assigned straight to an output. LowPressure := Pressure < 4.5; is a complete, latch-proof alarm, as you saw in chapter 5. Comparisons bind more loosely than arithmetic and more tightly than AND/OR, so Level + Margin > 90.0 AND Auto means what it reads. Brackets still help the reader; use them when a condition mixes more than two ideas.

Compare values of the same type. INT with INT, REAL with REAL, TIME with TIME. CODESYS and TwinCAT usually accept an INT compared with a REAL and convert it for you; TIA Portal is stricter, especially with its IEC check switched on, and the standard itself expects you to convert explicitly. Write the conversion yourself (INT_TO_REAL(Count) > Limit) and the line means the same on every platform.

Here is a compressed-air header with a 6.0 bar setpoint. All six comparisons run every scan:

PROGRAM Main
VAR
    Pressure  : REAL := 6.0;   (* input: header pressure in bar *)
    Equal     : BOOL;
    NotEqual  : BOOL;
    Below     : BOOL;
    AtOrBelow : BOOL;
    Above     : BOOL;
    AtOrAbove : BOOL;
END_VAR

Equal     := Pressure = 6.0;
NotEqual  := Pressure <> 6.0;
Below     := Pressure < 6.0;
AtOrBelow := Pressure <= 6.0;
Above     := Pressure > 6.0;
AtOrAbove := Pressure >= 6.0;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Pressure.

Type 5.9, 6.0 and 6.1 into Pressure. At exactly 6.0, Below and Above are both FALSE while AtOrBelow and AtOrAbove are both TRUE. That single value, the setpoint itself, is where most threshold bugs live.

Signed, unsigned and the value that is not what it looks like

The integer types disagree with each other as well, so the conversion question does not end with INT against REAL. A drive status word, a Modbus holding register or an analog value read as a raw word often arrives as UINT or WORD, 0 to 65,535. The same sixteen bits read as an INT run from -32,768 to 32,767. The bit pattern 16#FFFF is 65,535 as a UINT and -1 as an INT.

So the conversion you choose decides the answer. Suppose a power meter sends its signed active power in a 16-bit register and your program has declared it UINT. Export of 1 kW arrives as 16#FFFF. PowerRaw > 100 is TRUE, because 65,535 is more than 100, and your "importing more than 100 kW" alarm fires while the site is exporting. UINT_TO_INT(PowerRaw) > 100 gives the right answer, -1 against 100. The rule is to convert each value into the type that matches what it means, once, close to where it enters the program, and to compare only values that are already in the same, meaningful type.

The same goes for counters that wrap. A free-running UINT that counts encoder pulses goes from 65,535 to 0. Comparing positions taken on either side of the wrap with > gives nonsense; chapter 25 deals with that kind of value.

There is no "between" operator

Mathematics writes a range as 20 < x < 80. Structured Text has no such form, and writing it does not give you a range check.

Which side owns the setpoint

Every threshold splits the number line in two, and the setpoint has to belong to one side. > puts it on the low side; >= puts it on the high side. With a REAL from a transmitter the difference rarely shows in practice, because a noisy analog value is almost never exactly 90.000. With integers, counters and timers it decides the result every time.

Write the comparison from the specification, word for word. "Alarm above 85 °C" is Temp > 85.0. "Stop at 25 cartons" is Count >= 25. "Open the valve while the level is below 90 %" is Level < 90.0, which means it closes at 90 %. Specifications are often vague here. When yours is, decide, write the decision in a comment, and make the two thresholds in a pair use opposite operators so there is no value where both or neither apply. Level < 90.0 for "open" and Level >= 90.0 for "full" cover every value exactly once.

A short table of the integer cases saves arguments at commissioning. For a counter that must stop a batch of 25:

CountCount > 25Count >= 25Count = 25
24FALSEFALSEFALSE
25FALSETRUETRUE
26TRUETRUEFALSE
27TRUETRUEFALSE

Count > 25 stops one carton late, which on a cartoner is a 26-pack and a customer complaint. Count = 25 stops on time and then lets go again as soon as the count moves on. Only >= stops at the target and stays stopped.

The more dangerous mistake is using = for something that passes through a value. A target count compared with = is only true for the scans where the count is exactly equal. If the count ever jumps past the target, the condition is never true at all.

Timers deserve a word of their own here, because they are the case people believe is exact. A timer's elapsed time only changes when the timer is called, once per scan. On a 10 ms task, Elapsed = T#5s is TRUE only if some scan happens to land exactly on 5,000 ms, and with scan jitter it usually does not. Elapsed >= T#5s is TRUE on the first scan at or after 5 s, which is what you meant. Chapter 9 shows the one-scan granularity in a live example.

Hysteresis: two thresholds and a memory

A buffer tank is fed by a transfer pump. You want the pump to run when the tank is low and stop when it is full. The obvious code is Pump := Level < 80.0;, and on a real plant it destroys the pump contactor.

The level signal is never perfectly still. Ripples on the surface, turbulence from the inlet and electrical noise on the 4–20 mA loop make it jitter by a fraction of a percent. When the level sits near 80 %, the comparison flips every time the noise crosses the line. The pump starts, stops and starts again several times a second. A typical 7.5 kW motor is rated for perhaps ten to fifteen starts per hour; chattering burns through that in seconds, and the contactor tips weld long before the motor overheats.

The cure is hysteresis: two thresholds with a band between them. Start the pump when the level falls below 20 %. Stop it when the level rises above 80 %. Between 20 % and 80 %, do nothing, and the pump keeps whatever state it already had.

IF Level < 20.0 THEN
    Pump := TRUE;
ELSIF Level > 80.0 THEN
    Pump := FALSE;
END_IF;

That is an IF without an ELSE, the very pattern chapter 5 warned about. Here it is correct, and on purpose. The missing ELSE is the memory: inside the band, nothing writes Pump, so it holds. The difference from the trap is that there are two conditions, one that sets and one that resets, and together they cover both directions. Put a comment on it so the next engineer does not "fix" it.

The program below feeds both versions with the same noisy level. The noise is a fake ±0.6 % wobble added on alternate scans, so you can see what a real transmitter does. The two counters record how many times each pump has started.

PROGRAM Main
VAR
    BaseLevel    : REAL := 50.0;   (* input: the "true" level in % *)
    Level        : REAL;           (* what the transmitter reports: true level + noise *)
    ScanCount    : DINT;
    SimplePump   : BOOL;           (* single threshold *)
    BandPump     : BOOL;           (* hysteresis 20 % / 80 % *)
    SimplePrev   : BOOL;
    BandPrev     : BOOL;
    SimpleStarts : DINT;
    BandStarts   : DINT;
END_VAR

(* Fake noise: +0.6 % on even scans, -0.6 % on odd scans *)
ScanCount := ScanCount + 1;
IF ScanCount MOD 2 = 0 THEN
    Level := BaseLevel + 0.6;
ELSE
    Level := BaseLevel - 0.6;
END_IF;

SimplePump := Level < 80.0;

(* Hysteresis: no ELSE on purpose - between the thresholds the pump holds *)
IF Level < 20.0 THEN
    BandPump := TRUE;
ELSIF Level > 80.0 THEN
    BandPump := FALSE;
END_IF;

(* Count starts: output TRUE now, FALSE last scan - see chapter 8 *)
IF SimplePump AND NOT SimplePrev THEN
    SimpleStarts := SimpleStarts + 1;
END_IF;
IF BandPump AND NOT BandPrev THEN
    BandStarts := BandStarts + 1;
END_IF;
SimplePrev := SimplePump;
BandPrev   := BandPump;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: BaseLevel.

Set BaseLevel to 15, so the tank is low, then to 80, right on the upper threshold. SimpleStarts climbs by one every other scan, five starts a second at this scan rate. BandStarts moves once and stops. Then set BaseLevel to 50 and notice that BandPump stays off: inside the band, it remembers that the last thing that happened was "full".

Here is the same thing as a scan table, with the true level parked at 80 % and the noise alternating:

ScanLevel readLevel < 80.0 (simple)Band logic doesBandPump
n79.4TRUE, pump startsnothing (inside band)FALSE
n+180.6FALSE, pump stopsLevel > 80.0: resetFALSE
n+279.4TRUE, pump startsnothingFALSE
n+380.6FALSE, pump stopsreset (already FALSE)FALSE

The single threshold changes state on every scan, because every sample lands on the other side of the line. The band only has to decide once.

That memory raises a question you must answer for every hysteresis output: what state is it in when the PLC starts and the level is inside the band? Here BandPump starts FALSE because that is its initial value, so a tank at 50 % waits until it drains below 20 %. For a transfer pump that is usually right. For a heater keeping a tank of product above its freezing point it may not be. Choose the initial value deliberately.

How wide should the band be?

Wide enough that noise and normal process movement cannot cross it, and narrow enough that the process stays in spec. Those are two separate lower and upper limits, and you can put numbers on both.

The lower limit comes from the noise. Watch the signal online, or trend it, with the process steady. If it wanders ±0.6 %, the band must be well over 1.2 % wide, or one noise peak and one trough can still cross both thresholds. I double the peak-to-peak noise as a starting point.

The upper limit comes from the process, and for a pump the number that usually decides it is starts per hour. Take a 2,000-litre buffer tank with a steady draw of 50 L/min and a transfer pump that delivers 200 L/min. While the pump runs, the level rises at 150 L/min; while it is off, it falls at 50 L/min. One cycle is a fill across the band and a drain back across it:

BandVolume in bandPump onPump offCycleStarts per hour
20 % to 80 %1,200 L8 min24 min32 minabout 2
70 % to 80 %200 L80 s4 min5 min 20 sabout 11
78 % to 80 %40 L16 s48 s64 sabout 56

The 2 % band would be wide enough for the noise and would still wreck the motor, at more than three times the rate of the ten to fifteen starts per hour from earlier. Work the table out for your own tank before choosing the thresholds. If the process needs a narrow band, the answer is a variable-speed pump under closed-loop control (chapter 20), not a contactor that starts every minute.

On a level controlled this way, 5–10 % of span between the thresholds is common, and wider is fine if the tank allows it. On a temperature loop with on/off heating, a band of ±1 to ±2 °C around the setpoint is typical. When the band is expressed around a setpoint, it is usually called a dead band:

IF Temp < Setpoint - DeadBand THEN
    Heater := TRUE;
ELSIF Temp > Setpoint + DeadBand THEN
    Heater := FALSE;
END_IF;

Siemens, Beckhoff and CODESYS libraries all ship a ready-made hysteresis block, and you can wrap your own as a function block once you reach chapter 18. Underneath, every one of them is these five lines. Read the block's documentation before you use one, because the conventions differ. The HYSTERESIS block in the CODESYS Util library, for example, works on INT values, and its output OUT goes TRUE when IN falls below LOW and FALSE when IN rises above HIGH: it is written for the "start the pump when low" case, and you invert it for "start the fan when hot". CODESYS puts a REAL version, HysteresisReal, in its Building Automation library.

Dead band on alarms

Alarms chatter for the same reason pumps do, and the cost is an operator who stops reading the alarm list. A high-level alarm at 90 % that comes and goes every few seconds while the level sits at 90 % trains everyone to acknowledge without looking. Alarm-management practice gives an alarm a dead band: it raises at 90 % and only clears once the level has fallen back to, say, 88 %.

ANSI/ISA-18.2 requires the control system to support alarm dead bands, and the starting values usually quoted come from the EEMUA 191 guide: about 5 % of span for flow, 2 % for level and pressure, and 1 % for temperature. Treat those as a first setting and adjust them from the trend of the real signal. The code is the hysteresis pattern again, with the band below the alarm limit:

IF Level > 90.0 THEN
    HighLevelAlarm := TRUE;
ELSIF Level < 88.0 THEN          (* 2 % of span below the limit *)
    HighLevelAlarm := FALSE;
END_IF;

Dead band deals with noise that crosses the limit in both directions. An on-delay deals with brief excursions that only cross it once, such as a splash, and that needs a timer from chapter 9. Most production alarm blocks have both, and chapter 22 puts them together.

Never compare REALs with =

REAL values are binary floating point. Most decimal fractions (0.1, 0.2, 0.3) have no exact binary representation, so they are stored as the nearest value the format can hold. Add a few of them and the small errors accumulate. A dosing scale that adds 0.1 kg ten times does not, in general, arrive at exactly 1.0 kg. Chapter 3 showed the arithmetic; this section is about what it does to comparisons.

PROGRAM Main
VAR
    Doses : INT;
    Total : REAL;
    Exact : BOOL;    (* Total = 1.0 *)
    Close : BOOL;    (* Total within 0.001 of 1.0 *)
END_VAR

(* Add ten doses of 0.1 kg, one per scan *)
IF Doses < 10 THEN
    Total := Total + 0.1;
    Doses := Doses + 1;
END_IF;

Exact := Total = 1.0;
Close := ABS(Total - 1.0) < 0.001;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it.

After ten scans Doses is 10 and Total is a hair under 1.0. Exact stays FALSE forever; Close is TRUE. The digits you see here come from the browser's 64-bit arithmetic. A PLC's 32-bit REAL holds only about seven significant digits, so its error is larger and lands on a different digit, but on both, Total = 1.0 is FALSE. (LREAL, the 64-bit type, shrinks the error without removing it.)

The fix is to compare with a tolerance: ABS(A - B) < Tolerance. Choose the tolerance from the physics, not from habit. If the load cell resolves 5 g, a tolerance of 0.002 kg is meaningless and 0.5 kg is sloppy; half the resolution, 0.0025 kg, says "equal as far as this instrument can tell". For setpoint arrival on a process value, the tolerance is a process decision ("within 1 °C") and usually belongs in a parameter, not a literal.

How fine a REAL really is

There is a second limit on the tolerance, and it catches people who choose a "safe" small number. A 32-bit REAL (IEEE 754 single precision) has a 24-bit significand. The gap between one representable value and the next therefore grows with the size of the number:

Value nearGap to the next REAL
1.00.00000012
100.00.0000076
1,000.00.000061
10,000.00.00098
50,000.00.0039
100,000.00.0078
1,000,000.00.0625
16,777,216.02.0

A tolerance smaller than the gap does nothing. Compare a tank totaliser that reads 50,000 litres with a target using ABS(Total - Target) < 0.001, and the only way to be within 0.001 is to be exactly equal, because the nearest neighbours are 0.0039 away. You have written = with extra steps. The tolerance has to be bigger than the gap at the size the value will actually reach, and in practice bigger than the instrument resolution, which is almost always the larger of the two.

The errors also add up over many operations. Ten additions of 0.1 in 32-bit arithmetic give 1.0000001. Ten thousand additions give 999.9029 instead of 1,000: a tenth of a litre lost, because each addition rounds to the gap at the current total and the gap keeps growing. That is why chapter 2 told you to keep totals in integers or LREAL, and it is why a comparison against a large REAL total needs a tolerance chosen with the table above in front of you.

Often you do not need equality at all. A dosing step does not end when the weight equals the target; it ends when the weight reaches it: Weight >= Target - Preact, where the preact accounts for the material still falling from the valve. <, <=, > and >= work perfectly well on REALs. Only = and <> are the trap.

NaN: the value that fails every comparison

IEEE floating point has one more value that comparisons treat strangely: NaN, "not a number". It appears when a calculation has no answer, such as 0.0 divided by 0.0 or the square root of a negative number on a target that follows IEEE 754 for these operations, and it appears even more often in the field when a REAL is assembled from two 16-bit Modbus registers in the wrong word order, or read from a communication buffer that has not been filled yet.

Every comparison with NaN is FALSE except <>. NaN < 20.0 is FALSE, NaN > 80.0 is FALSE, NaN = NaN is FALSE, and NaN <> NaN is TRUE. A NaN level therefore slides past every threshold you have written. A low-level start (Level < 20.0) never fires, a high-level stop (Level > 80.0) never fires either, and a pump that was running stays running.

How you write a plausibility check decides whether it catches NaN:

(* rejects NaN: both comparisons are FALSE, so LevelOk is FALSE *)
LevelOk := (Level >= -2.0) AND (Level <= 105.0);

(* lets NaN through: both comparisons are FALSE, so NOT gives TRUE *)
LevelOk := NOT ((Level < -2.0) OR (Level > 105.0));

By De Morgan's laws from chapter 4 those two lines are identical for every ordinary number. They differ only for NaN, and only the first one is safe. Write range checks in the "inside the range" form, joined by AND. Rockwell takes the same view in its own LIMIT instruction, which clears its output when any operand is NaN. Some platforms also offer a dedicated test, such as the OK (check validity) instruction in TIA Portal's ladder and FBD. The engine on this site does not produce NaN (it faults on the division instead), so this one has no live example.

LIMIT, MIN, MAX and SEL

A lot of IF chains exist only to keep a number inside bounds or to pick one of two values. IEC 61131-3 has standard functions for exactly that, and they make the intent obvious at a glance.

  • MIN(A, B) returns the smaller value; MAX(A, B) the larger. MAX(0.0, Demand) stops a negative demand. MIN(Demand, PumpCapacity) caps it.
  • LIMIT(MN, IN, MX) clamps IN between MN and MX. It is MIN(MAX(IN, MN), MX) in one call. The argument order is minimum, value, maximum, with the value in the middle. Swap them and the function still compiles; it just returns the wrong thing.
  • SEL(G, IN0, IN1) returns IN0 when G is FALSE and IN1 when G is TRUE. The FALSE choice comes first, which surprises nearly everyone once.

An operator types a conveyor speed on the HMI. The drive must never be commanded below 10 % (the motor's cooling fan is shaft-mounted) or above 100 %. In manual mode a fixed jog speed replaces the recipe value.

PROGRAM Main
VAR
    OperatorSpeed : REAL := 60.0;   (* input: HMI speed entry in % *)
    Manual        : BOOL;           (* input: manual mode selected *)
    JogSpeed      : REAL := 15.0;   (* input: manual jog speed in % *)
    Clamped       : REAL;           (* operator value, kept inside 10..100 % *)
    SpeedCmd      : REAL;           (* output: speed reference for the drive *)
END_VAR

Clamped  := LIMIT(10.0, OperatorSpeed, 100.0);
SpeedCmd := SEL(Manual, Clamped, JogSpeed);   (* FALSE -> Clamped, TRUE -> JogSpeed *)

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: OperatorSpeed, Manual, JogSpeed.

Type 250 or -40 into OperatorSpeed: Clamped holds at 100 or 10. Flip Manual and SpeedCmd switches to the jog speed. Written as IFs, the same logic is a nested chain of eight or nine lines with three places to get a boundary wrong.

MUX is the many-way version of SEL: MUX(K, IN0, IN1, IN2, …) returns the input numbered K, counting from 0. A three-speed conveyor with a speed selector on the HMI is one line:

PROGRAM Main
VAR
    SpeedSel : INT := 1;   (* input: HMI speed choice, 0 = slow, 1 = normal, 2 = fast *)
    SelOk    : BOOL;       (* the selector holds a value MUX can use *)
    SpeedCmd : REAL;       (* output: speed reference in % *)
END_VAR

SelOk := (SpeedSel >= 0) AND (SpeedSel <= 2);
IF SelOk THEN
    SpeedCmd := MUX(SpeedSel, 25.0, 60.0, 90.0);
ELSE
    SpeedCmd := 0.0;       (* an impossible choice stops the belt *)
END_IF;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: SpeedSel.

Try 0, 1 and 2, then 3 and -1. The range check in front of MUX is deliberate. What a MUX does with a K that has no input differs between environments (some give an error, some return a default, Siemens SCL adds an explicit INELSE input for the purpose), so check K yourself and decide what an impossible choice means. That is the same discipline as the ELSE on a CASE, which is where chapter 7 picks it up.

Two cautions about all of these. First, clamping an operator entry hides the bad value; if the operator typed 250, the HMI should show it was rejected rather than silently running at 100. Clamp to protect the machine, and validate the entry in the HMI too. Second, function names do not always port. Rockwell's ladder instruction for a range test was called LIM for many years and was renamed LIMIT in Logix Designer version 36. It returns a BOOL that says whether a test value lies between two limits; it does not clamp anything, and it is not available in Rockwell's structured text. It also has a twist of its own: if you give it a low limit greater than the high limit, it tests for values outside the range instead. Convert a TIA Portal LIMIT to a Studio 5000 LIMIT by name alone and you get a very different machine.

Range checks: is this number believable?

A comparison can also ask whether a value makes physical sense at all. A level transmitter on a 4–20 mA loop should read between 0 % and 100 %. A reading of -12 % does not mean the tank is emptier than empty; it means the wire is broken, the transmitter has lost power or someone is calibrating it.

The NAMUR NE 43 recommendation, which most process transmitters follow or can be configured to follow, divides the loop current into zones. The transmitter uses 3.8 to 20.5 mA for measurement, which allows a little under-range and over-range around the nominal 4 to 20 mA. It signals a fault by driving the loop to 3.6 mA or less, or to 21 mA or more. Scaled to percent of a 4–20 mA span, that becomes:

Loop currentScaled valueMeaning under NE 43
3.6 mA or less-2.5 % or lesstransmitter reports a failure (low)
3.68 mA-2 %the low plausibility limit used below
3.8 mA-1.25 %lowest valid measurement
4.0 to 20.0 mA0 to 100 %nominal range
20.5 mA103.1 %highest valid measurement
20.8 mA105 %the high plausibility limit used below
21.0 mA or more106.25 % or moretransmitter reports a failure (high)

The scaling is percent = (mA − 4) / 16 × 100. Choose your plausibility limits between the last valid measurement and the failure level, as the -2 % and 105 % below do, and a transmitter that signals a fault will trip the check while a healthy one reading a slightly over-full tank will not. Check the transmitter's own datasheet as well: NE 43 sets the zones, and manufacturers choose the exact failure currents inside them.

A plausibility check is a pair of comparisons with a little margin either side of the span:

PROGRAM Main
VAR
    Level      : REAL := 55.0;   (* input: primary level transmitter in % *)
    LevelB     : REAL := 56.0;   (* input: redundant transmitter on the same tank *)
    LevelOk    : BOOL;           (* primary reading is physically possible *)
    Agree      : BOOL;           (* both transmitters tell the same story *)
    PumpAllowed: BOOL;           (* the pump may only run on a believable level *)
END_VAR

LevelOk     := (Level >= -2.0) AND (Level <= 105.0);
Agree       := ABS(Level - LevelB) <= 3.0;
PumpAllowed := LevelOk AND Agree;

END_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Level, LevelB.

Set Level to -15 or 130 and LevelOk drops. Set the two transmitters 5 % apart and Agree drops. In both cases the pump loses its permission. The margins (-2 % and 105 %) leave room for normal calibration error near the ends of the span, so a transmitter that reads 100.4 % in a full tank does not trip the plant.

The agreement check has its own design question: how far apart may two healthy transmitters be? Add up the stated accuracy of both instruments, then add something for the fact that they are not mounted at the same point in the tank. A pair of transmitters each good to 0.5 % of span can legitimately disagree by 1 %, and more while the surface is moving. The 3 % here is generous on purpose. Too tight and you get nuisance trips every time the agitator starts; too loose and a drifting transmitter is never caught.

The design rule behind all this: a control decision is only as good as the number it compares. A hysteresis pump fed by a broken transmitter reading -12 % will run until the tank overflows, perfectly obeying Level < 20.0. Check the signal first, then compare it. Chapter 19 builds this out into full 4–20 mA scaling with wire-break detection and delayed alarms.

None of these checks is a safety function. A plausibility check in the standard PLC program protects the process and the equipment. Where an overflow or a dry-running pump could hurt someone, the protection is an independent instrumented function in a safety system designed to IEC 61511, with its own sensor.

Thresholds, then choices among many

For every comparison you write from now on, there are three questions to ask before you move on. Which side owns the boundary value? Can the value pass through the threshold without ever equalling it, or flicker across it? And is the number believable at all, including the NaN case? If you can answer those for each line, the boundary bugs in this chapter will not reach your machines.

Many decisions are not about thresholds at all. A mode selector has three positions. A recipe number picks one of twelve parameter sets. A sequence step is 10, 20 or 30. You could write those as long ELSIF chains of = comparisons, and the next chapter shows why CASE does that job better.

Practise this chapter