Improvements to formatting numbers with more complex masks

This commit is contained in:
Mark Baker 2013-05-21 18:00:57 +01:00
parent 46982cf098
commit c17a4a62a3
4 changed files with 187 additions and 62 deletions

View File

@ -687,6 +687,11 @@ class PHPExcel_Shared_String
$localeconv = localeconv();
self::$_thousandsSeparator = ($localeconv['thousands_sep'] != '')
? $localeconv['thousands_sep'] : $localeconv['mon_thousands_sep'];
if (self::$_thousandsSeparator == '') {
// Default to .
self::$_thousandsSeparator = ',';
}
}
return self::$_thousandsSeparator;
}

View File

@ -431,6 +431,108 @@ class PHPExcel_Style_NumberFormat extends PHPExcel_Style_Supervisor implements P
'h' => 'g'
);
private static function _formatAsDate(&$value, &$format)
{
// dvc: convert Excel formats to PHP date formats
// strip off first part containing e.g. [$-F800] or [$USD-409]
// general syntax: [$<Currency string>-<language info>]
// language info is in hexadecimal
$format = preg_replace('/^(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
// OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case
$format = strtolower($format);
$format = strtr($format,self::$_dateFormatReplacements);
if (!strpos($format,'A')) { // 24-hour time format
$format = strtr($format,self::$_dateFormatReplacements24);
} else { // 12-hour time format
$format = strtr($format,self::$_dateFormatReplacements12);
}
$dateObj = PHPExcel_Shared_Date::ExcelToPHPObject($value);
$value = $dateObj->format($format);
}
private static function _formatAsPercentage(&$value, &$format)
{
if ($format === self::FORMAT_PERCENTAGE) {
$value = round( (100 * $value), 0) . '%';
} else {
if (preg_match('/\.[#0]+/i', $format, $m)) {
$s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
$format = str_replace($m[0], $s, $format);
}
if (preg_match('/^[#0]+/', $format, $m)) {
$format = str_replace($m[0], strlen($m[0]), $format);
}
$format = '%' . str_replace('%', 'f%%', $format);
$value = sprintf($format, 100 * $value);
}
}
private static function _formatAsFraction(&$value, &$format)
{
$sign = ($value < 0) ? '-' : '';
$integerPart = floor(abs($value));
$decimalPart = trim(fmod(abs($value),1),'0.');
$decimalLength = strlen($decimalPart);
$decimalDivisor = pow(10,$decimalLength);
$GCD = PHPExcel_Calculation_MathTrig::GCD($decimalPart,$decimalDivisor);
$adjustedDecimalPart = $decimalPart/$GCD;
$adjustedDecimalDivisor = $decimalDivisor/$GCD;
if ((strpos($format,'0') !== false) || (strpos($format,'#') !== false) || (substr($format,0,3) == '? ?')) {
if ($integerPart == 0) {
$integerPart = '';
}
$value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
} else {
$adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
$value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
}
}
private static function _complexNumberFormatMask($number, $mask) {
if (strpos($mask,'.') !== false) {
$numbers = explode('.', $number . '.0');
$masks = explode('.', $mask . '.0');
$result1 = self::_complexNumberFormatMask($numbers[0], $masks[0]);
$result2 = strrev(self::_complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
return $result1 . '.' . $result2;
}
$r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
if ($r > 1) {
$result = array_reverse($result[0]);
foreach($result as $block) {
$divisor = 1 . $block[0];
$size = strlen($block[0]);
$offset = $block[1];
$blockValue = sprintf(
'%0' . $size . 'd',
fmod($number, $divisor)
);
$number = floor($number / $divisor);
$mask = substr_replace($mask,$blockValue, $offset, $size);
}
if ($number > 0) {
$mask = substr_replace($mask, $number, $offset, 0);
}
$result = $mask;
} else {
$result = $number;
}
return $result;
}
/**
* Convert a value in a pre-defined format to a PHP string
*
@ -439,7 +541,7 @@ class PHPExcel_Style_NumberFormat extends PHPExcel_Style_Supervisor implements P
* @param array $callBack Callback function for additional formatting of string
* @return string Formatted string
*/
public static function toFormattedString($value = PHPExcel_Style_NumberFormat::FORMAT_GENERAL, $format = '', $callBack = null)
public static function toFormattedString($value = '0', $format = PHPExcel_Style_NumberFormat::FORMAT_GENERAL, $callBack = null)
{
// For now we do not treat strings although section 4 of a format code affects strings
if (!is_numeric($value)) return $value;
@ -499,46 +601,12 @@ class PHPExcel_Style_NumberFormat extends PHPExcel_Style_Supervisor implements P
// Let's begin inspecting the format and converting the value to a formatted string
if (preg_match('/^(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy]/i', $format)) { // datetime format
// dvc: convert Excel formats to PHP date formats
// strip off first part containing e.g. [$-F800] or [$USD-409]
// general syntax: [$<Currency string>-<language info>]
// language info is in hexadecimal
$format = preg_replace('/^(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
// OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case
$format = strtolower($format);
$format = strtr($format,self::$_dateFormatReplacements);
if (!strpos($format,'A')) { // 24-hour time format
$format = strtr($format,self::$_dateFormatReplacements24);
} else { // 12-hour time format
$format = strtr($format,self::$_dateFormatReplacements12);
}
$dateObj = PHPExcel_Shared_Date::ExcelToPHPObject($value);
$value = $dateObj->format($format);
self::_formatAsDate($value, $format);
} else if (preg_match('/%$/', $format)) { // % number format
if ($format === self::FORMAT_PERCENTAGE) {
$value = round( (100 * $value), 0) . '%';
} else {
if (preg_match('/\.[#0]+/i', $format, $m)) {
$s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
$format = str_replace($m[0], $s, $format);
}
if (preg_match('/^[#0]+/', $format, $m)) {
$format = str_replace($m[0], strlen($m[0]), $format);
}
$format = '%' . str_replace('%', 'f%%', $format);
$value = sprintf($format, 100 * $value);
}
self::_formatAsPercentage($value, $format);
} else {
if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) {
$value = 'EUR ' . sprintf('%1.2f', $value);
} else {
// In Excel formats, "_" is used to add spacing, which we can't do in HTML
$format = preg_replace('/_./', '', $format);
@ -574,25 +642,7 @@ class PHPExcel_Style_NumberFormat extends PHPExcel_Style_Supervisor implements P
if (preg_match('/#?.*\?\/\?/', $format, $m)) {
//echo 'Format mask is fractional '.$format.' <br />';
if ($value != (int)$value) {
$sign = ($value < 0) ? '-' : '';
$integerPart = floor(abs($value));
$decimalPart = trim(fmod(abs($value),1),'0.');
$decimalLength = strlen($decimalPart);
$decimalDivisor = pow(10,$decimalLength);
$GCD = PHPExcel_Calculation_MathTrig::GCD($decimalPart,$decimalDivisor);
$adjustedDecimalPart = $decimalPart/$GCD;
$adjustedDecimalDivisor = $decimalDivisor/$GCD;
if ((strpos($format,'0') !== false) || (strpos($format,'#') !== false) || (substr($format,0,3) == '? ?')) {
if ($integerPart == 0) { $integerPart = ''; }
$value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
} else {
$adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
$value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
}
self::_formatAsFraction($value, $format);
}
} else {
@ -602,7 +652,7 @@ class PHPExcel_Style_NumberFormat extends PHPExcel_Style_Supervisor implements P
$value = $value / $scale;
// Strip #
$format = preg_replace('/\\#/', '', $format);
$format = preg_replace('/\\#/', '0', $format);
$n = "/\[[^\]]+\]/";
$m = preg_replace($n, '', $format);
@ -614,7 +664,6 @@ class PHPExcel_Style_NumberFormat extends PHPExcel_Style_Supervisor implements P
// minimun width of formatted number (including dot)
$minWidth = strlen($left) + strlen($dec) + strlen($right);
if ($useThousands) {
$value = number_format(
$value
@ -622,14 +671,18 @@ class PHPExcel_Style_NumberFormat extends PHPExcel_Style_Supervisor implements P
, PHPExcel_Shared_String::getDecimalSeparator()
, PHPExcel_Shared_String::getThousandsSeparator()
);
$value = preg_replace($number_regex, $value, $format);
} else {
if (preg_match('/0([^\d\.]+)0/', $format, $matches)) {
$value = self::_complexNumberFormatMask($value, $format);
} else {
$sprintf_pattern = "%0$minWidth." . strlen($right) . "f";
$value = sprintf($sprintf_pattern, $value);
}
$value = preg_replace($number_regex, $value, $format);
}
}
}
}
if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
// Currency or Accounting
$currencyFormat = $m[0];

View File

@ -0,0 +1,33 @@
<?php
require_once 'testDataFileIterator.php';
class NumberFormatTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
if (!defined('PHPEXCEL_ROOT')) {
define('PHPEXCEL_ROOT', APPLICATION_PATH . '/');
}
require_once(PHPEXCEL_ROOT . 'PHPExcel/Autoloader.php');
}
/**
* @dataProvider providerNumberFormat
*/
public function testFormatValueWithMask()
{
$args = func_get_args();
$expectedResult = array_pop($args);
$result = call_user_func_array(array('PHPExcel_Style_NumberFormat','toFormattedString'),$args);
$this->assertEquals($expectedResult, $result);
}
public function providerNumberFormat()
{
return new testDataFileIterator('rawTestData/Style/NumberFormat.data');
}
}

View File

@ -0,0 +1,34 @@
# value format result
0.0, "0.0", "0.0"
0.0, "0", "0"
0, "0.0", "0.0"
0, "0", "0"
0, "##0", "000"
12, "#.0#", "12.0"
0.1, "0.0", "0.1"
0.1, "0", "0"
5.5555, "0.###", "5.556"
5.5555, "0.0##", "5.556"
5.5555, "0.00#", "5.556"
5.5555, "0.000", "5.556"
5.5555, "0.0000", "5.5555"
12345.6789, '"#,##0.00"', '"12,345.68"'
12345.6789, '"#,##0.000"', '"12,345.679"'
12345.6789, '"£ #,##0.00"', '"£ 12,345.68"'
12345.6789, '"$ #,##0.000"', '"$ 12,345.679"'
5.6789, '"#,##0.00"', '"5.68"'
12000, '"#,###"', '"12,000"'
12000, '"#,"', '12'
12200000, '"0.0,,"', '12.2' // Scaling test
0.08, "0%", "8%"
0.8, "0%", "80%"
2.8, "0%", "280%"
125.74, '$0.00" Surplus";$-0.00" Shortage"', "$125.74 Surplus"
-125.74, '$0.00" Surplus";$-0.00" Shortage"', "$-125.74 Shortage"
-125.74, '$0.00" Surplus";$0.00" Shortage"', "$125.74 Shortage"
5.25, '# ???/???', "5 1/4" // Fraction
5.3, '# ???/???', "5 3/10" // Vulgar Fraction
5.25, '???/???', "21/4"
123456789, '(000) 0-0000-000', "(001) 2-3456-789"
123456789, '0 (+00) 0000 00 00 00', "0 (+00) 0123 45 67 89"
123456789, '0000:00:00', "12345:67:89"