Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Add set of standard money formatter functions #12055

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 173 additions & 1 deletion CRM/Utils/Money.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,179 @@ class CRM_Utils_Money {
static $_currencySymbols = NULL;

/**
* Format a monetary string.
* Warn if php money_format() doesn't exist as they are likely to experience issues displaying currency.
* @return bool
*/
private static function moneyFormatExists() {
// money_format() exists only in certain PHP install (CRM-650)
if (!function_exists('money_format')) {
Civi::log()->warning('PHP money_format function does not exist. Monetary amounts may not format correctly for display.');
return FALSE;
};
return TRUE;
}

/**
* Format a number according to standard decimal notation, allowing fractions and using dot as decimal separator.
* Example: 12.34567
*
* @param $amount
*
* @return mixed
*/
public static function formatLongDecimal($amount) {
return filter_var($amount, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
}

/**
* Format money in "standard" decimal format, rounded to a specific number of decimal places
* Example: 12.34
*
* @param $amount
* @param null $decimalPlaces
* If specified, amount will be rounded to this number of decimal places, otherwise it will be rounded according to currency/locale.
* @param null $currency
* If specified, amount will be rounded according to the standard for this currency.
*
* @return float
*/
public static function formatDecimalRounded($amount, $decimalPlaces = NULL, $currency = NULL) {
if (!$decimalPlaces) {
$decimalPlaces = self::getCurrencyPrecision($currency);
}
$amount = self::formatLongDecimal($amount);
return round($amount, $decimalPlaces);
}

/**
* Format money for display (just numeric part) according to the current locale
* Example 12,34 or 12.34
*
* @param $amount
*
* @return string
*/
public static function formatLocaleNumeric($amount) {
$config = CRM_Core_Config::singleton();
$format = $config->moneyvalueformat;
return self::formatNumeric($amount, $format);
}

/**
* Format money for display (just numeric part). Specify format or use formatLocaleNumeric() instead.
* Example: Depends on parameters it is called with
*
* @param $amount
* @param $valueFormat
*
* @return string
*/
public static function formatNumeric($amount, $valueFormat) {
if (CRM_Utils_System::isNull($amount)) {
return '';
}

$moneyFormatExists = self::moneyFormatExists();
if (is_numeric($amount) && $moneyFormatExists) {
$lc = setlocale(LC_MONETARY, 0);
setlocale(LC_MONETARY, 'en_US.utf8', 'en_US', 'en_US.utf8', 'en_US', 'C');
$amount = money_format($valueFormat, $amount);
setlocale(LC_MONETARY, $lc);
}
return $amount;
}

/**
* Format money for display (with symbols etc) according to the current locale
* Example €12,34 or £12.34
*
* @param $amount
* @param null $currency
*
* @return string
*/
public static function formatLocaleFull($amount, $currency = NULL) {
$config = CRM_Core_Config::singleton();
$format = $config->moneyformat;
$valueFormat = $config->moneyvalueformat;
return self::formatFull($amount, $format, $valueFormat, $currency);
}

/**
* Format money in "cents" or the smallest denominator for the currency:
* Example £12.34 returns 1234
* @param $amount
* @param null $currency
*
* @return int
*/
public static function formatCents($amount, $currency = NULL) {
$amount = self::formatDecimalRounded($amount, self::getCurrencyPrecision($currency));
return (int) preg_replace('/[^\d]/', '', strval($amount));
}

/**
* Format money for display (with symbols etc). Specify format or use formatLocaleFull() instead.
* Example: Depends on parameters it is called with
*
* @param $amount
* @param $format
* @param $valueFormat
* @param null $currency
*
* @return string
*/
public static function formatFull($amount, $format, $valueFormat, $currency = NULL) {
if (CRM_Utils_System::isNull($amount)) {
return '';
}

// If it contains tags, means that HTML was passed and the
// amount is already converted properly, so don't mess with it again.
if (strpos($amount, '<') !== FALSE) {
return $amount;
}

if (!self::$_currencySymbols) {
self::$_currencySymbols = CRM_Core_PseudoConstant::get('CRM_Contribute_DAO_Contribution', 'currency', array(
'keyColumn' => 'name',
'labelColumn' => 'symbol',
));
}

if (!$currency) {
$config = CRM_Core_Config::singleton();
$currency = $config->defaultCurrency;
}

$moneyFormatExists = self::moneyFormatExists();
// setlocale() affects native gettext (CRM-11054, CRM-9976)
if (is_numeric($amount) && $moneyFormatExists) {
$lc = setlocale(LC_MONETARY, 0);
setlocale(LC_MONETARY, 'en_US.utf8', 'en_US', 'en_US.utf8', 'en_US', 'C');
$amount = money_format($valueFormat, $amount);
setlocale(LC_MONETARY, $lc);
}

// Replace separators
$rep = array(
',' => $config->monetaryThousandSeparator,
'.' => $config->monetaryDecimalPoint,
);
$amount = strtr($amount, $rep);

// Final formatting
$replacements = array(
'%a' => $amount,
'%C' => $currency,
'%c' => CRM_Utils_Array::value($currency, self::$_currencySymbols, $currency),
);
return strtr($format, $replacements);
}

/**
* @deprecated Format a monetary string.
* Replaced by multiple different functions above
*
* Format a monetary string basing on the amount provided,
* ISO currency code provided and a format string consisting of:
Expand Down
69 changes: 63 additions & 6 deletions tests/phpunit/CRM/Utils/MoneyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,70 @@ public function testSubtractCurrencies($leftOp, $rightOp, $currency, $expectedRe
*/
public function subtractCurrenciesDataProvider() {
return array(
array(number_format(300.00, 2), number_format(299.99, 2), USD, number_format(0.01, 2)),
array(2, 1, USD, 1),
array(0, 0, USD, 0),
array(1, 2, USD, -1),
array(number_format(19.99, 2), number_format(20.00, 2), USD, number_format(-0.01, 2)),
array('notanumber', 5.00, USD, NULL),
array(number_format(300.00, 2), number_format(299.99, 2), 'USD', number_format(0.01, 2)),
array(2, 1, 'USD', 1),
array(0, 0, 'USD', 0),
array(1, 2, 'USD', -1),
array(number_format(19.99, 2), number_format(20.00, 2), 'USD', number_format(-0.01, 2)),
array('notanumber', 5.00, 'USD', NULL),
);
}

/**
* @dataProvider currenciesDataProvider
* @param $currency
*/
public function testGetCurrencyPrecision($currency) {
$this->assertEquals($currency['precision'], CRM_Utils_Money::getCurrencyPrecision($currency['name']));
}

/**
* FIXME: This needs to use a proper source for currency precision (but we don't have one in CiviCRM yet (maybe MoneyPHP?)
* @return array
*/
public function currenciesDataProvider() {
$currencies = CRM_Core_PseudoConstant::get('CRM_Contribute_DAO_Contribution', 'currency', array(
'labelColumn' => 'name',
'orderColumn' => TRUE,
));
foreach ($currencies as $currency) {
$currencyList[]['name'] = $currency;
$currencyList[]['precision'] = 2;
}
return $currencyList;
}


/**
* @dataProvider longDecimalDataProvider
* @param $input
* @param $expected
*/
public function testFormatLongDecimal($input, $expected) {
$this->assertEquals($expected, CRM_Utils_Money::formatLongDecimal($input));
}

/**
* @return array
*/
public function longDecimalDataProvider() {
return array(
// array(input, expected)
array('10', '10'),
array('10.23', '10.23'),
array('10.2345678', '10.2345678'),
array('-10.2345678', '-10.2345678'),
array('10,2345678', '10.2345678'),
array('£10,2345678', '10.2345678'),
);
}

/*CRM_Utils_Money::formatDecimalRounded();
CRM_Utils_Money::formatFull();
CRM_Utils_Money::formatLocaleFull();
CRM_Utils_Money::formatLocaleNumeric();
CRM_Utils_Money::formatNumeric();
CRM_Utils_Money::formatCents();
*/

}