Fixing Floating Point Error - delphi

I have some code that gets the leading value (non-zero) of a Double using normal math instead of String Math...
For Example:
0.020 would return 2
3.12 would return 3
1000 should return 1
The code I have at the moment is:
LeadingValue := Trunc(ResultValue * Power(10, -(Floor(Log10(ResultValue)))))
However when ResultValue is 1000 then LeadingValue ends up as 0.
What can I do to fix this problem I'm assuming is being caused by floating point errors?
Thanks.

I'm curious as to why you can't / won't use String manipulation for this. Could you explain further?
The most obvious thing you can do to fix the problem is to convert the number to a String and find the first non zero digit.

A possible way to cheat around the rounding problems:
program Project1;
{$APPTYPE CONSOLE}
uses
SysUtils,
Math;
function LeadingValue(num: extended): integer;
const
ExtendedResolution = 1E-19 * 1000; // from Math
begin
num := Abs(num);
Result := Trunc(num * Power(10, -(Floor(Log10(num)))) + ExtendedResolution);
end;
begin
try
Writeln(LeadingValue(0.02));
Writeln(LeadingValue(3.12));
Writeln(LeadingValue(1000));
Writeln(LeadingValue(0.9999999999));
Writeln(LeadingValue(1.0000000001));
Writeln(LeadingValue(-0.02));
Writeln(LeadingValue(-3.12));
Writeln(LeadingValue(-1000));
Writeln(LeadingValue(-0.9999999999));
Writeln(LeadingValue(-1.0000000001));
Readln;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.

if you divide instead of multiply, it fixes your test cases. Overall, it will yield better result as the "Power" result won't have a fractional value. I'm afraid it might not have a 100% case coverage, but unless a better solution is proposed, this should be better than what you have.
function Func(Value : Double) : Integer;
begin
Result := Trunc(Value / Power(10, (Floor(Log10(Value)))))
end;

You're not going to be able to do this with 100% accuracy using floating-point math. What you need to do essentially is to convert a binary floating-point value to a decimal string. Routines that do that accurately, like David Gay's dtoa.c, use high precision (integer) arithmetic.

Related

What is the correct way to round currency data type in Delphi? [duplicate]

I'm trying to get Delphi to Round like Excel but I can't. Here is the code:
procedure TForm1.Button1Click(Sender: TObject);
var
s : string;
c : currency;
begin
c := 54321.245;
s := '';
s := s + Format('Variable: %m',[c]);
s := s + chr(13);
s := s + Format(' Literal: %m',[54321.245]);
ShowMessage(s);
end;
I'm using a currency variable that is set to 54321.245 and when I format this variable it rounds using Bankers Rounding. However, when I format the same value as a literal it rounds the way that Excel rounds.
I was expecting this to round to $54,321.25 whether it's formating a currency variable or a literal value. How can I make sure that Delphi rounds the same way as Excel every time?
Edit
The rounding I expect to see is as follows:
54,321.245 = 54,321.25
54,321.2449 = 54,321.24
54,431.2499 = 54,421.25
I am only using literals to show the different ways Delphi rounds. I expect to use variables in the actual code.
Note:
If I change the variable from currency to extended it rounds correctly
Edit #2
Some have suggested that I do not have a clear understanding of my requirements, this is absolutely not true. I have a very clear understanding of my requirements, I'm obviously not doing a very good job of explaining them. The rounding method I want is two decimal places. When the deimal part has a thousandths value >= 0.005 I want it rounded to 0.01 the currency type offered by Delphi does not do this. I also tried this example using Microsoft SQL with a money datatype (which I assumed was the same as Delphi's currency) and SQL rounds it's money type the way I described.
SQL Money >= 0.005 = 0.01
Delphi Currency >= 0.005 := 0.00
Edit #3
Good Article: http://rvelthuis.de/articles/articles-floats.html
Possible Solution: http://rvelthuis.de/programs/decimals.html
Edit #4
Here is one of the solutions from the Embarcadero discussion
function RoundCurrency(const Value: Currency): Currency;
var
V64: Int64 absolute Result;
Decimals: Integer;
begin
Result := Value;
Decimals := V64 mod 100;
Dec(V64, Decimals);
case Decimals of
-99 .. -50 : Dec(V64, 100);
50 .. 99 : Inc(V64, 100);
end;
end;
If I understand you correctly, you are looking for this:
function RoundTo2dp(Value: Currency): Currency;
begin
Result := Trunc(Value*100+IfThen(Value>0, 0.5, -0.5))/100;
end;
It's not possible to make RTL to round the way you want. The way to affect the rounding in Delphi is to use SetRoundMode which sets the FPU conrol word for rounding, however, as far as I can tell, there's no FPU support for rounding the exact in-between to upwards (which is generally avoided because it generates a bias for higher values).
You have to implement your own rounding function. There's an extended discussion in Delphi Rounding thread on Embarcadero forums, which includes several solutions.
use function System.Math.SimpleRoundTo
You can gain control on how delphi rounding numbers by :
uses Math;
...
procedure TForm1.Button1Click(Sender: TObject);
var
s : string;
c : currency;
begin
SetRoundMode(rmNearest);
c := 54321.245;
s := '';
s := s + Format('Variable: %m',[c]);
s := s + chr(13);
s := s + Format(' Literal: %m',[54321.245]);
ShowMessage(s);
end;
Unfortunately, using rmNearest, Delphi decides the number 54321.245 is closer to 54321.24 than 54321.25

Delphi - Calculate Minimum Maximum and Average in items in a list

I am building a single application to Calculate Min Max and Avg of Values in a List.
It is actually Temperatures. So I think I am Almost correct but there are 2 Errors.
var
Count, Average, Sum,i, Max, Min, K : Integer;
Temperatures : Array of Integer;
NoItems : Double;
begin
Count := 0;
Sum := 0;
Max := 0;
Min := 0;
Average := 0;
Count := lstTemp.Items.Count;
{Calculate Sum of Values in the list}
for i := 0 to Count - 1 do
Sum := Sum + StrToInt(lstTemp.Items[i]);
{Calculate Min and Max}
SetLength(Temperatures,Count);
for K:=0 to Count-1 do
Temperatures[K] := lstTemp.Items[K];
if (Temperatures[K] > Max) then
Max := Temperatures[K];
if (Temperatures[K] < Min) then
Min := Temperatures[K];
{Calculate Average}
Average := Sum / Count;
edtAvg.Text:=IntToStr(Average); //Display Average
edtAvg.Text:=IntToStr(Min); //Display Minimum Temp.
edtAvg.Text:=IntToStr(Max); //Display Maximum Temp.
end;
So the 2 Errors are
Error: Incompatible types: got "AnsiString" expected "LongInt"
This is for Average := Sum / Count;
Error: Incompatible types: got "Set Of Byte" expected "Double"
This Error is for Temperatures[K] := lstTemp.Items[K];
Any Ideas how to solve this?
Sum and Count are both Integers so I dont know why it shouldnt work!
Thanks
There is a number of problems. First, when you write
for K:=0 to Count-1 do
Temperatures[K] := lstTemp.Items[K];
if (Temperatures[K] > Max) then
Max := Temperatures[K];
if (Temperatures[K] < Min) then
Min := Temperatures[K];
you actually do
for K:=0 to Count-1 do
Temperatures[K] := lstTemp.Items[K];
if (Temperatures[K] > Max) then
Max := Temperatures[K];
if (Temperatures[K] < Min) then
Min := Temperatures[K];
which is nonsense. You want all these lines to be part of the for loop:
for K:=0 to Count-1 do
begin
Temperatures[K] := lstTemp.Items[K];
if (Temperatures[K] > Max) then
Max := Temperatures[K];
if (Temperatures[K] < Min) then
Min := Temperatures[K];
end;
Second, in order for this algorithm to work, the initial value of Min (Max) needs to be larger (smaller) than the values in the list. This might work for Max := 0, but probably not for Min := 0. You need to set Min to a very large value before you run the loop, obviously. The best value you can use is the highest-possible signed 32-bit integer value, that is, 2^31 - 1, which is the value of the MaxInt constant.
Third,
Temperatures[K] := lstTemp.Items[K];
is probably wrong. Temperatures is an array of integers, while lstTemp.Items[K] is a string (at least according to StrToInt(lstTemp.Items[i])), so you need
Temperatures[K] := StrToInt(lstTemp.Items[K]);
Fourth, you declare Average as an integer, but it needs to be a floating-point number (obviously), like real or double.
Fifth,
edtAvg.Text:=IntToStr(Average); //Display Average
edtAvg.Text:=IntToStr(Min); //Display Minimum Temp.
edtAvg.Text:=IntToStr(Max); //Display Maximum Temp.
is not techncally incorrect, but will most likely not do what you want.
Sixth, although not an error, there is no need for you to initialise Count and Average to 0. Finally, you only need a single for loop.
There is (at least in Delphi 2010 - unit Math) one function that will calculate the mean and standard deviation in one step and functions that return the minimum and maximum values in an array. BTW, Mean is the arithmetic average of all the values and is the correct term. (I copied an example that I am working on and modified to your example - it compiles at least):
type
a = array of double;
var
Temperatures : a;
Average,stddev3, Max, Min : extended;
// Compiler insists on extended for these properties
begin
Max := Math.MaxValue(Temperatures);
Min := Math.MinValue(Temperatures);
Math.MeanAndStdDev(Temperatures ,Average,stddev3);
end;
For the maximum value in an array use (it takes an array of double and returns double):
function MaxValue(const Data: array of Double): Double;
For the minimum value use the corresponding:
function MinValue(const Data: array of Double): Double;
I agree that average cannot be an integer, but there are 2 similar functions for integer arrays:
function MinIntValue(const Data: array of Integer): Integer; and
function MaxIntValue(const Data: array of Integer): Integer;
0909EM's reply was very well done, but I have a few disagreements. First, I don't believe there's a need to set any sentinel value at all; simply use the first temperature value. Second, if we put a Begin and End around every single line If statement we'd approach COBOL-like levels of English verbosity. As it is, it's a crying shame this simple problem takes so much code. Third, I would not use StrToIntDef. Remember these lines from the Zen Of Python (I don't care if you don't know Python; everyone should memorize it, at least until we get an I Ching of Intersimone):
Errors should never pass silently.
Unless explicitly silenced.
If a user passes incorrect data into the temperature stats procedure, StrToIntDef is going to silently convert these values to zeroes, an unexpected and undesired behavior. The caller is going to get back answers that they assume are ok (because of no errors), yet will have incorrect values (especially the average). It is a far better thing to let the procedure blow up so testing will reveal the incorrect input.
I'd also replace the For loops with For...in. I banged this together:
program temps;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, System.Classes, Generics.Collections, Math;
Var
someTemps : TStringList;
Procedure TempStats(temperatures : TStringList);
Var
temps : TList<Real>;
minTemp, maxTemp, sumTemps : Real;
numTemps : Integer;
tempStr : String;
temp : Real;
avgTemp : Real;
Begin
numTemps := temperatures.Count;
If numTemps > 0 then
Begin
temps := TList<Real>.Create;
For tempStr in temperatures Do
temps.Add(StrToFloat(tempStr));
minTemp := temps[0];
maxTemp := temps[0];
sumTemps := 0;
For temp in temps Do
Begin
minTemp := Min(minTemp, temp);
maxTemp := Max(maxTemp, temp);
sumTemps := sumTemps + temp;
End;
avgTemp := sumTemps / numTemps;
WriteLn(avgTemp:0:2);
WriteLn(minTemp:0:2);
WriteLn(maxTemp:0:2);
temps.Free;
End
Else
WriteLn('No temperatures passed.');
End;
Begin
someTemps := TStringList.Create;
someTemps.AddStrings(TArray<String>.Create('72', '93', '84', '76', '82'));
TempStats(someTemps);
ReadLn;
someTemps.Clear;
TempStats(someTemps);
someTemps.Free;
ReadLn;
end.
Firstly, Consider using StrToIntDef (String To Integer with a Default value) instead of StrToInt (String to Integer) this will yield the following...
value := StrToIntDef('Abcdef', 0); // value will be zero
vs
value := StrToInt('Abcdef'); // exception
But the question is do you want integers or floating point values for your temperatures? (eg 1 or 1.6?) If you want floating point values, maybe use StrToFloatDef...
Second, I've seen lots of grads that use Delphi make this mistake, try to always use begin and end, it'll help... because it makes it really clear what you are doing inside a if/for/while and what you intend to do outside..
for i := 0 to lstTemp.Items.Count - 1 do
begin
// Sum all the items in the list
Sum := Sum + StrToIntDef(lstTemp.Items[i], 0);
end;
Next up your array is a bit pointless, the SetLength and adding items bit is OK, but its not very functional, when you could just use the items in the list. All you need to do is hang onto the max and min values.
Then your last problem is that Average isn't going to be a whole integer, its going to have a fractional part. Eg. 5 divided by 2 is 2.5, not 2 and not 3. You could use trunc to return just the integer part, or change Average so that its a floating point number...
for K:=0 to lstTemp.Items.Count-1 do
begin
if (StrToIntDef(lstTemp.Items[K], 0) > Max) then
begin
Max := StrToIntDef(lstTemp.Items[K], 0);
end;
if (StrToIntDef(lstTemp.Items[K], 1000) < Min) then // note, really high number
begin
Min := StrToIntDef(lstTemp.Items[K], 1000);
end;
end;
{Calculate Average}
Average := Trunc(Sum / Count); // do you really want to trunc this? I suspect not.
if Min = 1000 then // just incase
begin
Min := 0;
end;
The final problem you will face is that your always setting the text of the same text box...
edtAvg.Text:=IntToStr(Average); //Display Average
edtMin.Text:=IntToStr(Min); //Display Minimum Temp. (I assume this is supposed to be edtMin)
edtMax.Text:=IntToStr(Max); //Display Maximum Temp. (I assume this is supposed to be edtMax)
I suppose the final improvement I'd make is noticing that you only need one for loop...
for K:=0 to lstTemp.Items.Count-1 do
begin
// Sum all the items in the list
Sum := Sum + StrToIntDef(lstTemp.Items[K], 0);
if (StrToIntDef(lstTemp.Items[K], Low(Integer)) > Max) then // A really low value
begin
Max := StrToIntDef(lstTemp.Items[K], Low(Integer));
end;
if (StrToIntDef(lstTemp.Items[K], High(Integer)) < Min) then // A really high value
begin
Min := StrToIntDef(lstTemp.Items[K], High(Integer));
end;
end;
The most important idea on how to solve this is to read your error messages properly. On a previous question you commented: "the error was saying it is an overloaded function or something". That attitude is not going to help you understand the problem. You need to read the error messages properly.
In this question you give the following description of your errors:
So the 2 Errors are Error: Incompatible types: got "AnsiString" expected "LongInt" This is for Average := Sum / Count; Error: Incompatible types: got "Set Of Byte" expected "Double" This Error is for Temperatures[K] := lstTemp.Items[K];
However, the description does not correspond to the errors you should be seeing based on the code provided.
It looks like you didn't read your errors, and just blindly started making changes in the hopes you would accidentally do something right. Because you didn't read the errors, you didn't notice that they changed. So when you came to us looking for help, you provided old errors with new code or vice-versa.
If you had actually read your error messages properly, you might have been able to solve the problem yourself. At the least, you would have been able to ask a better question with a description that actually matched the code.
Average := Sum / Count;
Average, Sum and Count are all declared as Integer. The error message you should be getting is: "Incompatible types: Integer and Extended".
If you read the error message, it should give you a clue to read up on Integer and Extended.
The problem here is that, in maths, division produces a Rational number. And correspondingly the result of a division operation in a program is not an Integer. So you need to declare Average as either Double or Extended.
Temperatures[K] := lstTemp.Items[K];
Temperatures is declared as an array of Integer. You haven't shown the declaration of lstTemp, but based on other code it's one of the standard Delphi Controls that has Items declared as TStrings. So the error message you should be getting is: "Incompatible types: Integer and string".
If you read the error message, it should give you a clue to do the same thing you did 5 lines earlier.
The reason for this error is that Delphi is a "strongly typed" language. The compiler tries to prevent you from making certain kinds of mistakes because it is much better catch them early. Imagine what might happen if one of the values in lstTemp were 'Hello'. That cannot be converted to an Integer; and would cause a "run-time" error in your program.
To fix this problem you need to tell the compiler: "I know the value is a string and could be any string, but I want you to convert it to an Integer". You do this by calling the StrToInt function. NOTE: You will still get a run time error if an invalid string is passed to the function, but by being forced to explicitly do the conversion, you can think about whether you want to do some pre-validation of your input data.
You asked about the errors reported by the compiler. That's just one kind of error you'll face when programming - and usually the easiest to resolve. You'll also encounter logic errors: where your program compiles successfully, but doesn't behave correctly. Andreas's excellent answer has covered those already, so I'll not repeat them.
However, I will give you some valuable advice. Once you've gotten over the hurdle of resolving compiler errors, and are able to do so easily - you need to as quickly as possible:
Get into the habit of testing your code thoroughly.
Learn how to use the integrated debugger.
Learn about its limitations.
Learn other debugging techniques: logging, profiling, pre- and post-condition checking.
Finally, as a response to alcalde's rant about there not being any simple functions to get Min, Max, Sum or Avg: I offer another possible implementation.
Basically the rant was about the fact that he'd far rather write something along the lines of:
begin
if (lstTemp.Count > 0) then
begin
edtMin.Text := lstTemp.Min;
edtMax.Text := lstTemp.Max;
edtAvg.Text := lstTemp.Average;
end
else
begin
ShowMessage('List is empty');
end;
end;
Obviously the above code won't compile, but with a little work we can achieve something similar.
He's perfectly right on two counts: (1) that this implementation would be cleaner, much easier to maintain and with less chance of errors. (2) Delphi doesn't provide a way to simply do that.
In fact, if you follow a top-down design approach, this might be your initial pseudo-code. You should be taught about top-down design, if not demand your money back. :)
The whole point behind the top-down-design approach is that you're looking for an ideal implementation. You're not worrying about what is/isn't there. If the current library and tools don't provide a Min function, you can write your own.
You are a programmer, you have the power!
I sometimes like to call this "wishful thinking programming". You're wishing if other things were in place, I could implement the functionality much more easily like "this". Then you go about making your wish come true.
Without further ado, here's the implementation. You will need to use the Math unit.
type
{ We will call existing functions that take TDoubleArray as input }
TDoubleArray = array of Double;
TStringsHelper = class(TStrings)
{ A useful class to help us convert TStrings into TDoubleArray }
public
class function Using(AStrings: TStrings): TStringsHelper;
function AsDoubleArray: TDoubleArray;
end;
{ TStringsHelper }
function TStringsHelper.AsDoubleArray: TDoubleArray;
var
LoopI: Integer;
begin
SetLength(Result, Count);
for LoopI := 0 to Count - 1 do
begin
Result[LoopI] := StrToFloat(Strings[LoopI]);
end;
end;
class function TStringsHelper.Using(AStrings: TStrings): TStringsHelper;
begin
Result := TStringsHelper(AStrings);
end;
var
LTemperatures: TDoubleArray;
begin
{ This code is almost the same as our "ideal" implementation }
if (lstTemp.Items.Count > 0) then
begin
LTemperatures := TStringsHelper.Using(lstTemp.Items).AsDoubleArray;
edtMin.Text := FloatToStr(MinValue(LTemperatures));
edtMin.Text := FloatToStr(MaxValue(LTemperatures));
edtMin.Text := FloatToStr(Mean(LTemperatures));
end
else
begin
ShowMessage('List is empty');
end;
end;
What values are in lstTemp.Items[i]?
I suppose the values are integers (without floating points), because you are using IntToStr.
Average cannot be an integer. Integer is a number (4 bytes) without a floating point. A simple numbers, such as 2,3,50,1500, -100
Assume that Sum = 100, and the Count = 3.
What Average will be?
So, you have to use float variable type, Double for example.
I hope it helps...

rounding a currency

i have the following code to round the currency
function MyRound(value :currency) : integer;
begin
if value > 0 then
result := Trunc(value + 0.5)
else
result := Trunc(value - 0.5);
end;
it worked well so far, my problem now is if i want to round a currency like 999999989000.40 it is giving negative value since Truc takes int and MyRound also returns int.
My possible solutions is to convert currency to string and get the string before . and convert the string back to currency. Is this a right approach? i am new to delpi so pls help me out.
From my point of view, you have two options:
You use the Round function, as David Heffernan pointed;
You can use the SimpleRoundTo function, as described here. The advantage of SimpleRoundTo is that it receives parameters of Single, Double and Extended data types and they convert round very well numbers like those stated.
You don't need any type conversions. There are plenty of rounding functions already available to you. Just round the desired number.
You are overcomplicating matters. You can simply use Round:
program Project1;
{$APPTYPE CONSOLE}
uses
SysUtils;
var
C: Currency;
begin
C := 999999989000.4;
Writeln(Round(C));
C := 999999989000.5;
Writeln(Round(C));
C := 999999989000.6;
Writeln(Round(C));
C := 999999989001.4;
Writeln(Round(C));
C := 999999989001.5;
Writeln(Round(C));
C := 999999989001.6;
Writeln(Round(C));
Readln;
end.
which outputs
999999989000
999999989000
999999989001
999999989001
999999989002
999999989002
If you don't want banker's rounding, and you really do want your Trunc logic then you do need to write your own function. But the problem with your function is that it was truncating to 32 bit integer. Make the function return a 64 bit integer:
program Project1;
{$APPTYPE CONSOLE}
uses
SysUtils, Math;
var
C: Currency;
function MyRound(const Value: Currency): Int64;
begin
if Value > 0 then
result := Trunc(Value + 0.5)
else
result := Trunc(Value - 0.5);
end;
begin
C := 999999989000.4;
Writeln(MyRound(C));
C := 999999989000.5;
Writeln(MyRound(C));
C := 999999989000.6;
Writeln(MyRound(C));
C := 999999989001.4;
Writeln(MyRound(C));
C := 999999989001.5;
Writeln(MyRound(C));
C := 999999989001.6;
Writeln(MyRound(C));
Readln;
end.
999999989000
999999989001
999999989001
999999989001
999999989002
999999989002
Take a look at John Herbster's rounding routines. They offer nearly any type of rounding you might want, e.g.:
drNone, {No rounding.}
drHalfEven,{Round to nearest or to even whole number. (a.k.a Bankers) }
drHalfPos, {Round to nearest or toward positive.}
drHalfNeg, {Round to nearest or toward negative.}
drHalfDown,{Round to nearest or toward zero.}
drHalfUp, {Round to nearest or away from zero.}
drRndNeg, {Round toward negative. (a.k.a. Floor) }
drRndPos, {Round toward positive. (a.k.a. Ceil ) }
drRndDown, {Round toward zero. (a.k.a. Trunc) }
drRndUp); {Round away from zero.}
I can't give you a link right now, but Google: decimal rounding John Herbster
I think his latest rounding routines are in DecimalRounding_JH1.pas. His discussion of floating point rounding (somewhere on Embarcadero's website is a "must-read".
This is what I actually use (would love to hear if there is any problems with this approach!):
function RoundingFunction(X: Real): Int64;
begin
Result := Trunc(SimpleRoundTo(X, 0));
end;

Is this correct implementing of Newton-Raphson method?

I need to implement Newtons method for finding square roots with given count count signs after floating point in Delphi 7. I wrote this code:
program NewtonMethod
{$APPTYPE CONSOLE}
uses
SysUtils, Math;
const
signCount = 50; //Signs after floating point count
var
number, curRoot, prevRoot: Extended;
precision: Extended;
function input(): Extended;
begin
Write('x=');
try
Readln(number)
except
Writeln('Invalid input data, please try again');
Input();
end;
end;
begin
input();
curRoot := sqrt(number); //Íà÷àëüíàÿ
precision := 1 / power(10, signCount);
prevRoot := 0;
repeat
prevRoot := curRoot;
curRoot := prevRoot - (Power(prevRoot, 2) - number) / (2 * prevRoot);
until
Abs(curRoot - prevRoot) < precision;
Writeln(curRoot);
ReadLn;
end.
Please, answer to my questions:
Is this code correct ?
How to print result root in normal (not exponential) form with all signs ?
Input values may be very large (up to 200 signs). Can this code work with them ?
And suggest me, how can I improve my code ?
curRoot:=sqrt(number); << looks like you're working out the square root of a number directly here, not using a method.
You should choose a "first guess" using some other method... If this is just for fun, try half the number
You might find this page useful: https://en.wikipedia.org/wiki/Newton%27s_method#Square_root_of_a_number
The Extended datatype can only do around 20 decimal places. 200 decimal places is a tough call and would probably be pretty difficult to do

How to get Delphi Currency Type to Round like Excel all the time?

I'm trying to get Delphi to Round like Excel but I can't. Here is the code:
procedure TForm1.Button1Click(Sender: TObject);
var
s : string;
c : currency;
begin
c := 54321.245;
s := '';
s := s + Format('Variable: %m',[c]);
s := s + chr(13);
s := s + Format(' Literal: %m',[54321.245]);
ShowMessage(s);
end;
I'm using a currency variable that is set to 54321.245 and when I format this variable it rounds using Bankers Rounding. However, when I format the same value as a literal it rounds the way that Excel rounds.
I was expecting this to round to $54,321.25 whether it's formating a currency variable or a literal value. How can I make sure that Delphi rounds the same way as Excel every time?
Edit
The rounding I expect to see is as follows:
54,321.245 = 54,321.25
54,321.2449 = 54,321.24
54,431.2499 = 54,421.25
I am only using literals to show the different ways Delphi rounds. I expect to use variables in the actual code.
Note:
If I change the variable from currency to extended it rounds correctly
Edit #2
Some have suggested that I do not have a clear understanding of my requirements, this is absolutely not true. I have a very clear understanding of my requirements, I'm obviously not doing a very good job of explaining them. The rounding method I want is two decimal places. When the deimal part has a thousandths value >= 0.005 I want it rounded to 0.01 the currency type offered by Delphi does not do this. I also tried this example using Microsoft SQL with a money datatype (which I assumed was the same as Delphi's currency) and SQL rounds it's money type the way I described.
SQL Money >= 0.005 = 0.01
Delphi Currency >= 0.005 := 0.00
Edit #3
Good Article: http://rvelthuis.de/articles/articles-floats.html
Possible Solution: http://rvelthuis.de/programs/decimals.html
Edit #4
Here is one of the solutions from the Embarcadero discussion
function RoundCurrency(const Value: Currency): Currency;
var
V64: Int64 absolute Result;
Decimals: Integer;
begin
Result := Value;
Decimals := V64 mod 100;
Dec(V64, Decimals);
case Decimals of
-99 .. -50 : Dec(V64, 100);
50 .. 99 : Inc(V64, 100);
end;
end;
If I understand you correctly, you are looking for this:
function RoundTo2dp(Value: Currency): Currency;
begin
Result := Trunc(Value*100+IfThen(Value>0, 0.5, -0.5))/100;
end;
It's not possible to make RTL to round the way you want. The way to affect the rounding in Delphi is to use SetRoundMode which sets the FPU conrol word for rounding, however, as far as I can tell, there's no FPU support for rounding the exact in-between to upwards (which is generally avoided because it generates a bias for higher values).
You have to implement your own rounding function. There's an extended discussion in Delphi Rounding thread on Embarcadero forums, which includes several solutions.
use function System.Math.SimpleRoundTo
You can gain control on how delphi rounding numbers by :
uses Math;
...
procedure TForm1.Button1Click(Sender: TObject);
var
s : string;
c : currency;
begin
SetRoundMode(rmNearest);
c := 54321.245;
s := '';
s := s + Format('Variable: %m',[c]);
s := s + chr(13);
s := s + Format(' Literal: %m',[54321.245]);
ShowMessage(s);
end;
Unfortunately, using rmNearest, Delphi decides the number 54321.245 is closer to 54321.24 than 54321.25

Resources