Delphi - RoundTo - always down - delphi

I need to round a floating point number to two decimal places, but always down. Now I use RoundTo(number, -2), but it does the rounding mathematically correctly, which is undesired behavior for my situation. Let the reason, why I need to do this, aside...
I eventually achieved it using this:
var a,b: currency;
floatStr: string;
format: TFormatSettings;
localeDec: char;
begin
format:= TFormatSettings.Create;
localeDec:= format.DecimalSeparator;
format.DecimalSeparator:= ',';
System.SysUtils.FormatSettings:= format;
a:= 2/30;
floatStr:= floatToStr(a);
b:= strToCurr(
copy(floatStr, 1, ansiPos(',', floatStr) + 2)
);
showMessage(currToStr(b));
format.DecimalSeparator := localeDec;
System.SysUtils.FormatSettings:= format;
end;
However, this solution just doesn't feel right. Is there a "mathematically clean" way to do it, without messing with strings and resetting decimal separators etc. ? I searched a lot, but didn't find any.

You can do the following:
Multiply the value by 100.
Truncate to an integer, towards zero.
Divide the value by 100.
Like this:
function RoundCurrTo2dpTruncate(const Value: Currency): Currency;
begin
Result := Trunc(Value*100) / 100;
end;
I've assumed that by rounding down you mean towards zero. So 0.678 rounds down to 0.67 and -0.678 to -0.67. However, if you want to round towards -∞ then you should replace Trunc with Floor.
function RoundCurrTo2dpDown(const Value: Currency): Currency;
begin
Result := Floor(Value*100) / 100;
end;
Another way to tackle the problem is to recognise that a Currency value is simply a 64 bit integer with an implicit shift of 10000. So the entire operation can be performed using integer operations, unlike the code above which uses floating point operations.
From the documentation:
Currency is a fixed-point data type that minimizes rounding errors in monetary calculations. It is stored as a scaled 64-bit integer with the 4 least significant digits implicitly representing decimal places. When mixed with other real types in assignments and expressions, Currency values are automatically divided or multiplied by 10000.
For example you could implement RoundCurrTo2dpTruncate like this:
function RoundCurrTo2dpTruncate(const Value: Currency): Currency;
begin
PInt64(#Result)^ := (PInt64(#Value)^ div 100)*100;
end;
Note that here the arithmetic has been an shifted by 10000. So multiplication by 100 has become division by 100. And so on.

You can use SetRoundMode with old Delphi RoundTo
SetRoundMode(rmDown);
function RoundTo(const AValue: Double; const ADigit: TRoundToRange): Double;
var
LFactor: Double;
begin
LFactor := IntPower(10, ADigit);
Result := Round(AValue / LFactor) * LFactor;
end;
Obviously it was changed in recent versions

Related

1 minute Error in time difference calculation

My Programming Environment is Borland C++ Builder 6.
I have encountered a problem with a bad result in the following code:
TDateTime dtEnter, dtExit;
dtEnter = EncodeDateTime(2016, 11, 29, 0, 49, 0, 0);
dtExit = EncodeDateTime(2016, 11, 29, 0, 50, 0, 0);
ShowMessage(dtEnter);
ShowMessage(dtExit);
ShowMessage(IntToStr(MinutesBetween(dtEnter, dtExit)));
The result is 0 instead of 1!
Why is this?
This is a known issue in older versions of the DateUtils unit, which was first introduced in Delphi/C++Builder 6. The issue lasted for several years until it was finally fixed in Delphi/C++Builder XE.
TDateTime is essentially just a double, where the date is stored in the integral portion and the time is stored in the fractional portion. As such, it is subject to approximate representations and rounding.
In your example, dtEnter is 42703.0340277778 and dtExit is 42703.0347222222.
The span between two TDateTime values is calculated using simple floating-point math:
function SpanOfNowAndThen(const ANow, AThen: TDateTime): TDateTime;
begin
if ANow < AThen then
Result := AThen - ANow
else
Result := ANow - AThen;
end;
In your example, SpanOfNowAndThen(dtEnter, dtExit) is 0.000694444439432118.
In the case of the MinutesBetween() function, prior to XE it would call MinuteSpan(), which returns a Double that is the result of SpanOfNowAndThen() multiplied by the MinsPerDay constant, and then it would truncate off the fractional portion to produce the final integer:
function MinuteSpan(const ANow, AThen: TDateTime): Double;
begin
Result := MinsPerDay * SpanOfNowAndThen(ANow, AThen);
end;
function MinutesBetween(const ANow, AThen: TDateTime): Int64;
begin
Result := Trunc(MinuteSpan(ANow, AThen));
end;
In your example, MinuteSpan() produces a decimal value that is slightly less than 1.0 (0.99999999278225, to be exact), which becomes 0 when the decimal is truncated off.
In XE, many of the DateUtils functions were re-written to use more reliable calculations that are not based on floating-point math. Although MinuteSpan() is still the same, MinutesBetween() no longer uses MinuteSpan(). Instead, it now converts the two TDateTime values to milliseconds (which is lossless since TDateTime has millisecond precision), subtracts the values, and then divides the absolute value of the difference by a constant number of milliseconds per minute:
function DateTimeToMilliseconds(const ADateTime: TDateTime): Int64;
var
LTimeStamp: TTimeStamp;
begin
LTimeStamp := DateTimeToTimeStamp(ADateTime);
Result := LTimeStamp.Date;
Result := (Result * MSecsPerDay) + LTimeStamp.Time;
end;
function MinutesBetween(const ANow, AThen: TDateTime): Int64;
begin
Result := Abs(DateTimeToMilliseconds(ANow) - DateTimeToMilliseconds(AThen))
div (MSecsPerSec * SecsPerMin);
end;
In your example, DateTimeToMilliseconds(dtEnter) is 63616063740000 and DateTimeToMilliseconds(dtExit) is 63616063800000, so the difference is 60000 ms, which is exactly 1 minute.
For versions prior to XE, you will have to implement a similar fix manually in your own code. This is discussed in various online blogs, such as:
How do I work around Delphi's inability to accurately handle datetime manipulations?
Accurate Difference Between Two TDateTime Values

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...

ABS function failing in Delphi

In Delphi6 or in Delphi 2010, declare two variables of type Currency (vtemp1,vtemp2) and feed them a value of 0.09.
Embed one of the variables with the ABS function and compare it to the other.
You would expect for the comparison to yield a positive result as the compiler
watch reveals the same value for abs(vtemp1) and vtemp2.
Oddly the if statement fails!!!
Notes:
-This problem is experienced only when dealing with the
number 0.09 (trying several other near values revealed normal results)
-Declaring the variable as Double instead of currency, the problem ceases to exist.
I think that the reason is type conversions. Abs() function returns real results, so currency variable casts to real. Take a look at documentation:
Currency is a fixed-point data type that minimizes rounding errors in
monetary calculations. On the Win32 platform, it is stored as a scaled
64-bit integer with the four last significant digits implicitly
representing decimal places. When mixed with other real types in
assignments and expressions, Currency values are automatically divided
or multiplied by 10000.
so Currency is fixed and real is floating-point.
Sample code for your question is :
program Project3;
{$APPTYPE CONSOLE}
const VALUE = 0.09;
var a,b : currency;
begin
a := VALUE;
b := VALUE;
if a = Abs(b) then writeln('equal')
else writeln('not equal', a - Abs(b));
readln;
end.
produces not equal result, because of type conversions;
compiler watch reveals the same value for abs(vtemp1) and vtemp2
Try to add x : real, then call x := abs(b);, add x to watches list, select it and press Edit watch, then select Floating point. X becomes 0.899...967.
not only 0.09 value produces such result. you can try this code to check:
for i := 0 to 10000 do begin
a := a + 0.001;
b := a;
if a <> abs(b) then writeln('not equal', a);
end;
so, if you need absolute value of Currency variable - just do it. don't use floating-point abs():
function Abs(x : Currency):Currency; inline;
begin
if x > 0 then result := x
else result := -x;
end;
A little clarification. The 'issue' appears if float values are compared:
var
A: Currency;
begin
A:= 0.09;
Assert(A = Abs(A));
end;
That is because Abs(A) returns a float value, and A = Abs(A) is implemented as a float compare.
I could not reproduce it if Currency values are compared:
var
A, B: Currency;
begin
A:= 0.09;
B:= Abs(A);
Assert(A = B);
end;
But the second sample is also a potential bug because B:= Abs(A) internally is a float division/multiplication by 10000 with rounding to Currency (int64), and depends on FPU rounding mode.
I have created a qc report #107893, it was opened.
I have just found out the hard way that Delphi XE2 Abs function does not overload the currency type.
See the Delphi XE2 docwiki
these are the only types supported by abs
function Abs(X: ): Real; overload;
function Abs(X: ): Int64; overload;
function Abs(X: ): Integer; overload;

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