logo To Foot
© J R Stockton, ≥ 2009-07-28

Pascal/Delphi Rounding and Truncation.

No-Frame * Framed Index * Frame This
Links within this site :-

The term "Borland's Pascal" here includes "Turbo Pascal" and "Borland Pascal".

The following sections are written for Borland's Pascal and Delphi. Here, Float represents any true floating-point type, and Integer any integer type.

I ran Win98, initial UK release of July 1998, on a PII/300; and sometimes MS-DOS 6.20 on a 486dx/33. I used BP7.01, installed on the 486 and copied to the PII, and D3, only on the PII. BP7.01 was as supplied, except for Pedt Scragg's Crt unit. I now run that BP7.01 on a 3GHz P4 desktop with XP sp3, and have D3 on an XP sp3 laptop.

I also have an Amstrad PPC640 with Amstrad DOS 3.3 and TP5.

Most of the files of my BP7.01 are datestamped 1993/03/09-07:01:00.

Converting from Float to Integer

This is used in Converting Float to n Decimal Places, where there are additional considerations.

Principles

When converting from Float to Integer, it can be vital to choose the correct type of conversion, and to consider how it may have been implemented. In Borland's PC products (Pascal/Delphi) the default operation of Round(x) appears to be "Bankers' Rounding" - to the nearest integer, and to the nearest even integer for x = X.5. Rounding, etc., is, at least for most types, done by the Floating Point Unit (FPU).

If the Float is obtained by a "Scientific" or "Technical" calculation, it is likely that there will be rounding errors in the arithmetic, on the scale of a few parts in ten to the 7, 11, 15, 19 for single, 6-byte real, double, extended types; the details of the rounding of half-integer values to a nearest integer then normally are unimportant. But if it comes in an "Administrative" or "Financial" calculation, it may be probable that its value can be exactly half-integer, and the rounding of such then is significant; the calculation should perhaps have been done with exact (integer) arithmetic using sufficiently small units, perhaps Comp or Currency.

Speed

Usually, the algorithm determines which form of conversion must be used, but sometimes there is a choice. I have read that Round can be faster than Trunc because Trunc alters the FPU control word.

Standard Routines

The routines provided in Pascal are Round and Trunc, which convert floats to Pascal longints; but note also Int and Frac, which give, as floats, the parts of a float interpreted as fixed-point. The following considers both Rounding and Truncation.

Delphi also can have Floor and Ceil - these are in the Math unit.

Delphi 6 introduced SimpleRoundTo(X, n). The rounding is to the nearest 10n, with '0.5' to the 'larger'. Test carefully, with inputs (like 1.625) that will be held exactly.

In specific cases, for example where the number is known to be positive, one can code faster than the Pascal or Delphi standard routine; I've seen the following Delphi method, for positive numbers, given for Trunc :-

const Half : double = 0.5 ;
function chomp(Flt : double) : integer ;
begin asm  fld Flt ; fsub Half ; fistp result  end end ;

The PC FPU Control Word

"I don't think that modifying the control register would help him because the only options that it offers are rcBankers, rcFloor, rcCeil, and rcChop (toward zero)" - JohnH.

FPU Control Word References? There's a little in Ralf Brown's List, in 86bugs.lst. See also in Pascal Floating-Point.

All Possible Conversions

There are of the order of a dozen or more distinct ways of converting a general float to an integer, any of which may be needed in a program. In particular, there are two main classes - those symmetrical about zero, so that -R(x) = R(-x); and those translationally symmetric, so that R(x+k) = R(x)+k for integer k - which overlap.

The following are possible ways of conversion, starting with a signed Float (there may be others); a Delphi routine is given only where standard or better than Pascal (but, as I do not have the Math unit, I cannot test these). The code assumes that the FPU control word is in its default state. The function Sign returns +1, 0, -1. See allround.pas, which tests these.

Some conversions can readily be derived from others, using negation, absolute value, addition of ±0.5, etc.

The Rounding of X
MethodPascalDelphi
0. NoneX
1. Towards minus infinityTrunc(X+y)-y, y>|X|Floor(X)
2. Towards zeroTrunc(X)Trunc(X)
3. Away from zeroTrunc(X+Sign(Frac(X)))
4. Towards plus infinityTrunc(X-y)+y, y>|X|Ceil(X)
5. To the nearest, and if two are equally near :-
    5a. Towards minus infinityRound(X-Ord(Abs(Frac(X))=0.5)*0.1)
    5b. Towards zeroRound(X-Ord(Abs(Frac(X))=0.5)*Sign(X)*0.1)
    5c. Away from zero Trunc(2.0*X-Trunc(X))
Trunc(X+0.5*Sign(X))
Round(X+Ord(Abs(Frac(X))=0.5)*Sign(X)*0.1)
    5d. Towards plus infinityRound(X+Ord(Abs(Frac(X))=0.5)*0.1)
    5e. To adjacent even -
        "Bankers' Rounding"
Round(X)Round(X)
    5f. To adjacent oddSucc(Round(X-1.0))
6. With some randomness :-
    6a. To a random
        neighbour, unless exact
Trunc(x)+Random(2)*Sign(X-Trunc(X))
    6b. To a random
        neighbour, probability
        inverse with distance
Round(X+Random-0.5)
    6c. To the nearest, else to a
        random neighbour
Round(X+
  Ord(Abs(Frac(X))=0.5)*
    (integer(2*Random(2))-1)*0.1 )
Test Before Use

Note that conversion to integer is to some extent like a mod 1 operation, and similar considerations can apply where something like mod N is needed - see Div & Mod. Additional rounding errors may be introduced by the arithmetic above.

In some work, e.g. finance, the zero value has special significance, and methods 2, 3, 5b or 5c may be needed. In other cases, e.g. longitude, temperature (C, F, or R, not K), the zero position is essentially arbitrary, and there must be no difference in the behaviour of numbers on different sides of it; 1, 4, 5a, 5d are better than those.

For 6b, the average of the results of repeatedly rounding any X tends to X itself.

International, national, professional, manufacturer's, etc., standards must be borne in mind; but it is the true needs of the work that must dictate the operations actually used.

Tests

REVISED 2005-07-02 to show the difference, in BP7, between run-time and compile-time evaluation of Round. NEEDS CHECKING.

Run TimeCompile Time
RoundTruncRoundTrunc
NumberBP7D 3BP7D 3BP7D 3BP7D 3
+4.5+4+4+4+4+5+4+4+4
+3.5+4+4+3+3+4+4+3+3
+2.5+2+2+2+2+3+2+2+2
+1.5+2+2+1+1+2+2+1+1
+0.5  0  0  0  0+1  0  0  0
-0.5  0  0  0  0-1  0  0  0
-1.5-2-2-1-1-2-2-1-1
-2.5-2-2-2-2-3-2-2-2
-3.5-4-4-3-3-4-4-3-3
-4.5-4-4-4-4-5-4-4-4
See test program roundemo.pas.

There is a bug in BP7 - in BP7BUGS2.LST :- 13. Compile time evaluation of the ROUND() function is different than run-time evaluation in $N+ mode on numbers ending in .5.

The list is cited in the mini-FAQ of news:comp.lang.pascal.borland.

Originally, I believed that (a) Trunc uses method 2; and (b) Delphi (V#?) Round uses method 5e, but in TP4..TP7 (& BP7), Round is different.

Users should test whether the compiler matches the documentation in use, not forgetting its own manual and Help file.

In all of TP7, BP7 and Delphi 3, on-line Help says "X is a real-type expression. Round returns a Longint value that is the value of X rounded to the nearest whole number. If X is exactly halfway between two whole numbers, the result is the number with the greatest absolute magnitude". The TP4, TP5, BP7, and TPW manuals agree.

The BP7 Run-time Round function does not comply with that - presumably a Help error.

I've been advised :- The upshot seems to be "user beware" when it comes using ROUND on TPascal. Compiler settings, type of float, and TPL library can all have an effect.

References

Notes

RAHP wrote : "Round and Trunc are non-reentrant." - ISR writers note this. He added that the RTL source can easily be changed; he has posted (2001-12-27, c.l.p.b, Message-ID: <ed6d5b69.0112271304.7019f32@posting.google.com>) revised, re-entrant F87H routines FTrunc, FRound, FInt & FFrac for those with the TP6/BP7 RTL. See also Pascal Floating-Point.

Converting Float to n Decimal Places

Try a Web/News search for "John Herbster" and/or "JohnH".

Note :- in rounding to n digits, only numbers which are multiples of 2-(n+1) are exactly half-way between possible results; only those cases can be expected to show Bankers' Rounding. For currency, these are 0.125, 0.375, 0.625, & 0.875.

Note :- for formal currency conversions involving the Euro there are very specific rules.

Rounding Float to Text

For the output of a program, or to produce a string, just use the modifiers :m:n in such as Write(X:m:n) or Str(X:m:n, S). Sometimes it is useful to edit the output of Str further. These use "." for the decimal separator, which is what Val requires. If the default rounding is not acceptable, scale and round to number first.

Delphi has additional "Format" functions. They use the Windows setting for the decimal separator; that is sometimes useful, but remember that localisation makes the separator mutable.

Rounding Float to Number

If this is required within a program, a number may be converted to n decimal places by multiplying by 10n, converting to integer or longint with Round / Int, and finally dividing by 10n. Note that the result can only rarely be held exactly in a Float variable.

There should be a good way of converting to n significant figures - Str(X:n+8, S) ; Val(S, X, J) would do it, assuming d.d..dE+dddd format. Otherwise, multiply or divide by powers of ten, convert to integer, divide or multiply.

Delphi RoundTo

In at least some versions of Delphi, RoundTo is defective.

For example, rounding to two places, the number 1.875 is represented exactly in floating-point, and Bankers' Rounding (round to even) requires a result of 1.88; but 1.87 is obtained.

The root cause is that in RoundTo(X, -2) the value of 10-2, which cannot be held exactly, is used to divide the input before Round is called. The cure is to use 10+2, which is held exactly, to multiply the input before Round is called.

Another apparent defect is that Doubles are used, which introduces an unnecessary approximative step if the input is Extended.

The following replacement appears satisfactory :-

function R2(const AValue : extended ; const ADigit : TRoundToRange) :
  extended ;
var X : extended ; i : integer ;
begin
  X := 1.0 ;
  for i := 1 to Abs(ADigit) do X := X * 10 ;
  if ADigit<0
    then Result := Round(AValue * X) / X
    else Result := Round(AValue / X) * X ;
  end {R2} ;

The i loop could be replaced by an array lookup, and perhaps X should be integer.

Rounding Float to Other than a Multiple of One

As Rounding Float to Number above, but with different factors.

Rounding the Delphi Currency Type

That is quite a different matter.

The Currency type, though stored in binary, is really an integer type denoting units of 10-4 coins; it is thus equivalent, in what it can represent, to a decimal type with four decimal places (as I recall, in accordance with what the documentation claims, which is not always the case). In particular, all N.xy50 are stored exactly, and it is meaningful to discuss their rounding.

In general discussion of rounding, the distinction between Currency and binary floats needs to be made permanently clear.

Pascal Index
Home Page
Mail: no HTML
© Dr J R Stockton, near London, UK.
All Rights Reserved.
These pages are tested mainly with Firefox 3.0 and W3's Tidy.
This site, http://www.merlyn.demon.co.uk/, is maintained by me.
Head.