Question

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

Was it helpful?

Solution 2

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

OTHER TIPS

try the NumberFormatter class

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top