Home > Enterprise >  Rounding Floating point numbers in Perl ..large inefficient but working solution
Rounding Floating point numbers in Perl ..large inefficient but working solution

Time:07-08

I had to round numbers that are a result of MySQL Querys. I didn't find a good solution for rounding in the correct way (truncating is not the correct way in my opinion).

I wrote this code that rounds float numbers and also formatted the numbers in the Spanish-Mexican way (using comma as a thousands, millions, etc.. (like 1425 -> 1,1425)

I know it is very inefficient, but it does the work in the right way. If you have any suggestions please let me know to make it more efficient.

Sorry for the Spanish variables and comments.. I'm Mexican!!! ;)

sub formatea_numero                 # recibe una cifra sin formato y le agrega comas por cada 3 digitos
{
    my($importe_orig,$dec_a_redondear) = @_;

    my @digitos_corregido;
    my $contador=0;
    my $signo;
    my $importex;
    my $decimal_con_ceros_al_inicio = 0;

    ### SIGNO ######################
    ### Obteniendo el signo si lo tiene
    if($importe_orig=~/([-| ])(.*)/)    # SI TIENE SIGNO $1 EL NUM ES $2
    {
        $signo = $1;
        $importex = substr $importe_orig, 1;
    }
    else                                # NO TIENE SIGNO EL NUM ES $importe_orig
    {
        $importex = $importe_orig;
    }
    
    ###  DECIMALES ################
    ### Si tiene decimales.. se obtiene la parte entera y la parte decimal
    if($importex=~/\./)     # Tiene decimales
    {   
        if ($dec_a_redondear ne "" && $dec_a_redondear == 0)    # Caso Especial de que son 0 decimales a redondear
        {
            $importex=~/(.*)\.(.)(.*)/;
            $parte_entera = $1;
            $primer_decimal = $2;
            $resto_decimal = $3;
            if ($primer_decimal >= 5)
            {$parte_entera = $parte_entera 1;}          
        }
        else
        {
        
                if($importex=~/(.*)\.(. )/)     # Parte entera $1  Parte decimal $2
                {
                    $parte_entera = $1;
                    $parte_decimal = $2;

                    if ($parte_decimal=~/^(0 ).*/ ) # Caso especial si el decimal inicia en 0  ejem 3.0015 1.0000003  1.04  etc
                    {
                        $decimal_con_ceros_al_inicio =1;
                        $parte_decimal = "1" . $parte_decimal;
                    }           
                }
                
            ########### REDONDEAR DECIMALES 
            
                if ($decimal_con_ceros_al_inicio)
                {   
                    $dec_a_redondear =  1   $dec_a_redondear;
                    $num_decimales_original = 1  length $parte_decimal;
                }
                else
                {   $num_decimales_original = length $parte_decimal;}



                if ($dec_a_redondear>=0 && $dec_a_redondear < $num_decimales_original)
                {
                    
                    $parte_decimal_1 = substr($parte_decimal,0,$dec_a_redondear);   # se obtienen los digitos hasta el numero de decimales que se quiere redondear
                    $siguiente_decimal = substr($parte_decimal,$dec_a_redondear,1); # se obtiene el primer dígito a descartar .. si es mayor que 5 se le agrega uno al anterior digito
                    
                    if($siguiente_decimal >=5)
                    {
                        $largo_inicial_parte_decimal_1 = length $parte_decimal_1;           
                        $parte_decimal_1 = $parte_decimal_1  1;
                        $largo_final_parte_decimal_1 = length $parte_decimal_1;

                        if($largo_final_parte_decimal_1 <= $largo_inicial_parte_decimal_1)
                        {
                            $parte_decimal = $parte_decimal_1;
                        }
                        else
                        {
                            $parte_entera = $parte_entera   1;
                            $parte_decimal = $parte_decimal_1 - 1;
                            $parte_decimal = 0;
                        }
                    }
                    else
                    {
                        $parte_decimal = $parte_decimal_1;
                    }
                    

                    
                } # cierra if ($dec_a_redondear>0 && $dec_a_redondear < $num_decimales_original)

                    if ($decimal_con_ceros_al_inicio)
                    {   
                        $parte_decimal=~/^1(. )/;
                        $parte_decimal = $1;
                    }



                ########### TERMINA DECIMALES (redondeando) 
        }   # Cierra if ($dec_a_redondear ne "" && $dec_a_redondear == 0)   # Caso Especial de que son 0 decimales a redondear
        
    } # cierra  if($importex=~/\./)     # Tiene decimales

        ########### FORMATEANDO LOS MILES ###########

    if($importex=~/\./)     # Tiene decimales
    {       
        @digitos = split(//,$parte_entera);
    }
    else                    # No tiene decimales
    {
        @digitos = split(//,$importex);
    }

    @digitos= reverse(@digitos);    
    
    foreach $digito(@digitos)
    {   
        if ($contador ==3)
        {
            push (@digitos_corregido,",");
            push (@digitos_corregido,$digito);
            $contador=1;
        }
        else
        {
            push (@digitos_corregido,$digito);
            $contador  ;
        }
    }
    
    @digitos_corregido = reverse(@digitos_corregido);

    $importe2 = join('',@digitos_corregido);
    ### Termina de procesar la parte entera 
    
    
    ### Se integra el signo, la parte entera formateada y la parte decimal
    if($importex=~/\./)
    {
        if ($dec_a_redondear ne "" && $dec_a_redondear == 0)
        {   $importe2 = $importe2;}
        else
        {   
            if ($parte_decimal >0)
            {$importe2 = $importe2 . "." .$parte_decimal;}
            else
            {$importe2 = $importe2;}
        }
    }

    if($importe_orig=~/([-| ])(.*)/)
    {
        $importe2 = $signo . $importe2 ;        
    }

    return $importe2;
} # cierra sub formatea_numero                  # recibe una cifra sin formato y le agrega comas por cada 3 digitos

CodePudding user response:

That seems to be a lot of code to round a number. I can't quite follow all details in Spanish but if you have no special criteria then basics may suffice, with sprintf

$num = sprintf "%.2f", $num  if $num =~ /\./;

(This would add .00 to an integer so I condition it on having .. It'd better be a legit number.)

The sprintf uses round half to even, per the IEEE754 round to nearest (integer) ties to even rule. (Windows may differ as it doesn't respect the IEEE spec but Strawberry Perl does round to even, thanks to ikegami for the note.)

There are also libraries, like Math::Round or Math::BigFloat. Please research this topic with care, in particular if your application is sensitive to details (like financial or scientific ones can be), since work with floating point is fraught with tricky points. See also perlfaq4 on rounding (etc)

Then you can add "thousands separator" (using , as desired), by replacing every triplet of digits by comma itself, from the back (in the substring up to the decimal point). Best wrap that in a sub

sub commify {
    local $_ = shift;  
    1 while s/^([- ]?[0-9] )([0-9]{3})/$1,$2/;   
    return $_;
}

See perlfaq5 for more

  • Related