Pues si... y no...
Sí que existe una forma de hacer lo que quieres... mejor dicho, hay más de una, pero de niveles distintos a los de una solución informática basada en un algoritmo.
Verás, el tema de la separación de los miles es algo que desde hace unos años está resuelto porque, según el país donde vivas, esos separadores:
* No existirán (o no están permitidos)
* Existen, y son los que concuerdan con tu salida desea
* Existen, pero no son los que quieres.
En los sistemas operativos actuales, las diferencias entre idiomas se llama 'localizaciones'. Y el ordenador 'sabe' en qué localización estás usando el sistema de las variables de entorno.
Así, en Linux (un sistema compatible con POSIX), yo tengo mis variables de entorno que indican en qué localización estoy:
- Código: Seleccionar todo
explorer@casa:~/Documents/Software/Perl> locale
LANG=es_ES@euro
LC_CTYPE="es_ES@euro"
LC_NUMERIC="es_ES@euro"
LC_TIME="es_ES@euro"
LC_COLLATE="es_ES@euro"
LC_MONETARY="es_ES@euro"
LC_MESSAGES="es_ES@euro"
LC_PAPER="es_ES@euro"
LC_NAME="es_ES@euro"
LC_ADDRESS="es_ES@euro"
LC_TELEPHONE="es_ES@euro"
LC_MEASUREMENT="es_ES@euro"
LC_IDENTIFICATION="es_ES@euro"
LC_ALL=
Mi localización indica que es española, variante España, y que la codificación de los caracteres será compatible con el carácter del Euro (iso-8859-15).
Bien, pero analizando esta localización, nos damos cuenta de que, en cuestión de separación de miles, no está definida la agrupación de dígitos:
- Código: Seleccionar todo
explorer@casa:~/Documents/Software/Perl> LC_NUMERIC=es_ES printf "%'.2f\n" 1234567809,34
1234567809,34
cosa que no sería lo mismo si yo fuera danés:
- Código: Seleccionar todo
explorer@casa:~/Documents/Software/Perl> LC_NUMERIC=da_DK printf "%'.2f\n" 1234567809,34
1.234.567.809,34
o norteamericano:
- Código: Seleccionar todo
explorer@casa:~/Documents/Software/Perl> LC_NUMERIC=en_US printf "%'.2f\n" 1234567809,34
1,234,567,809.34
Además, fíjate que he usado LC_NUMERIC para cambiar mi localización, porque el comando printf no distingue de si queremos sacar un número o una cantidad monetaria. La localización del dinero está indicada en LC_MONETARY y deberíamos usar la función
strfmon() del C para hacer las conversiones de dinero a cadena de caracteres.
Bueno, todo esto funciona en las librerías actuales de C para los sistemas operativos modernos (excepto Microsoft, que prefiere ser incompatible, como siempre).
Pero... en el caso de Perl tenemos un 'ligero' problema: no sabe usar el flag 'radical' de conversión de miles que posee la función
printf() del sistema, por lo que un programa en Perl como este:
Using perl Syntax Highlighting
#!/usr/bin/perl
use POSIX
qw(locale_h
);
setlocale
(LC_ALL
, "es_ES.ISO8859-15");
printf "%'.2f\n", 1234567809.34;Coloreado en 0.003 segundos, usando
GeSHi 1.0.8.4
sacará en pantalla algo como esto:
- Código: Seleccionar todo
%'.2f
Vamos, que no.
Pero bueno, no todo está perdido. Puede que print, printf o sprintf no trabajen bien con la única función que nos haría falta para resolver tu problema, pero Perl tiene otras posibilidades: podemos hacer el trabajo de ese flag nosotros mismos; al fin y al cabo... todo consiste en agrupar los dígitos de salida junto con los caracteres de separación de miles.
Todo esto está documentado en la parte que comenta la función
localeconv(). Incluso viene un ejemplo de generación de los números con agrupación de miles. Pero... ese ejemplo no funciona muy bien...
El ejemplo lee la configuración actual de la separación de miles en números:
Using perl Syntax Highlighting
#!/usr/bin/perl -l
require 5.004;
use Data
::Dumper;
use POSIX
qw(locale_h
);
use strict
;
setlocale
(LC_ALL
, "en_US");
# Obtener parámetros de formateo numérico
my ($separador_de_miles, $agrupacion) =
@{localeconv
()}{'mon_thousands_sep', 'grouping'};
print Dumper localeconv
();Coloreado en 0.002 segundos, usando
GeSHi 1.0.8.4
salida:
- Código: Seleccionar todo
$VAR1 = {
'n_sep_by_space' => 0,
'thousands_sep' => ',',
'p_sep_by_space' => 0,
'grouping' => '',
'p_cs_precedes' => 1,
'int_frac_digits' => 2,
'mon_grouping' => '',
'n_sign_posn' => 1,
'currency_symbol' => '$',
'int_curr_symbol' => 'USD ',
'negative_sign' => '-',
'p_sign_posn' => 1,
'frac_digits' => 2,
'n_cs_precedes' => 1,
'decimal_point' => '.',
'mon_thousands_sep' => ',',
'mon_decimal_point' => '.'
};
en el caso de ser norteamericano. Ves que la separación de miles tanto en números como en dinero es el carácter ',', por lo que, correctamente, la expresión regular que sigue es capaz de insertar ese separador entre la agrupación de caracteres (que es (3, 3) aunque no se vea en la salida de grouping y mon_grouping).
Bueno, el problema es que en Español, el carácter de separación de miles es el '.', y ya sabemos que ese carácter tiene un significado especial dentro de las expresiones regulares. Con
setlocale(LC_ALL, "es_ES\@euro");, sale:
- Código: Seleccionar todo
$VAR1 = {
'n_sep_by_space' => 1,
'currency_symbol' => '€',
'p_sign_posn' => 1,
'negative_sign' => '-',
'int_curr_symbol' => 'EUR ',
'frac_digits' => 2,
'p_sep_by_space' => 1,
'n_cs_precedes' => 1,
'p_cs_precedes' => 1,
'decimal_point' => ',',
'mon_grouping' => '',
'int_frac_digits' => 2,
'n_sign_posn' => 1,
'mon_thousands_sep' => '.',
'mon_decimal_point' => ','
};
Por ello, no podemos sin más meterlo dentro de ella. Hay que escaparlo. Con la ayuda de los indicadores de escapado de caracteres
\Q y
\E, será fácil hacerlo.
En resumen, el programa Perl que hace la presentación de cifras en formato monetario según la localización actual del sistema en que se ejecuta debería ser este (miles.pl):
Using perl Syntax Highlighting
#!/usr/bin/perl
require 5.004;
use Data
::Dumper;
use POSIX
qw(locale_h
);
use strict
;
# Cambio de localizacion
#setlocale(LC_ALL, "es_ES\@euro");
# Obtener parámetros de formateo numérico
my ($separador_de_miles, $agrupacion) =
@{localeconv
()}{'mon_thousands_sep', 'mon_grouping'};
# Aplicar valores por defecto en caso de que falten
$separador_de_miles = '.' unless $separador_de_miles;
#print "separador: ->$separador_de_miles<-. Grupos: ->$agrupacion<-\n";
# grouping y mon_grouping son listas empaquetadas
# de pequeños números enteros (caracteres) indicando
# la agrupación (thousand_seps y mon_thousand_seps
# indican los grupos a formar) de números y cantidades
# monetarias. El significado de los enteros es:
# 255 significa que no hay más agrupación, 0 significa
# repetir la agrupación anterior, 1-254 significa usar
# este valor como actual agrupación. Agrupación va de
# derecha a izquierda (de bits bajos a altos).
# En el ejemplo de abajo sólo usamos el primer valor
# indicado (sea el que sea).
my @agrupacion;
if ( $agrupacion ) {
# Desempaquetamos la lista de agrupaciones
@agrupacion = unpack("C*", $agrupacion);
#print "Agrupación: @agrupacion\n";
} else {
# Por defecto, indicamos que la agrupación es
# de 3 en 3 dígitos
@agrupacion = (3
);
}
# Formateo de los valores pasados por línea de comandos
for ( @ARGV ) {
$_ = int; # Sólo haremos una conversión de la parte entera
1
while
s/(\d)(\d{$agrupacion[0]}($|\Q$separador_de_miles\E))/$1$separador_de_miles$2/;
print "$_\n";
}Coloreado en 0.002 segundos, usando
GeSHi 1.0.8.4
Con una entrada como:
miles.pl 1234567890, la salida es
1.234.567.890.
El funcionamiento de la expresión regular es esta:
* por defecto, Perl intenta hacer coincidencias con la parte más a la derecha del patrón, por lo que se podría decir que estamos primero mirando de derecha a izquierda
* y a la derecha hay dos opciones: o fin de de línea ($) o un carácter anterior de $separador_de_miles. Aquí estaba el problema comentado antes, por lo que necesitamos 'escapar' los posibles caracteres especiales, con \Q y \E
* delante de estos esperamos una agrupación de dígitos (\d{$agrupacion[0]})
* precedidos a su vez por al menos un dígito más (\d).
* la conversión es introducir un nuevo separador de miles delante de la agrupación encontrada.
Hay una línea comentada, la del setlocale, que, de usarse, nos permitirá mostrar distintas presentaciones según la localización que elijamos, independientemente de la del sistema. Si usamos
setlocale(LC_ALL, "en_US");, para el ejemplo anterior saldrá
1,234,567,890.
Como ves, no hay una función (al menos ahora) que resuelva el problema, pero con un par de líneas lo tienes. Básicamente, el while hace toda la conversión por lo que es casi lo único que necesitas.