Transmutación de código VB6 a lenguajes .NET
Manifiesto de una traducción formal desde una clase VB6 hacia clases VB .NET y C#
Por Harvey Triana
Descargar Ejemplo de este artículo.
Contenido
1. Introducción
2. El clásico utilitario números a letras
3. Solución en VB .NET
4. Traducción eficiente desde VB6 hacia VB .NET
5. Probando la clase CN2W de VB .NET
6. Solución en C#
7. Probando la clase CN2W de C#
8. Conclusión
1. Introducción
Muchas veces me he preguntado si existe alguien que realmente programe VB .NET puro. Sabemos que VB .NET no es una metamorfosis de VB, sino una especificación nueva derivada de otro lenguaje: C#. ¿Es justo que, por un puñado de palabras reservadas, llamemos Visual Basic a VB .NET? Seamos realistas: las dos primeras palabras de VB .NET no son más que una mera etiqueta. Pero esto no es dramático; no tengo el propósito de polemizar. VB .NET es un buen lenguaje, y muy elegante.
Percibo que VB .NET puro está sometido a un segundo plano por C#. La gran mayoría de los ejemplos de VB .NET que encuentras están escritos por programadores C#, para programadores C#, y traducidos a VB .NET por un tonto robot. Esto deslegitima aún más a VB .NET como familia de VB. Por otra parte, mucho código de VB .NET, en especial el de los programadores emigrantes desde VB6, usa el espacio de nombres Microsoft.VisualBasic; y éstos creen erróneamente que están produciendo código VB .NET. Esto último es una cuestión a la que naturalmente huyen los programadores puristas de lenguajes .NET.
Pero entonces ¿A qué deberían aspirar los programadores de Visual Basic clásico? El objetivo de este artículo es tomar una buena clase de VB6, traducirla a VB .NET puro (sin el espacio de nombres Microsoft.VisualBasic), y luego a C#. Quizás te sorprendas de la similitud de VB .NET con C# cuando programemos VB .NET puro, y la gran distancia que hay entre VB6 y VB .NET.
Mucho se ha escrito acerca de la migración de VB6 hacia VB .NET; no obstante, me pregunto si se estará orientando bien a los programadores VB6. Quizás sería preferible olvidarse de que existe VB .NET y comenzar con C#.
2. El clásico utilitario números a letras
Imagino que, por su utilidad, el problema de traducir una cifra de números en letras es tratado desde que existe la programación. La solución del problema es un buen ejercicio para todos los programadores dedicados. En este artículo trataremos este problema en su versión en inglés. Escogí la versión en inglés porque el código es más corto y tiene una lógica más simple de seguir en cuanto a propósitos didácticos.
Cuando abordé este problema, cumplí con los siguientes objetivos:
Convertir una cifra entera, de números a letras.
Traducir una cifra real, de números a letras.
Traducir una cantidad monetaria, de signos y números a letras; por ejemplo, el número 123.456 debe producir la siguiente salida:
Función Respuesta Moneda a Letras One Hundred Twenty Three Dolars And Forty Six Cents Real a Letras One Hundred Twenty Three Dot Four Hundred Fifty Six Entero a Letras One Hundred Twenty Three
Básicamente se trata de una sola función, la cual convierte un número entero en letras; las demás son derivadas de la misma, con ciertos detalles. A los fines de este artículo he creado 3 clases: la primera en VB6, la segunda en VB .NET, y la tercera en C#.
3. Solución en VB .NET
En un principio podría haber tomado la clase escrita en VB6, importado la librería Microsoft.VisualBasic, y pegado código. Con unos cuantos ajustes podría hacer funcionar esto en VB .NET. Con esa estrategia en mente, voy a producir un código VB .NET sucio y poco didáctico a la hora de aprovechar el potencial real de VB .NET. Tampoco voy a ser honesto con la eficiencia, ya que voy a colocar una capa de software delante de VB .NET para hacer funcionar mi código. Si vamos a programar formalmente en un lenguaje .NET deberíamos evitar el espacio de nombres Microsoft.VisualBasic.
Usar el espacio de nombres Microsoft.VisualBasic es como si, por alguna razón de logística, un programador VB .NET que nunca ha escrito VB6 tuviera que programar en VB6. Al desconocer VB6, le parecerían extrañas funciones tales como Left$(), Right$() ó Mid$(). Posiblemente optaría por crear una clase Global Multiuse con un método SubString() - si fuera más osado tal vez crearía una clase StringVBNET y se ahorraría el análisis. No obstante, estaría subutilizando VB6 ya que al colocar una capa de software delante de VB6 para que su código funcione, estaría omitiendo las funciones de la especificación VB6, y obligaría al modulo de ejecución de VB6 a hacer trabajo extra. Esta analogía es equivalente al programador VB .NET que usa el espacio de nombres Microsoft.VisualBasic para migrar un código de VB6 a VB .NET.
El código que se muestra a continuación es VB .NET puro. Incluso la función IsNumeric fue reemplazada por unas líneas muy eficientes:
Option Strict On '//required if we desired traslate to C# Imports System.Globalization Public Class CN2W Private Const ZERO As String = "Zero" Private IntegerPart As String = ZERO Private RealPart As String = ZERO Private m_DecimalSeparator As String Private m_GroupSeparator As String Private aT0() As String = {"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"} Private aT1() As String = {"Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"} Private aT2() As String = {"Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"} Private aT3() As String = {"", "", " Thousand ", " Million ", " Billion ", " Trillion ", "", "", "", ""} Public Sub New() m_DecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.CurrencyDecimalSeparator m_GroupSeparator = CultureInfo.CurrentCulture.NumberFormat.CurrencyGroupSeparator End Sub Public Function IntegerToWords(ByVal Number As String) As String Call RealToWords(Number) Return IntegerPart End Function Public Function RealToWords(ByVal Number As String) As String Dim DotPos As Integer = 0 IntegerPart = ZERO RealPart = ZERO If IsNumeric(Number) Then Number = Number.Replace(m_GroupSeparator, "") DotPos = Number.IndexOf(DecimalSeparator) If DotPos >= 0 Then IntegerPart = ToWords(Convert.ToInt32(Number.Substring(0, DotPos))) RealPart = ToWords(Convert.ToInt32(Number.Substring(DotPos + 1, Number.Length - DotPos - 1))) Else IntegerPart = ToWords(Convert.ToInt32(Number)) End If End If Return IntegerPart & " Dot " & RealPart End Function Public Function CurrencyToWords(ByVal Number As String, ByVal MoneyName As String) As String Dim s As String = "" IntegerPart = ZERO RealPart = ZERO If IsNumeric(Number) Then ' round to money number s = RealToWords(RoundNumberString(Convert.ToDouble(Number), 2)) End If ' add money text If IntegerPart = ZERO Then IntegerPart = "No" Select Case RealPart Case ZERO : RealPart = "And No Cents" Case "One" : RealPart = "And One Cent" Case Else : RealPart = "And " & RealPart & " Cents" End Select Return IntegerPart & " " & MoneyName & " " & RealPart End Function Private Function ToWords(ByVal Number As Integer) As String Dim s As String = "" Dim d As String = "" Dim r As String = "" Dim n As Integer = 0 s = Number.ToString n = 1 Do Until s.Length = 0 ' convert last 3 digits of s to english words If s.Length < 3 Then d = ConvertHundreds(s) Else d = ConvertHundreds(s.Substring(s.Length - 3, 3)) End If If d.Length > 0 Then r = d & aT3(n) & r If s.Length > 3 Then ' remove last 3 converted digits from s. s = s.Substring(0, s.Length - 3) Else s = "" End If n += 1 Loop If r.Length = 0 Then r = ZERO Return r.Trim End Function Private Function ConvertHundreds(ByVal pNumber As String) As String Dim rtn As String = "" If Not Convert.ToInt32(pNumber) = 0 Then ' append leading zeros to number. pNumber = ("000" & pNumber).Substring(pNumber.Length, 3) ' do we have a hundreds place digit to convert? If Not pNumber.Substring(0, 1) = "0" Then rtn = ConvertDigit(pNumber.Substring(0, 1)) & " Hundred " End If ' do we have a tens place digit to convert? If pNumber.Length >= 2 Then If Not pNumber.Substring(1, 1) = "0" Then rtn &= ConvertTens(pNumber.Substring(1)) Else rtn &= ConvertDigit(pNumber.Substring(2)) End If End If Return rtn.Trim Else Return "" End If End Function Private Function ConvertTens(ByVal pTens As String) As String Dim r As String = "" ' is value between 10 and 19? If Convert.ToInt32(pTens.Substring(0, 1)) = 1 Then r = aT1(Convert.ToInt32(pTens) - 10) Else ' otherwise it's between 20 and 99. r = aT2(Convert.ToInt32(pTens.Substring(0, 1)) - 2) & " " ' convert ones place digit r &= ConvertDigit(pTens.Substring(pTens.Length - 1, 1)) End If Return r End Function Private Function ConvertDigit(ByVal pNumber As String) As String If pNumber = "0" Then Return "" Else Return aT0(Convert.ToInt32(pNumber) - 1) End If End Function Private Function RoundNumberString(ByVal Number As Double, ByVal Decimals As Integer) As String Dim s As String Dim r As Double ' round first r = 10 ^ Decimals ' Math.Floor make sure the float round. Suggested by Guillermo Som r = CType(Math.Floor(Number * r + 0.5), Integer) / r ' complete with zeros s = r.ToString If s.IndexOf(".") < 0 Then s &= "." Do While s.Substring(s.IndexOf(".")).Length <= Decimals s &= "0" Loop Return s End Function '/ Source: http://aspalliance.com/articleViewer.aspx?aId=80&pId= '/ Traslate and review to VB.NET by Harvey Triana Private Function IsNumeric(ByVal s As String) As Boolean Dim HasDecimal As Boolean = False Dim i As Integer = 0 Dim r As Boolean = False Dim ds As Char = Convert.ToChar(DecimalSeparator) Dim gs As Char = Convert.ToChar(GroupSeparator) Do While i < s.Length ' Check for decimal If s(i) = ds Then If HasDecimal Then r = False Else ' 1st decimal ' inform loop decimal found and continue HasDecimal = True i += 1 Continue Do End If End If ' check if number If Char.IsNumber(s(i)) Or (s(i) = gs) Then r = True Else r = False Exit Do End If i += 1 Loop Return r End Function Public ReadOnly Property GroupSeparator() As String Get Return m_GroupSeparator End Get End Property Public ReadOnly Property DecimalSeparator() As String Get Return m_DecimalSeparator End Get End Property End Class
VB.NET: Clase N2W.vb
4. Traducción eficiente desde VB6 hacia VB .NET
El siguiente análisis de la traducción formal desde VB6 hacia VB .NET pone de manifiesto los siguientes detalles importantes que se deberían tener en cuenta para una traducción eficiente:
No emplear el espacio de nombres Microsoft.VisualBasic
Omitir el espacio de nombres Microsoft.VisualBasic hace bastante más difícil la migración del código, pero establece que vamos a producir un código .NET puro. El espacio de nombres Microsoft.VisualBasic se agrega de manera predeterminada a los proyectos VB .NET; para omitirlo debes entrar a Propiedades, ubicar la ficha Referencias, y desactivar la casilla correspondiente a Microsoft.VisualBasic.Usar Option Strict On
Esto es particularmente importante si deseas producir código muy resistente; evita late bindig que no percibimos. Por otra parte, facilita el camino para migrar el código VB .NET a C#, si se quisiera. Al obligar a usar la conversión formal de tipos, estamos cumpliendo una regla formal de C#. La ausencia de esta directiva tiende a producir un código .NET no puro.Usar métodos del framework siempre que sea posible
Al omitir el espacio de nombres Microsoft.VisualBasic, las funciones de cadena clásicas de VB6 como Mid$ , Left$ , y Rigth$ , InStr, y otras, deben ser reemplazadas por los métodos que suministra el framework. En general, SubString reemplaza a Mid$ , Left$ , y Right$, e IndexOf a InStr. No obstante, deben tomarse precauciones ya que SubString por sí solo no produce todo la funcionalidad de estas funciones. Si lo deseáramos, podríamos escribir dichas funciones en un lenguaje .NET para su uso.Asignación del valor de las variables en su declaración
VB6 no soporta la asignación del valor de las variables en su declaración. Es conveniente ahorrar líneas de código en VB .NET y escribir las líneas pertinentes.Reemplazo de arreglos Variant
La declaración explícita de los arreglos es más intuitiva y elegante en VB .NET; además, podemos usar el tipo pertinente, lo que produce un código de mejor desempeño. Se debe recordar que .NET inicia los arreglos con índice cero; no es adecuado recurrir a artificios .NET para producir arreglos que no inicien en cero pues la potencia de .NET es opacada. Siempre hay forma de reescribir fórmulas que manipulen índices para ajustarse a la norma de .NET.Usar métodos de operador
Ya que VB .NET soporta operadores tales como += , &=, conviene usarlos. El desempeño es mejor que usar la sintaxis convencional de VB6.Usar herencia siempre que sea posible
Este detalle por sí solo da para un artículo de varias páginas. En realidad, desde una aplicación normal de VB6 es difícil percibir en qué nos beneficiaría la herencia formal. Sin embargo, para aplicaciones grandes, la necesidad se puede vislumbrar con facilidad. Otra cuestión que viene a colación es la herencia que produce Implements de VB6 contra la herencia formal de los lenguajes .NET, o la herencia que produce Implements en los lenguajes .NET. Cuando una aplicación robusta de VB6 ha usado Implements masivamente, es preferible no migrarla a lenguajes .NET. Quizás sea preferible analizar la arquitectura desde un punto de vista .NET y hacer todo de nuevo si el tiempo lo permite. Ese es el caso en algunas de mis aplicaciones.Operaciones con variables de cadena
Sabemos que las variables de cadena en .NET son inmutables; es decir, se crea un nuevo espacio de memoria en cada asignación de una variable tipo string. A raíz de ello, el framework suministra la clase StringBuilder para hacer eficientes las operaciones con cadenas. He discutido el asunto con varios expertos en los foros de C# de Microsoft (Jon Skeet, Greg Young e Ignacio Machin, entre otros). La conclusión es que si las operaciones con cadena requieren varios ciclos de computación, típico en un bucle largo, es imperioso usar StringBuilder; si por el contrario las iteraciones son de un número discreto, no muy grande, está permitido el uso directo de String en operaciones de cadena, sin mucha pérdida de desempeño. En el ejemplo del artículo, las operaciones con cadena rara vez superan las ocho iteraciones, por lo que es eficiente mantener el código si recurrir a StringBuilder.Una función IsNumeric eficiente
Los lenguajes .NET en su especificación no incorporan IsNumeric, pero hay varias formas de reproducir la función. El artículo "Which IsNumeric method should you use?", por Ambrose Little, polemiza sobre el asunto y suministra una función con mejor desempeño, que es la que uso en este artículo, traducida desde C# a VB .NET. Recomendable.
Posiblemente se te ocurran otras reglas de traducción VB6 a VB .NET; por mi parte, he descrito las que a mi parecer son el principio.
5. Probando la clase CN2W de VB .NET
La clase CN2W puede usarse en cualquier contexto .NET. A continuación vemos un ejemplo extraído de una aplicación de Windows con VB .NET. Con los nombres debería ser suficiente para intuir su uso.
Private Sub btnCurrencyToWords_Click( ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCurrencyToWords.Click Dim n2w As CN2W = New CN2W() Me.txtOutput.Text = n2w.CurrencyToWords(Me.txtNumber.Text, "Dolars") End Sub
Ejemplo de uso de la clase N2W de VB .NET
En la descarga de este artículo encontrarás la aplicación N2W_VB completa.
6. Solución en C#
Es posible traducir una aplicación VB6 a C#, pero es mucho más dificil que migrar primero a VB .NET y luego a C#. Los lenguages .NET tienen su asidero en C#, y éste debería ser la opcion para comenzar una aplicación .NET desde cero. Abandoné C en 1995 cuando descubrí Visual Basic; uno hacia las cosas en menos de la tercera parte del tiempo. No obstante, la programacion por punteros se olvida si no se practica; además es horrorosa. Así pues, me casé con Visual Basic. Ahora, VS 2005 nos trae un C que se depura como Visual Basic y el Intellisense es potente ¿Por qué no usarlo? Además, ¡Este maldito C# es muy bonito!
El código del problema expuesto en este artículo en C#, es el siguiente:
using System; using System.Globalization; public class CN2W { const string ZERO = "Zero"; string IntegerPart = ZERO; string RealPart = ZERO; string _DecimalSeparator; string _GroupSeparator; string[] aT0 = new string[] {"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"}; string[] aT1 = new string[] {"Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"}; string[] aT2 = new string[] {"Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety" }; string[] aT3 = new string[] {"", "", " Thousand ", " Million ", " Billion ", "Trillion ", "", "", "", "" }; public CN2W() { _DecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.CurrencyDecimalSeparator; _GroupSeparator = CultureInfo.CurrentCulture.NumberFormat.CurrencyGroupSeparator; } public string IntegerToWords(string Number) { RealToWords(Number); return IntegerPart; } public string RealToWords(string Number) { int DotPos = 0; IntegerPart = ZERO; RealPart = ZERO; if (IsNumeric(Number)) { Number = Number.Replace(_GroupSeparator, ""); DotPos = Number.IndexOf(_DecimalSeparator); if (DotPos >= 0) { IntegerPart = ToWords(Convert.ToInt32(Number.Substring(0, DotPos))); RealPart = ToWords(Convert.ToInt32(Number.Substring(DotPos + 1, Number.Length - DotPos - 1))); } else { IntegerPart = ToWords(Convert.ToInt32(Number)); } } return IntegerPart + " Dot " + RealPart; } public string CurrencyToWords(string Number, string MoneyName) { string s; IntegerPart = ZERO; RealPart = ZERO; if(IsNumeric(Number)) { // round to money number s = RealToWords(RoundNumberString(Convert.ToDouble(Number), 2)); } // add money text if (IntegerPart == ZERO) {IntegerPart = "No";} switch(RealPart) { case ZERO: RealPart = "And No Cents"; break; case "One": RealPart = "And One Cent"; break; default: RealPart = "And " + RealPart + " Cents"; break; } return IntegerPart + " " + MoneyName + " " + RealPart; } private string ToWords(int Number) { string s = ""; string d = ""; string r = ""; int n; s = Number.ToString(); n = 1; while (s.Length != 0) { // convert last 3 digits of s to English r. if (s.Length < 3) { d = ConvertHundreds(s); } else { d = ConvertHundreds(s.Substring(s.Length - 3, 3)); } if (d.Length > 0) { r = d + aT3[n] + r; } if (s.Length > 3) { s = s.Substring(0, s.Length - 3); } else { s = ""; } n++; } if (r.Length == 0) { r = ZERO; } return r; } private string ConvertHundreds(string pNumber) { string rtn = ""; if (!(Convert.ToInt32(pNumber) == 0)) { // append leading zeros to number pNumber = ("000" + pNumber).Substring(pNumber.Length, 3); // do we have a hundreds place digit to convert? if (!(pNumber.Substring(0, 1) == "0")) { rtn = ConvertDigit(pNumber.Substring(0, 1)) + " Hundred "; } // do we have a tens place digit to convert? if (pNumber.Length >= 2) { if (!(pNumber.Substring(1, 1) == "0")) { rtn += ConvertTens(pNumber.Substring(1)); } else { rtn += ConvertDigit(pNumber.Substring(2)); } } return rtn.Trim(); } else { return ""; } } private string ConvertTens(string pTens) { string r = ""; // is value between 10 and 19? if ((Convert.ToInt32(pTens.Substring(0, 1)) == 1)) { r = aT1[Convert.ToInt32(pTens) - 10]; } else { // otherwise it's between 20 and 99. r = aT2[Convert.ToInt32(pTens.Substring(0, 1)) - 2] + " "; // convert ones place digit r += ConvertDigit(pTens.Substring((pTens.Length - 1), 1)); } return r; } private string ConvertDigit(string pNumber) { if (pNumber == "0") { return ""; } else { return aT0[Convert.ToInt32(pNumber) - 1]; } } private string RoundNumberString(double Number, int Decimals) { string s; double r; // round first r = Math.Pow(10d, Decimals); // Math.Floor make sure the float round. Suggested by Guillermo Som r = (int)(Math.Floor(Number * r + 0.5d)) / r; // complete with zeros s = r.ToString(); if ((s.IndexOf(_DecimalSeparator) < 0)) { s += _DecimalSeparator; } while ((s.Substring(s.IndexOf(_DecimalSeparator)).Length <= Decimals)) { s += "0"; } return s; } // Source: http://aspalliance.com/articleViewer.aspx?aId=80&pId= // Review by Harvey Triana private bool IsNumeric(string s) { bool hasDecimal = false; bool r = false; char ds = Convert.ToChar(_DecimalSeparator); char gs = Convert.ToChar(_GroupSeparator); for (int i = 0; i < s.Length; i++) { // check for decimal if (s[i] == ds) { if (hasDecimal) // 2nd decimal r = false; else // 1st decimal { // inform loop decimal found and continue hasDecimal = true; continue; } } // check if number if (char.IsNumber(s[i]) || (s[i] == gs)) r = true; else { r = false; break; } } return r; } public string GroupSeparator { get { return _GroupSeparator; } } public string DecimalSeparator { get { return _DecimalSeparator; } } }
Clase N2W en C#
Si comparamos el código VB .NET contra el código C#, no hay mucho para decir. El código C# es como un dibujo calcado del codigo VB .NET puro; o al contrario, si así lo prefieres. C# produce un código horizontal más corto y uno vertical más largo (que es diferente a menor número de líneas de código).
Aunque sí hay un detalle simpático que se puede mencionar: el operador ^ de VB .NET no tiene un equivalente en C#; es por ello que usamos Math.Pow()-supongo que se conservó el operador ^ en VB .NET porque ya sería el colmo del olvido con los emigrantes de VB6.
7. Probando la clase CN2W de C#
La clase CN2W puede usarse en cualquier contexto .NET. A continuación vemos un ejemplo extraído de una aplicación de Windows con C#:
private void btnCurrencyToWords_Click(object sender, EventArgs e) { CN2W n2w = new CN2W(); this.txtOutput.Text = n2w.CurrencyToWords(this.txtNumber.Text, "Dolars"); }
Ejemplo de uso de la clase N2W de C#
En la descarga de este artículo encontrarás la aplicación N2W_VB completa.
8. Conclusión
Si vamos a traducir una aplicación VB6 a VB .NET, no deberíamos usar el espacio de nombres Microsoft.VisualBasic. Además, deberíamos pegarnos a Option Strict On, ya que de esta forma se va a producir un código con mejor fidelidad y eficiencia. Escribir VB .NET puro es equivalente a escribir C#; no existe gran diferencia. Cuando escribes VB .NET puro realmente estás usando .NET en su verdadero potencial. VB .NET no es, como algunos piensan, la versión moderna de Microsoft Visual Basic.
Harvey Triana es Ingeniero de Petróleos y se especializa en el desarrollo de software para ingeniería con las tecnologías .net y VB clásico. Ha sido MVP VB y actualmente participa en forma activa en los news públicos de MS.