This is a first stab at writing classes for currencies and currency locales. I think the currency class speaks for itself. The currencyLanguage class contains the configuration for displaying amounts of money in a certain language. It is separate from currency information, but one currencyLanguage object combined with a currency object provides all the necessary information for rendering amounts.

Comments

amateescu’s picture

Version: 7.x-1.x-dev » 7.x-2.x-dev
xano’s picture

StatusFileSize
new2.62 KB
amateescu’s picture

Category: feature » task
Status: Needs review » Active
xano’s picture

Status: Active » Needs review
StatusFileSize
new933 bytes

Added constructors and a locked property for currencies.

amateescu’s picture

Status: Needs review » Active
alan d.’s picture

Version: 7.x-2.x-dev » 7.x-1.x-dev
Category: task » feature
Status: Active » Needs review

I've just started a Android project for my brother that is going to take almost all of my spare time if this goes ahead, but I will try and contribute where possible.

Before jumping into the actual implementation, maybe a plan of attack should be formalized to what a core currency api should be trying to achieve. This would radically alter the internal implementation.

The number one decision to consider is how to handle various formats. I think that each currency rule defined should be defined against a locale, with US English as a base (This is wrong, as English English is the real en locale, but in Drupal en-US is default for en). While it seems that we should be inserting the correct symbols and formatting to each currency, but this is likely to cause confusion:

IE: 元123,4567.89
This does not look like a currency (this is Chinese yuan using Latin numbering) and could be possibly misread as 123 thousand rather than 1.23 million.

Or an example close to home, the Canadian dollar:

Canadian French: 123,45 $
Canadian English: $123.45

So for each currency, I would suggest having at least one rule for english and one for each locale of that currency. Just look at the Swiss franc in English, Swiss, German and you have 3 different formats.

Another thing that I discovered, all those months ago, was that it is extremely difficult to just provide a simple set of rules to handle the formatting of the currencies. Just check out localeconv() to see the required combinations, and even these do not cover the entire set of possibilities! A great resource is http://www.xencraft.com/resources/multi-currency.html

The solution that I used came down to a simple set of parameters that is mainly based off the currencies symbol and a format string.

The two core functions that used the currency object were, (these may or may not have been in the zip):


define('CURRENCY_API_REGEX_MAJOR', '/[^#0\,]*([#0,]*)[^#0\,]*/');
define('CURRENCY_API_REGEX_MINOR', '/[^0]*([0]{1,9})[^0]*/');

function theme_currency_short($vars) {
  $currency = $vars['currency'];
  $format = $vars['format'];
  $precision = $vars['precision'];
  $decimal_separator = $vars['decimal_separator'];
  $grouping_separator = $vars['grouping_separator'];

  $symbol = currency_api_i18n_symbol($currency);
  
  // These can be parsed from $vars['amount'].
  $major = $vars['major'];
  $minor = $vars['minor'];
  $sign = $vars['sign'];
  
  // Locate and insert placeholders
  $major_pattern = '';
  if (preg_match(CURRENCY_API_REGEX_MAJOR, $format, $matches)) {
    $major_pattern = $matches[1];
    $format = str_replace($matches[1], '__major__', $format);
  }
  else {
    // The pattern is invalid! Return basic themed result
    return t('!symbol!amount !code', array(
        '!symbol' => $symbol,
        '!amount' => number_format($vars['amount'], $precision >= 0 ? $precision : 0),
        '!code' => $currency->iso3));
  }

  if (preg_match(CURRENCY_API_REGEX_MINOR, $format, $matches)) {
    $replacement = ($precision <= 0) ? '' : '__minor__';
    $format = str_replace($matches[1], $replacement, $format);
  }

  if ($sign) {
    $format = str_replace(array(')', '(', '-'), '', $format);
  }
  $replacements = array(
    '__major__' => currency_major_unit_format($major, $grouping_separator, $major_pattern),
    '__minor__' => ($precision <= 0) ? '' : sprintf("%0{$precision}s", $minor),
    '¤' => $symbol,
    '.' => ($precision <= 0) ? '' : $decimal_separator,
  );
  return trim(str_replace(array_keys($replacements), $replacements, $format));
}

function currency_major_unit_format($major, $ths_sep = ',', $major_pattern = '#,##0') {
  if (empty($major)) {
    return '0';
  }

  if (empty($major_pattern)) {
    $major_pattern = '#,##0';
  }
  $grouping_patterns = explode(',', $major_pattern);
  if (count($grouping_patterns) > 1) {
    array_shift($grouping_patterns);
  }
  $major = (string) $major;
  $group_size = 3;
  $groups = array();
  while ($major) {
    $group_size = empty($grouping_patterns) ? $group_size : drupal_strlen(array_pop($grouping_patterns));
    if (drupal_strlen($major) > $group_size) {
      $group = drupal_substr($major, -($group_size));
      $major = drupal_substr($major, 0, drupal_strlen($major) - $group_size);
      array_unshift($groups, $group);
    }
    else {
      array_unshift($groups, $major);
      $major = '';
    }
  }

  return implode($ths_sep, $groups);
}

Two examples:

-₨ 1,23,45,678.90
-12,345,678.90 Pakistani rupees using the pattern -¤ #,##,##0.00 (notice the separator pattern)

(RM12,345,678.90)
-12,345,678.90 Malaysian ringgits using the pattern (¤#,##0.00)

Needs though testing, but from memory, it passed all of the manual tests that I tried.

Hopefully some food for thought, and this only covers the most basic formatting of a number!

Also, I hadn't even started to consider covering this, but electronic precision and Point of Sales precision would alter how a program should handle rounding... 1 cent in an Australian bank account is still used and significant, but in a shop, 5 cents is the lowest unit in circulation. Sign. It is a complex world out there, how much do we want to cover!?

xano’s picture

So for each currency, I would suggest having at least one rule for english and one for each locale of that currency. Just look at the Swiss franc in English, Swiss, German and you have 3 different formats.

Most examples I have seen (and most of those you have given) only show differences between languages and not between currencies. However, the rounding issue may be relevant, but also very complex. Those rounding rules, do they only apply for a nation's own currency, or for foreign currencies used within that nation as well?

Next to that, I believe we may want to get rid of the thousands/grouping separator, because it's not a currency setting. Neither is the decimal separator, but that one is absolutely necessary to correctly display amounts and it's easier to implement

alan d.’s picture

I guess it is a question of keeping things simple or complex :)

In regards to currency formats per language, here are 3 Spanish variants from Latin America (parsed from the xml files from http://www.unicode.org/).

Argentine pesos: ¤-#,##0.00
Chilean pesos: -¤#,##0
Mexican pesos: (¤#,##0.00)

A couple of English ones:
Belize dollars: (¤#,##0.00)
Canadian dollars: -¤#,##0.00

Regarding precision. While I have only been to 30 countries, I have never been able to exchange minor units in any of these countries, or more specifically, you can not trade coins, only notes. However, all electronic exchanges are rounded to the nearest cent / minor unit, and I think that the official iso currency feed precision represents this digital representation. I think that the general rule of thumb is to never round until the final transaction tally is calculated and then rounding applies to the nearest precision from the official feeds.

Finally, the grouping separator is important in larger numbers, it instantly allows you to pick out the scale of large numbers accurately. Number formats usually correlate to currency formats, but not always. I do not think that PHP can handle the more complex grouping formats, aka these are still referred to as thousand separators even though this is a very western view :)

xano’s picture

Can you give me a link or a pointer to documentation about that representation format? (¤#,##0 etc)

So the Argentine peso uses ¤-#,##0.00, but how does one display the euro to an Argentine audience? Should the peso format be used for all currencies in an Argentine context? The same goes for rounding, sign position, etc: on how many contexts does either one of those properties depend? If every property depends on one context (currency/language/country), it may be a lot of work to implement all specific rules, but it won't be too complex. If those properties depend on more than one context, say a language AND a country, things become somewhat harder to administer for users and to maintain.

// Edit: if your example of Canadian dollars in Canadian English and Canadian French is correct, notation DOES depend on more than one context.

xano’s picture

Let's continue the display discussion in #1367862: Currency and amount display.

xano’s picture

Status: Needs review » Fixed
amateescu’s picture

Version: 7.x-1.x-dev » 7.x-2.x-dev

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

xano’s picture

Assigned: xano » Unassigned
xano’s picture

Issue summary: View changes

Kittens!