trunc

Most programming languages offer a function to remove the fractional part of a floating point value which is typically called trunc().

Language support

Tcl 8.x and earlier versions

There's no function named trunc() in Tcl but math functions entier, int and wide serve similar purpose. The function entier() truncates the fractional part and returns the result as an arbitrary precision integer value:

% expr {entier(3.14)}
3
% expr {entier(8.9)}
8

The functions int() and wide() additionally limit the value range discarding superfluous bits. The value range of int() is determined by $tcl_platform(wordSize) and the value range of wide() is 64 bits:

% puts $tcl_platform(wordSize)
4
% expr {int(0x7FFFFFFF)}
2147483647
% expr {int(0xFFFFFFFF)}
-1
% expr {int(0x80000000)}
-2147483648
% expr {int(0x1FFFFFFFF)}
-1
% expr {int(+inf)}
integer value too large to represent
% expr {int(nan)}
floating point value is Not a Number

The behavior of entier() is not the same as using ceil() and floor() because the latter return floating point values.

# Do not use this
proc ::tcl::mathfunc::trunc {x} {
   return [expr {($x >= 0) ? floor($x) : ceil($x)}]
}

This can make a difference when dealing with arbitrary precision values:

% entier(99999999999999999)*2
199999999999999998
% trunc(99999999999999999)*2
1.9999999999999997e+17

Other programming languages

  • Python: math.trunc() - see math module
  • MATLAB and GNU Octave: fix()
  • C: Any typecast of a float or double value to an integer type. Casting non-finite floating point values or values exceeding the integer type value range is undefined behavior.
  • JavaScript: Math.trunc()

An extended Tcl trunc() math function

It's a nice thing that Tcl can compute in arbitrary precision but some situations require limited value ranges. Typically this is the case when implementing communication protocols or low-level drivers. In such cases computation results must fit particular data types like an unsigned byte or a 32 bit signed integer.

Doing this saturation arithmetic is often the preferred choice as discarding bits can change the value dramatically. The following trunc() function might be useful:

# trunc(x) -> truncate to integer (round towards zero)
#    if x is non-finite (+inf, -inf, nan) the function raises an error
# trunc(x, n) -> saturate and truncate to integer having given number of bits
#    if n > 0: unsigned integer range (0 .. 2**n-1)
#    if n < 0: signed integer range (two's complement)
#    else: if n is zero an error is raised
# trunc(x, a, z) -> saturate within range [a..z] and truncate to integer
#    if x is -inf: saturate result to a
#    if x is +inf: saturate result to z
#    can set a or z to -inf or +inf to get an open range

proc ::tcl::mathfunc::trunc {args} {
   set count [llength $args]
   if {($count <= 0) || ($count > 3)} {
      error "wrong # args: should be trunc(x) | trunc(x, n) | trunc(x, a, z)"
   }
   switch -- $count {
      1 {
         lassign $args x
         set result [expr {entier($x)}]
      }
      2 {
         lassign $args x n
         if {![string is entier -strict $n]} {
            error [format {bad bit size "%s": must be integer} $n]
         }
         if {$n != 0} {
            if {$n > 0} {
               set p [expr {1 << $n}]
               set a 0
            } else {
               set p [expr {1 << abs($n + 1)}]
               set a [expr {-$p}]
            }
            set result [expr {entier(min($p - 1, max($a, $x)))}]
         } else {
            # raises domain error: argument not in valid range
            set result nan
         }
      }
      3 {
         lassign $args x a z
         if {![string is double -strict $a]} {
            error [format {bad range boundary "%s": must be numeric value} $a]
         }
         if {![string is double -strict $z]} {
            error [format {bad range boundary "%s": must be numeric value} $z]
         }
         set result [expr {entier(min($z, max($a, $x)))}]
      }
      default {
         error "internal error"
      }
   }
   return $result
}

Usage:

% expr {trunc(123.4)}
123
; # fit to 8 bit unsigned integer type
% expr {trunc(257.9, 8)}
255
% expr {trunc(-1, 8)}
0
; # fit to 16 bit signed integer type
% expr {trunc(-123456.7, -16)}
-32768
% expr {trunc(12345.6789, -16)}
12345
; # fit into range 0 to 9
% expr {trunc(rand()*10, 0, 9)}
...