Numalet: convertir números a letras en C# y VB

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.

  • 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 uno elefantes” 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 cuando ConvertirDecimales 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 ApocoparUnoParteEntera pasa 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 a SeparadorDecimalSalida 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ú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

130 opiniones en “Numalet: convertir números a letras en C# y VB”

  1. Actualización 8/12/2007:

    He estado mirando el código (que programé hace ya un par de años, cuando empezaba en .NET) y me he encontrado alguna que otra semi-brutalidad, asique lo toqué un poco. Ahora funciona 30% más rápido.

    También cambié el método «ToString» por «ToCardinal» y «ToCustomString» por «ToCustomCardinal», ya que ToString debería transformar en cadena información que ya está en la clase y no un parámetro que recibe.

    Esto no afecta el resultado del algoritmo que sigue siendo exactamente el mismo.

    Para terminar agregué un par de ejemplos.

    Saludos.

    1. Muchas Gracias, el trabajo es completo sin fallas, eso si el cambio que tu comentas aun esta en el Zip, que es disponible para descargar, si lo actualizas seria bueno, gracias de nuevo me sacastes de un problema jaja.

    2. Hola amigo buenas noches mi pregunta como lo implemento en un reportview para poner el nuemro en letras gracias pro tu respuesta

  2. Hohoho…. muchas gracias compadre, esto me ha salvado el dia, ando a las carreras haciendo un pequeño módulo de facturación, necesitaba ésto y funciona perfecto!.

    Gracias por compartir!

  3. Te pasaste !!
    Que buena libreria aun que la he modificado un poco pero funciona de mil maravillas gracias por tu colaboracion me ahorras mucho tiempo de programacion

  4. ¡Gracias Richard!

    Me alegro que te haya sido util. Para vos y los que modifiquen el código en el futuro, si los cambios no son específicos de su aplicación, les agradecería que me los comenten para ver si se puede corregir o ampliar el código.

    Saludos: Alejandro

  5. Hola

    Una preguntita: ¿El codigo que publicas es una clase en visual basic ?
    Y me podrias decir como la uso dentro de un formulario, es decir, como la instancio o como la mando llamar para que me realice las acciones en el formulario:
    el codigo que uso es:

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    TB_letra.Text = «»
    If IsNumeric(TB_numero.Text) Then
    TB_letra.Text = Convertir(TB_numero.Text)
    Else
    MessageBox.Show(«Ingrese por favor números», «Aviso», MessageBoxButtons.OK, MessageBoxIcon.Information)
    End If
    TB_numero.Focus()
    TB_numero.SelectionStart = 0
    TB_numero.SelectionLength = TB_numero.ToString.Length
    End Sub

    pero me dice que no he declarado la variable convertir y se supone que ese es el nombre de la función.

    De antemano, gracias por tu ayuda

  6. Silvia:

    Si reemplazaras:
    TB_letra.Text = Convertir(TB_numero.Text)
    por:
    TB_letra.Text = Numalet.ToCardinal(TB_numero.Text)

    Debería funcionar.

    Si mirás el código, ‘Convertir’ es un método privado, o sea que es para uso interno de la clase. El método público es ‘ToCardinal’, que es el que tenés que usar.

    Aparte, el hecho de que el método sea estático (Shared) implica que lo podés llamar sin instanciar la clase, pero tenés que nombrarla antes, si no el compilador no sabe de que clase tiene que sacar el método.

    Te comento por las dudas que la palabra ‘método’ en OOP = ‘Function’ o ‘Sub’ en VB.

    Saludos y espero que con esto lo puedas hacer funcionar.

  7. Gracias Al!, estoy corto de tiempo y por eso no estoy publicando, pero cualquier cambio u observación te agradecería que me lo envíes así lo corrijo en cuanto tenga tiempo.

    Saludos

  8. Muy bueno.! 🙂

    Son las 4:30 de la madrugada.. pensaba aventarme el algoritmo, pero quise darle un ojo antes a google para ver si ya existía algo.. y definitivamente tuve bastante suerte.. encontré tú código y despues de probarlo no me queda más que agradecer 🙂

    Gracias y saludos 🙂

  9. Muchas gracias por compartir, esto ahorra mucho tiempo, los hombres grandes se hacen más grandes al compartir el conocimiento.

  10. Leonardo, Phylevn y todos los lectores de VarioNet: Me alegro si la información que aquí encuentran les es util, esa es la idea. Como pueden ver el blog está desactualizado debido a trabajo + vacaciones + la muerte de mi motherboard + etc. Pero prometo publicar relativamente pronto algunos artículos que ya estoy preparando. Saludos!

  11. María: Lamentablemente no domino php, pero mirando el código tanto en vb como en c# creo que no te será muy dificil convertir la función ya que el algoritmo no es muy complicado. ¡Suerte!

  12. Hola Dk. Te agradecería que pongas tus dudas como comentario de la entrada para que queden como referencia para los demás.

    Gracias y saludos

  13. Dk, Lamentablemente no tengo VS2003 y no veo en el código que puede ser lo que no funcione.

    Con una descripción de por que no compila tal vez pueda ayudarte.

    Saludos

  14. Hombre!! Aplausos!!! Tienes razón, hay muchísimos algoritmos por ahi por Google… pero hasta ahorita ninguno como el tuyo, felicidades.. sobre todo por hacerlo tan orientado a objetos…. hay unos jeroglificos por ahi.. que para que te cuento.. Mil gracias!

  15. using System;

    using System.Collections.Generic;

    using System.Text;

    namespace Conversiones

    {

    class Conv

    {

    public string enletras(string num)

    {

    string res, dec = «»;

    Int64 entero;

    int decimales;

    double nro;

    try

    {

    nro = Convert.ToDouble(num);

    }

    catch

    {

    return «»;

    }

    entero = Convert.ToInt64(Math.Truncate(nro));

    decimales = Convert.ToInt32(Math.Round((nro – entero) * 100, 2));

    if (decimales > 0)

    {

    dec = » CON » + decimales.ToString() + «/100»;

    }

    res = toText(Convert.ToDouble(entero)) + dec;

    return res;

    }

    private string toText(double value)

    {

    string Num2Text = «»;

    value = Math.Truncate(value);

    if (value == 0) Num2Text = «CERO»;

    else if (value == 1) Num2Text = «UNO»;

    else if (value == 2) Num2Text = «DOS»;

    else if (value == 3) Num2Text = «TRES»;

    else if (value == 4) Num2Text = «CUATRO»;

    else if (value == 5) Num2Text = «CINCO»;

    else if (value == 6) Num2Text = «SEIS»;

    else if (value == 7) Num2Text = «SIETE»;

    else if (value == 8) Num2Text = «OCHO»;

    else if (value == 9) Num2Text = «NUEVE»;

    else if (value == 10) Num2Text = «DIEZ»;

    else if (value == 11) Num2Text = «ONCE»;

    else if (value == 12) Num2Text = «DOCE»;

    else if (value == 13) Num2Text = «TRECE»;

    else if (value == 14) Num2Text = «CATORCE»;

    else if (value == 15) Num2Text = «QUINCE»;

    else if (value < 20) Num2Text = «DIECI» + toText(value – 10);

    else if (value == 20) Num2Text = «VEINTE»;

    else if (value < 30) Num2Text = «VEINTI» + toText(value – 20);

    else if (value == 30) Num2Text = «TREINTA»;

    else if (value == 40) Num2Text = «CUARENTA»;

    else if (value == 50) Num2Text = «CINCUENTA»;

    else if (value == 60) Num2Text = «SESENTA»;

    else if (value == 70) Num2Text = «SETENTA»;

    else if (value == 80) Num2Text = «OCHENTA»;

    else if (value == 90) Num2Text = «NOVENTA»;

    else if (value < 100) Num2Text = toText(Math.Truncate(value / 10) * 10) + » Y » + toText(value % 10);

    else if (value == 100) Num2Text = «CIEN»;

    else if (value < 200) Num2Text = «CIENTO » + toText(value – 100);

    else if ((value == 200) || (value == 300) || (value == 400) || (value == 600) || (value == 800)) Num2Text = toText(Math.Truncate(value / 100)) + «CIENTOS»;

    else if (value == 500) Num2Text = «QUINIENTOS»;

    else if (value == 700) Num2Text = «SETECIENTOS»;

    else if (value == 900) Num2Text = «NOVECIENTOS»;

    else if (value < 1000) Num2Text = toText(Math.Truncate(value / 100) * 100) + » » + toText(value % 100);

    else if (value == 1000) Num2Text = «MIL»;

    else if (value < 2000) Num2Text = «MIL » + toText(value % 1000);

    else if (value 0) Num2Text = Num2Text + » » + toText(value % 1000);

    }

    else if (value == 1000000) Num2Text = «UN MILLON»;

    else if (value < 2000000) Num2Text = «UN MILLON » + toText(value % 1000000);

    else if (value 0) Num2Text = Num2Text + » » + toText(value – Math.Truncate(value / 1000000) * 1000000);

    }

    else if (value == 1000000000000) Num2Text = «UN BILLON»;

    else if (value 0) Num2Text = Num2Text + » » + toText(value – Math.Truncate(value / 1000000000000) * 1000000000000);

    }

    return Num2Text;

    }

    }

    }

  16. HOLA A TODOS ESTA BIEN EL CODIGO, PERO ALGUIEN SABE COMO HACERLO EN MODO CONSOLE EN WINDOWS, USO VISUALSTUDIO 2005, SE LOS AGRADECERIA MUCHO

  17. Fulanito: No hay diferencia entre los tipos de proyecto, consola, web, winform, lo que quieras. Incluí la clase y las llamadas te retornan un string:

    Console.Write(Numalet.ToCardinal(«155.38», new CultureInfo(«en-US»)));

    Saludos: Alejandro

  18. Excelente algoritmo, muy completo y muy util, me salvaste muchas horas de programación.
    En general el blog esta muy bueno, hay mucha información que es practicamente imposible encontrar en libros o en la ayuda del visual studio.

    Gracias.

  19. Hola, Gracias por tu trabajo.
    Tengo un problema. Yo estoy trabajando con visual studio 2003 y solo hay una parte del código que no funciona.
    Dentro de la propiedad «Public Property MascaraSalidaDecimal() As String», donde se establece «Set(ByVal value As String)» al compilar da un error,en la linea While i < value.Length AndAlso (value(i) = «0»c) Or value(i) = «»
    donde dice que «value» no es una matriz o un método y no puede tener tener una lista de argumentos. He intantado sustituir la línea por:
    While i < value.Length AndAlso ((Mid(value, i, 1)) = «0»c) Or Mid(value, i, 1) = «»
    pero claro i debe ser mayor que cero para poder evaluar la expresion y el resto del código funciona si i=0 o mayor.
    Alguna idea para solucionarlo??
    Gracias.
    Jose
    La Coruña
    España.

  20. josé: No tengo un VS 2003 para probar, se me ocurren dos cosas:

    1)Probaría: While i < value.Length AndAlso (value.ToCharArray()(i) = «0»c) OrElse value.ToCharArray()(i) = «»

    Esto es nada más otra forma de escribir lo mismo, y no se si te ayudará, a falta de compilador, espero tu comentario.

    2) En .net 2.0, And y Or deberían ser reemplazados por AndAlso y OrElse (je, Al ‘Or’ de la sentencia debería agregársele el ‘Else’ que me comí y que todavía estoy digiriendo), ya que de esa manera no se evalúa el resto de la sentencia si alguno de los términos anteriores determina el valor de verdad de la misma.
    Me explico: si en la evaluación de «a AndAlso b», a da como resultado false, NO se evalúa b, utilizando And se evalúa igualmente b. Este es el comportamiento por defecto en C# y el recomendado en VB, así sentencias como:
    If (miDataSet.Tables.Count > 0 AndAlso miDataSet.Tables[0].Rows.Count > 0)…
    no fallarán aunque no haya DataTables, lo que si harían utilizando «And» (al evaluar la colección de rows de un DataTable que no existe).

    No recuerdo que estos operadores estén disponibles en el fw1.1 y en la msdn indica a partir del 2.0, entiendo que no debería compilar la sentencia que pusiste con AndAlso en fw 1.1, pero vos me dirás si es así.

    Saludos y suerte, si te funciona avisame así lo dejo publicado para todas las versiones del fw.

    Alejandro

  21. Hola Alejandro, buenísima esta librería. Gracias por todo lo que me has ahorrado.

    Tengo una duda, referente a un error que me marca: estoy utilizando VB 9 y en
    let = Nothing
    let = new Numalet();

    me marca que Let ya no es compatible con esta versión.

    Me pudieras decir qué es lo que puedo utilizar para solucionar este problemilla. De antemano, muchas gracias.

  22. Gracias Alejandro por tu respuesta.
    Yo he probado con la línea de abajo, pero siempre obtengo un error ya que al final del bucle, i siempre es mayor que la propiedad lengh del string value.
    While i < value.Length AndAlso (value.Substring(i) = «0»c) Or value.Substring(i) = «»
    Con la línea que sugieres, tambien obtengo un error de compilacion, don de dice que no se pueden guardar los datos «UNICODE».

  23. Otra duda, pero ya del propio funcionamiento.
    Aunque ya hace muchos años que mis clases de Castellano se me han olvidado, la clase cuando traduce, por ejemplo 1798,12 da como salida
    UN MIL SETECIENTOS NOVENTA Y OCHO CON DOCE. En mi humilde opinion creo que el «UN» sobraría.

  24. José:

    Respecto del error, necesitaríamos alguien con VS 2003 que nos de una mano. En cuanto al bucle, si la condición es While i > value.length, nunca podría ser i igual o mayor que value.length!

    En cuanto a la manera de escribir el número, la manera correcta es incluyendo «un», siempre según la Real Academia (el link está en el artículo), y si bien es normal no incluirlo en el uso diarío, si cuando se escriben importes, ¡no sería la primera vez que alguien agregue un «siete» delante del mil y un 1 se convierta en 7! 🙂

    Saludos y si veo la manera de arreglarlo, lo posteo, te pido lo mismo para dejar el codigo funcional en todos los fw.

    Alejandro

    1. No se que veas primero si mis coments o mi mensaje… pero ya solucione esta parte..

      switch (iTerna)
      {
      case 3:
      if (Num 0)
      {
      if (cadTerna == » un»)
      {
      cadTerna = » mil»;
      }
      else
      {
      cadTerna += » mil»;
      }
      }
      break;
      }

  25. hola tengo una pregunta, cuando en un for (VB) pregunta la clave y despues de 3 intentos manda un msgbox donde pone el resultado de intentos en forma de numero (numerointentos – veces) = 2 o 3, como hago para que esto se vea en letras

    MessageBox.Show(«Incorrect User Name and Password attempts remaining » & (NumeroIntentos – veces) & » more»)
    ahora
    incorrect User Name amd Password attempts remaining 2 more
    lo que quiero se ve asi
    incorrect User Name amd Password attempts remaining two more
    de ante mano gracias por la atencion

  26. Hola solo quisiera preguntar como puedo hacer el metodo main pra q me funcione, no se mucho de programacion y me han dejado la tarea de convertir numeros la letras y probe el programa pero no me corre porque mefalta el metodo main, pense en algo como esto epro no me funciona:

    public static void Main(string[] args)
    {
    Console.WriteLine(«Ingrese un Numero Entero»);
    int Numero = 0;
    string resultado =»»;
    Numero=Convert.ToInt32(Console.ReadLine());
    resultado=Numalet(Numero);
    Console.WriteLine(resultado);
    Console.ReadLine();

  27. Para los que no saben en c# como agregar la clase de arriba (no menos preciando a nadie 🙂 ) es simple solo deben crear su formulario si quieren un 2 textbox y ya . luego ponen agregar elemetno nuevo y eligen clase. pegan el codigo de arriba en la nueva clase borrando todo lo que habia ahi escrito.

    luego van al form y le dan doble click al textbox1 y en el codigo ponen esto:

    private void textBox1_TextChanged(object sender, EventArgs e)
    {
    try
    {
    decimal numero = Decimal.Parse(textBox1.Text);
    // MessageBox.Show(Numalet.ToCardinal(temp));
    textBox2.Text = Numalet.ToCardinal(numero);
    }
    catch
    {
    textBox2.Clear();
    }
    }

    y listo con eso les va a funcionar es facil. PEro recomiendo leer y analizar el codigo de Alejandro para asi entender mejor y adaptarlo a sus necesidades.

    Gracias Alejandro

  28. hola alejandro que tal, yo solo necesito hacer que funcione en modo consola, poner un numero y que me despliegue en letras el numero que es….. copio y pego tu codigo y me sale en el debugueador. Program ‘C:Documents and setting/RMEDINA/Mis doctos…………ConsoleApplication31.exe’ does not contain a static ‘Main’ method suitable for an enttry point …… la verdad yo soy muy novato en esto de la programacion y por cuestion laborales estoy aprendiendo…. me podrias ayudar? ya sea agregandome al msn o no se.. te lo agradeceria DEMASIADO

  29. Russell:

    Entiendo que al copiar mi clase lo estás haciendo sobre la clase ‘Program’ del archivo Program.cs de tu proyecto.

    Esta clase contiene el método ‘Main’ que te pide el compilador y que es el punto de inicio del programa, por lo que no hay que tocarlo.

    El procedimiento correcto más sencillo sería agregar una nueva clase ‘Numalet’ (botón derecho sobre el proyecto en Visual Studio), ahí si reemplazar el código por el de la clase Numalet que te bajas del blog y, en el interior del método ‘Main’ de la clase ‘Program’, escribir algo así como:

    String EntradaUsuario = Console.ReadLine();
    Console.WriteLine(Numalet.ToCardinal(EntradaUsuario));
    Console.Read();

    En C#, para este ejemplo, si trabajas en visual basic, sería:

    Dim EntradaUsuario As String = Console.ReadLine()
    Console.WriteLine(Numalet.ToCardinal(EntradaUsuario))
    Console.Read()

    pero en este caso el método Main se encuentra en el archivo Module1.vb.

    Respecto a aprender programación, te recomiendo el sitio de la msdn: (http://msdn.microsoft.com/es-es/default.aspx) y
    el de El Guille (http://www.elguille.info)

    ¡Suerte!

  30. Saludos, sabes es impresionante la limpieza con la que trabajas.. Me gusto mucho tu codigo.. solo hay un detalle, bueno.. yo soy de mexico… y aca nadie dice UN mil solo dices mil…

    creo que es lo unico que le note mal..

    en fin que estes muy bien.. cuidate

  31. PARA LOS QUE VIVAN CON EL MISMO PROBLEMA DEL «UN MIL» CON ESTO SE SOLUCIONA, ES LA LINEA 331 DE LA CLASE, SOLO ESA LINEA.. TODO LO DEMÀS ESTA MAS QUE MAGNIFICO…

    switch (iTerna)
    {
    case 3:
    if (Num 0)
    {
    if (cadTerna == » un»)
    {
    cadTerna = » mil»;
    }
    else
    {
    cadTerna += » mil»;
    }
    }
    break;
    }

  32. Edgar, gracias por el comentario.

    «Un mil» es la manera indicada por la Real Academia, pero en la mayoría de los países de habla hispana (si no en todos) tampoco se pronuncia en el habla corriente «Un mil», sin embargo es una cuestión de uso y costumbre al escribir importes en documentos con caracter legal:

    Pensemos que es facil agregar un «nueve» delante de la palabra «mil» en un cheque, y también hacer un «pancita» en un 1, convirtiendo 1.234,00 en 9.234,00.

    Claro que esto es más importante cuando se escribe a mano, pero como dice por ahí: «Camarón que se duerme, se lo lleva la corriente».

    Saludos!

  33. Tienes razón en eso, sin embargo el cliente manda. Y si al cliente no le gusta que diga un mil pues no hay mucho que hacer.. jejejeje

    Y pues la soluciòn es solo para aquellos que asi lo rquieran y que no puedan hacer entrar en razon a sus clients..

    Aunque tambièn es cierto que la real academia de la lengua no ha evolucionado mucho desde hace muchisimos años, ya que hay muchisimas palabras en desuso en méxico..

  34. Edgar, de nuevo gracias por los comentarios.

    Veamos, en el caso de 1.001.000, creo que el resultado es correcto en función del tema que ya hablamos sobre el «un mil». Para tener otro comportamiento hay que tocar el código.

    Respecto al 21 te agradecería algún ejemplo que de acentuado y otro que no.

    Por las dudas que sea eso te comento que hay un tema en castellano y es que los números se escriben distinto en función del contexto en que se usan.

    Como dice el artículo: «en vez de poner “treinta y uno elefantes” ponemos “treinta y un elefantes”». Para trabajar con esto se ofrecen las propiedades ApocoparUnoParteEntera y ApocoparUnoParteDecimal, explicadas en el texto.

    También siguiendo la misma linéa ocurre que, cito el artículo:

    «El código tiene en cuenta que: […] 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”.»

    Como puedes ver «veintiuno» no lleva acento pero «veintiún» si lo hace.

    Ya se que la API de la clase no es muy linda, pero quise ofrecer la mayor cantidad de posibilidades pensando en los diferentes usos y costumbres de los países y en usos para cantidades no solo de dinero sino de elementos tangibles (aunque no creo que nadie lo haya usado para elefantes 🙂 ).

    Note: Para hacer uso de este tipo de ajustes, hay que instanciar la clase y setear las propiedades, o bien cambiar los valores por defecto en el código.

    Edgar, nuevamente gracias por las pruebas y por reportar lo que encuentras, si no es el caso que te comento aquí lo que detectaste, por favor no dejes de informarme. Un saludo: Alejandro

  35. Hola Alejandro,

    Muchas gracias por compartir tu código. Lo acabo de implementar con las modificaciones necesarias para que el texto sea en catalán y funciona perfecto. Si por un casual lo necesitas te paso la clase modificada cuando quieras.

    Para lo del ‘un mil’ (que en catalán siempre es mil sin el ‘un’ yo hice una pequeña chapuza. Justo antes del condicional para capitalizar el Resultado he metido la siguiente línea:

    Resultado.Replace(«un mil», «mil»);

    Grácias de nuevo y un saludo,
    slimer

    1. slimer:

      Bienvenido tu código. Si no te molesta enviármelo, quisiera incluirlo en el post, seguramente le será útil a muchos desarrolladores.

      Saludos: Alejandro

      1. Ok. Te lo C&P aquí. La verdad es que no varía mucho:

        using System;
        using System.Text;
        using System.Globalization;

        ///
        /// Convierte números en su expresión numérica a su numeral cardinal
        ///
        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,» un», » dos», » tres», » quatre», » cinc», » sis», » set», » vuit», » nou»},
        {» deu»,» onze»,» dotze»,» tretze»,» catorze»,» quinze»,» setze»,» disset»,» divuit»,» dinou»},
        {null,null,null,» trenta»,» quaranta»,» cinquanta»,» seixanta»,» setanta»,» viutanta»,» noranta»},
        {null,null,null,null,null,» cinc-cents»,null,» set-cents»,null,» nou-cents»}
        };

        private const Char sub = (Char)26;
        //Cambiar acá si se quiere otro comportamiento en los métodos de clase
        public const String SeparadorDecimalSalidaDefault = «amb»;
        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;

        ///
        /// Indica la cantidad de decimales que se pasarán a entero para la conversión
        ///
        /// Esta propiedad cambia al cambiar MascaraDecimal por un valor que empieze con ‘0’
        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;
        }
        }

        ///
        /// Objeto CultureInfo utilizado para convertir las cadenas de entrada en números
        ///
        public CultureInfo CultureInfo
        {
        get { return _cultureInfo; }
        set { _cultureInfo = value; }
        }

        ///
        /// Indica la cadena a intercalar entre la parte entera y la decimal del número
        ///
        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;
        }
        }

        ///
        /// Indica el formato que se le dara a la parte decimal del número
        ///
        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 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(), «‘»);
        }
        }

        ///
        /// Indica si la primera letra del resultado debe estár en mayúscula
        ///
        public Boolean LetraCapital
        {
        get { return _letraCapital; }
        set { _letraCapital = value; }
        }

        ///
        /// Indica si se deben convertir los decimales a su expresión nominal
        ///
        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;
        }
        }

        ///
        /// Indica si de debe cambiar «uno» por «un» en las unidades.
        ///
        public Boolean ApocoparUnoParteEntera
        {
        get { return _apocoparUnoParteEntera; }
        set { _apocoparUnoParteEntera = value; }
        }

        ///
        /// Determina si se debe apococopar el «uno» en la parte decimal
        ///
        /// El valor de esta propiedad cambia al setear ConvertirDecimales
        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)
        {
        iTerna++;
        cadTerna = String.Empty;
        terna = (Int32)(Num % 1000);

        centenaTerna = (Int32)(terna / 100);
        decenaTerna = terna % 100;
        unidadTerna = terna % 10;

        if ((decenaTerna > 0) && (decenaTerna = 10) && (decenaTerna 20) && (decenaTerna = 30) && (decenaTerna 0) cadTerna = » cent» + cadTerna;
        else cadTerna = » cent» + 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)] + «-cents» + cadTerna;
        break;
        }

        // Zona comentada per que en català no cal.
        //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(«vint-i-un», «vint-i-un»);
        //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(«vint-i-dos», «vint-i-dos»);
        //else if (decenaTerna == 23) cadTerna = cadTerna.Replace(«vint-i-tres», «vint-i-tres»);
        //else if (decenaTerna == 26) cadTerna = cadTerna.Replace(«vint-i-sis», «vint-i-sis»);

        //Completo miles y millones
        switch (iTerna)
        {
        case 3:
        if (Numero 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);
        }
        //En català no es fa servir un mil si no només mil
        Resultado.Replace(«un mil», «mil»);
        //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);
        }
        }

  36. Alejandro, quiero felicitarte por tu codigo, no solamente porque es una de las mejores clases que he visto (facil de utilizar, flexible, comprensible y eficiente… realmente excelente) sino tambien por publicarla y compartirla con el mundo. Estoy seguro que, al igual que a mi, ahorraste mucho tiempo de programacion a mucha gente. Muchisimas gracias por ayudarnos a todos con tu experiencia y felicidades nuevamente.

    1. Marco, Maykel: Me alegro de que les haya servido el código. La satisfacción de que lo que hice sea útil es la compensación por el trabajo que pudo haber llevado.

      Saludos!

  37. Excelente, me has ahorrado como 2 hrs de programación y tengo poco tiempo disponible.
    Muy agradecido. Si te gusta la música pásate por mi blog, seguro encuentras algo que te gusta.
    Saludos!

  38. Hola, te agradezco realmente tu aporte ya que es justo lo que necesito para realizar una tarea en mi trabajo. Mi única duda es si existe alguna manera de que cuando convierto un número entero (sin decimales) y tengo seteada la propiedad ConvertirDecimales en true, no me escriba al final la frase «con cero». Por ejemplo:

    Numalet let = new Numalet();
    let.ConvertirDecimales = true;
    MessageBox.Show(let.ToCustomCardinal(21));
    //veintiuno con cero

    Existe la forma de que solo retorne «veintiuno» ??

    1. Martín, la propiedad «ConvertirDecimales» indica si los decimales deben aparecer como cardinal, por ejemplo 10,15 como «diez con quince» o como «diez con 15».
      A priori hay una sobrecarga estática de ToCardinal que recibe un entero y da el resultado que creo que te sirve:
      Numalet.ToCardinal(21)
      te retornará «veintiuno».

      ¡Saludos y gracias!

  39. hola alejandro estoy tratando de hacer funcionar pero me sale el siguiente error:
    Falta el modificador parcial en la declaración de tipo ‘Numalet’;
    ya existe otra declaración parcial de este tipo

  40. Hola, alguien me podría ayudar en la duda que tengo porfavor, lo que pasa que es la primera vez que trabajo con clases en C#, entonces nose como llamarla o hacerla funcionar para ver el resultado en la interfaz gráfica.
    Gracias.

  41. EN primer lugar felicitaciones por toda tu ayuda para con los demás…
    Disculpa la molestia, estoy haciendo un forma para mostrar de numeros a letras, un textBox donde ingreso el numero, y una buttonMostrar para mostrarme ya sea en Label o en otro TextBox… pero nosé como asociarlo…

    Soy nuevo en esto… apenas estoy comenzando, agradecería tu ayuda.. 🙂

  42. Hola amigo, desde hace un tiempo he estado haciendo una recopilación de lo que yo considero librerías útiles (propias y de otros programadoras) y con tu permiso me gustaría agregar la tuya, mi idea es publicarla en el momento que ya tenga una cantidad razonable de clases añadidas. Aunque la liberaré como archivo .dll añadiré el código fuente para que cualquiera pueda mejorarla o adaptarla como mejor le parezca.

    Espero tu respuesta y gracias de antemano 🙂

    1. Hola rdaw. No hace falta ni que pidas permiso, solo que que cites la fuente aunque sea en el código fuente por mi está bien. ¡Gracias y saludos!

      1. Gracias por tu respuesta, y por tu confirmación y no te preocupes todos los códigos tienen los nombres de los autores en los comentarios, así como la página de donde fueron pedidos, me llevará aún algo de tiempo, pero al finalizar enviaré la dll y el código a todas estas personas 😀

        Saludos!

  43. olas ps disculpa ps soi un estudiante ps nose si me pueden ayudar a prender a programar n tendran unos manuales o unas paginas q t me enseñen desde cero 😀 les agreseria mucho

  44. olas ps disculpa ps soi un estudiante ps nose si me pueden ayudar a prender a programar n tendran unos manuales o unas paginas q t me enseñen desde cero les agreseria mucho

  45. hola que tal amigo

    queria darte las gracias por compartir esta funcion.

    la verdad esque yo no soy para nada programador pero ahi a base de puros golpes y tamborasos ya la pude implementar y me sirvo mucho.

    sigue asi amigo

    saludos

  46. Hola Alejandro, primero que nada agradecerte por tu gran trabajo y felicitarte por lo mismo, creo que a estas alturas tu clase es ya un estándar 😀

    También quiero hacer una pequeña observación, y es que al querer convertir un número decimal con el método ToCustomCardinal, el resultado no fue el deseado, por lo que inspeccioné el código y encontré un pequeño error. En la línea 219 esta el siguiente código:

    { return ToCardinal((Numero)); }
    

    por lo que basándome en las otras sobrecargas cambié dicha línea por:

    { return Convertir(Numero, _decimales, _separadorDecimalSalida, _mascaraSalidaDecimalInterna, _esMascaraNumerica, _letraCapital, _convertirDecimales, _apocoparUnoParteEntera, _apocoparUnoParteDecimal); }
    

    y Ahora funciona correctamente con un número decimal.

    Mil gracias nuevamente
    Salu2

  47. MUchas muachas gracias, aun sigo agradecido por tu codigo, creo que he tardado mas en llegar al ultimo de esta pagina que en lo que realizaba las pruebas, estoy muy agradeciddo me ahorraste 1 dia de trabajo y mucho pensar gracias sigue asi. y felicidades

  48. me sucede lo siguinte estoy usando el algoritmo para los cheques del banco
    y el banco cunado convierto el numero 6000.00 el algoritmo lo pone asi «seis mil con 0/100 y lo necesito asi «seis mil con 00/100» o se antes del caracter / necesito dos digitos.Otros ejmplo si convierto 6000.04 me pone el algoritmo «seis mil con 4/100» y necesito seis mil con 04/100.hay alguna propiedad que haga eso.Muchas gracias agradeceria su ayuda

  49. Muy buen trabajo, tengo una pregunta, si fuera el caso del número 001, y quiero que lo convierta a «cero cero uno», esto se puede? que convierta los ceros. Otro ejemplo 00125, que convierta a «cero cero ciento veinticinco». Alguien puede ayudarme

  50. Buen dia este codigo es justo lo que necesito para terminar un programa de facturacion pequeño que estoy haciendo, el enlace ya esta roto 😦 podria por favor volverlo a subir se lo agradeceria mucho
    saludos 😀

  51. Muchas Gracias Alejandro por tu gran aportate al publico, sabiendo del tiempo que siempre toma el estar sentado programando y que no toda la gente valora o aprecia este tipo de cosas. Saludos desde Mexico.

  52. Hola.. muy bueno..
    la verdad te felicito doc. es muy bueno y bien documentado.

    pero sabes una observacion? en la RAE

    1000 = mil (también, como sust., un millar)
    pero cuando se escribe en el programa sale «un mil»
    me di cuenta cuando quería imprimir 1000 en letras jeje
    no le he dado solución aun.

    1. Jimmy: es correcto que el código no contempla el uso de «millar». Entiendo que este es correcto como sustantivo aunque en general para adjetivar, («un millar de personas»). Pero su uso creo es más acorde con cantidades indeterminadas «Varios millares», «Llegaban de a millares» y está fuera del objetivo del código.
      Sin embargo me suena correcto cuando no se llega a los cientos de miles, sería incorrecto pronunciar 201.400 como «doscientos un millar cuatrocientos», por lo que analizando el número entrante podrías reemplazar la salida facilmente si se encuentra entre 1000 para «millar» y 2000 a 99.999 para «millares» (noventa y nueve millares novecientos noventa y nueve).
      Aun así, debería ser un caso muy específico, al menos para los usos que conozco.

      Saludos y gracias por el comentario.

  53. Hola, muchas gracias por este ejemplo, es lo que estaba buscando, lo probé y funciono a la primera ejecución, nuevamente gracias por tu aporte.

  54. Excelente trabajo, te felicito; solo tengo una duda, como puedo dar formato para que cuando sea un digito los centavos los convierta a dos, por ejemplo

    2.01 resultado = dos con 1/100

    Y necesito:

    2.01 resultado = dos con 01/100

    Mil gracias.

  55. Muchisimas gracias, funciona muy bien, casualmente encotre un bug si mandas por ejemplo 10.66.

    La funcion Convertir marca error: Decimal byte array constructor requires an array of length four containing valid decimal bytes.

    Lo que se peude evittar simplemente cambiando ByVal Numero As Decimal por ByVal Numero As Double

    Saludos!

  56. Buenas amigo, un poco tarde pero al fin consegui este código, mi consulta es como realizo la instalación de lo que se descarga, soy nuevo en programación pero con un poco de conocimiento y estoy realizando un programa para mi empresa y necesito reflejar en el presupuesto la cantidad expresada en letras y este código iria excelente, mas no se como instalarla para que el VB lo reconosca, trabajo con VB6. Gracias de antemano

  57. Muy buen codigo. Imaginate, lo creastes en el 2007 y hasta ahora se sigue usando, y mas aun con buenos comentarios. Muchas Gracias por compartir.

  58. hola en C# me pregunto como puedo hacer un programa que diga según la siguiente tabla cuantas horas necesitas dormir.
    a. o-3 meses: entre 14 a 17 horas
    b. 4-11 meses: 12 a 15 horas
    c. 1-2 años : entre 11 a 14 horas
    d. 3-5 años: 10 a 13 horas
    e. 6-13 años: 9 a 11 horas
    f. 14-17 años: 10 horas
    g 18-25 años: 7 a 9 horas, no menos de 6 y no mas de 11
    h. 26-64 años: 7 a 9 horas
    i. adultos mayores de 65 años: 7 a 8 horas

Replica a Mau Cancelar la respuesta