Wednesday, May 18, 2011

Para escribir cheques (II)


Hace un par de días (aquí) escribí sobre un programa que transforma una cifra en números a letras.La idea es que ponía una cifra: 2347, por ejemplo, y el programa debía darme como resultado "dos mil trescientos cuarenta y siete pesos". Puse en el artículo mencionado mi primera solución en Prolog, pero he aquí que un avezado lector, buen  programador, mejor amigo y técnicamente infalible, Ernesto Blum, me dijo que no estaba considerando algunos casos.

Hoy que fui a dar mi clase, expuse el código fuente y al irlo escribiendo en el pizarrón hallé que algo andaba mal. Así pues, decidí que al llegar a casa re-escribiría el problema para darle una solución definitiva. En un rato hallé las dificultades y según yo, ya las resolví. He aquí mi código fuente:

/********************************************/
/* Programa que traduce de números a letras */
/*             Versión 1.1                  */
/*            18 mayo 2011                  */
/*          Programó: La Morsa              */
/********************************************/ 
 
predicates

   equiv(integer,symbol)
   pasa_num_a_letras(integer)
   
   
clauses
   equiv(1,un).
   equiv(2,dos).
   equiv(3,tres).
   equiv(4,cuatro).
   equiv(5,cinco).
   equiv(6,seis).
   equiv(7,siete).
   equiv(8,ocho).
   equiv(9,nueve).
   equiv(10,diez).
   equiv(11,once).
   equiv(12,doce).
   equiv(13,trece).
   equiv(14,catorce).
   equiv(15,quince).
   equiv(16,dieciseis).
   equiv(17,diecisiete).
   equiv(18,dieciocho).
   equiv(19,diecinueve).
   equiv(20,veinte).
   equiv(21,veintiuno).
   equiv(22,veintidos).
   equiv(23,veintitres).
   equiv(24,veinticuatro).
   equiv(25,veinticinco).
   equiv(26,veintiseis).
   equiv(27,veintisiete).
   equiv(28,veintiocho).
   equiv(29,veintinueve).
   equiv(30,treinta).
   equiv(40,cuarenta).
   equiv(50,cincuenta).
   equiv(60,sesenta).
   equiv(70,setenta).
   equiv(80,ochenta).
   equiv(90,noventa).
   equiv(100,ciento).
   equiv(200,doscientos).
   equiv(300,trescientos).
   equiv(400,cuatrocientos).
   equiv(500,quinientos).
   equiv(600,seiscientos).
   equiv(700,setecientos).
   equiv(800,ochocientos).
   equiv(900,novecientos).
   equiv(1000,mil).
   
   pasa_num_a_letras(X) :- /* condicion terminales */
      X < 30,
      equiv(X,Resultado),
      write(Resultado).


   /* predicados recursivos */
   
   pasa_num_a_letras(Cifra) :-
      Cifra div 1000 <> 0,          /* la cifra tiene diez miles */
      DMiles = Cifra div 1000,
      DMiles > 30,
      pasa_num_a_letras(DMiles), /*doble recursion */
      write(" mil "),
      Miles = Cifra - (DMiles * 1000),
      pasa_num_a_letras(Miles).      
      
   pasa_num_a_letras(Cifra) :-
      Cifra div 1000 <> 0,           /* la cifra tiene miles */
      Miles = Cifra div 1000,
      equiv(Miles,Resultado),
      write(Resultado, " mil "),
      Cientos = Cifra - (Miles * 1000),
      pasa_num_a_letras(Cientos).      
         
   pasa_num_a_letras(Cifra) :-
      Cifra div 100 <> 0,           /* la cifra tiene cientos */
      Cien = Cifra div 100,
      Cien1 = Cien * 100,
      equiv(Cien1,Resultado),
      write(Resultado," "),
      Decenas = Cifra - (Cien * 100),
      pasa_num_a_letras(Decenas).    
      
   pasa_num_a_letras(Cifra) :-
      Cifra div 10 <> 0,           /* la cifra tiene decenas */
      Dec = Cifra div 10,
      Dec1 = Dec * 10,
      equiv(Dec1,Resultado),
      write(Resultado," y "),
      Unidades = Cifra - (Dec * 10),
      pasa_num_a_letras(Unidades).    
      


      


Cabe señalar que el programa está escrito en Turbo Prolog 2.0 y que se está usando el tamaño de variable entera para el procesamiento de la cifra, por ende, solamente puede resolver el problema para números enteros mayores a cero y menores a 32768, que es el límite de un entero. Entre otras cosas, este código ya me dejó más contento porque solamente contiene una salida de la función recursiva y eso me parece más "elegante", valga la expresión.

Igualmente, expresiones como: 
 
Cientos = Cifra - (Miles * 1000),
 
 
Podrían ponerse como

Cientos = Cifra mod 1000

Hice algunas pruebas y parece que ahora sí todo funciona bien.

A todo esto, el mismo Ernesto Blum me mandó la solución de este programa en el lenguaje de programación Ruby:


#!/usr/bin/ruby



def cen_out(numero)
 uni = numero % 10 / 1
 dec = numero % 100 / 10
 cen = numero % 1000 / 100
 case cen 
 when 0; return ""
 when 1; if uni == 0 && dec == 0
   return "cien"
  else
   return "ciento "
  end
 when 2; return "doscientos "
 when 3; return "trescientos "
 when 4; return "cuatrocientos "
 when 5; return "quinientos "
 when 6; return "seiscientos "
 when 7; return "setecientos "
 when 8; return "ochocientos "
 when 9; return "novecientos "
 end
end



def dec_out(numero)
 uni = numero % 10 / 1
 dec = numero % 100 / 10
 if dec == 1
  case uni
  when 0; return "diez"
  when 1; return "once"
  when 2; return "doce"
  when 3; return "trece"
  when 4; return "catorce"
  when 5; return "quince"
  when 6; return "dieciseis"
  when 7; return "diecisiete"
  when 8; return "dieciocho"
  when 9; return "diecinueve"
  end
 elsif uni == 0
  case dec
  when 0;  return ""
  when 1;  return ""
  when 2;  return "veinte"
  when 3;  return "treinta"
  when 4;  return "cuarenta"
  when 5;  return "cincuenta"
  when 6;  return "sesenta"
  when 7;  return "setenta"
  when 8;  return "ochenta"
  when 9;  return "noventa"
  end
 else
  case dec
  when 0;  return ""
  when 1;  return ""
  when 2;  return "veinti"
  when 3;  return "treinta y "
  when 4;  return "cuarenta y "
  when 5;  return "cincuenta y "
  when 6;  return "sesenta y "
  when 7;  return "setenta y "
  when 8;  return "ochenta y "
  when 9;  return "noventa y "
  end
 end
end




def uni_out(numero)
 uni = numero % 10 / 1
 dec = numero % 100 / 10
 if dec == 1
  return ""
 else
 case uni
  when 0; return ""
  when 1; return "un"
  when 2; return "dos"
  when 3; return "tres"
  when 4; return "cuatro"
  when 5; return "cinco"
  when 6; return "seis"
  when 7; return "siete"
  when 8; return "ocho"
  when 9; return "nueve"
  end
 end
end



def letra(numero)
 return  cen_out(numero), dec_out(numero), uni_out(numero), "\n"
end

for n in 1..1000
 print letra(n)
end 
 

Por el momento así las cosas. Cualquier cambio o problema con el código mío, lo pondré en el blog en su oportunidad.

9 comments:

Pedro Tinoco said...

Mi querido amigo: cuáles eran las dificultades que encontraste y cómo las resolviste?
ptinoco

Chochos said...

TDD... este es un ejemplo perfecto de la utilidad de las pruebas unitarias. Los casos de prueba son muy sencillos y se pueden escribir de antemano, y luego ejecutar la implementación para verificar que las conversiones configuradas en las pruebas, arrojen los resultados esperados. Hay que diseñar muy bien las pruebas y con eso ya no tenemos que estar pensando si tal vez todavía queda por ahí una conversión que no hace bien, etc.

Y para algo tan estándar supongo que hasta debe haber casos de prueba ya publicados...

Morsa said...

Pedro,

No consideré los casos desde el 21 al 29. Igualmente, ponía veinte y algo, pero se dice veintitres, no veintetres, por ejemplo.

Otro asunto fue la necesidad de una doble recursión. Por ejemplo, en la cifra 31,129 primero separé la cifra 31 y la procesé como un número de dos cifras y entonces, regresé de esa llamada recursiva para seguir con los cientos de pesos...

saludos
Manuel

R said...

¿No te falta el caso del 100 (que sí está en el ruby)?

Chochos said...

Y por si te interesa otra versión, con menos if's, usando listas para construir los números, aquí te va una en Groovy:

def letra = { int num ->
List units = ['cero','uno','dos','tres','cuatro','cinco','seis','siete','ocho','nueve']
List decs = ['X', 'y', 'veinte', 'treinta', 'cuarenta', 'cincuenta', 'sesenta', 'setenta', 'ochenta', 'noventa']
List dieces=['diez','once','doce','trece','catorce','quince','dieciseis','diecisiete','dieciocho','diecinueve']
List cientos=['x', 'cien', 'doscientos', 'trescientos', 'cuatrocientos', 'quinientos', 'seiscientos', 'setecientos', 'ochocientos', 'novecientos']
int millones = num / 1000000
int millares = (num / 1000) % 1000
int centenas = (num / 100) % 10
int decenas = (num / 10) % 10
int unidades = num % 10
String letras = ''
if (millones == 1) {
letras += 'un millón'
} else if (millones > 1) {
letras += call(millones) + ' millones'
}
if (millares > 0) {
if (millares == 1) {
letras += ' un mil' // o 'mil' nada mas, si no se quiere tipo "moneda"
} else {
letras += ' ' + call(millares) + ' mil'
}
}
if (centenas > 0) {
if (centenas == 1) {
if (num % 100 == 0) {
letras += ' cien'
} else {
letras += ' ciento'
}
} else {
letras += ' ' + cientos[centenas]
}
}
if (decenas == 1) {
letras += ' ' + dieces[num % 10]
unidades = 0
} else if (decenas == 2 && unidades > 0) {
letras += ' veinti' + units[unidades]
unidades = 0
} else if (decenas > 1) {
letras += ' ' + decs[decenas]
if (unidades > 0) {
letras += ' y'
}
}
if (unidades > 0) {
letras += ' ' + units[unidades]
} else if (num == 0) {
letras = units[0]
}
letras.trim()
}

Las pruebas unitarias las pongo aparte porque no me caben en un comentario.

Chochos said...

Pruebas unitarias para mi conversión en Groovy:

assert letra(0) == 'cero'
assert letra(1) == 'uno'
assert letra(9) == 'nueve'
assert letra(10) == 'diez'
assert letra(15) == 'quince'
assert letra(20) == 'veinte'
assert letra(25) == 'veinticinco'
assert letra(30) == 'treinta'
assert letra(42) == 'cuarenta y dos'
assert letra(99) == 'noventa y nueve'
assert letra(100) == 'cien'
assert letra(101) == 'ciento uno'
assert letra(110) == 'ciento diez'
assert letra(117) == 'ciento diecisiete'
assert letra(120) == 'ciento veinte'
assert letra(128) == 'ciento veintiocho'
assert letra(130) == 'ciento treinta'
assert letra(192) == 'ciento noventa y dos'
assert letra(200) == 'doscientos'
assert letra(300) == 'trescientos'
assert letra(512) == 'quinientos doce'
assert letra(768) == 'setecientos sesenta y ocho'
assert letra(1000) == 'un mil'
assert letra(1001) == 'un mil uno'
assert letra(1024) == 'un mil veinticuatro'
assert letra(1234) == 'un mil doscientos treinta y cuatro'
assert letra(5678) == 'cinco mil seiscientos setenta y ocho'
assert letra(9000) == 'nueve mil'
assert letra(10000) == 'diez mil'
assert letra(10001) == 'diez mil uno'
assert letra(10019) == 'diez mil diecinueve'
assert letra(10047) == 'diez mil cuarenta y siete'
assert letra(10240) == 'diez mil doscientos cuarenta'
assert letra(11111) == 'once mil ciento once'
assert letra(12345) == 'doce mil trescientos cuarenta y cinco'
assert letra(100000) == 'cien mil'
assert letra(200001) == 'doscientos mil uno'
assert letra(200015) == 'doscientos mil quince'
assert letra(200027) == 'doscientos mil veintisiete'
assert letra(200099) == 'doscientos mil noventa y nueve'
assert letra(200500) == 'doscientos mil quinientos'
assert letra(234567) == 'doscientos treinta y cuatro mil quinientos sesenta y siete'
assert letra(1000000) == 'un millón'
assert letra(2000000) == 'dos millones'
assert letra(50000000) == 'cincuenta millones'
assert letra(1000001) == 'un millón uno'
assert letra(1000102) == 'un millón ciento dos'
assert letra(1001000) == 'un millón un mil'
assert letra(1002345) == 'un millón dos mil trescientos cuarenta y cinco'
assert letra(1025678) == 'un millón veinticinco mil seiscientos setenta y ocho'
assert letra(1234567) == 'un millón doscientos treinta y cuatro mil quinientos sesenta y siete'

Ernesto said...

Hola Manuel!

Me halaga que hayas considerado mostrar mi código.

@Chochos,
Le comentaba a Manuel, que este problema lo resolví por primera vez, cuando era un adolescente en XBase, el lenguaje que usaba el DBase III Plus. Por el 89 o 90.

En aquel entonces me di cuenta de lo difícil que era hacer esto de forma autodidacta.

Sobretodo por las pruebas unitarias y definir los casos de estas pruebas.

Años después lo volví a implementar en excel con funciones estándar; sin visual basic, con tablas y vlookups.

Esta versión en ruby, es una aproximación burda del algoritmo de la hoja de excel que usábamos para facturar en un pequeño negocio en el 96.

Estoy muy empolvado con la programación y sentí que me costó mucho más trabajo hacer esta versión.

Hoy en la mañana que volví a ver mi código, me di cuenta que se podría hacer más pequeño y elegante.

El símbolo % en ruby es el operador de modulo que Manuel describe en su texto.

así que la expresión:

dec = numero % 100 / 10

Decenas = número modulo 100 y todo dividido entre 10 para quitarle las unidades.

Esa división es una división entera así que 7 / 3 =es 2 y no 2.333...

Ernesto said...

Groovy se parece mucho a Ruby.

Me pareció muy interesante usar listas con índice, para sustituir los case y los if.

De esta forma puedes reutilizar las listas para los miles y lo millones, ya que se repite el patrón.

Saludos

Chochos said...

El operador % es módulo en casi cualquier lenguaje moderno: Ruby, PHP, Java, C, Groovy, C#, etc. Si te fijas también lo uso en mi código para sacar decenas, centenas, millares, etc. y hay algo de recursión para sacar fragmentos que son repetidos (por ejemplo 245,245 es simplemente generar "doscientos cuarenta y cinco" luego pegarle "mil" y luego "doscientos cuarenta y cinco" otra vez - la de los millares invoca la función para generar la cadena con el número 245).

Qué bueno que retomes la programación aunque sea por hobby.