Search code examples
delphi

When to specify a custom epsilon in Delphi's SameValue function?


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:

  1. In general use-cases, is it reliable to use the SameValue function without specifying a custom epsilon?
  2. Are there specific scenarios where one must or should provide a custom epsilon value?
  3. If providing a custom epsilon, what's a good strategy to determine an appropriate value based on the context?

Would appreciate insights or best practices!


Solution

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