I recently came across the SameValue
function to compare floating-point numbers. I noticed that we can optionally specify an epsilon value for this function.
From the documentation, I understand that Delphi provides default epsilon values (SingleEpsilon
, DoubleEpsilon
, ExtendedEpsilon
) based on the data type of the floating-point values being compared.
function SameValue(const A, B: Single; Epsilon: Single = 0): Boolean; overload;
function SameValue(const A, B: Double; Epsilon: Double = 0): Boolean; overload;
function SameValue(const A, B: Extended; Epsilon: Extended = 0): Boolean; overload;
My questions are:
Would appreciate insights or best practices!
One of the issue with the default epsilon supplied by delphi is that, in a chain of operations, the cumulative rounding error can outgrow the default epsilon. So, the more operations you apply to a value, the less likely SameValue will behave as desired with the default epsilon.
As for a strategy for determining a custom epsilon. I personnaly find that using a value that is 1 order of magnitude lower than what we could call the "base meaningful unit"(BMU) of the context is decent.
For example, if we're talking about a retail USD price, the BMU would be 0.01$ so I'd use 0.001 as an epsilon. But then, unitary prices are often required to have a 4 decimals precision, in which case the BMU would be 0.0001$. In that situation, I'd use 0.00001 as the epsilon.
There are most likely contexts where the idea of a BMU doesn't apply. In those cases, I'm not sure how I would go about selecting a custom epsilon. Using the default one would probably be the safest bet.
As everything floating point related, this remains a "must read"
EDIT: Andreas brings a good point. Delphi uses by default an epsilon that is at least FloatTypeResolution. For Double(Other types have similar situation), that value is 1E-12, but Double can have values of the order of 1E-308 (or even smaller). So, when working with very small values, an explicit epsilon would also be required.