Difference between #7 and #6 of Custom Number Formatting or Decimal Separators and i18n

unchanged
Title
Custom Number Formatting or Decimal Separators and i18n
unchanged
Category
Tutorials
unchanged
Tags
decimal separator, number format, i18n, customize, floating point numbers, localization
changed
Content
By default, the decimal separator in php (also in mysql) is a dot (.). 
So when we work with floats in Yii (in calculations, validation, sql statements
etc.), the decimal separator has to be a dot.
If we want to use for example a comma (,) as the decimal separator in our
application,separator, that is if we want to display numbers and
enable users to enter numbers with a comma before the decimals, we have
to:to...

1. format the float value before printing/echoing : handle the output : 4.45
=> "4,45"
2. transform user input into a valid float value before using in calculations
etc. : handle the input : "4,45" => 4.45

This article suggests where/when and how exactly to do this handling.

In the following we'll use the model 'Product' with the decimal attribute
'price' as an example.

THOUGHTS
--------
### 1. The formatting of a number
#### HOW
In Yii, a number can be formatted using the `formatNumber()` function of the
class CFormatter in the following way:
~~~
[php]
Yii::app()->format->formatNumber($value);
Yii::app()->format->number($value); // is the same as above, see details
to CFormatter
~~~
By default, this will format the value without decimals, and with a comma (,)
between every group of thousands.
To change this default behavior we have to override the `numberFormat` attribute
of the same class, which defines the format to be used. For example:
~~~
[php] 
public $numberFormat=array('decimals'=>2, 'decimalSeparator'=>',',
'thousandSeparator'=>''); 
~~~
After this, numbers will be formatted with 2 decimals, a comma (,) before the
decimals and no separator between groups of thousands.
	
	
####WHERE/WHEN
The question is: at what point in the application flow would it be best to
format a numerical value? The answer depends...

**---**<br> 
If you 'always' send the price of a product directly to the output after reading
it from the database, without using it in calculations, you could do the
formatting in the `afterFind()` function of the model `Product.php`: 
~~~
[php]
public function afterFind() {
	$this->price = Yii::app()->format->number($this->price);
	return parent::afterFind();
}
~~~
The advantage is that the formatting is done centrally in one place, and we
don't need to change the views. The disatvantage is that `$product->price`
contains the formatted number string, thus is not a valid numerical value and
can not be used as such:
~~~
[php]
$product = Product::model()->findByPk($id); // ... when we select a model
like this ...
echo $product->price; // "14,90" (if price stored in db is 14.90)
echo $product->price * 10; // 140 ( = "14,90" * 10) - not the
expected result!
~~~

**---**<br> 
[This wiki
article](http://www.yiiframework.com/wiki/298/how-to-handle-decimal-separators-e-g-comma-instead-of-dot-for-l18n
"how to handle decimal separators") suggests to do the formatting in
the CHtml class, by overriding its two functions `value()` and
`resolveValue()`.<br> 
This seems to be a good approach. But don't forget that it does not cover the
cases where CHtml is not used to generate the ouput. The following CGridView
column for example will not be affected:
~~~
[php]
array(
	'name'=>'price',
	'value'=>'$data->price - $data->discount',
),
~~~

**---**<br>
There doesn't seem to be a better way than to format each float value in the
view file, direclty before printing/echoing it. At least none that makes sure
every type of output is covered. 
		
###2. The 'unformatting' of a number
####HOW
To transform the user input into a valid numerical value, we remove the
thosund-seperators from the input string, and then replace the decimal-separator
with a dot (.). In a way, we simply 'unformat' the number.
	
####WHERE/WHEN
**--**<br>
In [this Yii
extension](http://www.yiiframework.com/extension/decimali18nbehavior
"decimali18nbehavior"), the 'unformatting' is done in the
`beforeSave()` function of a model 
(more specifically, in the beforeSave() function of a behavior, that can be
attached to any model). <br>
This is not a good place, because the validating happens before the saving; and
since the price is not a valid numerical value at that point, the validation
will fail. 

**--**<br>
[This wiki
article](http://www.yiiframework.com/wiki/298/how-to-handle-decimal-separators-e-g-comma-instead-of-dot-for-l18n
"how to handle decimal separators") suggests to do the 'unformatting'
in the `beforeValidate()` function of a model. <br>
This is a good place. But if you want to use the user input in calculations
before saving the model, make sure that the price attribute is 'unformatted'
beforehand, by validating the model (`$product->validate()`).
 
**--**<br>
We usually assign the received user input (=POST variables) to the model before
performing other operations (calculations, validaton, saving, etc.).<br>
Therefore we sugges to do the 'unformatting' during the assignment process. This
way, after one of the following lines is executed:
~~~
[php] 
$product->attributes = $_POST['Product']; // or ...
$product->setAttribute('price', $_POST['Product']['price']); // or ...
$product->setAttributes(array('price'=>$_POST['Product']['price']));
~~~
`$product->price` will be a valid numerical value and available for further
use.
	

THE CODE
--------
###1. The formatting of a number
Extend the CFormatter class, override its `numberFormat` attribute and
optionally also its `formatNumber()` function. Add the new function
`unformatNumber()` to the class, so it is available application wide. <br>

For this, create the new file `components/Formatter.php` with the content:
~~~
[php]
class Formatter extends CFormatter
{
	/**
	 * @var array the format used to format a number with PHP number_format()
function.
	 * Three elements may be specified: "decimals",
"decimalSeparator" and 
	 * "thousandSeparator". They correspond to the number of digits after

	 * the decimal point, the character displayed as the decimal point,
	 * and the thousands separator character.
	 * new: override default value: 2 decimals, a comma (,) before the decimals 
	 * and no separator between groups of thousands
	*/
	public $numberFormat=array('decimals'=>2, 'decimalSeparator'=>',',
'thousandSeparator'=>'');
		
	/**
	 * Formats the value as a number using PHP number_format() function.
	 * new: if the given $value is null/empty, return null/empty string
	 * @param mixed $value the value to be formatted
	 * @return string the formatted result
	 * @see numberFormat
	 */
	public function formatNumber($value) {
		if($value === null) return null; 	// new
		if($value === '') return '';		// new
		return number_format($value, $this->numberFormat['decimals'],
$this->numberFormat['decimalSeparator'],
$this->numberFormat['thousandSeparator']);
		}
		
	/*
	 * new function unformatNumber():
	 * turns the given formatted number (string) into a float
	 * @param string $formatted_number A formatted number 
	 * (usually formatted with the formatNumber() function)
	 * @return float the 'unformatted' number
	 */
	public function unformatNumber($formatted_number) {
		if($formatted_number === null) return null;
		if($formatted_number === '') return '';
		if(is_float($formatted_number)) return $formatted_number; // only 'unformat'
if parameter is not float already
			
		$value = str_replace($this->numberFormat['thousandSeparator'], '',
$formatted_number);
		$value = str_replace($this->numberFormat['decimalSeparator'], '.', $value);
		return (float) $value;
	}
}
~~~

Adjust main config file `config/main.php` to ensure this new class is used
instead of the CFormatter class:
~~~
[php]
'components'=>array( 
	//...
	'format'=>array(
		'class'=>'application.components.Formatter',
	),
	//...
)
~~~

In a view file, or anywhere else for that matter, you can format a numerical
value, or 'unformat' a formatted number string, simply by calling:
~~~
[php] 
$formatted_number1 = Yii::app()->format->number($numerical_value); 
$formatted_number2 = Yii::app()->format->formatNumber($numerical_value); 
// same as above
$numerical_value2 =
Yii::app()->format->unformatNumber($formatted_number1); 
~~~

**NOTE:** We declared the format centrally in one file (Formatter.php), so that
it can be changed easily. It can also be configured in the main config file,
which will override the value declared in the class file:
~~~
[php]
'components'=>array( 
	//...
	'format'=>array(
		'class'=>'application.components.Formatter',
		'numberFormat'=>array('decimals'=>3, 'decimalSeparator'=>',',
'thousandSeparator'=>'-'),
	),
	//...
),
// ...
~~~

###2. The 'unformatting' of a number
Extend the CActiveRecord class and override its functions `setAttribute()` and
`setAttributes()` to add the 'unformatting' functionality. <br>
For this, create the new file `components/ActiveRecord.php` with the following
content:
~~~
[php]
class ActiveRecord extends CActiveRecord
{
  public function setAttributes($values,$safeOnly=true) {
    if(!is_array($values)) return;
    $attributes=array_flip($safeOnly ? $this->getSafeAttributeNames() :
$this->attributeNames());
    foreach($values as $name=>$value) {
      if(isset($attributes[$name])) {
        $column = $this->getTableSchema()->getColumn($name); // new
        if (stripos($column->dbType, 'decimal') !== false) // new
          $value = Yii::app()->format->unformatNumber($value); // new
        $this->$name=$value;
      }
      else if($safeOnly)
        $this->onUnsafeAttribute($name,$value);
    }
  }
	
  public function setAttribute($name,$value) {
    $column = $this->getTableSchema()->getColumn($name); // new
    if (stripos($column->dbType, 'decimal') !== false) // new
      $value = Yii::app()->format->unformatNumber($value); // new
    if(property_exists($this,$name))
      $this->$name=$value;
    else if(isset($this->getMetaData()->columns[$name]))
      $this->_attributes[$name]=$value;
    else
      return false;
    return true;
  }
}
~~~

Derive all models from this new ActiveRecord class. For example in the Product
model `models/Product.php` edit the following line:
~~~
[php]
class Product extends ActiveRecord 	//old version: "class Product extends
CActiveRecord"
~~~

In the controller, don't use the POST variables directly, but assign them to the
model before further use. 
If there is a POST variable that is not to be assigned to any model and that
contains a formatted number string, make sure to 'unformat' it before further
use.
That is:
~~~
[php]
$product->attributes = $_POST['Product']; 	// This massively assigns all
received values using the setAttributes() function.
$reduced_price = $product->price - 4.50;	// Correct, $product->price is
valid numerical value
// DO NOT USE A POST VARIABLE DIRECTLY, SINCE IT IS NOT 'UNFORMATTED'!!
$reduced_price = $_POST['Product']['price']; // Incorrect!
$_POST['Product']['price'] is not valid numerical value
// 'Unformat' a formatted number input manually if necessary:
$wish_price = Yii::app()->format->unformatNumber($_POST['wish_price']);
~~~

Related Links
-------------

If- Wiki: [How to handle decimal separators (e.g comma instead
of dot) for
l18n](http://www.yiiframework.com/wiki/298/how-to-handle-decimal-separators-e-g-comma-instead-of-dot-for-l18n
"How to handle decimal separators (e.g comma instead of dot) for
l18n")
- Forum: [Handling decimal
separators](http://www.yiiframework.com/forum/index.php?/topic/28048-handling-decimal-separators-wiki-article/
"Handling decimal separators")
- Extension:
[decimali18nbehavior](http://www.yiiframework.com/extension/decimali18nbehavior
"decimali18nbehavior")
- Wiki: [How to extend CFormatter, add i18n support to booleanFormat and use it
in
CDetailView](http://www.yiiframework.com/wiki/305/how-to-extend-cformatter-add-i18n-support-to-booleanformat-and-use-it-in-cdetailview/
"How to extend CFormatter, add i18n support to booleanFormat and use it in
CDetailView")

That's it. If something is unclear, wrong or incomplete, please let me
know. Any suggestions for improvements will be appreciated!