Converting currencies is not exactly the same as converting measurement units, because currency rates change constantly. In theory, you can register a conversion rate with Delphi's conversion engine. From time to time, you check the new rate of exchange, unregister the existing conversion, and register a new one. However, keeping up with the actual rate means changing the conversion so often that the operation might not make a lot of sense. Also, you'll have to triangulate conversions: You must define a base unit (probably the U.S. dollar if you live in America) and convert to and from this currency even if you're converting between two different currencies.
It's more interesting to use the engine to convert member currencies of the euro, for two reasons. First, conversion rates are fixed (until the single euro currency takes over). Second, the conversion among euro currencies is legally done by converting a currency to euros first and then from the euro amount to the other currency—the exact behavior of Delphi's conversion engine. There is one small problem: You should apply a rounding algorithm at every step of the conversion. I'll consider this problem after I've provided the base code for integrating euro currencies with the Delphi conversion engine.
The ConvertIt demo available among the Delphi examples provides support for euro conversions, using a slightly different rounding approach, still not as precise as demanded by the European currency conversion rules. I've decided to keep this example, because it is instructive in showing how to create a new measurement system.
The example, called EuroConv, teaches how to register any new measurement unit with the engine. Following the template provided by the StdConvs unit, I've created a new unit (called EuroConvConst). In the interface portion, I've declared variables for the family and the specific units, as follows:
interface var // Euro Currency Conversion Units cbEuroCurrency: TConvFamily; cuEUR: TConvType; cuDEM: TConvType; // Germany cuESP: TConvType; // Spain cuFRF: TConvType; // France // and so on...
The implementation portion of the unit defines constants for the various official conversion rates:
implementation const DEMPerEuros = 1.95583; ESPPerEuros = 166.386; FRFPerEuros = 6.55957; // and so on...
Finally, the unit initialization code registers the family and the various currencies, each with its own conversion rate and a readable name:
initialization // Euro Currency's family type cbEuroCurrency := RegisterConversionFamily('EuroCurrency'); cuEUR := RegisterConversionType( cbEuroCurrency, 'EUR', 1); cuDEM := RegisterConversionType( cbEuroCurrency, 'DEM', 1 / DEMPerEuros); cuESP := RegisterConversionType( cbEuroCurrency, 'ESP', 1 / ESPPerEuros); cuFRF := RegisterConversionType( cbEuroCurrency, 'FRF', 1 / FRFPerEuros);
The engine uses as a conversion factor the amount of the base unit necessary to obtain the secondary units, with a constant like MetersPerInch, for example. The standard rate of euro currencies is defined the opposite way. For this reason, I've kept the conversion constants with the official values (like DEMPerEuros) and passed them to the engine as fractions (1/DEMPerEuros).
Having registered this unit, you can now convert 120 German marks to Italian liras as follows:
Convert (120, cuDEM, cuITL)
The demo program does a little more: It provides two list boxes with the available currencies, extracted as in the previous example, and edit boxes for the input value and final result. You can see the form at run time in Figure 3.4.
The program works nicely but is not perfect, because the proper rounding is not applied; you should round not only the final result of the conversion but also the intermediate value. Using the conversion engine to accomplish this rounding directly is not easy. The engine allows you to provide either a custom conversion function or a conversion rate. But writing identical conversion functions for all the currencies seems like a bad idea, so I've decided to take a different path. (You can see examples of custom conversion functions in the StdConvs unit, in the portion related to temperatures.)
In the EuroConv example, I've added to the unit with the conversion rates a custom EuroConv function that does the proper conversion. Simply calling this function instead of the standard Convert function does the trick (and I see no drawback to this approach, because in such programs you'll rarely mix currencies with distances or temperatures). As an alternative, I could have inherited a new class from TConvTypeFactor, providing a new version of the FromCommon and ToCommon methods; or I could have called the overloaded version of the RegisterConversionType that accepts these two functions as parameters. However, neither of these techniques would have allowed me to handle special cases, such as the conversion of a currency to itself.
This is the code of the EuroConv function, which uses the internal EuroRound function to round to the number of digits specified in the Decimals parameter (which must be between 3 and 6, according to the official rules):
type TEuroDecimals = 3..6; function EuroConvert (const AValue: Double; const AFrom, ATo: TConvType; const Decimals: TEuroDecimals = 3): Double; function EuroRound (const AValue: Double): Double; begin Result := AValue * Power (10, Decimals); Result := Round (Result); Result := Result / Power (10, Decimals); end; begin // check special case: no conversion if AFrom = ATo then Result := AValue else begin // convert to Euro, then round Result := ConvertFrom (AFrom, AValue); Result := EuroRound (Result); // convert to currency then round again Result := ConvertTo (Result, ATo); Result := EuroRound (Result); end; end;
Of course, you might want to extend the example by providing conversion to other non-euro currencies, eventually picking the values automatically from a website. I'll leave this as a rather complex exercise for you.