Skip to content

Instantly share code, notes, and snippets.

@netcarver
Created February 1, 2012 21:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save netcarver/fc8b9fd41e66cb36af2c to your computer and use it in GitHub Desktop.
Save netcarver/fc8b9fd41e66cb36af2c to your computer and use it in GitHub Desktop.
fxMoney & fxCurencyExchange extensions to flourishlib's fMoney class
<?php
/**
* Represents an interface for Currency Exchange feeds. All exchange rate feeds need to implmenent this
* interface and can throw the exceptions defined here.
*
* @copyright Copyright (c) 2012 Netcarver (Steve Dickinson)
* @author Netcarver (Steve Dickinson)
**/
interface fxCurrencyExchange
{
/**
* Gets the exchange rate between two currencies. If a date is specified, then the historical rate
* on that date will be returned. If no date is given, then the live rate at the time of the call
* will be returned. If freshness of this returned rate will depend upon the caching scheme used by the
* fxCurrencyExchange and its underlying feed.
*
*
* @param string $pair Consists of two ISO 4217 three-letter codes concatenated together.
* eg. Going from USD to GBP would need the pair 'USDGBP'; from Yen to Euros 'JPYEUR'
*
* @param string $date Omit for live exchange rate, otherwise supply a string in 'YYYY-mm-dd' format
* eg, First of February, 2010 would be '2010-02-01'
*
* @return string
**/
public function getPair( $pair, $date );
/**
* Identifies the exchange.
*
* @return string The name of the exchange.
**/
public function identify();
}
/**
* Class-specific exceptions...
**/
class fxCurrencyExchangeDateException extends exception {}
class fxCurrencyExchangeReadException extends exception {}
#eof
<?php
/**
* Extends the flourish class fMoney to allow the conversion between fMoney
* amounts in different currencies using fixed,live or historic rates from a
* currency exchange.
*
* @copyright Copyright (c) 2012 Netcarver (Steve Dickinson)
* @author Netcarver (Steve Dickinson)
**/
class fxMoney extends fMoney
{
/**
* The fxCurrencyExchange to use for conversions.
*
* @var fxCurrencyExchange
**/
static private $exchange = null;
/**
* Allows setting of the currency exchange to be used.
*
* @param fxCurrencyExchange The exchange to be used.
* @return void
**/
static public function setCurrencyExchange( fxCurrencyExchange &$exchange )
{
self::$exchange = $exchange;
}
/**
* Checks that a valid exchange has been setup. Throws an fProgrammerException
* if the exchange is invalid.
*
* @return void
**/
protected static function validateExchange() {
if( !(self::$exchange instanceof fxCurrencyExchange) ) {
throw new fProgrammerException( 'Setup an fxCurrencyExchange before using toXYZ() currency conversion calls.' );
}
}
/**
* Converts a given amount of this currency into a new currency using the given exchange rate.
*
* @param fNumber $amount The amount of this currency to convert.
* @param string $rate The exchange rate to use. This is a formatted number.
* @param string $new_currency The ISO code of the new currency
* @return fxMoney A new instance of fxMoney in the target currency
**/
protected function _convert( $amount, $rate, $new_currency )
{
if ($new_currency == $this->currency) {
return new fxMoney( $amount, $this->currency );
}
$currencies = self::getCurrencies();
if (!in_array($new_currency, $currencies)) {
throw new fProgrammerException(
'The currency specified, %1$s, is not a valid currency. Must be one of: %2$s.',
$new_currency,
join(', ', $currencies)
);
}
$new_precision = self::getCurrencyInfo($new_currency, 'precision');
$new_amount = $amount
->mul($rate, $new_precision+6)
->round($new_precision+2)
;
return new fxMoney($new_amount->__toString(), $new_currency);
}
/**
* Implements the toXYZ() exchange method, where XYZ is a valid currency you have setup in
* fMoney. To convert from USD to GBP using the current live exchange rate you would need to...
*
* $usd = new fxMoney( '100.00', 'USD' );
* $gbp = $usd->toGBP();
*
* To use an historical conversion rate do (in this case for 18th Sept, 2011)...
* $gbp = $usd->toGBP( '2011-09-18' );
*
* To use a fixed exchange rate do...
* $gbp = $usd->toGBP( '0.650000' );
**/
public function __call( $name, $arguments )
{
if( preg_match( '@to[A-Z]{3}@', $name ) ) {
$to = substr( $name, -3 );
$from = $this->getCurrency();
$amount = $this->getAmount();
$num = count( $arguments );
switch( $num ) {
case 0 : self::validateExchange();
$rate = self::$exchange->getPair( "$from$to" );
$new = $this->_convert( $amount, $rate, $to );
return $new;
break;
case 1 : if( preg_match( '@[0-9]{4}[-/][0-9]{2}[-/][0-9]{2}@', $arguments[0] ) ) {
self::validateExchange();
$rate = self::$exchange->getPair( "$from$to", $arguments[0] );
}
else {
$rate = new fNumber( $arguments[0], 6 );
$rate = $rate->__toString();
}
$new = $this->_convert( $amount, $rate, $to );
return $new;
break;
default:
break;
}
}
return parent::__call( $name, $arguments );
}
}
#eof
<?php
/**
* Class mapping to open currency exchange data...
**/
class fxOpenExchangeRatesCurrencyExchange implements fxCurrencyExchange
{
public function __construct( fCache &$cache = null )
{
$this->cache = $cache;
$now = time();
$this->currentyear = date( "Y", $now );
$this->today = date( "Y-m-d", $now );
}
public function identify()
{
return 'OpenExchangeRate.org feed';
}
/**
* Create the storage keys for the cache...
**/
static protected function makeL1Key( $pair, $date=null )
{
if( null === $date )
$date = 'live';
$key = "$date-$pair";
return $key;
}
static protected function makeL2Key( $date=null )
{
if( null === $date )
$date = 'live';
return $date;
}
protected function curl( $url, &$return_code )
{
$c = curl_init();
if( FALSE === $c )
throw new exception( 'Could not create a CURL resource handle.' );
$headers[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
if (function_exists('gzinflate'))
$headers[] = 'Accept-Encoding: gzip,deflate';
$headers[] = 'Accept-Language: en-gb,en;q=0.5';
$headers[] = 'Accept-Charset: UTF-8,ISO-8859-1;q=0.7,*';
curl_setopt($c, CURLOPT_HTTPHEADER, $headers );
curl_setopt($c, CURLOPT_VERBOSE, false);
curl_setopt($c, CURLOPT_URL, $url );
curl_setopt($c, CURLOPT_TIMEOUT, 30);
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
curl_setopt($c, CURLOPT_USERAGENT, 'Links (2.3pre1; Linux 2.6.32-5-686 i686; 216x50)' );
$page = curl_exec($c);
// Decompress page if needed...
if (strncmp($page, "\x1F\x8B", 2) === 0)
{
if (function_exists('gzinflate'))
$page = gzinflate(substr($page, 10));
}
$info = curl_getinfo($c);
curl_close($c);
$return_code = (isset($info['http_code'])) ? $info['http_code'] : 200 ;
return $page;
}
/**
* Read the data from OpenExchangeRates.org.
**/
protected function getJSONData( &$date )
{
$url = 'http://openexchangerates.org/latest.json';
if( is_string( $date ) ) {
$this->checkDate( $date );
$url = 'http://openexchangerates.org/historical/'.$date.'.json';
}
$code = '';
$page = $this->curl( $url, $code );
if( 200 == $code ) {
$page = json_decode( $page, true );
unset( $page['disclaimer'] );
unset( $page['license'] );
foreach( $page['rates'] as $k=>&$v ) {
$v = (string)$v;
}
}
else
throw new fxCurrencyExchangeReadException("Could not get data from [$url].");
return $page;
}
/**
* Serialize and compress for storage in cache...
**/
static protected function enBlob( $data )
{
$data = serialize($data);
$data = gzencode($data);
return $data;
}
/**
* Expand and unserialize for use...
**/
static protected function deBlob( $blob )
{
if (strncmp($blob,"\x1F\x8B",2)===0) {
$blob = gzinflate(substr($blob, 10));
}
$blob = unserialize($blob);
return $blob;
}
/**
* Allows you to get the current exchange rate between 2 three-letter currency codes such
* as 'USDGBP' which gives the rate going from USD (United States Dollars) to GBP (Pounds
* Sterling.)
**/
public function getPair( $pair, $date = null )
{
$rate = null;
if( $this->cache ) {
$keyL1 = self::makeL1Key( $pair, $date );
$rate = $this->cache->get( $keyL1 );
if( null === $rate ) {
#
# L1 cache miss, try L2...
#
$keyL2 = self::makeL2Key( $date );
$rates = $this->cache->get( $keyL2 );
if( null === $rates ) {
#
# L2 cache miss, read from web, compress & cache...
#
$rates = $text = $this->getJSONData( $date );
$text = self::enBlob($text);
$this->cache->set( $keyL2, $text, 60 ); # Cache L2 page for a limited time.
}
if( !is_array($rates) )
$rates = self::deBlob( $rates );
$rate = self::calcRate( $pair, $rates['base'], $rates['rates'] );
$this->cache->set( $keyL1, $rate );
}
}
else {
$rates = $this->getJSONData( $date );
$rate = self::calcRate( $pair, $rates['base'], $rates['rates'] );
}
return $rate;
}
/**
*
**/
protected function checkDate( &$date )
{
if( !is_string( $date ) )
throw new fxCurrencyExchangeDateException( '$date should be a yyyy-mm-dd string.' );
if( preg_match( '@[0-9]{4}[-/][0-9]{2}[-/][0-9]{2}@', $date ) ) {
$date = strtr( $date, array( '/' => '-' ) );
$p = explode( '-', $date );
# Sanity check the date...
if( ($p[0] < 1999) || ($p[0] > $this->currentyear) )
throw new fxCurrencyExchangeDateException( "Year [{$p[0]}] should be between 1999 and {$this->currentyear}" );
if( ($p[1] < 1) || ($p[1] > 12) )
throw new fxCurrencyExchangeDateException( "Month [{$p[1]}] should be between 01 and 12" );
$daysIn = fxYear::getDaysIn( $p[0] );
if( ($p[2] < 1) || ((int)$p[2] > $daysIn[ (int)$p[1] ] ) )
throw new fxCurrencyExchangeDateException( "Day [{$p[2]}] should be between 01 and {$daysIn[(int)$p[1]]}" );
if( $date > $this->today )
throw new fxCurrencyExchangeDateException( "Date [$date] should be before today [{$this->today}]" );
}
}
/**
* Use the raw extracted data to get the requested pair. If the from code is not the base code of the page
* then the pair will need to be calculated.
**/
protected function calcRate( $pair, $base, &$rates )
{
$from = substr( $pair, 0, 3 );
$to = substr( $pair, -3 );
# If we already have the from data in the base currency just return the 'to' currency...
if( $base == $from && array_key_exists( $to, $rates ) ) {
$rate = new fNumber( $rates[$to], 6 );
return $rate->__toString();
}
# Otherwise we need to see if we can calculate it from a pair of pairs...
if( array_key_exists( $from, $rates ) && array_key_exists( $to, $rates ) ) {
$base_to = new fNumber( $rates[$to] , 6 );
$base_from = new fNumber( $rates[$from], 6 );
$from_to = $base_to->div( $base_from, 6 );
return $from_to->__toString();
}
return 'Unknown';
}
}
/**
* Constructor function... Allows chaining from the constructor...
**/
function fxOpenExchangeRatesCurrencyExchange( fCache &$cache = null )
{
return new fxOpenExchangeRatesCurrencyExchange( $cache );
}
#eof
<?php
/**
* Convenience class for dealing with years and their 'leapyness'
*
* @copyright Copyright (c) 2012 Netcarver (Steve Dickinson)
* @author Netcarver (Steve Dickinson)
**/
class fxYear
{
static $days_in_months = array(
1 => 31, # Jan
2 => 28, # Feb
3 => 31, # Mar
4 => 30, # Apr
5 => 31, # May
6 => 30, # Jun
7 => 31, # Jul
8 => 31, # Aug
9 => 30, # Sep
10 => 31, # Oct
11 => 30, # Nov
12 => 31, # Dec
);
/**
* Test a year to see if it is a leap year.
*
* @return bool True if the given year is a leap year.
**/
static public function isLeap( $year )
{
if( 0 === $year % 400 ) return true;
if( 0 === $year % 100 ) return false;
if( 0 === $year % 4 ) return true;
return false;
}
/**
* Returns an array of days in each month of the given year, adjusting for days in February if needed.
*
* @return array Indexed 1-12, with number of days per month.
**/
static public function getDaysIn( $year )
{
$o = self::$days_in_months;
if( self::isLeap( $year ) )
$o[2] += 1;
return $o;
}
}
#eof
@netcarver
Copy link
Author

An experimental extension to flourish's fMoney class to add live + historical exchange rates (cached if needed.) As usual, you can do the following as you can with fMoney from flourish...

fxMoney::setDefaultCurrency('USD');
$usd = new fxMoney( '100' ); // Create a USD amount.
fxMoney::defineCurrency( 'GBP', 'Pound Sterling', '£', 2 ); // Define a new currency, 'GBP' (Pound Sterling) -- use ISO 4217 three-letter codes.

But, you can now specify a currency exchange (and, if needed, an exchange rate cache using flourish's fCache)...

$currency_cache = new fCache(...); // Setup an fCache for currency values as required. Using a non-volatile cache is probably a good idea here.
$exchange = new fxOpenExchangeRateCurrencyExchange( $currency_cache ); // Setup an exchange
fxMoney::setCurrencyExchange( $exchange );  // Tells fxMoney to use this as its exchange.

You can now use fxMoney to convert using live exchange rates...

$gbp = $usd->toGBP(); // Convert to GBP using the live exchange rate from OpenExchangeRate at the time of call.

... or fixed exchange rates...

$gbp = $usd->toGBP('0.65'); // Conversion done using a fixed exchange rate between the two currencies : GBP = 0.65 * USD.

... or some historical exchange rate if you give it a date...

$gbp = $usd->toGBP( '2011-01-31' ); // Conversion done using historical market rate. The historical data will be cached if a cache is defined.

@netcarver
Copy link
Author

The above does somewhat obsolete fMoney's current support for a fixed exchange rate via the last parameter to the defineCurrency static method (which I've always found unsatisfying.)

Perhaps a better scheme would be to have...

$gbp = $usd->toGBP( fxMoney::LIVE );

...do the conversion using the current, live, rate and...

$gbp = $usd->toGBP();

...use the fixed rate given when the GBP currency was defined.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment