Search code examples
delphiprecisiondelphi-10-seattletclientdatasetbcd

How can I avoid losing precision with ftFmtBcd?


The documentation says:

TFMTBCDField encapsulates the fundamental behavior common to binary-coded decimal (BCD) fields. BCD values provide greater precision and accuracy than floating-point numbers. BCD fields are often used for storing and manipulating monetary values.

Unfortunately, I'm finding that using such a field in combination with an Extended value, I'm losing precision (in this example two digits): if I use

BcdField.AsExtended := Value;

the value gets actually truncated to four digits. What can I do about this?

Complete example:

procedure TForm1.Button1Click(Sender: TObject);
var
  LValue: Double;
  LDataset: TClientDataSet;
  LFieldDef: TFieldDef;
begin
  LValue := 1 / 3;
  LDataset := TClientDataSet.Create(self);
  try
    LFieldDef := LDataset.FieldDefs.AddFieldDef;
    LFieldDef.DataType := ftFMTBcd;
    LFieldDef.Size := 6;
    LFieldDef.Precision := 10;
    LFieldDef.Name := 'A';
    LDataset.CreateDataset;
    LDataset.Append;
    LDataset.FieldByName('A').AsExtended := LValue;
    LDataset.Post;
    ShowMessage(FloatToStr(LDataset.FieldByName('A').AsExtended));
    ShowMessage(FloatToStr(LValue));
  finally
    FreeAndNil(LDataset);
  end;
end;

Output (in message boxes):

0,3333
0,333333333333333

Solution

  • Strictly speaking it would be more accurate to use TField.AsBCD which returns a TBcd record. TFmtBcdField overrides the default implementation and returns an accurate TBcd record.

    TBcd is a record structure with support for simple arithmetic operators and implicit conversion from Integer and Double. So it should be suitable for most purposes.

    The drawbacks are:

    • Mathematical operations may be marginally slower due to lack of built in machine instruction. But it's probably a reasonable trade-off for cases where precise representation is required. Benchmark and evaluate as needed.
    • Some functions that take Double or Integer parameters may need TBcd overload implementations.

    Some related considerations:

    • The use of Double is not appropriate if you require accurate precision due to the nature floating point representation. See Is floating point math broken? for more info.
    • The use of Extended has the same problems as Double even though the extra 2 bytes provide greater range and higher precision - it's still a floating point data type. Furthermore, Extended has its own problems. Take note of the warnings here.
    • If you don't need precise representation, then you can convert to Double using BcdToDouble. See BCD Support Routines.
    • Another option to consider in cases where you don't need more than 4 decimals, but do need precise representation: Use the Currency data type. It's represented as a 64-bit integer with assumed division by 10000 which is how it supports 4 decimal digits.