Alternativa para money_format do PHP

Artigo que apresenta a implementação de uma função PHP alternativa para a função money_format, que não está disponível em todas as versões do PHP.

O script a seguir é destinado àqueles que utilizaram a função money_format no Linux e ficaram decepcionados ao notar que a função não estava disponível para Windows. Criei este script para tentar simular o comportamento da função original. Entretanto, nem todas as opções foram implementadas. Caso queira ajudar, fique a vontade, pois o código é livre e gratuito.

Linguagem: PHP

Copyright 2010 Rubens Takiguti Ribeiro

Licença: LGPL 3 ou superior

/**
 * Formata um numero em notacao de moeda, assim como a funcao money_format do PHP
 * @author Rubens Takiguti Ribeiro
 * @see http://php.net/manual/en/function.money-format.php
 * @param string $formato Formato aceito por money_format
 * @param float $valor Valor monetario
 * @return string Valor formatado
 */
function my_money_format($formato, $valor) {

    // Se a funcao money_format esta disponivel: usa-la
    if (function_exists('money_format')) {
        return money_format($formato, $valor);
    }

    // Se nenhuma localidade foi definida, formatar com number_format
    if (setlocale(LC_MONETARY, 0) == 'C') {
        return number_format($valor, 2);
    }

    // Obter dados da localidade
    $locale = localeconv();

    // Extraindo opcoes do formato
    $regex = '/^'.             // Inicio da Expressao
             '%'.              // Caractere %
             '(?:'.            // Inicio das Flags opcionais
             '\=([\w\040])'.   // Flag =f
             '|'.
             '([\^])'.         // Flag ^
             '|'.
             '(\+|\()'.        // Flag + ou (
             '|'.
             '(!)'.            // Flag !
             '|'.
             '(-)'.            // Flag -
             ')*'.             // Fim das flags opcionais
             '(?:([\d]+)?)'.   // W  Largura de campos
             '(?:#([\d]+))?'.  // #n Precisao esquerda
             '(?:\.([\d]+))?'. // .p Precisao direita
             '([in%])'.        // Caractere de conversao
             '$/';             // Fim da Expressao

    if (!preg_match($regex, $formato, $matches)) {
        trigger_error('Formato invalido: '.$formato, E_USER_WARNING);
        return $valor;
    }

    // Recolhendo opcoes do formato
    $opcoes = array(
        'preenchimento'   => ($matches[1] !== '') ? $matches[1] : ' ',
        'nao_agrupar'     => ($matches[2] == '^'),
        'usar_sinal'      => ($matches[3] == '+'),
        'usar_parenteses' => ($matches[3] == '('),
        'ignorar_simbolo' => ($matches[4] == '!'),
        'alinhamento_esq' => ($matches[5] == '-'),
        'largura_campo'   => ($matches[6] !== '') ? (int)$matches[6] : 0,
        'precisao_esq'    => ($matches[7] !== '') ? (int)$matches[7] : false,
        'precisao_dir'    => ($matches[8] !== '') ? (int)$matches[8] : $locale['int_frac_digits'],
        'conversao'       => $matches[9]
    );

    // Sobrescrever $locale
    if ($opcoes['usar_sinal'] && $locale['n_sign_posn'] == 0) {
        $locale['n_sign_posn'] = 1;
    } elseif ($opcoes['usar_parenteses']) {
        $locale['n_sign_posn'] = 0;
    }
    if ($opcoes['precisao_dir']) {
        $locale['frac_digits'] = $opcoes['precisao_dir'];
    }
    if ($opcoes['nao_agrupar']) {
        $locale['mon_thousands_sep'] = '';
    }

    // Processar formatacao
    $tipo_sinal = $valor >= 0 ? 'p' : 'n';
    if ($opcoes['ignorar_simbolo']) {
        $simbolo = '';
    } else {
        $simbolo = $opcoes['conversao'] == 'n' ? $locale['currency_symbol']
                                               : $locale['int_curr_symbol'];
    }
    $numero = number_format(abs($valor), $locale['frac_digits'], $locale['mon_decimal_point'], $locale['mon_thousands_sep']);

/*
//TODO: dar suporte a todas as flags
    list($inteiro, $fracao) = explode($locale['mon_decimal_point'], $numero);
    $tam_inteiro = strlen($inteiro);
    if ($opcoes['precisao_esq'] && $tam_inteiro < $opcoes['precisao_esq']) {
        $alinhamento = $opcoes['alinhamento_esq'] ? STR_PAD_RIGHT : STR_PAD_LEFT;
        $numero = str_pad($inteiro, $opcoes['precisao_esq'] - $tam_inteiro, $opcoes['preenchimento'], $alinhamento).
                  $locale['mon_decimal_point'].
                  $fracao;
    }
*/

    $sinal = $valor >= 0 ? $locale['positive_sign'] : $locale['negative_sign'];
    $simbolo_antes = $locale[$tipo_sinal.'_cs_precedes'];

    // Espaco entre o simbolo e o numero
    $espaco1 = $locale[$tipo_sinal.'_sep_by_space'] == 1 ? ' ' : '';

    // Espaco entre o simbolo e o sinal
    $espaco2 = $locale[$tipo_sinal.'_sep_by_space'] == 2 ? ' ' : '';

    $formatado = '';
    switch ($locale[$tipo_sinal.'_sign_posn']) {
    case 0:
        if ($simbolo_antes) {
            $formatado = '('.$simbolo.$espaco1.$numero.')';
        } else {
            $formatado = '('.$numero.$espaco1.$simbolo.')';
        }
        break;
    case 1:
        if ($simbolo_antes) {
            $formatado = $sinal.$espaco2.$simbolo.$espaco1.$numero;
        } else {
            $formatado = $sinal.$numero.$espaco1.$simbolo;
        }
        break;
    case 2:
        if ($simbolo_antes) {
            $formatado = $simbolo.$espaco1.$numero.$sinal;
        } else {
            $formatado = $numero.$espaco1.$simbolo.$espaco2.$sinal;
        }
        break;
    case 3:
        if ($simbolo_antes) {
            $formatado = $sinal.$espaco2.$simbolo.$espaco1.$numero;
        } else {
            $formatado = $numero.$espaco1.$sinal.$espaco2.$simbolo;
        }
        break;
    case 4:
        if ($simbolo_antes) {
            $formatado = $simbolo.$espaco2.$sinal.$espaco1.$numero;
        } else {
            $formatado = $numero.$espaco1.$simbolo.$espaco2.$sinal;
        }
        break;
    }

    // Se a string nao tem o tamanho minimo
    if ($opcoes['largura_campo'] > 0 && strlen($formatado) < $opcoes['largura_campo']) {
        $alinhamento = $opcoes['alinhamento_esq'] ? STR_PAD_RIGHT : STR_PAD_LEFT;
        $formatado = str_pad($formatado, $opcoes['largura_campo'], $opcoes['preenchimento'], $alinhamento);
    }

    return $formatado;
}

Exemplo de uso:

// Definindo a localidade "portugues do Brasil"
setlocale(LC_MONETARY, 'pt_BR.UTF-8', 'Portuguese_Brazil.1252');
echo my_money_format('%n', 1.9);
// R$ 1,90

// Definindo a localidade "ingles americano"
setlocale(LC_MONETARY, 'en_US.UTF-8');
echo my_money_format('%n', 1.9);
// $1.90

6 comentários

Evandromar M. disse...

Olá Rubens.
Parabéns pelo post, foi incrível, salvou meus dias.
Só um detalhe, quando você diz: "Entretando nem todas as opções foram implementadas", a quais opções você se refere.
Obrigado

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá Evandromar,

As flagas que me refiro estão lá na documentação da função money_format. Veja só:

http://br.php.net/manual/en/function.money-format.php

São só algumas flags pouco usadas (precisão à esquerda/direita). Mas para o básico a função funciona bem.

Unknown disse...

a posição estar errada!
O cerreto é:
setlocale(LC_MONETARY, 'pt_BR.UTF-8', 'Portuguese_Brazil.1252');
echo money_format('%n', 1.9);
// R$ 1,90
echo "

";

// Definindo a localidade "ingles americano"
setlocale(LC_MONETARY, 'en_US.UTF-8');
echo my_money_format( '%n', 1.9);
// $1.90
echo "

";