Indefectiblemente en algún momento necesitamos o vamos a necesitar código para transformar un número a su numeral cardinal, o sea: 44 en “Cuarenta y cuatro”. Este es otro caso en donde buscamos en google “numeros a letras” o algo así esperando una solución hecha. Bueno, aca la mía: Bajar Numalet, Esta es una solución de VS 2005 con una clase en C#, en VB, un form para probar la clase y un sencillo test unitario para comprobar que las clases en ambos lenguajes funcionan igual.
Al final del post está el código de la clase en C# y en VB para copiarlo directamente.
¿por que usar esta clase y no otra? realmente no hay ningún motivo en especial, cualquier código que resuelva este trivial tema esta bien. La clase que presento genera números acorde a la especificación de la RAE, respetando acentos, apócopes, etc.
Por otro lado, todos los algoritmos que vi generan la salida según el uso del país de quien lo programa, y si queremos otra salida debemos copiar el algoritmo o perder la configuración anterior, asique agregué algunas propiedades que permiten configurar la salida con bastante libertad para cada instancia de la clase.
Hechas estas aclaraciones, veamos un ejemplo sencillo:
C#: Numalet.ToCardinal(8,25);
VB: Numalet.ToCardinal(8,25)
retorna:
ocho con 25/100.-
No hace falta instanciar la clase ya que el método ToCardinal es estático, ahora, si queremos una salida distinta (ahora si instanciando la clase), podemos jugar con las siguientes propiedades:
- CultureInfo: Permite especificar que configuración regional se utiliza para parsear los números en la sobrecarga que toma strings, en nuestro caso nos importa qué separador decimal (punto o coma) reconocerá, por defecto está seteado en la configuración del thread en que corre el proceso, para cambiarlo de manera que reconozca el punto, por caso, el valor de la propiedad podría ser new CultureInfo(“en-US”). El método estático ToCardinal tiene opcionalmente una sobrecarga que acepta un CultureInfo, por lo que en una máquina donde el separador decimal es la coma, podemos traducir un número que tiene punto como separador decimal de la siguiente manera:
Numalet.ToCardinal(“15.2″,CultureInfo(”en-US”));
Lo que retorna: “quince con 20/100.-”.
- SeparadorDecimalSalida: Es el texto que aparece entre la parte entera y la decimal, por defecto “con”, por ejemplo: 2,28 = Dos con 28/100.-
- LetraCapital: Por defecto false, permite obtener “quince” o “Quince“.
- ConvertirDecimales: Por defecto false, permite especificar si los decimales se muestran como número o como texto.
- MascaraSalidaDecimal: Es la forma en la que se muestran los decimales, utiliza una cadena para dar formato, por defecto es “00/100.-”.
El comportamiento varía según se estén convirtiendo o no los decimales. Para ello he implementado la siguiente lógica: Si el valor de MascaraSalidaDecimal comienza con ‘#’ o ‘0′, se interpreta como una máscara ‘numérica’ y se utiliza como máscara para los decimales, por ejemplo el caso por defecto (00/100.-).
Si en cambio la cadena empieza por cualquier otro caracter, se toma como literal y se agrega al final del resultado. Por ejemplo: MascaraSalidaDecimal = “centavos” => 45,28 = cuarenta y cinco con 28 centavos.
Hay que notar que todos los caracteres posteriores a los ‘0′ o ‘#’ se toman como literales, internamente se entrecomillan con (‘ ) comilla simple, por lo tanto, si queremos que aparezca una comilla simple en la máscara, debemos escribirla doble, por ej. si MascaraSalidaDecimal = litros de Jack Daniel’’s, (y SeparadorDecimalSalida = ‘coma’ ) entonces 1,2 = uno coma dos litros de Jack Daniel’s.
- Decimales: Indica la posición decimal que se utilizará para redondear los decimales al convertirlos a enteros. Recordemos que al pasar un número a su nombre, los decimales se expresan como un entero (Ej. 0,21 = ‘cero con 21 centésimas’, el 21 es entero), pero en un número como 8,54613654 ¿cual será el entero que represente los decimales?. Bueno, es lo que se especifica con esta propiedad, por ejemplo si Decimales=2 (valor por defecto) entonces 8,54613654 = ‘ocho con 55/100.-’
Esta propiedad cambia automáticamente al variar MascaraSalidaDecimal. La cantidad de caracteres ‘0′ o ‘#’ hasta el primer caracter distinto se toma como valor para Decimales. O sea, si MascaraSalidaDecimal = “##0 sobre mil”, entonces Decimales = 3, por lo que 456,45667 se convierte en ‘cuatrocientos cincuenta y seis con 457 sobre mil’
Tener en cuenta que el funcionamiento de MascaraSalidaDecimal es un poco diferente que las cadenas de formato del framework, por ejemplo, es lo mismo en el framework hacer #/100 que ###/100, pero en nuestro caso sirve para establecer que queremos redondear en tres decimales.
Sea cual sea el valor de MascaraSalidaDecimal podemos cambiar la propiedad Decimales con posterioridad y a nuestro gusto.
El código tiene en cuenta que:
- Los números 16,21,22,23 y 26 se acentúan.
- El 21 cuando se apocopa se acentúa (veintiún).
- Cuando cuantificamos miles, cientos de miles o millones, hay que apocopar la palabra o terminación “uno”, por ejemplo 21 es “veintiuno”, pero 21.000 es “veintiún mil”.
- Permite convertir números mayores o iguales a cero y menores que un billón. o sea, el mayor número permitido es 999.999.999.999 ¿te alcanza?
No tiene en cuenta el género femenino, por ejemplo no genera “quinientas mujeres”. Se puede tocar el código para tener en cuenta esto, pero hay que tener cuidado porque los millones son siempre masculinos: “quinientos millones quinientas mil mujeres”, y los apocopés no se utilizan en numéros femeninos, por ej. ‘treinta y una montañas’ y no ‘treinta y un montañas’. Si alguien lo utiliza lo agrego.
Rendimiento:
La clase convierte 1.000.000 de números generados al azar en menos de 7 segundos en mi máquina (P4). Casi no hay ventaja en instanciar la clase, conviene hacerlo solo si vamos a variar la configuración.
Algunos ejemplos:
MessageBox.Show(Numalet.ToCardinal("18,25"));
//dieciocho con 25/100.-
//Si tenemos el número en un string con otro separador decimal (por ejemplo punto):
MessageBox.Show(Numalet.ToCardinal("155.38", new CultureInfo("en-US")));
//ciento cincuenta y cinco con 38/100.-
//instanciando la clase podemos generar salidas variadas
Numalet let;
let = new Numalet();
//un porcentaje como he visto en algunos documentos de caracter legal:
let.MascaraSalidaDecimal = "por ciento";
let.SeparadorDecimalSalida = "con";
let.ConvertirDecimales = true;
MessageBox.Show(let.ToCustomCardinal(21.2));
//veintiuno con veinte por ciento
let = null;
let = new Numalet();
//al uso en México (creo):
let.MascaraSalidaDecimal = "00/100 M.N.";
let.SeparadorDecimalSalida = "pesos";
//observar que sin esta propiedad queda "veintiuno pesos" en vez de "veintiún pesos":
let.ApocoparUnoParteEntera = true;
MessageBox.Show("Son: " + let.ToCustomCardinal(1121.24));
//Son: un mil ciento veintiún pesos 24/100 M.N.
//algo más raro
let.MascaraSalidaDecimal = "###0 dracmas";
//###0 quiere hace que se redondee a 4 decimales y no se muestren los ceros a la izquierda,
//en cambio 0000 haría que, aparte de redondear en 4, se muestren los ceros a la izquierda.
let.SeparadorDecimalSalida = "talentos y";
let.LetraCapital = true;
MessageBox.Show(let.ToCustomCardinal(12.085));
//Doce talentos y 850 dracmas
//una variación del anterior redondeando decimales a mano
let.ConvertirDecimales = true;
//redondeando en cuatro decimales
let.Decimales = 4;
let.MascaraSalidaDecimal = "dracmas";
MessageBox.Show(let.ToCustomCardinal(21.50028354));
//Veintiún talentos y cinco mil tres dracmas
let = null;
Para quien quiera un comportamiendo fijo distinto al que tiene la clase, puede cambiar los valores de las constantes en la clase.
Espero que les sirva y saludos.
Clase en C#:
using System;
using System.Text;
using System.Globalization;
/// <summary>
/// Convierte números en su expresión numérica a su numeral cardinal
/// </summary>
public sealed class Numalet
{
#region Miembros estáticos
private const int UNI = 0, DIECI = 1, DECENA = 2, CENTENA = 3;
private static string[,] _matriz = new string[CENTENA + 1, 10]
{
{null," uno", " dos", " tres", " cuatro", " cinco", " seis", " siete", " ocho", " nueve"},
{" diez"," once"," doce"," trece"," catorce"," quince"," dieciséis"," diecisiete"," dieciocho"," diecinueve"},
{null,null,null," treinta"," cuarenta"," cincuenta"," sesenta"," setenta"," ochenta"," noventa"},
{null,null,null,null,null," quinientos",null," setecientos",null," novecientos"}
};
private const Char sub = (Char)26;
//Cambiar acá si se quiere otro comportamiento en los métodos de clase
public const String SeparadorDecimalSalidaDefault = "con";
public const String MascaraSalidaDecimalDefault = "00'/100.-'";
public const Int32 DecimalesDefault = 2;
public const Boolean LetraCapitalDefault = false;
public const Boolean ConvertirDecimalesDefault = false;
public const Boolean ApocoparUnoParteEnteraDefault = false;
public const Boolean ApocoparUnoParteDecimalDefault = false;
#endregion
#region Propiedades
private Int32 _decimales = DecimalesDefault;
private CultureInfo _cultureInfo = CultureInfo.CurrentCulture;
private String _separadorDecimalSalida = SeparadorDecimalSalidaDefault;
private Int32 _posiciones = DecimalesDefault;
private String _mascaraSalidaDecimal, _mascaraSalidaDecimalInterna = MascaraSalidaDecimalDefault;
private Boolean _esMascaraNumerica = true;
private Boolean _letraCapital = LetraCapitalDefault;
private Boolean _convertirDecimales = ConvertirDecimalesDefault;
private Boolean _apocoparUnoParteEntera = false;
private Boolean _apocoparUnoParteDecimal;
/// <summary>
/// Indica la cantidad de decimales que se pasarán a entero para la conversión
/// </summary>
/// <remarks>Esta propiedad cambia al cambiar MascaraDecimal por un valor que empieze con '0'</remarks>
public Int32 Decimales
{
get { return _decimales; }
set
{
if (value > 10) throw new ArgumentException(value.ToString() + " excede el número máximo de decimales admitidos, solo se admiten hasta 10.");
_decimales = value;
}
}
/// <summary>
/// Objeto CultureInfo utilizado para convertir las cadenas de entrada en números
/// </summary>
public CultureInfo CultureInfo
{
get { return _cultureInfo; }
set { _cultureInfo = value; }
}
/// <summary>
/// Indica la cadena a intercalar entre la parte entera y la decimal del número
/// </summary>
public String SeparadorDecimalSalida
{
get { return _separadorDecimalSalida; }
set
{
_separadorDecimalSalida = value;
//Si el separador decimal es compuesto, infiero que estoy cuantificando algo,
//por lo que apocopo el "uno" convirtiéndolo en "un"
if (value.Trim().IndexOf(" ") > 0)
_apocoparUnoParteEntera = true;
else _apocoparUnoParteEntera = false;
}
}
/// <summary>
/// Indica el formato que se le dara a la parte decimal del número
/// </summary>
public String MascaraSalidaDecimal
{
get
{
if (!String.IsNullOrEmpty(_mascaraSalidaDecimal))
return _mascaraSalidaDecimal;
else return "";
}
set
{
//determino la cantidad de cifras a redondear a partir de la cantidad de '0' o '#'
//que haya al principio de la cadena, y también si es una máscara numérica
int i = 0;
while (i < value.Length
&& (value[i] == '0')
| value[i] == '#')
i++;
_posiciones = i;
if (i > 0)
{
_decimales = i;
_esMascaraNumerica = true;
}
else _esMascaraNumerica = false;
_mascaraSalidaDecimal = value;
if (_esMascaraNumerica)
_mascaraSalidaDecimalInterna = value.Substring(0, _posiciones) + "'"
+ value.Substring(_posiciones)
.Replace("''", sub.ToString())
.Replace("'", String.Empty)
.Replace(sub.ToString(), "'") + "'";
else
_mascaraSalidaDecimalInterna = value
.Replace("''", sub.ToString())
.Replace("'", String.Empty)
.Replace(sub.ToString(), "'");
}
}
/// <summary>
/// Indica si la primera letra del resultado debe estár en mayúscula
/// </summary>
public Boolean LetraCapital
{
get { return _letraCapital; }
set { _letraCapital = value; }
}
/// <summary>
/// Indica si se deben convertir los decimales a su expresión nominal
/// </summary>
public Boolean ConvertirDecimales
{
get { return _convertirDecimales; }
set
{
_convertirDecimales = value;
_apocoparUnoParteDecimal = value;
if (value)
{// Si la máscara es la default, la borro
if (_mascaraSalidaDecimal == MascaraSalidaDecimalDefault)
MascaraSalidaDecimal = "";
}
else if (String.IsNullOrEmpty(_mascaraSalidaDecimal))
//Si no hay máscara dejo la default
MascaraSalidaDecimal = MascaraSalidaDecimalDefault;
}
}
/// <summary>
/// Indica si de debe cambiar "uno" por "un" en las unidades.
/// </summary>
public Boolean ApocoparUnoParteEntera
{
get { return _apocoparUnoParteEntera; }
set { _apocoparUnoParteEntera = value; }
}
/// <summary>
/// Determina si se debe apococopar el "uno" en la parte decimal
/// </summary>
/// <remarks>El valor de esta propiedad cambia al setear ConvertirDecimales</remarks>
public Boolean ApocoparUnoParteDecimal
{
get { return _apocoparUnoParteDecimal; }
set { _apocoparUnoParteDecimal = value; }
}
#endregion
#region Constructores
public Numalet()
{
MascaraSalidaDecimal = MascaraSalidaDecimalDefault;
SeparadorDecimalSalida = SeparadorDecimalSalidaDefault;
LetraCapital = LetraCapitalDefault;
ConvertirDecimales = _convertirDecimales;
}
public Numalet(Boolean ConvertirDecimales, String MascaraSalidaDecimal, String SeparadorDecimalSalida, Boolean LetraCapital)
{
if (!String.IsNullOrEmpty(MascaraSalidaDecimal))
this.MascaraSalidaDecimal = MascaraSalidaDecimal;
if (!String.IsNullOrEmpty(SeparadorDecimalSalida))
_separadorDecimalSalida = SeparadorDecimalSalida;
_letraCapital = LetraCapital;
_convertirDecimales = ConvertirDecimales;
}
#endregion
#region Conversores de instancia
public String ToCustomCardinal(Double Numero)
{ return Convertir((Decimal)Numero, _decimales, _separadorDecimalSalida, _mascaraSalidaDecimalInterna, _esMascaraNumerica, _letraCapital, _convertirDecimales, _apocoparUnoParteEntera, _apocoparUnoParteDecimal); }
public String ToCustomCardinal(String Numero)
{
Double dNumero;
if (Double.TryParse(Numero, NumberStyles.Float, _cultureInfo, out dNumero))
return ToCustomCardinal(dNumero);
else throw new ArgumentException("'" + Numero + "' no es un número válido.");
}
public String ToCustomCardinal(Decimal Numero)
{ return ToCardinal((Numero)); }
public String ToCustomCardinal(Int32 Numero)
{ return Convertir((Decimal)Numero, 0, _separadorDecimalSalida, _mascaraSalidaDecimalInterna, _esMascaraNumerica, _letraCapital, _convertirDecimales, _apocoparUnoParteEntera, false); }
#endregion
#region Conversores estáticos
public static String ToCardinal(Int32 Numero)
{
return Convertir((Decimal)Numero, 0, null, null, true, LetraCapitalDefault, ConvertirDecimalesDefault, ApocoparUnoParteEnteraDefault, ApocoparUnoParteDecimalDefault);
}
public static String ToCardinal(Double Numero)
{
return ToCardinal((Decimal)Numero);
}
public static String ToCardinal(String Numero, CultureInfo ReferenciaCultural)
{
Double dNumero;
if (Double.TryParse(Numero, NumberStyles.Float, ReferenciaCultural, out dNumero))
return ToCardinal(dNumero);
else throw new ArgumentException("'" + Numero + "' no es un número válido.");
}
public static String ToCardinal(String Numero)
{
return Numalet.ToCardinal(Numero, CultureInfo.CurrentCulture);
}
public static String ToCardinal(Decimal Numero)
{
return Convertir(Numero, DecimalesDefault, SeparadorDecimalSalidaDefault, MascaraSalidaDecimalDefault, true, LetraCapitalDefault, ConvertirDecimalesDefault, ApocoparUnoParteEnteraDefault, ApocoparUnoParteDecimalDefault);
}
#endregion
private static String Convertir(Decimal Numero, Int32 Decimales, String SeparadorDecimalSalida, String MascaraSalidaDecimal, Boolean EsMascaraNumerica, Boolean LetraCapital, Boolean ConvertirDecimales, Boolean ApocoparUnoParteEntera, Boolean ApocoparUnoParteDecimal)
{
Int64 Num;
Int32 terna, centenaTerna, decenaTerna, unidadTerna, iTerna;
String cadTerna;
StringBuilder Resultado = new StringBuilder();
Num = (Int64)Math.Abs(Numero);
if (Num >= 1000000000000 || Num < 0) throw new ArgumentException("El número '" + Numero.ToString() + "' excedió los límites del conversor: [0;1.000.000.000.000)");
if (Num == 0)
Resultado.Append(" cero");
else
{
iTerna = 0;
while (Num > 0)
{
iTerna++;
cadTerna = String.Empty;
terna = (Int32)(Num % 1000);
centenaTerna = (Int32)(terna / 100);
decenaTerna = terna % 100;
unidadTerna = terna % 10;
if ((decenaTerna > 0) && (decenaTerna < 10))
cadTerna = _matriz[UNI, unidadTerna] + cadTerna;
else if ((decenaTerna >= 10) && (decenaTerna < 20))
cadTerna = cadTerna + _matriz[DIECI, unidadTerna];
else if (decenaTerna == 20)
cadTerna = cadTerna + " veinte";
else if ((decenaTerna > 20) && (decenaTerna < 30))
cadTerna = " veinti" + _matriz[UNI, unidadTerna].Substring(1);
else if ((decenaTerna >= 30) && (decenaTerna < 100))
if (unidadTerna != 0)
cadTerna = _matriz[DECENA, (Int32)(decenaTerna / 10)] + " y" + _matriz[UNI, unidadTerna] + cadTerna;
else
cadTerna += _matriz[DECENA, (Int32)(decenaTerna / 10)];
switch (centenaTerna)
{
case 1:
if (decenaTerna > 0) cadTerna = " ciento" + cadTerna;
else cadTerna = " cien" + cadTerna;
break;
case 5:
case 7:
case 9:
cadTerna = _matriz[CENTENA, (Int32)(terna / 100)] + cadTerna;
break;
default:
if ((Int32)(terna / 100) > 1) cadTerna = _matriz[UNI, (Int32)(terna / 100)] + "cientos" + cadTerna;
break;
}
//Reemplazo el 'uno' por 'un' si no es en las únidades o si se solicító apocopar
if ((iTerna > 1 | ApocoparUnoParteEntera) && decenaTerna == 21)
cadTerna = cadTerna.Replace("veintiuno", "veintiún");
else if ((iTerna > 1 | ApocoparUnoParteEntera) && unidadTerna == 1 && decenaTerna != 11)
cadTerna = cadTerna.Substring(0, cadTerna.Length - 1);
//Acentúo 'veintidós', 'veintitrés' y 'veintiséis'
else if (decenaTerna == 22) cadTerna = cadTerna.Replace("veintidos", "veintidós");
else if (decenaTerna == 23) cadTerna = cadTerna.Replace("veintitres", "veintitrés");
else if (decenaTerna == 26) cadTerna = cadTerna.Replace("veintiseis", "veintiséis");
//Completo miles y millones
switch (iTerna)
{
case 3:
if (Numero < 2000000) cadTerna += " millón";
else cadTerna += " millones";
break;
case 2:
case 4:
if (terna > 0) cadTerna += " mil";
break;
}
Resultado.Insert(0, cadTerna);
Num = (Int32)(Num / 1000);
} //while
}
//Se agregan los decimales si corresponde
if (Decimales > 0)
{
Resultado.Append(" " + SeparadorDecimalSalida + " ");
Int32 EnteroDecimal = (Int32)Math.Round((Double)(Numero - (Int64)Numero) * Math.Pow(10, Decimales), 0);
if (ConvertirDecimales)
{
Boolean esMascaraDecimalDefault = MascaraSalidaDecimal == MascaraSalidaDecimalDefault;
Resultado.Append(Convertir((Decimal)EnteroDecimal, 0, null, null, EsMascaraNumerica, false, false, (ApocoparUnoParteDecimal && !EsMascaraNumerica/*&& !esMascaraDecimalDefault*/), false) + " "
+ (EsMascaraNumerica ? "" : MascaraSalidaDecimal));
}
else
if (EsMascaraNumerica) Resultado.Append(EnteroDecimal.ToString(MascaraSalidaDecimal));
else Resultado.Append(EnteroDecimal.ToString() + " " + MascaraSalidaDecimal);
}
//Se pone la primer letra en mayúscula si corresponde y se retorna el resultado
if (LetraCapital)
return Resultado[1].ToString().ToUpper() + Resultado.ToString(2, Resultado.Length - 2);
else
return Resultado.ToString().Substring(1);
}
}
Clase en VB:
Imports System
Imports System.Text
Imports System.Globalization
''' <summary>
''' Convierte números en su expresión numérica a su numeral cardinal
''' </summary>
Public NotInheritable Class Numalet
#Region "Miembros estáticos"
Private Const UNI As Integer = 0, DIECI As Integer = 1, DECENA As Integer = 2, CENTENA As Integer = 3
Private Shared _matriz As String(,) = New String(CENTENA, 9) { _
{Nothing, " uno", " dos", " tres", " cuatro", " cinco", " seis", " siete", " ocho", " nueve"}, _
{" diez", " once", " doce", " trece", " catorce", " quince", " dieciséis", " diecisiete", " dieciocho", " diecinueve"}, _
{Nothing, Nothing, Nothing, " treinta", " cuarenta", " cincuenta", " sesenta", " setenta", " ochenta", " noventa"}, _
{Nothing, Nothing, Nothing, Nothing, Nothing, " quinientos", Nothing, " setecientos", Nothing, " novecientos"}}
Private Const [sub] As Char = CChar(ChrW(26))
'Cambiar acá si se quiere otro comportamiento en los métodos de clase
Public Const SeparadorDecimalSalidaDefault As String = "con"
Public Const MascaraSalidaDecimalDefault As String = "00'/100.-'"
Public Const DecimalesDefault As Int32 = 2
Public Const LetraCapitalDefault As Boolean = False
Public Const ConvertirDecimalesDefault As Boolean = False
Public Const ApocoparUnoParteEnteraDefault As Boolean = False
Public Const ApocoparUnoParteDecimalDefault As Boolean = False
#End Region
#Region "Propiedades"
Private _decimales As Int32 = DecimalesDefault
Private _cultureInfo As CultureInfo = Globalization.CultureInfo.CurrentCulture
Private _separadorDecimalSalida As String = SeparadorDecimalSalidaDefault
Private _posiciones As Int32 = DecimalesDefault
Private _mascaraSalidaDecimal As String, _mascaraSalidaDecimalInterna As String = MascaraSalidaDecimalDefault
Private _esMascaraNumerica As Boolean = True
Private _letraCapital As Boolean = LetraCapitalDefault
Private _convertirDecimales As Boolean = ConvertirDecimalesDefault
Private _apocoparUnoParteEntera As Boolean = False
Private _apocoparUnoParteDecimal As Boolean
''' <summary>
''' Indica la cantidad de decimales que se pasarán a entero para la conversión
''' </summary>
''' <remarks>Esta propiedad cambia al cambiar MascaraDecimal por un valor que empieze con '0'</remarks>
Public Property Decimales() As Int32
Get
Return _decimales
End Get
Set(ByVal value As Int32)
If value > 10 Then
Throw New ArgumentException(value.ToString() + " excede el número máximo de decimales admitidos, solo se admiten hasta 10.")
End If
_decimales = value
End Set
End Property
''' <summary>
''' Objeto CultureInfo utilizado para convertir las cadenas de entrada en números
''' </summary>
Public Property CultureInfo() As CultureInfo
Get
Return _cultureInfo
End Get
Set(ByVal value As CultureInfo)
_cultureInfo = value
End Set
End Property
''' <summary>
''' Indica la cadena a intercalar entre la parte entera y la decimal del número
''' </summary>
Public Property SeparadorDecimalSalida() As String
Get
Return _separadorDecimalSalida
End Get
Set(ByVal value As String)
_separadorDecimalSalida = value
'Si el separador decimal es compuesto, infiero que estoy cuantificando algo,
'por lo que apocopo el "uno" convirtiéndolo en "un"
If value.Trim().IndexOf(" ") > 0 Then
_apocoparUnoParteEntera = True
Else
_apocoparUnoParteEntera = False
End If
End Set
End Property
''' <summary>
''' Indica el formato que se le dara a la parte decimal del número
''' </summary>
Public Property MascaraSalidaDecimal() As String
Get
If Not [String].IsNullOrEmpty(_mascaraSalidaDecimal) Then
Return _mascaraSalidaDecimal
Else
Return ""
End If
End Get
Set(ByVal value As String)
'determino la cantidad de cifras a redondear a partir de la cantidad de '0' o ''
'que haya al principio de la cadena, y también si es una máscara numérica
Dim i As Integer = 0
While i < value.Length AndAlso (value(i) = "0"c OrElse value(i) = "#")
i += 1
End While
_posiciones = i
If i > 0 Then
_decimales = i
_esMascaraNumerica = True
Else
_esMascaraNumerica = False
End If
_mascaraSalidaDecimal = value
If _esMascaraNumerica Then
_mascaraSalidaDecimalInterna = value.Substring(0, _posiciones) + "'" + value.Substring(_posiciones).Replace("''", [sub].ToString()).Replace("'", [String].Empty).Replace([sub].ToString(), "'") + "'"
Else
_mascaraSalidaDecimalInterna = value.Replace("''", [sub].ToString()).Replace("'", [String].Empty).Replace([sub].ToString(), "'")
End If
End Set
End Property
''' <summary>
''' Indica si la primera letra del resultado debe estár en mayúscula
''' </summary>
Public Property LetraCapital() As Boolean
Get
Return _letraCapital
End Get
Set(ByVal value As Boolean)
_letraCapital = value
End Set
End Property
''' <summary>
''' Indica si se deben convertir los decimales a su expresión nominal
''' </summary>
Public Property ConvertirDecimales() As Boolean
Get
Return _convertirDecimales
End Get
Set(ByVal value As Boolean)
_convertirDecimales = value
_apocoparUnoParteDecimal = value
If value Then
' Si la máscara es la default, la borro
If _mascaraSalidaDecimal = MascaraSalidaDecimalDefault Then
MascaraSalidaDecimal = ""
End If
ElseIf [String].IsNullOrEmpty(_mascaraSalidaDecimal) Then
MascaraSalidaDecimal = MascaraSalidaDecimalDefault
'Si no hay máscara dejo la default
End If
End Set
End Property
''' <summary>
''' Indica si de debe cambiar "uno" por "un" en las unidades.
''' </summary>
Public Property ApocoparUnoParteEntera() As Boolean
Get
Return _apocoparUnoParteEntera
End Get
Set(ByVal value As Boolean)
_apocoparUnoParteEntera = value
End Set
End Property
''' <summary>
''' Determina si se debe apococopar el "uno" en la parte decimal
''' </summary>
''' <remarks>El valor de esta propiedad cambia al setear ConvertirDecimales</remarks>
Public Property ApocoparUnoParteDecimal() As Boolean
Get
Return _apocoparUnoParteDecimal
End Get
Set(ByVal value As Boolean)
_apocoparUnoParteDecimal = value
End Set
End Property
#End Region
#Region "Constructores"
Public Sub New()
MascaraSalidaDecimal = MascaraSalidaDecimalDefault
SeparadorDecimalSalida = SeparadorDecimalSalidaDefault
LetraCapital = LetraCapitalDefault
ConvertirDecimales = _convertirDecimales
End Sub
Public Sub New(ByVal ConvertirDecimales As Boolean, ByVal MascaraSalidaDecimal As String, ByVal SeparadorDecimalSalida As String, ByVal LetraCapital As Boolean)
If Not [String].IsNullOrEmpty(MascaraSalidaDecimal) Then
Me.MascaraSalidaDecimal = MascaraSalidaDecimal
End If
If Not [String].IsNullOrEmpty(SeparadorDecimalSalida) Then
_separadorDecimalSalida = SeparadorDecimalSalida
End If
_letraCapital = LetraCapital
_convertirDecimales = ConvertirDecimales
End Sub
#End Region
#Region "Conversores de instancia"
Public Function ToCustomCardinal(ByVal Numero As Double) As String
Return Convertir(Convert.ToDecimal(Numero), _decimales, _separadorDecimalSalida, _mascaraSalidaDecimalInterna, _esMascaraNumerica, _letraCapital, _
_convertirDecimales, _apocoparUnoParteEntera, _apocoparUnoParteDecimal)
End Function
Public Function ToCustomCardinal(ByVal Numero As String) As String
Dim dNumero As Double
If [Double].TryParse(Numero, NumberStyles.Float, _cultureInfo, dNumero) Then
Return ToCustomCardinal(dNumero)
Else
Throw New ArgumentException("'" + Numero + "' no es un número válido.")
End If
End Function
Public Function ToCustomCardinal(ByVal Numero As Decimal) As String
Return ToCardinal(Numero)
End Function
Public Function ToCustomCardinal(ByVal Numero As Int32) As String
Return Convertir(Convert.ToDecimal(Numero), 0, _separadorDecimalSalida, _mascaraSalidaDecimalInterna, _esMascaraNumerica, _letraCapital, _
_convertirDecimales, _apocoparUnoParteEntera, False)
End Function
#End Region
#Region "Conversores estáticos"
Public Shared Function ToCardinal(ByVal Numero As Int32) As String
Return Convertir(Convert.ToDecimal(Numero), 0, Nothing, Nothing, True, LetraCapitalDefault, _
ConvertirDecimalesDefault, ApocoparUnoParteEnteraDefault, ApocoparUnoParteDecimalDefault)
End Function
Public Shared Function ToCardinal(ByVal Numero As Double) As String
Return Convertir(Convert.ToDecimal(Numero), DecimalesDefault, SeparadorDecimalSalidaDefault, MascaraSalidaDecimalDefault, True, LetraCapitalDefault, _
ConvertirDecimalesDefault, ApocoparUnoParteEnteraDefault, ApocoparUnoParteDecimalDefault)
End Function
Public Shared Function ToCardinal(ByVal Numero As String, ByVal ReferenciaCultural As CultureInfo) As String
Dim dNumero As Double
If [Double].TryParse(Numero, NumberStyles.Float, ReferenciaCultural, dNumero) Then
Return ToCardinal(dNumero)
Else
Throw New ArgumentException("'" + Numero + "' no es un número válido.")
End If
End Function
Public Shared Function ToCardinal(ByVal Numero As String) As String
Return Numalet.ToCardinal(Numero, CultureInfo.CurrentCulture)
End Function
Public Shared Function ToCardinal(ByVal Numero As Decimal) As String
Return ToCardinal(Convert.ToDouble(Numero))
End Function
#End Region
Private Shared Function Convertir(ByVal Numero As Decimal, ByVal Decimales As Int32, ByVal SeparadorDecimalSalida As String, ByVal MascaraSalidaDecimal As String, ByVal EsMascaraNumerica As Boolean, ByVal LetraCapital As Boolean, _
ByVal ConvertirDecimales As Boolean, ByVal ApocoparUnoParteEntera As Boolean, ByVal ApocoparUnoParteDecimal As Boolean) As String
Dim Num As Int64
Dim terna As Int32, centenaTerna As Int32, decenaTerna As Int32, unidadTerna As Int32, iTerna As Int32
Dim cadTerna As String
Dim Resultado As New StringBuilder()
Num = Math.Floor(Math.Abs(Numero))
If Num >= 1000000000001 OrElse Num < 0 Then
Throw New ArgumentException("El número '" + Numero.ToString() + "' excedió los límites del conversor: [0;1.000.000.000.001]")
End If
If Num = 0 Then
Resultado.Append(" cero")
Else
iTerna = 0
Do Until Num = 0
iTerna += 1
cadTerna = String.Empty
terna = Num Mod 1000
centenaTerna = Int(terna / 100)
decenaTerna = terna - centenaTerna * 100 'Decena junto con la unidad
unidadTerna = (decenaTerna - Math.Floor(decenaTerna / 10) * 10)
Select Case decenaTerna
Case 1 To 9
cadTerna = _matriz(UNI, unidadTerna) + cadTerna
Case 10 To 19
cadTerna = cadTerna + _matriz(DIECI, unidadTerna)
Case 20
cadTerna = cadTerna + " veinte"
Case 21 To 29
cadTerna = " veinti" + _matriz(UNI, unidadTerna).Substring(1)
Case 30 To 99
If unidadTerna <> 0 Then
cadTerna = _matriz(DECENA, Int(decenaTerna / 10)) + " y" + _matriz(UNI, unidadTerna) + cadTerna
Else
cadTerna += _matriz(DECENA, Int(decenaTerna / 10))
End If
End Select
Select Case centenaTerna
Case 1
If decenaTerna > 0 Then
cadTerna = " ciento" + cadTerna
Else
cadTerna = " cien" + cadTerna
End If
Exit Select
Case 5, 7, 9
cadTerna = _matriz(CENTENA, Int(terna / 100)) + cadTerna
Exit Select
Case Else
If Int(terna / 100) > 1 Then
cadTerna = _matriz(UNI, Int(terna / 100)) + "cientos" + cadTerna
End If
Exit Select
End Select
'Reemplazo el 'uno' por 'un' si no es en las únidades o si se solicító apocopar
If (iTerna > 1 OrElse ApocoparUnoParteEntera) AndAlso decenaTerna = 21 Then
cadTerna = cadTerna.Replace("veintiuno", "veintiún")
ElseIf (iTerna > 1 OrElse ApocoparUnoParteEntera) AndAlso unidadTerna = 1 AndAlso decenaTerna <> 11 Then
cadTerna = cadTerna.Substring(0, cadTerna.Length - 1)
'Acentúo 'veintidós', 'veintitrés' y 'veintiséis'
ElseIf decenaTerna = 22 Then
cadTerna = cadTerna.Replace("veintidos", "veintidós")
ElseIf decenaTerna = 23 Then
cadTerna = cadTerna.Replace("veintitres", "veintitrés")
ElseIf decenaTerna = 26 Then
cadTerna = cadTerna.Replace("veintiseis", "veintiséis")
End If
'Completo miles y millones
Select Case iTerna
Case 3
If Numero < 2000000 Then
cadTerna += " millón"
Else
cadTerna += " millones"
End If
Case 2, 4
If terna > 0 Then cadTerna += " mil"
End Select
Resultado.Insert(0, cadTerna)
Num = Int(Num / 1000)
Loop
End If
'Se agregan los decimales si corresponde
If Decimales > 0 Then
Resultado.Append(" " + SeparadorDecimalSalida + " ")
Dim EnteroDecimal As Int32 = Int(Math.Round((Numero - Int(Numero)) * Math.Pow(10, Decimales)))
If ConvertirDecimales Then
Dim esMascaraDecimalDefault As Boolean = MascaraSalidaDecimal = MascaraSalidaDecimalDefault
Resultado.Append(Convertir(Convert.ToDecimal(EnteroDecimal), 0, Nothing, Nothing, EsMascaraNumerica, False, _
False, (ApocoparUnoParteDecimal AndAlso Not EsMascaraNumerica), False) + " " + (IIf(EsMascaraNumerica, "", MascaraSalidaDecimal)))
ElseIf EsMascaraNumerica Then
Resultado.Append(EnteroDecimal.ToString(MascaraSalidaDecimal))
Else
Resultado.Append(EnteroDecimal.ToString() + " " + MascaraSalidaDecimal)
End If
End If
'Se pone la primer letra en mayúscula si corresponde y se retorna el resultado
If LetraCapital Then
Return Resultado(1).ToString().ToUpper() + Resultado.ToString(2, Resultado.Length - 2)
Else
Return Resultado.ToString().Substring(1)
End If
End Function
End Class