-
-
Save netcarver/fc8b9fd41e66cb36af2c to your computer and use it in GitHub Desktop.
fxMoney & fxCurencyExchange extensions to flourishlib's fMoney class
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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...
...do the conversion using the current, live, rate and...
...use the fixed rate given when the GBP currency was defined.