Tcl Part I · Module 4 · Operators and Expressions
Math Functions Inside expr
Alongside its operators, expr understands a set of functions — rounding, absolute value, minimum and maximum, square roots and the trigonometric family. They are written like function calls and they are not Tcl commands, which is the first thing to get straight. The second, and the one that causes actual bugs, is that some of them hand back whole numbers and some hand back real numbers, and the difference shows up when you print or compare the result.
Foundation11 min readTclPart IOperatorsMathRounding
Part I · Module 4 · Chapter 4.8 · Math Functions Inside expr
1. They Are Not Commands
puts [catch {abs -7} m]
puts $m1
invalid command name "abs"There is no abs command. abs exists only inside an expression:
puts [expr {abs(-7)}]7This is Chapter 3.7 again: expr has its own small language, and these functions belong to it. The parentheses are part of that language, not Tcl syntax.
2. The Set You Will Use
puts "abs(-7) = [expr {abs(-7)}]"
puts "abs(-7.5) = [expr {abs(-7.5)}]"
puts "min(3, 9, 4) = [expr {min(3, 9, 4)}]"
puts "max(3, 9, 4) = [expr {max(3, 9, 4)}]"
puts "round(2.6) = [expr {round(2.6)}]"
puts "int(2.9) = [expr {int(2.9)}]"
puts "entier(2.9) = [expr {entier(2.9)}]"
puts "floor(2.9) = [expr {floor(2.9)}]"
puts "ceil(2.1) = [expr {ceil(2.1)}]"
puts "sqrt(16) = [expr {sqrt(16)}]"
puts "double(7) = [expr {double(7)}]"
puts "fmod(7.5, 2) = [expr {fmod(7.5, 2)}]"abs(-7) = 7
abs(-7.5) = 7.5
min(3, 9, 4) = 3
max(3, 9, 4) = 9
round(2.6) = 3
int(2.9) = 2
entier(2.9) = 2
floor(2.9) = 2.0
ceil(2.1) = 3.0
sqrt(16) = 4.0
double(7) = 7.0
fmod(7.5, 2) = 1.5min and max take any number of arguments. fmod is the remainder for real numbers, which Chapter 4.1 §4 said % refuses to do.
3. Which Ones Return Whole Numbers
Read the output above again, looking only at the decimal points. This is the part worth memorising:
| Returns a whole number | Returns a real number |
|---|---|
round() | floor() |
int() | ceil() |
entier() | sqrt() |
abs() of a whole number | double() |
abs() of a real number |
floor(2.9) is 2.0, not 2. sqrt(16) is 4.0, not 4.
4. The Four Ways to Reach a Whole Number
They differ on negatives, so choose deliberately:
foreach v {2.9 2.5 -2.5 -2.9} {
puts [format "%5s round=%-4s int=%-4s floor=%-6s ceil=%-6s" $v \
[expr {round($v)}] [expr {int($v)}] [expr {floor($v)}] [expr {ceil($v)}]]
} 2.9 round=3 int=2 floor=2.0 ceil=3.0
2.5 round=3 int=2 floor=2.0 ceil=3.0
-2.5 round=-3 int=-2 floor=-3.0 ceil=-2.0
-2.9 round=-3 int=-2 floor=-3.0 ceil=-2.0round()— nearest, and away from zero on an exact half.int()— towards zero. This is C-style truncation.floor()— always down.ceil()— always up.
Note round(-2.5) is -3 while int(-2.5) is -2. If you have been using them interchangeably on positive numbers, negatives will separate them.
5. A Rounding Result Worth Understanding
This one looks like a bug and is not:
puts "2.675 * 100 = [expr {2.675 * 100}]"
puts "round(2.675 * 100) = [expr {round(2.675 * 100)}]"
puts "format %.2f 2.675 = [format %.2f 2.675]"2.675 * 100 = 267.5
round(2.675 * 100) = 268
format %.2f 2.675 = 2.67Two reasonable ways to round 2.675 to two decimal places, and they disagree — 2.68 against 2.67.
Here is why, and it is worth following:
puts "2.675 stored as: [format %.20f 2.675]"2.675 stored as: 2.674999999999999822362.675 cannot be stored exactly, so what you really have is a hair under 2.675. format %.2f works from that stored value and correctly rounds down to 2.67.
But multiplying by 100 produces a result that has to be rounded to the nearest storable value, and that lands on exactly 267.5. round() then takes the half upwards and gives 268.
Neither function is wrong. They were given slightly different numbers. Chapter 4.10 is entirely about this.
6. Trigonometry and Logarithms
Present, and rarely needed in scripting, so one block for completeness:
puts "acos(-1) = [expr {acos(-1)}]"
puts "sin(0) = [expr {sin(0)}]"
puts "log(2.718281828459045) = [expr {log(2.718281828459045)}]"
puts "log10(1000) = [expr {log10(1000)}]"
puts "pow(2, 10) = [expr {pow(2, 10)}]"
puts "hypot(3, 4) = [expr {hypot(3, 4)}]"acos(-1) = 3.141592653589793
sin(0) = 0.0
log(2.718281828459045) = 1.0
log10(1000) = 3.0
pow(2, 10) = 1024.0
hypot(3, 4) = 5.0Angles are in radians. acos(-1) is the usual way to obtain pi, since Tcl has no built-in constant for it.
pow(2, 10) is 1024.0 where 2 ** 10 is 1024 — the function always works in real numbers.
7. Random Numbers
expr {srand(42)}
puts "first run: [expr {rand()}]"
expr {srand(42)}
puts "same seed: [expr {rand()}]"first run: 0.5245871020129822
same seed: 0.5245871020129822rand() returns a real number from 0 up to but not including 1. srand(seed) sets the starting point, and the same seed gives the same sequence — which is the property worth knowing, because it is what makes a run reproducible.
For a whole number in a range:
expr {srand(7)}
puts "1 to 6: [expr {int(rand() * 6) + 1}]"1 to 6: 6int() rather than round(), because round() would make the top and bottom values half as likely.
8. Common Mistakes
Calling a function as a command. sqrt 16 is not a command. §1.
Expecting floor() and sqrt() to return whole numbers. They return reals. §3.
Using round() for display. Use format %.2f. §5.
Treating round() and int() as the same. They differ on negatives. §4.
Using round() to pick a random integer. It skews the ends. §7.
Expecting rand() to be unpredictable without seeding. If you need reproducibility, seed deliberately.
9. Exercises
Exercise 1 — whole or real? Without running, say which of these print a decimal point: abs(-3), abs(-3.0), round(3.7), floor(3.7), sqrt(25), int(3.7), double(3). Then check.
Exercise 2 — the four roundings. Build the §4 table yourself for the values 1.5, -1.5, 0.5 and -0.5. Which two functions never disagree on positives but always disagree on negatives?
Exercise 3 — format versus round. Produce 3.14 from pi two ways: once with format, once with arithmetic and round(). Which would you use in a report?
Exercise 4 — the 2.675 case. Reproduce §5. Then try 2.665 and 2.685 the same way. Do they all disagree, or only some?
Exercise 5 — a dice roll. Write a reproducible roll of two six-sided dice that prints the same result every run. Then make it different each run, and say what you changed.
Exercise 6 — remainder of a real number. expr {7.5 % 2} fails. Get the answer with a function, and say why the operator refuses.
10. Remember
- These are functions inside
expr, not Tcl commands.abs -7fails. round(),int()andentier()return whole numbers.floor(),ceil(),sqrt()anddouble()return reals.floor(2.9)is 2.0 andsqrt(16)is 4.0 — the decimal point matters for printing and for text comparison.- The four roundings differ on negatives:
roundto nearest away from zero,inttowards zero,floordown,ceilup. - For display, use
format %.2f, notround(). minandmaxtake any number of arguments.fmodis the real-number remainder.- Angles are in radians, and
acos(-1)gives pi. - The same
srand()seed gives the same sequence, which is how you make a run reproducible.
Next: testing membership of a list, inside an expression.
Where this fits
Part of the Tcl curriculum.
