Skip to content

Instantly share code, notes, and snippets.

@bojanz
Last active June 27, 2017 11:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bojanz/4fbead42616027f5776851ddb4d4c840 to your computer and use it in GitHub Desktop.
Save bojanz/4fbead42616027f5776851ddb4d4c840 to your computer and use it in GitHub Desktop.
Script for regenerating Commerce 1.x currencies based on CLDR
<?php
/**
* Regenerates the Commerce 1.x currency list from CLDR.
*
* Assumes the existence of commerce/ and intl/ folders.
* The intl/ folder should contain the commerceguys/intl
* library with the CLDR dataset downloaded via
* scripts/fetch_data.sh
*/
include __DIR__ . '/commerce/includes/commerce.currency.inc';
include __DIR__ . '/intl/vendor/autoload.php';
$dataDir = 'intl/scripts/assets/';
$commerceCurrencies = commerce_commerce_currency_info();
$isoCurrencies = getIsoCurrencies();
$addedCurrencies = array_keys(array_diff_key($isoCurrencies, $commerceCurrencies));
$removedCurrencies = array_keys(array_diff_key($commerceCurrencies, $isoCurrencies));
$currencyData = json_decode(file_get_contents($dataDir . 'cldr-core/supplemental/currencyData.json'), true);
$currencyData = $currencyData['supplemental']['currencyData'];
$territoryInfo = json_decode(file_get_contents($dataDir . 'cldr-core/supplemental/territoryInfo.json'), true);
$territoryInfo = $territoryInfo['supplemental']['territoryInfo'];
$newCommerceCurrencies = [];
foreach ($isoCurrencies as $currencyCode => $numericCode) {
// Determine the currency's default locale (currency -> country -> locale).
// This can't work for EUR because it's used by multiple countries.
$locale = 'en';
$fallbackLocale = 'en';
$languageCode = NULL;
if ($currencyCode == 'EUR') {
$locale = 'fr';
}
else {
$countryCode = getCurrencyCountry($currencyCode);
if ($countryCode) {
$languageCode = getCountryLanguage($countryCode);
$locale = $languageCode . '-' . $countryCode;
$fallbackLocale = $languageCode;
}
}
$englishData = json_decode(file_get_contents($dataDir . 'cldr-numbers-full/main/en/currencies.json'), true);
$name = $englishData['main']['en']['numbers']['currencies'][$currencyCode]['displayName'];
$currency = getCurrency($currencyCode, $locale, $fallbackLocale);
$symbol = $currency['symbol'];
$numberFormat = getNumberFormat($locale, $fallbackLocale);
$currencyPattern = $numberFormat['currencyFormats-numberSystem-latn']['standard'];
// Ignore the negative portion of the currency pattern.
$currencyPattern = strtok($currencyPattern, ';');
$numberPosition = strpos($currencyPattern, '0.00');
$symbolPosition = strpos($currencyPattern, '¤');
$symbolSpacer = ' ';
if ($symbolPosition < $numberPosition) {
$symbolPlacement = 'before';
$symbolSpacer = mb_substr($currencyPattern, $symbolPosition + 1, 1, 'UTF-8');
$symbolEdge = mb_substr($symbol, -1, 1, 'UTF-8');
}
else {
$symbolPlacement = 'after';
$symbolSpacer = mb_substr($currencyPattern, $symbolPosition - 2, 1, 'UTF-8');
$symbolEdge = mb_substr($symbol, 0, 1, 'UTF-8');
}
if (in_array($symbolSpacer, ['#', '0', '¤'])) {
$symbolSpacer = '';
}
// CLDR quote:
// "By default a space is automatically added between letters in a currency symbol and adjacent numbers."
if ($symbolSpacer == '' && ctype_alpha($symbolEdge)) {
$symbolSpacer = ' ';
}
$decimals = 2;
if (isset($currencyData['fractions'][$currencyCode]['_digits'])) {
$decimals = $currencyData['fractions'][$currencyCode]['_digits'];
}
// Override for Hungary, which has digits only in theory.
if ($currencyCode == 'HUF') {
$decimals = 0;
}
$newCommerceCurrencies[$currencyCode] = [
'code' => $currencyCode,
'numeric_code' => $numericCode,
'symbol' => $symbol,
'name' => 't(' . $name . ')t',
'symbol_placement' => $symbolPlacement,
'symbol_spacer' => $symbolSpacer,
// The symbol should always be used, not the code.
'code_placement' => 'hidden',
// CLDR has no information on minor_unit and major_unit. Use the old data when available.
'minor_unit' => isset($commerceCurrencies[$currencyCode]['minor_unit']) ? 't(' . $commerceCurrencies[$currencyCode]['minor_unit'] . ')t' : '',
'major_unit' => isset($commerceCurrencies[$currencyCode]['major_unit']) ? 't(' . $commerceCurrencies[$currencyCode]['major_unit'] . ')t' : '',
'decimals' => (int) $decimals,
'thousands_separator' => $numberFormat['symbols-numberSystem-latn']['group'],
'decimal_separator' => $numberFormat['symbols-numberSystem-latn']['decimal'],
];
// Strip default values.
$defaults = [
'symbol' => '',
'minor_unit' => '',
'decimals' => 2,
'rounding_step' => 0,
'thousands_separator' => ',',
'decimal_separator' => '.',
'symbol_placement' => 'hidden',
'symbol_spacer' => ' ',
'code_placement' => 'after',
'code_spacer' => ' ',
];
foreach ($newCommerceCurrencies[$currencyCode] as $key => $value) {
if (isset($defaults[$key]) && $defaults[$key] == $value) {
unset($newCommerceCurrencies[$currencyCode][$key]);
}
}
// 1.x doesn't support arabic number formatting, so it's best to
// fallback to the default behavior, just show the currency code
// after the number.
if ($languageCode == 'ar') {
unset($newCommerceCurrencies[$currencyCode]['symbol_placement']);
unset($newCommerceCurrencies[$currencyCode]['symbol_spacer']);
unset($newCommerceCurrencies[$currencyCode]['code_placement']);
}
}
// CLDR doesn't seem to have a pt-BR number format, and the default pt one
// uses no symbol spacer. Confirmed on http://www.telhanorte.com.br/
// and http://www.cec.com.br/ that a spacer is needed, so unset the empty
// string to use the default.
unset($newCommerceCurrencies['BRL']['symbol_spacer']);
// Probably not even used, but we keep it for BC.
$newCommerceCurrencies['CHF']['rounding_step'] = '0.05';
// Fix the symbol for HRK ("kn" instead of "HRK").
$newCommerceCurrencies['HRK']['symbol'] = 'kn';
// @todo Get commerce to stop re-sorting the currencies by code, then sort
// by name here.
ksort($newCommerceCurrencies);
file_put_php('currencies.php', $newCommerceCurrencies);
if ($count = count($addedCurrencies)) {
echo "Added $count currencies: " . implode(', ', $addedCurrencies) . "\n";
} else {
echo "No currencies were added. \n";
}
if ($count = count($removedCurrencies)) {
echo "Removed $count currencies: " . implode(', ', $removedCurrencies) . "\n";
} else {
echo "No currencies were removed. \n";
}
$updatedCurrencies = [];
foreach ($commerceCurrencies as $currencyCode => $currency) {
if (isset($newCommerceCurrencies[$currencyCode]) && array_diff($newCommerceCurrencies[$currencyCode], $currency)) {
$updatedCurrencies[] = $currencyCode;
}
}
if ($count = count($updatedCurrencies)) {
echo "Updated $count currencies: " . implode(', ', $updatedCurrencies) . "\n";
} else {
echo "No currencies were updated. \n";
}
/**
* t() shim to allow commerce_commerce_currency_info() to work.
*/
function t($string)
{
return $string;
}
/**
* Gets the ISO currencies.
*
* ISO only lists active currencies, making it practical to use as a base.
*
* @return array
* Format: $currencyCode => $numericCode.
*/
function getIsoCurrencies()
{
global $dataDir;
$isoCurrencies = [];
$isoData = simplexml_load_file($dataDir . 'c2.xml');
foreach ($isoData->CcyTbl->CcyNtry as $currency) {
$attributes = (array) $currency->CcyNm->attributes();
if (!empty($attributes) && !empty($attributes['@attributes']['IsFund'])) {
// Ignore funds.
continue;
}
$currency = (array) $currency;
if (empty($currency['Ccy'])) {
// Ignore placeholders like "Antarctica".
continue;
}
if (substr($currency['CtryNm'], 0, 2) == 'ZZ' || in_array($currency['Ccy'], ['XUA', 'XSU', 'XDR'])) {
// Ignore special currencies.
continue;
}
if (in_array($currency['Ccy'], ['SVC', 'ZWL'])) {
// No longer used, but still included in the ISO list.
continue;
}
$isoCurrencies[$currency['Ccy']] = $currency['CcyNbr'];
}
return $isoCurrencies;
}
/**
* Gets the country for a specific currency.
*/
function getCurrencyCountry($currencyCode)
{
global $currencyData;
foreach ($currencyData['region'] as $countryCode => $data) {
foreach ($data as $index => $currencies) {
if (isset($currencies[$currencyCode]) && empty($currencies[$currencyCode]['_to'])) {
return $countryCode;
}
}
}
}
/**
* Gets the language for a specific country.
*/
function getCountryLanguage($countryCode)
{
global $territoryInfo;
$languages = $territoryInfo[$countryCode]['languagePopulation'];
// Sort by population percent date.
uasort($languages, function ($a, $b) {
$a = $a['_populationPercent'];
$b = $b['_populationPercent'];
if ($a == $b) {
return 0;
}
return ($a > $b) ? -1 : 1;
});
$languageCode = key($languages);
$languageCode = str_replace('_', '-', $languageCode);
return $languageCode;
}
function getCurrency($currencyCode, $locale, $fallbackLocale)
{
global $dataDir;
$usedLocale = NULL;
$localeCandidates = [$locale, $fallbackLocale, 'en'];
foreach ($localeCandidates as $localeCandidate) {
if (file_exists($dataDir . 'cldr-numbers-full/main/' . $localeCandidate . '/currencies.json')) {
$usedLocale = $localeCandidate;
break;
}
}
$data = json_decode(file_get_contents($dataDir . 'cldr-numbers-full/main/' . $usedLocale . '/currencies.json'), true);
$currency = $data['main'][$usedLocale]['numbers']['currencies'][$currencyCode];
return $currency;
}
function getNumberFormat($locale, $fallbackLocale)
{
global $dataDir;
$usedLocale = NULL;
$localeCandidates = [$locale, $fallbackLocale, 'en'];
foreach ($localeCandidates as $localeCandidate) {
if (file_exists($dataDir . 'cldr-numbers-full/main/' . $localeCandidate . '/numbers.json')) {
$usedLocale = $localeCandidate;
break;
}
}
$data = json_decode(file_get_contents($dataDir . 'cldr-numbers-full/main/' . $usedLocale . '/numbers.json'), true);
$numberFormat = $data['main'][$usedLocale]['numbers'];
return $numberFormat;
}
/**
* Converts the provided data into php and writes it to the disk.
*/
function file_put_php($filename, $data)
{
$data = var_export($data, true);
$data = str_replace(['array (', "=> \n "], ['array(', '=> '], $data);
$data = str_replace('=> array(', '=> array(', $data);
$data = '<?php' . "\n\n" . '$data = ' . $data . ';';
// Put back t().
$data = str_replace(["'t(", ")t',"], ["t('", "'),"], $data);
file_put_contents($filename, $data);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment