views:

40

answers:

2

Is there an arbitrary-precision alternative to money_format available that could take a string instead of a float as a parameter?

It's not that I plan on doing calculations on trillions of monetary units, but after going through the trouble to properly handle monetary arithmetic without abusing floats, it'd be nice to have a function that doesn't spew random numbers after about 15 digits, even if users decide to give it nonsense data. Or, hey, maybe someone wants to buy two sticks of gum in Zimbabwe dollars?

I hesitate to use regular expressions because I was hoping to make use of the localization of money_format.

edit - found a workable solution; see below

A: 

try the NumberFormatter class

stillstanding
I'm afraid I don't have that PECL extension to test it out, but it looks like that still a float as a paramter? Maybe I'm misunderstanding? The examples have a ton of digits, but most are insignificant and just rounded off.
Greg
A: 

Cobbled together from the commenter-submitted functions on PHP's site here and here. Edited to work with arbitrary-precision parameters.

class format {
    function money($format, $number) 
    { 
        // Takes plain-format, arbitrary-length decimal string (eg: '123456789123456789.123456')
        // Returns localized monetary string, truncated at the hundredth value after the decimal point.
        // (eg: $ 123,456,789,123,456,789.12)
        $regex  = '/%((?:[\^!\-]|\+|\(|\=.)*)([0-9]+)?'. 
                  '(?:#([0-9]+))?(?:\.([0-9]+))?([in%])/'; 
        if (setlocale(LC_MONETARY, 0) == 'C') { 
            setlocale(LC_MONETARY, ''); 
        } 
        $locale = localeconv(); 
        preg_match_all($regex, $format, $matches, PREG_SET_ORDER); 
        foreach ($matches as $fmatch) { 
            $value = (string) $number;
            $flags = array( 
                'fillchar'  => preg_match('/\=(.)/', $fmatch[1], $match) ? 
                               $match[1] : ' ', 
                'nogroup'   => preg_match('/\^/', $fmatch[1]) > 0, 
                'usesignal' => preg_match('/\+|\(/', $fmatch[1], $match) ? 
                               $match[0] : '+', 
                'nosimbol'  => preg_match('/\!/', $fmatch[1]) > 0, 
                'isleft'    => preg_match('/\-/', $fmatch[1]) > 0 
            ); 
            $width      = trim($fmatch[2]) ? (int)$fmatch[2] : 0; 
            $left       = trim($fmatch[3]) ? (int)$fmatch[3] : 0; 
            $right      = trim($fmatch[4]) ? (int)$fmatch[4] : $locale['int_frac_digits']; 
            $conversion = $fmatch[5]; 

            $positive = true; 
            if ($value[0] == '-') { 
                $positive = false; 
                $value  = bcmul($value, '-1');
            } 
            $letter = $positive ? 'p' : 'n'; 

            $prefix = $suffix = $cprefix = $csuffix = $signal = ''; 

            $signal = $positive ? $locale['positive_sign'] : $locale['negative_sign']; 

            if ($locale["{$letter}_sign_posn"] == 1 && $flags['usesignal'] == '+')
                $prefix = $signal; 
            elseif ($locale["{$letter}_sign_posn"] == 2 && $flags['usesignal'] == '+') 
                $suffix = $signal; 
            elseif ($locale["{$letter}_sign_posn"] == 3 && $flags['usesignal'] == '+')
                $cprefix = $signal; 
            elseif ($locale["{$letter}_sign_posn"] == 4 && $flags['usesignal'] == '+')
                $csuffix = $signal; 
            elseif ($flags['usesignal'] == '(' || $locale["{$letter}_sign_posn"] == 0) {
                $prefix = '('; 
                $suffix = ')'; 

            } 
            if (!$flags['nosimbol']) { 
                $currency = $cprefix . 
                            ($conversion == 'i' ? $locale['int_curr_symbol'] : $locale['currency_symbol']) . 
                            $csuffix; 
            } else { 
                $currency = ''; 
            } 
            $space  = $locale["{$letter}_sep_by_space"] ? ' ' : ''; 

            $value = format::number($value, $right, $locale['mon_decimal_point'], 
                     $flags['nogroup'] ? '' : $locale['mon_thousands_sep']);

            $value = @explode($locale['mon_decimal_point'], $value); 

            $n = strlen($prefix) + strlen($currency) + strlen($value[0]); 
            if ($left > 0 && $left > $n) { 
                $value[0] = str_repeat($flags['fillchar'], $left - $n) . $value[0]; 
            } 
            $value = implode($locale['mon_decimal_point'], $value); 
            if ($locale["{$letter}_cs_precedes"]) { 
                $value = $prefix . $currency . $space . $value . $suffix; 
            } else { 
                $value = $prefix . $value . $space . $currency . $suffix; 
            } 
            if ($width > 0) { 
                $value = str_pad($value, $width, $flags['fillchar'], $flags['isleft'] ? 
                         STR_PAD_RIGHT : STR_PAD_LEFT); 
            } 

            $format = str_replace($fmatch[0], $value, $format); 
        } 
        return $format; 
    } 

    function number  ($number  , $decimals = 2 , $dec_point = '.' , $sep = ',', $group=3   ){
        // Arbitrary-precision number formatting:
        // Takes plain-format, arbitrary-length decimal string (eg: '123456789123456789.123456').
        // Takes the same parameters as PHP's native number_format plus a flexible 'grouping' parameter. 
        // WARNINGS: Truncates -- does not round; not inherently locale-aware

        $num = (string) $number;   
        if (strpos($num, '.')) $num = substr($num, 0, (strpos($num, '.') + 1 + $decimals)); // truncate
        $num = explode('.',$num);
        while (strlen($num[0]) % $group) $num[0]= ' '.$num[0];
        $num[0] = str_split($num[0],$group);
        $num[0] = join($sep[0],$num[0]);
        $num[0] = trim($num[0]);
        $num = join($dec_point[0],$num);

        return $num;
    }
}

Tests:

 setlocale(LC_MONETARY, 'en_ZW'); // pick your favorite hyperinflated currency
 $string = '123456789123456789.123456';

 echo "original string: " . 
  $string . "<br>";
  // 123456789123456789.123456
 echo "float cast - " . 
  ((float) $string) . "<br>";
  // 1.23456789123E+17
 echo "number_format original: " . 
  number_format($string, 4) . "<br>";
  // 123,456,789,123,456,768.0000
 echo "number_format new: " . 
  format::number($string, 4) . "<br>";
  // 123,456,789,123,456,789.1234
 echo "money_format original: " . 
  money_format('%n', $string) . "<br>";
  // Z$ 123,456,789,123,456,784.00 
 echo "money_format new: " . 
  format::money('%n',$string) . "<br>";
  // Z$ 123,456,789,123,456,789.12
Greg