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 siDecimales=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.
- ApocoparUnoParteDecimal: En castellano, cuando los números cuantifican un sustantivo masculino, se apocopa (se recorta el final de la palabra) de las decenas mayores a veinte terminadas en uno, ¿que? que en vez de poner “treinta y unoelefantes” ponemos “treinta y un elefantes”. Esta propiedad se puede setear individualmente pero también cambia junto con el valor de ConvertirDecimales ya que si queremos convertir decimales a texto, lo normal es apocopar la palabra uno, por ejemplo veintiún centavos. Tener en cuenta que se utiliza solo cuandoConvertirDecimales es verdadero.
- ApocoparUnoParteEntera: Lo mismo que la anterior pero para las unidades de nuestro número. Esta propiedad cambia junto con SeparadorDecimalSalida. Si seteamos esta última a, por ejemplo, “pesos con”, entonces se deduce que estoy cualificando algo (pesos) y la segunda palabra (con) se toma como conjunción (adverbio conjuntivo para los puristas creo), por lo que ApocoparUnoParteEnterapasa a ser true. ¿No entendiste? somos dos, veamos algunos ejemplos:
- si SeparadorDecimalSalida = ‘con’ entonces ApocoparUnoParteEntera es false y 31,2 = ‘treinta y uno con 20/100.-‘
- si SeparadorDecimalSalida = ‘euros con’ entonces ApocoparUnoParteEntera es true y 31,2 = ‘treinta y un euros con 20/100.-‘
Esta propiedad también se puede cambiar con posterioridad aSeparadorDecimalSalida si no se desea este comportamiento.
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únmil”.
- 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.
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.
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
ConversionConversion EmoticonEmoticon