1: <?php
2: /**
3: * CNumberFormatter class file.
4: *
5: * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
6: * @author Qiang Xue <qiang.xue@gmail.com>
7: * @link http://www.yiiframework.com/
8: * @copyright 2008-2013 Yii Software LLC
9: * @license http://www.yiiframework.com/license/
10: */
11:
12: /**
13: * CNumberFormatter provides number localization functionalities.
14: *
15: * CNumberFormatter formats a number (integer or float) and outputs a string
16: * based on the specified format. A CNumberFormatter instance is associated with a locale,
17: * and thus generates the string representation of the number in a locale-dependent fashion.
18: *
19: * CNumberFormatter currently supports currency format, percentage format, decimal format,
20: * and custom format. The first three formats are specified in the locale data, while the custom
21: * format allows you to enter an arbitrary format string.
22: *
23: * A format string may consist of the following special characters:
24: * <ul>
25: * <li>dot (.): the decimal point. It will be replaced with the localized decimal point.</li>
26: * <li>comma (,): the grouping separator. It will be replaced with the localized grouping separator.</li>
27: * <li>zero (0): required digit. This specifies the places where a digit must appear (will pad 0 if not).</li>
28: * <li>hash (#): optional digit. This is mainly used to specify the location of decimal point and grouping separators.</li>
29: * <li>currency (¤): the currency placeholder. It will be replaced with the localized currency symbol.</li>
30: * <li>percentage (%): the percentage mark. If appearing, the number will be multiplied by 100 before being formatted.</li>
31: * <li>permillage (‰): the permillage mark. If appearing, the number will be multiplied by 1000 before being formatted.</li>
32: * <li>semicolon (;): the character separating positive and negative number sub-patterns.</li>
33: * </ul>
34: *
35: * Anything surrounding the pattern (or sub-patterns) will be kept.
36: *
37: * The followings are some examples:
38: * <pre>
39: * Pattern "#,##0.00" will format 12345.678 as "12,345.68".
40: * Pattern "#,#,#0.00" will format 12345.6 as "1,2,3,45.60".
41: * </pre>
42: * Note, in the first example, the number is rounded first before applying the formatting.
43: * And in the second example, the pattern specifies two grouping sizes.
44: *
45: * CNumberFormatter attempts to implement number formatting according to
46: * the {@link http://www.unicode.org/reports/tr35/ Unicode Technical Standard #35}.
47: * The following features are NOT implemented:
48: * <ul>
49: * <li>significant digit</li>
50: * <li>scientific format</li>
51: * <li>arbitrary literal characters</li>
52: * <li>arbitrary padding</li>
53: * </ul>
54: *
55: * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
56: * @author Qiang Xue <qiang.xue@gmail.com>
57: * @package system.i18n
58: * @since 1.0
59: */
60: class CNumberFormatter extends CComponent
61: {
62: private $_locale;
63: private $_formats=array();
64:
65: /**
66: * Constructor.
67: * @param mixed $locale locale ID (string) or CLocale instance
68: */
69: public function __construct($locale)
70: {
71: if(is_string($locale))
72: $this->_locale=CLocale::getInstance($locale);
73: else
74: $this->_locale=$locale;
75: }
76:
77: /**
78: * Formats a number based on the specified pattern.
79: * Note, if the format contains '%', the number will be multiplied by 100 first.
80: * If the format contains '‰', the number will be multiplied by 1000.
81: * If the format contains currency placeholder, it will be replaced by
82: * the specified localized currency symbol.
83: * @param string $pattern format pattern
84: * @param mixed $value the number to be formatted
85: * @param string $currency 3-letter ISO 4217 code. For example, the code "USD" represents the US Dollar and "EUR" represents the Euro currency.
86: * The currency placeholder in the pattern will be replaced with the currency symbol.
87: * If null, no replacement will be done.
88: * @return string the formatting result.
89: */
90: public function format(
91: $pattern,$value,$currency=null/* x2modstart */,$negativePrefix=null/* x2modend */)
92: {
93: $format=$this->parseFormat($pattern);
94: if ($negativePrefix !== null) {
95: $format['negativePrefix'] = $negativePrefix;
96: $format['negativeSuffix'] = '';
97: }
98: $result=$this->formatNumber($format,$value);
99: if($currency===null)
100: return $result;
101: elseif(($symbol=$this->_locale->getCurrencySymbol($currency))===null)
102: $symbol=$currency;
103: return str_replace('¤',$symbol,$result);
104: }
105:
106: /**
107: * Formats a number using the currency format defined in the locale.
108: * @param mixed $value the number to be formatted
109: * @param string $currency 3-letter ISO 4217 code. For example, the code "USD" represents the US Dollar and "EUR" represents the Euro currency.
110: * The currency placeholder in the pattern will be replaced with the currency symbol.
111: * @return string the formatting result.
112: */
113: public function formatCurrency(
114: $value,$currency/* x2modstart */,$negativePrefix=null/* x2modend */)
115: {
116: return $this->format(
117: $this->_locale->getCurrencyFormat(),$value,
118: $currency/* x2modstart */,$negativePrefix/* x2modend */);
119: }
120:
121: /**
122: * Formats a number using the percentage format defined in the locale.
123: * Note, if the percentage format contains '%', the number will be multiplied by 100 first.
124: * If the percentage format contains '‰', the number will be multiplied by 1000.
125: * @param mixed $value the number to be formatted
126: * @return string the formatting result.
127: */
128: public function formatPercentage($value)
129: {
130: return $this->format($this->_locale->getPercentFormat(),$value);
131: }
132:
133: /**
134: * Formats a number using the decimal format defined in the locale.
135: * @param mixed $value the number to be formatted
136: * @return string the formatting result.
137: */
138: public function formatDecimal($value)
139: {
140: return $this->format($this->_locale->getDecimalFormat(),$value);
141: }
142:
143: /**
144: * Formats a number based on a format.
145: * This is the method that does actual number formatting.
146: * @param array $format format with the following structure:
147: * <pre>
148: * array(
149: * // number of required digits after the decimal point,
150: * // will be padded with 0 if not enough digits,
151: * // -1 means we should drop the decimal point
152: * 'decimalDigits'=>2,
153: * // maximum number of digits after the decimal point,
154: * // additional digits will be truncated.
155: * 'maxDecimalDigits'=>3,
156: * // number of required digits before the decimal point,
157: * // will be padded with 0 if not enough digits
158: * 'integerDigits'=>1,
159: * // the primary grouping size, 0 means no grouping
160: * 'groupSize1'=>3,
161: * // the secondary grouping size, 0 means no secondary grouping
162: * 'groupSize2'=>0,
163: * 'positivePrefix'=>'+', // prefix to positive number
164: * 'positiveSuffix'=>'', // suffix to positive number
165: * 'negativePrefix'=>'(', // prefix to negative number
166: * 'negativeSuffix'=>')', // suffix to negative number
167: * 'multiplier'=>1, // 100 for percent, 1000 for per mille
168: * );
169: * </pre>
170: * @param mixed $value the number to be formatted
171: * @return string the formatted result
172: */
173: protected function formatNumber($format,$value)
174: {
175: $negative=$value<0;
176: $value=abs($value*$format['multiplier']);
177: if($format['maxDecimalDigits']>=0)
178: $value=number_format($value,$format['maxDecimalDigits'],'.','');
179: $value="$value";
180: if(false !== $pos=strpos($value,'.'))
181: {
182: $integer=substr($value,0,$pos);
183: $decimal=substr($value,$pos+1);
184: }
185: else
186: {
187: $integer=$value;
188: $decimal='';
189: }
190: if($format['decimalDigits']>strlen($decimal))
191: $decimal=str_pad($decimal,$format['decimalDigits'],'0');
192: elseif($format['decimalDigits']<strlen($decimal))
193: {
194: $decimal_temp='';
195: for($i=strlen($decimal)-1;$i>=0;$i--)
196: if($decimal[$i]!=='0' || strlen($decimal_temp)>0)
197: $decimal_temp=$decimal[$i].$decimal_temp;
198: $decimal=$decimal_temp;
199: }
200: if(strlen($decimal)>0)
201: $decimal=$this->_locale->getNumberSymbol('decimal').$decimal;
202:
203: $integer=str_pad($integer,$format['integerDigits'],'0',STR_PAD_LEFT);
204: if($format['groupSize1']>0 && strlen($integer)>$format['groupSize1'])
205: {
206: $str1=substr($integer,0,-$format['groupSize1']);
207: $str2=substr($integer,-$format['groupSize1']);
208: $size=$format['groupSize2']>0?$format['groupSize2']:$format['groupSize1'];
209: $str1=str_pad($str1,(int)((strlen($str1)+$size-1)/$size)*$size,' ',STR_PAD_LEFT);
210: $integer=ltrim(implode($this->_locale->getNumberSymbol('group'),str_split($str1,$size))).$this->_locale->getNumberSymbol('group').$str2;
211: }
212:
213: if($negative)
214: $number=$format['negativePrefix'].$integer.$decimal.$format['negativeSuffix'];
215: else
216: $number=$format['positivePrefix'].$integer.$decimal.$format['positiveSuffix'];
217:
218: return strtr($number,array('%'=>$this->_locale->getNumberSymbol('percentSign'),'‰'=>$this->_locale->getNumberSymbol('perMille')));
219: }
220:
221: /**
222: * Parses a given string pattern.
223: * @param string $pattern the pattern to be parsed
224: * @return array the parsed pattern
225: * @see formatNumber
226: */
227: protected function parseFormat($pattern)
228: {
229: if(isset($this->_formats[$pattern]))
230: return $this->_formats[$pattern];
231:
232: $format=array();
233:
234: // find out prefix and suffix for positive and negative patterns
235: $patterns=explode(';',$pattern);
236: $format['positivePrefix']=$format['positiveSuffix']=$format['negativePrefix']=$format['negativeSuffix']='';
237: if(preg_match('/^(.*?)[#,\.0]+(.*?)$/',$patterns[0],$matches))
238: {
239: $format['positivePrefix']=$matches[1];
240: $format['positiveSuffix']=$matches[2];
241: }
242:
243: if(isset($patterns[1]) && preg_match('/^(.*?)[#,\.0]+(.*?)$/',$patterns[1],$matches)) // with a negative pattern
244: {
245: $format['negativePrefix']=$matches[1];
246: $format['negativeSuffix']=$matches[2];
247: }
248: else
249: {
250: $format['negativePrefix']=$this->_locale->getNumberSymbol('minusSign').$format['positivePrefix'];
251: $format['negativeSuffix']=$format['positiveSuffix'];
252: }
253: $pat=$patterns[0];
254:
255: // find out multiplier
256: if(strpos($pat,'%')!==false)
257: $format['multiplier']=100;
258: elseif(strpos($pat,'‰')!==false)
259: $format['multiplier']=1000;
260: else
261: $format['multiplier']=1;
262:
263: // find out things about decimal part
264: if(($pos=strpos($pat,'.'))!==false)
265: {
266: if(($pos2=strrpos($pat,'0'))>$pos)
267: $format['decimalDigits']=$pos2-$pos;
268: else
269: $format['decimalDigits']=0;
270: if(($pos3=strrpos($pat,'#'))>=$pos2)
271: $format['maxDecimalDigits']=$pos3-$pos;
272: else
273: $format['maxDecimalDigits']=$format['decimalDigits'];
274: $pat=substr($pat,0,$pos);
275: }
276: else // no decimal part
277: {
278: $format['decimalDigits']=0;
279: $format['maxDecimalDigits']=0;
280: }
281:
282: // find out things about integer part
283: $p=str_replace(',','',$pat);
284: if(($pos=strpos($p,'0'))!==false)
285: $format['integerDigits']=strrpos($p,'0')-$pos+1;
286: else
287: $format['integerDigits']=0;
288: // find out group sizes. some patterns may have two different group sizes
289: $p=str_replace('#','0',$pat);
290: if(($pos=strrpos($pat,','))!==false)
291: {
292: $format['groupSize1']=strrpos($p,'0')-$pos;
293: if(($pos2=strrpos(substr($p,0,$pos),','))!==false)
294: $format['groupSize2']=$pos-$pos2-1;
295: else
296: $format['groupSize2']=0;
297: }
298: else
299: $format['groupSize1']=$format['groupSize2']=0;
300:
301: return $this->_formats[$pattern]=$format;
302: }
303: }
304: