I am trying to format from EURO to Latin American countries. But I can't get all of them to format correctly.
These two lines work fine:
$currencies['ESP'] = array(2, ',', '.'); // Euro
$currencies['USD'] = array(2, '.', ','); // US Dollar
The ones that don't work are these:
Mexico I have $ 1,800,520 Mexican Peso and I want to obtain this result $ 3,698.00
$currencies['MXN'] = array(3, ",", '.'); // México Peso
Colombia $ 2,097,106.36 Colombian peso and I want to get $ 104,637,255.96
$currencies['COP'] = array(2, ',', '.'); // Colombiano Peso
Argentina $ 53,609.02 Argentine peso and I want to get $ 10,490
$currencies['ARS'] = array(2, ',', '.'); // Argentina Peso
Does anyone know what I'm doing wrong? I appreciate any help.
Example of my functions:
/**
* @param self::$curr
* @return string
*/
public static function setCurrency($tipo) {
// Creamos tipo moneda
$tipoMoneda = ($tipo =='') ? self::$curr : $tipo;
$moneda = match ($tipoMoneda) {
'CLF' => "$",
'COP' => "$",
'ARS' => "$",
'USD' => "$",
'EUR' => "€",
'MXN' => "$",
};
return $moneda;
}
/**
* Format price
* @param string
* @param string
*/
public static function toMoney($price,$tipo='') {
$currencies['EUR'] = array(2, ',', '.'); // Euro
$currencies['ESP'] = array(2, ',', '.'); // Euro
$currencies['USD'] = array(2, '.', ','); // US Dollar
$currencies['COP'] = array(2, ',', '.'); // Colombian Peso
$currencies['MXN'] = array(3, ",", '.'); // Mexico Peso
$currencies['CLP'] = array(0, '', '.'); // Chilean Peso
$currencies['ARS'] = array(2, ',', '.'); // Argentina Peso
if ($tipo == '') :
$money_format = number_format($price, ...$currencies[self::$curr]) . ' ' . self::setCurrency($tipo);
else:
$money_format = self::setCurrency($tipo) . number_format($price, ...$currencies[$tipo]);
endif;
return $money_format;
}
EDIT: The exchange rate i get it from the DB
/**
* Calcular TAXES about original price base
* @param string
* @return string
*/
public static function CalcIva($valor, $arr =[]) {
// Get default IVA o by (USD-MXN) (COOKIE)
$getIva = self::$defaultIva;
// Price original
$price = $valor;
// Get taxes
$iva = ($getIva / 100) * $price;
// Sum taxes to base price
$precio = $price $iva;
// On this line if $arr is not null i calculate 1.13 or some else x price
if ($arr != null) :
// Calcul exchange rate (example: 1.13 * 20)
$precio = $arr['cambio'] * $price;
endif;
// Price
return $precio;
}
To set the cookie i do it on JS
/**
* Select money (header)
*/
let moneda = document.getElementById('slc-moneda');
moneda.addEventListener('change', function (e) {
// Get value option
let tipo = this.value;
// Not null
if (tipo != 0) {
// Value default, delete cookie
if (tipo == 'EUR-ES') {
// Eliminamos cookie, usamos configuracion por defecto
delCookie('moneda');
location.reload()
// Set cookie - new money format
} else {
setCookie('moneda', tipo, 365)
location.reload()
}
}
e.preventDefault()
})
CodePudding user response:
As added background information based on my earlier comments, in case you were having trouble separating all the inter-related concerns, and putting all the pieces together, (i don't know if you are or not); Here is some code i've used in the past solving a similar issue. I've adjusted it based on your datamodel/code and added some comments:
Personally, since keeping half the currency information in the database and the other half in code seems messy, i would add 4 columns to your monedas
database table; namely (in the case of 'Ecuador' for example):
`currency_symbol` => '$'
`decimal_separator` => '.'
`thousands_separator` => ','
`decimals` => 2
Next you want to decide what datatype you use for price values in PHP.
I'm guessing they are DECIMAL
s inside your database, in which case you would either use strings ('65.99'
) or floats (65.99
) in PHP; generally string
is prefered as it doesn't suffer from all the oddities that floating point numbers bring to the table.
Alternatively, you could choose to store prices in cents in your database, which would allow you to use INTEGER
s (6599
) in both the database and in PHP.
Lets assume you use DECIMAL
in your database, and string
in PHP; that way you can use the PHP BCMath functions to perform calculations reliably.
Lets also assume that all prices in your database always represent the same currency (eg: your business's local currency, lets assume its EUR
).
Since prices are a complex value in your webshop-style application, you'll want a simple value class to define them.
class Price {
private $value;
public function __construct($value) {
$value = trim((string) $value);
if (!is_numeric($value) || preg_match('#^(\-)?([0-9] )(\.[0-9]{1,2})?$#D', $value) !== 1) throw Exception('Invalid price value');
$this->value = $value;
}
public function getRawValue() {
return $this->value;
}
// When printing a price (using echo for example), print it in its converted form (defined later)
public function __toString() {
return PriceLocalization::displayLocalPrice( $this );
}
}
Next, you want an object that holds (or caches) all the information about all currencies:
class Currencies {
protected static $data = null;
protected static function pullData() {
if (is_null(static::$data)) {
$data = [];
// Pull the currency/priceconversion info from the DB
$rows = run_your_dbquery('SELECT * FROM `monera`');
foreach ($rows as $row) {
$row['id_moneda'] = (int) $row['id_moneda'];
$row['decimals'] = (int) $row['decimals'];
$data[( $row['id_moneda'] )] = $row;
}
// Cache the data incase we have to do more conversions on the current page
static::$data = $data;
}
return static::$data;
}
// Returns the entire table of currency/priceconversion info from the DB
public static function getAll() {
return static::pullData();
}
// Returns one record out of the table of currency/priceconversion info (or exception if invalid)
public static function getSpecific($id) {
$data = static::pullData();
if (array_key_exists($id, $data)) return $data[$id];
throw new Exception('Bad input');
}
}
And another object that deals with the user being able to select a currency sessionwide
class UserCurrencySelection {
// store the users choice in $_COOKIE or $_SESSION or the like (used by your currency-selection selectbox)
public static function setUserPreference($choice) {
$_SESSION['currencychoice'] = $choice;
return true;
}
// read the raw value from $_COOKIE or $_SESSION or the like (if any)
public static function getUserPreference() {
return ($_SESSION['currencychoice'] ?? null);
}
// get either the active currency's record (if any), or otherwise the default record (throw exception if neither exists)
public static function getActive() {
try {
if ($current = static::getUserPreference()) {
return Currencies::getSpecific( $current );
}
} catch (Exception $e) {}
return Currencies::getSpecific( 5 ); // <-- the id of the "default" currency (in this case 5 = EUR)
}
}
And finally, the class that actually ties everything together
class PriceLocalization {
// display a specific price adjusted to the -active- currency (with the default currency as fallback)
public static function displayLocalPrice(Price $price, array $style=[]) {
$currencyinfo = UserCurrencySelection::getActive();
return static::displayPriceAs($price, $currencyinfo, $style);
}
// display a specific price adjusted to a -specific- currency (eg: id=3 gives colombian price)
public static function displayPriceInCurrency(Price $price, $id, array $style=[]) {
$currencyinfo = Currencies::getSpecific( $id );
return static::displayPriceAs($price, $currencyinfo, $style);
}
// perform the actual conversion and formatting
protected static function displayPriceAs(Price $price, array $currencyinfo, array $style=[]) {
/* $currencyinfo = [
'id_monera' => 4,
'moneda' => 'USD',
'pais' => 'Ecuador',
'ido' => 'EC',
'cambio' => '1.13',
'impuesto' => '12',
'currency_symbol' => '$',
'decimal_separator' => '.',
'thousands_separator' => ',',
'decimals' => 2,
]; */
// the original price:
$value_src = $price->getRawValue();
// Multiply the original price with the conversion rate (`cambio`) to adjust it to this currency (giving us the pre-tax price)
$value_excl = bcmul($value_src, $currencyinfo['cambio']);
// Calculate the tax, by multiplying the adjusted price with the taxrate (`impuesto`*0.01 to adjust for it being a percentage)
$tax = bcmul($value_excl, bcmul('0.01', $currencyinfo['impuesto']));
// Add the tax to the price to get the "price including tax"
$value_incl = bcadd($value_excl, $tax);
// Decide which of the values you want to display (including or excluding tax)
$value = $value_incl;
// Decide what we want to add before/after the numeric part of the price (the html-encoded version of the currency symbol)
$label_prefix = htmlentities( $currencyinfo['currency_symbol'] . ' ');
$label_suffix = ''; // or: htmlentities( ' ' . $currencyinfo['moneda']);
// Change the number into human readable form
$label = number_format((float) $value, $currencyinfo['decimals'], $currencyinfo['decimal_separator'], $currencyinfo['thousands_separator']);
// Convert that into html
$label = htmlentities($label);
// Define some CSS classes to allow for styling
$classes_prefix = 'p';
$classes_number = 'v';
$classes_suffix = 's';
$classes_full = 'price';
// Now assemble all the pieces
$html_prefix = sprintf('<span >%s</span>', htmlentities($classes_prefix), $label_prefix);
$html_number = sprintf('<span >%s</span>', htmlentities($classes_number), $label);
$html_suffix = sprintf('<span >%s</span>', htmlentities($classes_suffix), $label_suffix);
$html_full = sprintf('<span >%s%s%s</span>', htmlentities($classes_full), $html_prefix, $html_number, $html_suffix );
// Done
return $html_full;
}
}
That's the gist of it.
You can use the $style
argument that's available on each of the PriceLocalization
methods to pass arbitrary information along to displayPriceAs
. Based on that information you could change the way that function assembles its output. For example, you could check if $style['include_tax']
is set to true
/false
, and ifso adjust accordingly:
$value = (($style['include_tax'] ?? true) ? $value_incl : $value_excl);
You could style prices with:
.price { background-color: #EEE; }
.price.p { color: red; font-weight: bold; }
.price.v { color: green; font-family: courier; }
.price.s { color: blue; font-weight: bold; }
And you could also use the $style
argument above, to introduce additional classes (in specific cases).
It may also be worth setting bcscale(15);
in your application, to ensure the math is done in a way that cannot result in lost partial pennies.
ps: haven't tested the code after adapting it to your code/datamodel, so it is possible i made a typo somewhere.