| // +----------------------------------------------------------------------+ // // $Id: ExchangeRates.php,v 1.7 2005/06/23 20:29:20 cross Exp $ /** * @package Services_ExchangeRates * @category Services */ /**#@+ * Error codes */ define('SERVICES_EXCHANGERATES_ERROR_RETURN', 1); define('SERVICES_EXCHANGERATES_ERROR_DIE', 8); define('SERVICES_EXCHANGERATES_ERROR_INVALID_DRIVER', 101); define('SERVICES_EXCHANGERATES_ERROR_INVALID_CURRENCY', 102); define('SERVICES_EXCHANGERATES_ERROR_CONVERSION_ERROR', 103); define('SERVICES_EXCHANGERATES_ERROR_RETRIEVAL_FAILED', 104); /**#@-*/ /** * Exchange Rate package * * This package converts back and forth between different currencies, in any * combination. All data used is updated automatically from interchangable * sources. That is, if there is a source publishing exchange rates that * isn't supported yet, you could write a driver and use that source * seamlessly with the rest of the package. * * Disclaimer: The rates are nominal quotations - neither buying nor * selling rates - and are intended for statistical or analytical * purposes. Rates available from financial institutions will differ. * * The United Nations Economic Commission for Europe is implementing new web * services. Keep an eye on progress here: http://www.unemed.org/edocs/index.htm * * @todo Add locale support for different currency formatting * * @example ExchangeRates/docs/example.php * * @author Marshall Roch * @author Colin Ross * @copyright Copyright 2003 Marshall Roch * @license http://www.php.net/license/2_02.txt PHP License 2.0 * @package Services_ExchangeRates */ class Services_ExchangeRates { /** * Sets the number of places to round the currencies to at the end * @access private * @var int */ var $_roundToDecimal = 2; /** * Determines whether the returned conversion is rounded or not * @access private * @var bool */ var $_roundAutomatically = true; /** * Defines single character used to separate each group of thousands in returned conversion * @access private * @var string */ var $_thousandsSeparator = ","; /** * Defines single character to use as a decimal place in returned conversion * @access private * @var string */ var $_decimalCharacter = "."; /** * Sets the path to where cache files are stored (don't forget the trailing slash!) * @access private * @var string */ var $_cacheDirectory = '/tmp/'; /** * Sets the length (in seconds) to cache the exchange rate data. This information * is updated daily. Default is 1 hour. * @access private * @var int */ var $_cacheLengthRates = 3600; /** * Sets the length (in seconds) to cache the list of currencies. This information * is very rarely updated. Default is 4 weeks. * @access private * @var int */ var $_cacheLengthCurrencies = 2419200; /** * Sets the length (in seconds) to cache the list of countries. This information * is very rarely updated. Default is 4 weeks. * @access private * @var int */ var $_cacheLengthCountries = 2419200; /** * PEAR error mode (when raiseError is called) * * @see setToDebug() * @access private * @var int */ var $_pearErrorMode = SERVICES_EXCHANGERATES_ERROR_RETURN; /** * Constructor * * This method overrides any default settings based on the $options * parameter and retrieves feed data from the cache or their sources. * * $options is an associative array: * * $options = array( * 'roundToDecimal' => number of decimal places to round to (int), * 'roundAutomatically' => whether to automatically round to * $roundToDecimal digits (bool), * 'thousandsSeparator' => character to separate every 1000 (string), * 'decimalCharacter' => character for decimal place (string), * 'cacheDirectory' => path (with trailing slash) to store cache * files (string), * 'cacheLengthRates' => length of time to cache exchange rates * file (int), * 'cacheLengthCurrencies' => length of time to cache currency * list (int), * 'cacheLengthCountries' => length of time to cache country list (int), * 'pearErrorMode' => pear error mode (int)); * * * @param string Driver name (filename minus 'Rates_' and .php) for exchange rate feed * @param string Driver name for currency code list * @param string Driver name for country code list (not yet used for anything) * @param array Array to override default settings */ function __construct($ratesSource = 'ECB', $currencySource = 'UN', $countrySource = 'UN', $options = array(NULL)) { $availableOptions = array('roundToDecimal', 'roundAutomatically', 'thousandsSeparator', 'decimalCharacter', 'cacheDirectory', 'cacheLengthRates', 'cacheLengthCurrencies', 'cacheLengthCountries'); foreach($options as $key => $value) { if(in_array($key, $availableOptions)) { $property = '_'.$key; $this->$property = $value; } } $rateData = $this->retrieveData('Rates_' . $ratesSource, $this->_cacheLengthRates); $this->rates = $rateData['rates']; $this->ratesUpdated = $rateData['date']; $this->ratesSource = $rateData['source']; $this->currencies = $this->retrieveData('Currencies_' . $currencySource, $this->_cacheLengthCurrencies); // not yet implimented, here for future features: // $this->countries = $this->retrieveData('Countries_' . $countriesSource, $this->_cacheLengthCountries); $this->validCurrencies = $this->getValidCurrencies($this->currencies, $this->rates); } /** * Factory * * Includes the necessary driver, instantiates the class, retrieves the feed, * and returns an associative array. * * @param string Driver filename (minus .php; this includes 'Rates_', etc.) * @param int Cache length * @return array Associative array containing the data requested */ function retrieveData($source, $cacheLength) { include_once("Services/ExchangeRates/${source}.php"); $classname = "Services_ExchangeRates_${source}"; if (!class_exists($classname)) { return $this->raiseError("No driver exists for the source ${source}... aborting.", SERVICES_EXCHANGERATES_ERROR_INVALID_DRIVER); } $class = new $classname; return $class->retrieve($cacheLength, $this->_cacheDirectory); } /** * Get list of currencies with known exchange rates * * Creates an array of currency codes and their names, based on * overlapping elements in $rates and $currencies. * * @param array Array of currency codes to currency names * @param array Array of currency codes to exchange rates * @return array Array of currency codes to currency names that have a known exchange rate (sorted alphabetically) */ function getValidCurrencies($currencies, $rates) { // loop through list of currencies $validCurrencies = array(); foreach ($currencies as $code => $currency) { // check to see if that currency has a known exchange rate if (in_array($code, array_keys($rates))) { // if so, add it to the array to return $validCurrencies[$code] = $currency; } } asort($validCurrencies); return $validCurrencies; } function isValidCurrency($code) { if (!in_array($code, array_keys($this->validCurrencies))) { $this->raiseError('Error: Invalid currency: ' . $code, SERVICES_EXCHANGERATES_ERROR_INVALID_CURRENCY); return false; } return true; } /** * Convert currencies * * @param string Currency code of original currency * @param string Currency code of target currency * @param double Amount of original currency to convert * @param boolean Format the final currency (add commas, round, etc.) * @return mixed Currency converted to $to */ function convert($from, $to, $amount, $format = true) { if ($this->isValidCurrency($from) && $this->isValidCurrency($to)) { // Convert $from to whatever the base currency of the // exchange rate feed is. $base = (1 / $this->rates[$from]) * $amount; // Convert from base currency to $to $final = $this->rates[$to] * $base; return ($format) ? $this->format($final) : $final; } $this->raiseError('Unable to convert!', SERVICES_EXCHANGERATES_ERROR_CONVERSION_ERROR); return false; } /** * Formats the converted currency * * This method adds $this->_thousandsSeparator between every group of thousands, * and rounds to $this->_roundToDecimal decimal places. Use the $options parameter * on the constructor to set these values. * * @param double Number to format * @param mixed Number of decimal places to round to (null for default) * @param mixed Character to use for decimal point (null for default) * @param mixed Character to use for thousands separator (null for default) * @return string Formatted currency */ function format($amount, $roundTo = null, $decChar = null, $sep = null) { $roundTo = (($this->_roundAutomatically) ? (($roundTo == null) ? $this->_roundToDecimal : $roundTo) : ''); $decChar = ($decChar == null) ? $this->_decimalCharacter : $decChar; $sep = ($sep == null) ? $this->_thousandsSeparator : $sep; return number_format($amount, $roundTo, $decChar, $sep); } /** * Get all rates as compared to a reference currency * * Returns an associative array with currency codes as keys and * formated rates as values, as computed against a reference currency. * * @param string $referenceCurrency Reference currency code * @return array List of currencies => rates * @see Services_ExchangeRates::convert() * @access public */ function getRates ($referenceCurrency) { $rates = array(); foreach ($this->validCurrencies as $code => $name) { $rates[$code] = $this->convert($referenceCurrency, $code, 1, false); } ksort($rates); return $rates; } /** * Set to debug mode * * When an error is found, the script will stop and the message will be displayed * (in debug mode only). */ function setToDebug() { self::$_pearErrorMode = SERVICES_EXCHANGERATES_ERROR_DIE; } /** * Trigger a PEAR error * * To improve performances, the PEAR.php file is included dynamically. * The file is so included only when an error is triggered. So, in most * cases, the file isn't included and performance is much better. * * @param string error message * @param int error code */ static function raiseError($msg, $code) { include_once('PEAR.php'); PEAR::raiseError($msg, $code, ( isset(self::$_pearErrorMode) ) ? self::$_pearErrorMode : SERVICES_EXCHANGERATES_ERROR_RETURN ); } } ?>