Created

Embed URL

HTTPS clone URL

SSH clone URL

You can clone with HTTPS or SSH.

Download Gist

fxMoney & fxCurencyExchange extensions to flourishlib's fMoney class

View fxCurrencyExchange.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
<?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
View fxCurrencyExchange.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
<?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
View fxCurrencyExchange.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
<?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
View fxCurrencyExchange.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
<?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
Owner

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

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
Something went wrong with that request. Please try again.