Post temporal: obras en agosto.

Agosto 3rd, 2009

obrasVaya la verdad por delante: éste es uno de esos ridículos metaposts (posts que hablan de otros posts) con los que algunos bloggers nos saturan la bandeja del Google Reader. Pero es una excepción y lo utilizo como aviso para informar a aquellos que visiten este blog durante el mes de agosto, ya que conviene que sepan que estoy haciendo bastantes cambios en el blog y por tanto se pueden encontrar cosas que no funcionen como es debido (muy especialmente en cuanto a formatos).

Hacía tiempo que tenía en mente llevar a cabo algunos cambios importantes, porque el blog estaba tomando una línea que no me convencía en absoluto. Así que este mes de agosto voy a aprovechar.

Como primer cambio visible, el área de publicación ahora es más grande, pasando más o menos de 750px a cerca de 1000px. En consecuencia, los bloques de código también pueden ser más amplios, llegando a los 100 caracteres por línea, que es justamente el número máximo con el que trabajo cuando llevo a cabo algún proyecto.

He cambiado también algunos formatos de textos y códigos. Ahora es, digamos, algo más serio.

Pero sobretodo el cambio está enfocado al contenido. He puesto en "pendientes de revisión" muchísimos posts que quiero repasar antes de decidir si siguen publicados o no. El motivo es claro: pretendo que este blog sea cada vez más profesional y menos personal (asumiendo que un blog es personal casi por definición, pero ya nos entendemos). Quiero hablar aquí de algunos de mis proyectos y aportar materiales técnicos. En cambio no quiero seguir pataleando cuando en algún comercio no me atiendan como es debido (para eso existen las hojas de reclamación a las que últimamente me estoy aficionando).

Crearé otras categorías. Quiero hablar de temas más relacionados con la empresa para desempolvar mis oxidados conocimientos empresariales. De paso puede que a algún emprendedor puedan servirle de algo mis ideas -en cualquier caso seguro que no le estorbarán-. También han pasado a interesarme mucho los temas relacionados con la productividad desde que leí el libro de David Allen y quiero ir entrando poco a poco en esas cuestiones.

Puede que las únicas licencias no profesionales que me permita sean las relacionadas con la fotografía, pero todavía esta por ver cómo tocaré este tema.

No me alargo más. Eliminaré este post cuando crea que todo está más o menos como lo quiero.

Miniacertijo en MySQL.

Junio 23rd, 2009

Esta mañana he estado un rato alucinando con unos resultados que me estaba dando el servidor de MySQL ante unas instrucciones de lo más simples cuyos resultados esperados eran a priori evidentes. Es probable que estuviera bajo los influjos de demasiado café, quién sabe, pero el caso es que durante unos buenos minutos no podía dar crédito a lo que estaba viendo. He aquí la secuencia de consultas llevadas a cabo:




mysql> select count(*) from t;
+----------+
| count(*) |
+----------+
|       73 |
+----------+
1 row in set (0.00 sec)

mysql> update t set f = null;
Query OK, 73 rows affected (0.00 sec)
Rows matched: 73  Changed: 73  Warnings: 0

mysql> select count(*) from t where not isnull(f);
+----------+
| count(*) |
+----------+
|        4 |
+----------+
1 row in set (0.00 sec)

Ahí al final, por supuesto, esperaba recibir un 0. Pero no...

He llegado a pensar que MySQL se había vuelto loco, que el servidor había cascado... qué se yo, tonterías. Al final he descubierto qué estaba pasando. ¿Alguien se anima a especular? De hecho con el nivelillo de algunos de los que a veces comentáis, estoy seguro que mientras leíais este post ya se os ha ocurrido qué estaba pasando, así que más que la solución, os propongo que comentéis cuánto rato os ha llevado dar con ella.

Resolución.

Pues el ganador por aproximación ha sido Pedro Cambra que ha comentado "que la tabla tenga algún tipo de clave foránea, un trigger o mucha actividad y según hayas hecho el update se hayan modificado datos". Efectivamente, en su día creé dos triggers sobre la tabla en cuestión, uno sobre inserciones (irrelevante en este caso) y otro para actualizaciones. Los dos hacían lo mismo: si al insertar o modificar un registro el campo g tenía un valor concreto 'x', el campo f tomaría otro valor concreto 'y' independientemente del que el usuario hubiera introducido. Esto estaba haciendo ahora que para 4 registros concretos (los que cumplián la condición para el campo g) el trigger impidiera que f tomara valor NULL para pasar a tomar el valor que en el trigger estaba previsto que tomara. Pero claro, había olvidado que esta tabla tenía estos triggers y de ahí vinieron esos minutos de incredulidad...

Rapidez de operaciones entre Integer, Long y Double.

Febrero 22nd, 2009

Como quiera que estos últimos días ando trabajando algunos aspectos que hacen relación a los distintos tipos de datos, sobretodo en lo que a la clase Nullable se refiere, me he topado con una referencia a la velocidad de cálculo de operaciones en función del tipo de dato. Y como me ha parecido curiosa, he buscado la manera de poder comparar de manera fácil y rápida la velocidad entre distintos tipos de datos para ver qué valores me daba.

Los tipos de datos a comparar son Integer (Int32), Long (Int64) y Double.

El código que utilizo para comparar tiempos es el siguiente (la variable v cambia entre Integer, Long y Double en cada ejecución):

Dim v As Integer = 0
Dim Begin As Date = Now()

For i As Integer = 1 To 1000000000
    v += 1
Next

Dim Finish As Date = Now()
Debug.Print(Finish.Subtract(Begin).TotalMilliseconds.ToString)

Básicamente no hace más que incrementar el valor de una variable mil millones de veces (¿quién no ha necesitado algo así alguna vez, eh?).

Para un Integer obtengo estos tiempos en tres ejecuciones consecutivas:
3884 - 3890 - 3876

Para un Long obtengo estos otros:
4437 - 4453 - 4444

Y para un Double estos de aquí:
4980 - 4990 - 4990

Pues sí. Realmente un Integer es algo más rápido que los demás en cuanto a velocidad de cálculo (en realidad alrededor de un 20% sobre los resultados de Double, aunque con unas cifras tan insignificantes el resultado nace de por sí algo desvirtuado).

Por último una recomendación personal, puestos a escoger y como cuesta lo mismo una cosa que otra, mejor trabajar con Int16, Int32 o Int64 que con Short, Integer o Long. Son exactamente lo mismo pero la primera sintaxis es exclusiva de Visual Basic, mientras que la segunda hace referencia al .NET Framework y por tanto es común a todos los lenguajes que lo utilizan (incluído el propio Visual Basic).

SummaryDataGridView o cómo mostrar fila de totales en un DataGridView en VisualBasic.NET.

Febrero 17th, 2009

Ayer estaba intentando mostrar una fila de totales en un objeto DataGridView y tras unas horitas de buscar, investigar, probar, depurar y desesperar... descubrí que en realidad no se puede hacer tal cosa de manera directa. Si el DataGridView está enlazado a datos (caso más habitual), no nos permitirá añadir una fila directamente. Sí que se la podremos añadir al propio origen de datos (supongamos que un DataTable), pero aun así tendremos muchos problemas cuando el usuario ordene los resultados del DataGridView por alguna de las columnas simplemente haciendo click sobre su encabezado. Por supuesto podemos eliminar la posibilidad de que se pueda hacer click en los encabezados de las columnas para ordenar los resultados, pero eso no es más que eliminar un problema quitando una funcionalidad importante, así que no se acepta dicha solución.

Al final he optado por crearme mi propio SummaryDataGridView que hereda directamente de la clase DataGridView, con lo cual tiene intactos todos sus métodos, eventos y propiedades y por tanto se puede utilizar en cualquier punto donde quisiéramos utilizar un DataGridView. Pero además incorpora los métodos siguientes:

Public Sub SetDataSource(ByVal dt As DataTable)

Debe utilizarse cuando queramos establecer el origen de datos del SummaryDataGridView. Se podría utilizar simplemente la propiedad DataSource, pero en ese caso no funcionarían las características extras del SummaryDataGridView, por lo cual conviene utilizar este método SetDataSource en lugar de la propiedad DataSource. Probablemente se podría haver sobrecargado la propiedad DataSource del DataGridView, pero he preferido dejarla intacta y utilizar un nuevo método público propio.

Public Sub AddSummaryRow(ByVal ParamArray ColumnName() As String)

Añade una última fila al SummaryDataGridView con los totales (sumas) de todas las columnas cuyos nombres se hayan indicado como parámetros.

Public Sub SetFilter(ByVal Filter As String)

Permite filtrar los resultados a mostrar en el SummaryDataGridView mediante una condición tipo SQL simple o compuesta. Evidentemente, tras filtrar los resultados mostrados la fila de totales se actualiza convenientemente.

Así pues, una vez tenemos un objeto SummaryDataGridView en nuestro formulario, podemos poblarlo, añadirle una fila de totales y filtrarlo del siguiente modo:

Me.dgvMain.SetDataSource(dt)
Me.dgvMain.AddSummaryRow("field1", "field2", "fieldN")
Me.dgvMain.SetFilter("field1 < 10 AND field2 > 5")

A continuación dejo el código fuente íntegro de esta clase SummaryDataGridView. Y ya sabes... si te ha sido útil tienes la obligación moral de... a) enviarme una transferencia por valor de 500 euros para compensarme por las horas de desarrollo... o bien b) dejar un comentario excusándote por no hacerme la transferencia y contándome qué tal te funcionó el SummaryDataGridView.

'--------------------------------------------------------------------
' Author:             Albert Mata (www.albertmata.net)
' Last time modified: 2009-02-17
' Description:        Acts like a standard DataGridView but with the
'                     possibility to add a summary row fixed at the 
'                     end even when user sorts data.
'--------------------------------------------------------------------
Public Class SummaryDataGridView
    Inherits System.Windows.Forms.DataGridView

#Region "PrivateAttributes"

    '----------------------------------------------------------------
    ' Private attributes.
    '----------------------------------------------------------------
    'After it's been set, it will not change anymore.
    Private MainDataTable As DataTable
    'It can be Nothing and it changes every time filter does.
    Private SummaryDataRow As DataRow
    'It always has same data shown in DataGridView.
    Private ShownDataTable As DataTable
    'It's Nothing or it stores a column name.
    Private LastSortedColumn As String
    'It's Nothing or it stores ASC/DESC string value.
    Private LastSortOrder As String
    'Array of column names to summarize.
    Private SummaryColumns() As String

#End Region

#Region "PublicMethods"

    '----------------------------------------------------------------
    ' Sets DataSource and stores initial DataTable.
    '----------------------------------------------------------------
    Public Sub SetDataSource(ByVal dt As DataTable)

        'Setting MainDataTable and adding auxiliar column
        'for SummaryRow information.
        Me.MainDataTable = dt
        Me.MainDataTable.Columns.Add(New DataColumn("@SummaryRow", _
                                 System.Type.GetType("System.Byte")))

        'Setting new auxiliar column value to 0 
        '(in SummaryRow it'll be 1).
        For Each dr As DataRow In Me.MainDataTable.Rows
            dr.Item("@SummaryRow") = 0
        Next

        'Filling DataGridView with ShownDataTable and hidding 
        'auxiliar column.
        Me.ShownDataTable = Me.MainDataTable.Copy()
        Me.DataSource = Me.ShownDataTable
        Me.Columns("@SummaryRow").Visible = False

    End Sub

    '----------------------------------------------------------------
    ' Adds a summary row to DataGridView with values in all
    ' specified columns.
    '----------------------------------------------------------------
    Public Sub AddSummaryRow(ByVal ParamArray ColumnName() As String)

        'Removing SummaryRow from ShownDataTable 
        'if it already contains it.
        If Me.ShownDataTable.Rows.IndexOf(Me.SummaryDataRow) > -1 _
        Then
            Me.ShownDataTable.Rows.Remove(Me.SummaryDataRow)
        End If

        'Storing columns to be summarized 
        '(can be useful for other methods).
        Me.SummaryColumns = ColumnName

        'Creating SummaryDataRow.
        Me.SummaryDataRow = Me.ShownDataTable.NewRow()

        'Adding SUM values to each specified column.
        For i As Integer = 0 To ColumnName.Length - 1
            Me.SummaryDataRow.Item(ColumnName(i)) = _
                Me.ShownDataTable.Compute("SUM(" & ColumnName(i) _
                                        & ")", "")
        Next

        'Setting auxiliar column value to 1 
        '(useful for sorting purposes).
        Me.SummaryDataRow.Item("@SummaryRow") = 1

        'Adding TotalsDataRow to ShownDataTable.
        Me.ShownDataTable.Rows.Add(Me.SummaryDataRow)

    End Sub

    '----------------------------------------------------------------
    ' Sets a filter to DataGridView.
    '----------------------------------------------------------------
    Public Sub SetFilter(ByVal Filter As String)

        'Creating DataView with desired filter.
        Dim dv As New DataView(Me.MainDataTable, Filter, "", _
                               DataViewRowState.CurrentRows)

        Me.ShownDataTable = dv.ToTable()

        'Adding new calculated SummaryRow if it already exists.
        If Not IsNothing(Me.SummaryDataRow) Then
            Me.AddSummaryRow(Me.SummaryColumns)
        End If

        'Creating sorting SQL clausule.
        Dim OrderBy As String = ""

        If Not IsNothing(Me.LastSortedColumn) _
        And Not IsNothing(Me.LastSortOrder) Then
            OrderBy = "@SummaryRow ASC, " _
                    & Me.LastSortedColumn & " " & Me.LastSortOrder
        End If

        'Sorting DataView.
        dv = New DataView(Me.ShownDataTable, "", OrderBy, _
                          DataViewRowState.CurrentRows)

        'Filling DataGridView with DataView.
        Me.DataSource = dv

    End Sub

#End Region

#Region "EventHandlers"

    '----------------------------------------------------------------
    ' Event Me.Sorted.
    '----------------------------------------------------------------
    Private Sub ExtendedDataGridView_Sorted(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.Sorted

        'Setting sorted column and sort order.
        If Me.SortedColumn.Name = Me.LastSortedColumn Then
            If Me.LastSortOrder = "ASC" Then
                Me.LastSortOrder = "DESC"
            Else
                Me.LastSortOrder = "ASC"
            End If
        Else
            Me.LastSortedColumn = Me.SortedColumn.Name
            Me.LastSortOrder = "ASC"
        End If

        'Creating sorting SQL clausule.
        Dim OrderBy As String = "@SummaryRow ASC, " _
                              & Me.LastSortedColumn & " " _
                              & Me.LastSortOrder

        'Creating DataView with desired sort order.
        Dim dv As New DataView(Me.ShownDataTable, "", OrderBy, _
                               DataViewRowState.CurrentRows)

        'Filling DataGridView with DataView.
        Me.DataSource = dv

    End Sub

#End Region

End Class

Me.DesignMode o ¿podrías por favor no interpretar el código mientras sólo estoy diseñando? (Gracias)

Enero 31st, 2009

VisualBasic.NET (supongo que todo el VisualStudio.NET) tiene un comportamiento que me desquicia. Bueno, tiene unos cuantos, pero hoy hablo de uno en particular. Imaginemos un caso muy habitual en el que tengo un control de usuario y un formulario. Primero diseño el control de usuario y luego en el formulario lo insertaré.

El control de usuario (ctrlDGV.vb) lo creo simplemente añadiéndole un objeto de tipo DataGridView (dgvMain) y el siguiente código:

Public Class ctrlDGV

    '----------------------------------------------------------------
    ' Private attribute.
    '----------------------------------------------------------------
    Private DBC As DatabaseConnection

    '----------------------------------------------------------------
    ' Setter for DBC.
    '----------------------------------------------------------------
    Public Sub LoadDBC(ByVal DBC As DatabaseConnection)
        Me.DBC = DBC
    End Sub

    '----------------------------------------------------------------
    ' Event Me.Load.
    '----------------------------------------------------------------
    Private Sub ctrlDGV_Load(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.Load
        Dim DT As DataTable
        DT = Me.DBC.GetDataTable("SELECT field FROM table")
        Me.dgvMain.DataSource = DT
    End Sub

End Class

El objeto Me.DBC es un objeto de conexión a datos cuyo método GetDataTable me devolverá un DataTable a partir de una cadena SQL (es todo lo que necesitamos saber aceca de él en lo que a este post se refiere). La manera de crear ese objeto Me.DBC no atañe a este control, porque asumimos que siempre nos preocuparemos de llamar al método LoadDBC (y por tanto asegurarnos que Me.DBC ha dejado de ser Nothing) antes de que se dispare el evento Me.Load, así que no habrá problemas.

¿No habrá problemas? Sí, sí que los habrá... Los habrá simplemente cuando intentemos insertar este control en un formulario en vista diseño. En ese momento obtendremos el siguiente error...

...y no nos dejará insertarlo. ¿Por qué? Pues porque el código en el evento Me.Load se está interpretando y descubre que Me.DBC es todavía Nothing... No entiendo por qué narices tiene que interpretar código en tiempo de diseño, la verdad. Y lo curioso del caso es que si comentamos el código del evento Me.Load entonces nos deja insertar dicho control en un formulario sin problemas. Y si después volvemos a descomentar el código, aunque no podremos ver el formulario en modo diseño porque nos volverá a dar el error, sí que podremos ejecutar la aplicación y funcionará correctamente (si llamamos al método LoadDBC antes de que se lance el evento Me.Load, insisto). Y es normal que funcione porque el flujo es correcto en tiempo de ejecución...

El caso es que para solucionar este problema tenemos dos posibilidades. La primera es actuar a lo bestia y meterlo todo dentro de un Try-Catch y muerto el perro se acabó la rabia:

'----------------------------------------------------------------
' Event Me.Load.
'----------------------------------------------------------------
Private Sub ctrlDGV_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.Load
    Try
        Dim DT As DataTable
        DT = Me.DBC.GetDataTable("SELECT field FROM table")
        Me.dgvMain.DataSource = DT
    Catch ex As Exception
        'On error no nothing, it's just for design time.
    End Try
End Sub

El segundo es ser un poco más sutil y hacer uso de la propiedad DesignMode del propio control para decirle algo así:

'----------------------------------------------------------------
' Event Me.Load.
'----------------------------------------------------------------
Private Sub ctrlDGV_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.Load
    If Not Me.DesignMode Then
        Dim DT As DataTable
        DT = Me.DBC.GetDataTable("SELECT field FROM table")
        Me.dgvMain.DataSource = DT
    End If
End Sub

Es decir, si no estás en modo diseño ejecútame este código (si estás en modo diseño no hagas nada). Me parece algo surrealista, sinceramente. Para mí lo normal sería... si estás en modo diseño, no hagas nada, ni te mires el código, ni olerlo, dejámelo a mí... Y como mucho que la propiedad Me.DesignMode sirviera para lo contrario: para explícitamente solicitarle que hiciera una determinada cosa incluso en tiempo de diseño.

Pues hasta que descubrí esto... ufffff...

Quitar la selección de un ComboBox en un DataGridView en .NET.

Enero 22nd, 2009

Minipost recordatorio para mí mismo... Hace un rato he descubierto con gran sorpresa y desencanto que si en un objeto de tipo DataGridView tengo puestas algunas columnas como tipo ComboBox a través del objeto propio del .NET Framework DataGridViewComboBoxColumn (toma nombre cortito), y el campo en cuestión es de algún tipo que admite nulos -por ejemplo un Nullable(Of UInt16) o un simple String-... hasta que en el ComboBox no seleccionamos ningún valor, en la tabla relacionada de la base de datos se almacenará un valor NULL sin problemas, pero en el momento en que en el ComboBox seleccionamos un valor, no tenemos manera fácil de quitar esa selección. Dicho en plan fácil, un ComboBox dentro de un DataGridView nos permite seleccionar fácilmente un valor pero difícilmente quitar esa selección (aunque parezca mentira).

No obstante, si el campo es de tipo String no hay demasiado problema: nos podemos situar encima y con la combinación CTRL+0 (control cero) quitamos la selección. Ahora bien, el problema viene cuando el campo es numérico -como Nullable (Of UInt16)-. Aquí no podemos hacer un CTRL+0 sin más o nos lanzará un error.

Buscando buscando he encontrado un par de páginas (ésta y esta otra) que me han ayudado a resolver el problema y de paso a conocer que se trata de un bug reportado a Microsoft (sin que se hayan esmerado gran cosa en corregirlo, dicho sea de paso). En cualquier caso, la solución pasa por incorporar el siguiente código:

'----------------------------------------------------------------
' Event DGV.KeyDown.
'----------------------------------------------------------------
Private Sub DGV_KeyDown(ByVal sender As Object, _
ByVal e As System.Windows.Forms.KeyEventArgs) _
Handles DGV.KeyDown
    If TypeOf Me.DGV.Columns(Me.DGV.CurrentCell.ColumnIndex) _
    Is System.Windows.Forms.DataGridViewComboBoxColumn Then
        If e.KeyCode = Keys.D0 Or e.KeyCode = Keys.NumPad0 _
        Or e.KeyCode = Keys.Delete Then
            Me.DGV.CurrentCell.Value = System.DBNull.Value
            e.Handled = True
        End If
    End If
End Sub

De paso he incorporado la misma funcionalidad para la tecla Suprimir, que siempre es bastante más intuitivo para quitar la selección de un ComboBox que la combinación CTRL+0.

Todo lo que quisiste hacer con un PDF pero no supiste cómo.

Enero 11th, 2009

A través de Menéame descubro este genial artículo de Blogoff en el que explican cómo trastear y manipular a nuestro antojo documentos PDF sin necesidad de tener el programa Adobe Acrobat versión completa (es decir, no el Reader que es gratuíto sino el completo, el de pago). Decido incorporarlo a mi propio blog para tenerlo más mano cuando lo necesite y para de paso hacerlo llegar a más gente. El artículo de Blogoff se puede encontrar aquí y a su vez está basado en el artículo en inglés original que se puede leer aquí.

Todo lo que sigue a continuación no está escrito por mí sino que es el contenido íntegro de la entrada en el blog Blogoff...

1. ¿Cómo creo documentos PDF en mi ordenador sin necesidad de Adobe Acrobat?

Hazte con una copia del programa gratuito DoPDF [Ver tutorial]

2. Pero no quiero instalar un programa cuando lo único que voy a convertir son unos pocos documentos.

Sube tus documentos a Google Docs a través del explorador y luego expórtalos como PDF. Así de simple

3. Un cliente me ha mandado una presentación PowerPoint por e-mail y aquí no tengo nada para abrirlo ¿qué puedo hacer?

Reenvía el correo (con el powerpoint adjunto) a pdf@koolwire.com. Ellos convertirán la presentación a PDF y te la mandarán de vuelta para que la puedas ver en la mayoría de los dispositivos portátiles.

4. ¿Cómo guardo una página web como PDF sin guardarla como HTML primero?

Sólo tienes que ir a PrimoPDF, escribir la dirección de la página y recibirás una copia de la misma en PDF en el correo que les hayas proporcionado.

5. ¿Cómo puedo transformar un PDF a otros formatos como documento de Word, imagen, HTML, etc…?

A través de una web como Zamzar [ver tutorial]

6. ¿Cómo puedo juntar dos archivos PDF en uno? ¿Y cambiar el orden de las páginas?

PDFill es una utilidad muy versátil que te permitirá combinar varios documentos PDF en uno, reordenar las páginas e incluso rotarlas desde dentro del PDF.

7. Quiero extraer el texto de un PDF para usarlo en mi documento de Word ¿cuál es la mejor opción?

Abre PDF Text Extraction y sube tu PDF. Extraerá las 10 primeras páginas de tu documento en formato texto.

8. No puedo usar el truco de arriba porque el documento PDF no se hizo desde Word sino que fue escaneado.

Hay un modo de extraer el texto de estos documentos a través del OCR de Google. Quizás no la forma más rápida pero posiblemente la mejor solución gratuita.

9. Tengo documentos PDF en mi ordenador en el sentido de que no se pueden imprimir o seleccionar texto en ellos con el ratón.

PDF Unlocker es una utilidad gratuita que borra las restricciones más habituales sobre un PDF sin pedir ningún tipo de password.

10. Algunos PDF de mi empresa están protegidos con contraseñas que nadie recuerda después de la cena de empresa de ayer ¿qué puedo hacer?

Échale un vistazo a How to Open Password Protected PDF

11. Busco un servicio que permita a mis visitantes bajarse los artículos como PDF.

Añade el botón Web2PDF en algún sitio de tu página web. Convertirá la página a PDF de forma instantánea y además te permitirá mantener un registro de las conversiones.

12. Alguien me ha enviado un documento PDF en un idioma que no entiendo.

Puedes traducirlo con Google Translate y Zoho Viewer.

13. ¿Cómo puedo añadir anotaciones o notas de texto a mis documentos PDF?

Descarga PDF-X Viewer que es como Adobe Reader pero con algunas funciones adicionales. Puedes añadir las anotaciones que quieras e incluso pegar imágenes. Otra opción parecida es PDF Escape.

14. ¿Cómo puedo abrir un documento PDF online sin tener el software de Adobe?

Una alternativa fácil para abrir PDF en el navegador es el Free Online PDF Viewer, aunque hay otras muchas.

15. ¿Puedo rellenar formularios PDF online sin Acrobat Reader?

Sólo tienes que ir a PDF Filler, subir el archivo y empezar a escribir.

16. ¿Cómo puedo añadir una marca de agua o mi propia firma a un PDF?

Lo primero que tienes que hacer es crear tu marca de agua o firma con Paint o cualquier otro programa de dibujo y luego guardarla como imagen. Asegúrate de haber recortado bien la firma y abre el documento PDF en el PDF-X Viewer que ya hemos mencionado. Una vez allí sólo tienes que pegarla dentro del documento.

17. Tengo un libro en PDF que llega a las 200 páginas ¿hay algún modo de extraer algunas páginas en concreto y salvarlas como otro PDF?

Puedes usar tanto PDF Merge como PDF SAM para hacer esta operación.

18. ¿Cómo puedo proteger mi documento PDF con una contraseña?

En PDF Hammer subes tu documento y le asignas la contraseña que quieras.

19. Un PDF tiene muchos enlaces pero no puedo hacer click en ellos porque están en texto plano. ¿Cómo puedo convertilos en enlaces estándar?

Una vez más, acude a PDF Escape, sube el PDF y coloca los hipervínculos donde consideres oportuno.

20. ¿Qué servicios me rec0mendarías para subir archivos PDF a internet?

En el artículo original recomiendan Issuu que aún no he probado. De los que he visto me quedo con Scribd.

21 [Blogoff Bonus Track]. ¿Cómo puedo abrir un PDF más rápido con Adobe Reader?

Desactivando los plugins o usando Foxit PDF como lector de escritorio.

21 [Blogoff Bonus Track]. ¿Cómo crear un PDF de un documento de Word?

Con Express PDF (mi herramienta favorita)

22. [Blogoff Bonus Track]. ¿Cómo puedo manejar archivos PDF en OpenOffice?

Plugin para editar archivos PDF en Open Office sin Adobe Reader

TextBox mejorado en .NET.

Diciembre 23rd, 2008

Cuando desarrollamos en .NET es habitual utilizar controles personalizados que mejoran los preexistentes. Habitualmente podremos hacer uso de librerías públicas que podemos obtener en internet (liberadas o de pago). Otras veces podremos crearnos nosotros mismos nuestras propias librerías de controles. Yo suelo hacer esto último cuando no se trata de algo demasiado complejo, ya que termino antes que buscando componentes de terceros y peleándome después con sus correspondientes licencias. Entre otras cosas porque cualquier tipo de licencia "share alike" no me sirve.

Uno de los controles que me ahorran mucho trabajo en la creación de formularios es este que presento hoy y que en su día llamé TextBoxFocused (en adelante TBF). Se trata de un TextBox que cambia de color al recibir y perder el foco (de ahí su nombre -por cierto, sé que debería ser FocusedTextBox, pero quería mantener lo de TextBox al inicio-) y permite controlar a priori y a posteriori qué se puede introducir en él.

Un formulario con algunos TBF se vería así:

Como se puede apreciar, sin tener que añadir código alguno al formulario, el TBF aparecerá sombreado cuando tenga el foco. Además cuando se esté introduciendo un texto se mostrará en rojo, y cuando ya esté validado quedará en verde, tal como se aprecia en las siguientes imágenes:

Nada realmente espectacular, pero queda bonito. Sin embargo en donde el TBF se muestra útil es en el control del contenido introducido. Este control se realiza de una doble manera: a priori y mediante una propiedad del control se establece qué tipo de introducciones se admitirán, a posteriori -si se ha seleccionado- se controla que lo que se ha introducido realmente fuera lo que se debía introducir, impidiendo abandonar el TBF en caso contrario.

Para ello, en la ventana de propiedades del control nos aparecen dos nuevas creadas para la ocasión, tal como se muestra a continuación:

En las propiedades de cosecha propia siempre añado el prefijo "am" para localizarlas todas juntas. En este caso amAllow permite fijar qué introducciones se permitirán (cualquier cosa, nada de nada, sólo enteros positivos, dobles negativos, etc.) y amPostVerification decide si se aplicará verificación posterior o no.

Esto de amPostVerification tiene un sentido claro. Y es que podemos decirle al control que admita números dobles negativos y así sólo permitirá introducir números, el signo negativo y el signo decimal. Pero es evidente que con esos caracteres se pueden crear expresiones que no se correspondan con un número doble negativo (p.ej. -30-3.8..-25). Con este control a posteriori verificamos que efectivamente lo sea.

A nivel de código el control a posteriori se podría haber implementado a base de cástings y control de errores, pero no ha sido la opción que he escogido. Queda al antojo de cada cual modificar el código como le plazca.

Para su implementación en una aplicación, basta con añadir una clase TextBoxFocused.vb, copiar todo el código que se adjunta a continuación, compilar la aplicación y a partir de entonces tendremos el control disponible para añadirlo en cualquier formulario. O también se puede añadir a una librería de controles personalizados (que es lo que hago yo) y mantenerlo un poquito más organizado.

Código completo (de libre uso como todo lo que aparece publicado en este blog):

'--------------------------------------------------------------------
' Author:             Albert Mata (www.albertmata.net)
' Last time modified: 2008-11-13
' Description:        Acts like a standard TextBox but with some 
'                     better features like changing color when gets 
'                     or losts focus and controlling allowed 
'                     introductions.
'--------------------------------------------------------------------
Imports System.Drawing

Public Class TextBoxFocused
    Inherits System.Windows.Forms.TextBox

#Region "Constants"

    '----------------------------------------------------------------
    ' Constants for colors.
    '----------------------------------------------------------------
    Private COLOR_FOCUSED As Color = Color.FromArgb(255, 240, 157)
    Private COLOR_NON_FOCUSED As Color = Color.White
    Private COLOR_VALIDATED As Color = Color.Green
    Private COLOR_NON_VALIDATED As Color = Color.Red

    '----------------------------------------------------------------
    ' Constants for special keys.
    '----------------------------------------------------------------
    Private ASC_BACKSPACE As Integer = 8
    Private ASC_SUPPRESS As Integer = 127
    Private ASC_DASH As Integer = 45
    Private ASC_COMMA As Integer = 44

#End Region

#Region "Enumerations"

    '----------------------------------------------------------------
    ' Enumerations.
    '----------------------------------------------------------------
    Public Enum Introduction
        NothingAtAll
        PositiveInteger
        NegativeInteger
        PositiveDouble
        NegativeDouble
        OnlyLetter
        OnlyLetterOrDigit
        Password
        Everything
        EverythingNonCasing
    End Enum

#End Region

#Region "Attributes&Properties"

    '----------------------------------------------------------------
    ' Attributes.
    '----------------------------------------------------------------
    Private aAllow As Introduction = Introduction.Everything
    Private aPostVerification As Boolean = True

    '----------------------------------------------------------------
    ' Public properties to be shown in design time.
    '----------------------------------------------------------------
    Public Property amAllow() As Introduction
        Get
            Return Me.aAllow
        End Get
        Set(ByVal value As Introduction)
            Me.aAllow = value
        End Set
    End Property
    Public Property amPostVerification() As Boolean
        Get
            Return Me.aPostVerification
        End Get
        Set(ByVal value As Boolean)
            Me.aPostVerification = value
        End Set
    End Property

#End Region

#Region "GraphicalControls"

    '----------------------------------------------------------------
    ' Event Me.GotFocus.
    '----------------------------------------------------------------
    Private Sub TextBoxFocused_GotFocus(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.GotFocus
        MyBase.BackColor = COLOR_FOCUSED
        MyBase.ForeColor = COLOR_NON_VALIDATED
    End Sub

    '----------------------------------------------------------------
    ' Event Me.LostFocus.
    '----------------------------------------------------------------
    Private Sub TextBoxFocused_LostFocus(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles Me.LostFocus
        If Me.CheckText() Or Not Me.aPostVerification Then
            MyBase.BackColor = COLOR_NON_FOCUSED
            MyBase.ForeColor = COLOR_VALIDATED
        Else
            Me.Focus()
        End If
    End Sub

    '----------------------------------------------------------------
    ' Event Me.KeyPress.
    '----------------------------------------------------------------
    Private Sub TextBoxFocused_KeyPress(ByVal sender As Object, _
    ByVal e As System.Windows.Forms.KeyPressEventArgs) _
    Handles Me.KeyPress
        Select Case Me.aAllow
            Case Introduction.NothingAtAll
                'Allowing no character.
                e.Handled = True
                If AscW(e.KeyChar) = ASC_BACKSPACE Then
                    MyBase.Text = ""
                End If
            Case Introduction.PositiveInteger
                'Allowing just digits.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                End If
            Case Introduction.NegativeInteger
                'Allowing just digits and '-' character.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS _
                And AscW(e.KeyChar) <> ASC_DASH Then
                    e.Handled = True
                End If
            Case Introduction.PositiveDouble
                'Allowing just digits and ',' character.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS _
                And AscW(e.KeyChar) <> ASC_COMMA Then
                    e.Handled = True
                End If
            Case Introduction.NegativeDouble
                'Allowing just digits and '-' and ',' characters.
                If Not Char.IsDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS _
                And AscW(e.KeyChar) <> ASC_DASH _
                And AscW(e.KeyChar) <> ASC_COMMA Then
                    e.Handled = True
                End If
            Case Introduction.OnlyLetter
                'Allowing only letters.
                If Not Char.IsLetter(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                Else
                    e.KeyChar = Char.ToUpper(e.KeyChar)
                End If
            Case Introduction.OnlyLetterOrDigit
                'Allowing only letters and digits and upper casing 
                'letters.
                If Not Char.IsLetterOrDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                Else
                    e.KeyChar = Char.ToUpper(e.KeyChar)
                End If
            Case Introduction.Password
                'Allowing only letters and digits but not upper 
                'casing letters.
                If Not Char.IsLetterOrDigit(e.KeyChar) _
                And AscW(e.KeyChar) <> ASC_BACKSPACE _
                And AscW(e.KeyChar) <> ASC_SUPPRESS Then
                    e.Handled = True
                End If
            Case Introduction.Everything
                'Allowing everything upper casing letters.
                e.KeyChar = Char.ToUpper(e.KeyChar)
            Case Introduction.EverythingNonCasing
                'Allowing everything not upper casing letters (so 
                'doing nothing).
        End Select
    End Sub

#End Region

#Region "PrivateMethods"

    '----------------------------------------------------------------
    ' Checks introduced text according to Me.amAllow property and 
    ' returns True only when introduced text respects desired 
    ' Me.amAllow property.
    '----------------------------------------------------------------
    Private Function CheckText() As Boolean
        Select Case Me.aAllow
            Case Introduction.NothingAtAll
                'Checking no character has been introduced.
                Return Me.Text = ""
            Case Introduction.PositiveInteger
                'Checking a right positive integer has been 
                'introduced.
                Return Me.IsPositiveInteger(Me.Text) Or Me.Text = ""
            Case Introduction.NegativeInteger
                'Checking a right negative integer has been 
                'introduced.
                Return Me.IsNegativeInteger(Me.Text) _
                    Or Me.IsPositiveInteger(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.PositiveDouble
                'Checking a right positive double has been 
                'introduced.
                Return Me.IsPositiveDouble(Me.Text) _
                    Or Me.IsPositiveInteger(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.NegativeDouble
                'Checking a right negative double has been 
                'introduced.
                Return Me.IsNegativeDouble(Me.Text) _
                    Or Me.IsPositiveDouble(Me.Text) _
                    Or Me.IsNegativeInteger(Me.Text) _
                    Or Me.IsPositiveInteger(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.OnlyLetter
                'Checking only letters have been introduced.
                Return Me.HasOnlyLetters(Me.Text) Or Me.Text = ""
            Case Introduction.OnlyLetterOrDigit, _
                 Introduction.Password
                'Allowing only letters and digits have been 
                'introduced.
                Return Me.HasOnlyLettersOrDigits(Me.Text) _
                    Or Me.Text = ""
            Case Introduction.Everything, _
                 Introduction.EverythingNonCasing
                'Allowing everything.
                Return True
        End Select
    End Function

    '----------------------------------------------------------------
    ' Checks a right positive integer has been introduced.
    '----------------------------------------------------------------
    Private Function IsPositiveInteger(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C)
            End While
        Else
            R = False
        End If
        Return R
    End Function

    '----------------------------------------------------------------
    ' Checks a right negative integer has been introduced.
    '----------------------------------------------------------------
    Private Function IsNegativeInteger(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C) OrElse C = ChrW(ASC_DASH)
            End While
        Else
            R = False
        End If
        'Checking:
        '  - first character is a dash character,
        '  - length must be at least 2 characters.
        Return R And S.LastIndexOf(ChrW(ASC_DASH)) = 0 _
                 And S.Length >= 2
    End Function

    '----------------------------------------------------------------
    ' Checks a right positive double has been introduced.
    '----------------------------------------------------------------
    Private Function IsPositiveDouble(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C) OrElse C = ChrW(ASC_COMMA)
            End While
        Else
            R = False
        End If
        'Checking:
        '  - there's one comma character and it's not the first one,
        '  - there's exactly only one comma character,
        '  - the comma character it's not the last one.
        Return R And S.IndexOf(ChrW(ASC_COMMA)) > 0 _
                 And S.IndexOf(ChrW(ASC_COMMA)) = _
                     S.LastIndexOf(ChrW(ASC_COMMA)) _
                 And S.LastIndexOf(ChrW(ASC_COMMA)) < S.Length - 1
    End Function

    '----------------------------------------------------------------
    ' Checks a right negative double has been introduced.
    '----------------------------------------------------------------
    Private Function IsNegativeDouble(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsDigit(C) OrElse C = ChrW(ASC_DASH) _
                                    OrElse C = ChrW(ASC_COMMA)
            End While
        Else
            R = False
        End If
        'Checking:
        '  - first character is a dash character,
        '  - length must be at least 4 characters,
        '  - there's one comma character and it's not the first or 
        '    second one (-0,0),
        '  - there's exactly only one comma character,
        '  - the comma character it's not the last one.
        Return R And S.LastIndexOf(ChrW(ASC_DASH)) = 0 _
                 And S.Length >= 4 _
                 And S.IndexOf(ChrW(ASC_COMMA)) > 1 _
                 And S.IndexOf(ChrW(ASC_COMMA)) = _
                     S.LastIndexOf(ChrW(ASC_COMMA)) _
                 And S.LastIndexOf(ChrW(ASC_COMMA)) < S.Length - 1
    End Function

    '----------------------------------------------------------------
    ' Checks only letters have been introduced.
    '----------------------------------------------------------------
    Private Function HasOnlyLetters(ByVal S As String) As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsLetter(C)
            End While
        Else
            R = False
        End If
        Return R
    End Function

    '----------------------------------------------------------------
    ' Checks only letters and digits have been introduced.
    '----------------------------------------------------------------
    Private Function HasOnlyLettersOrDigits(ByVal S As String) _
    As Boolean
        Dim R As Boolean
        If Not String.IsNullOrEmpty(S) Then
            Dim IT As IEnumerator = S.GetEnumerator()
            Dim C As Char
            R = True
            While IT.MoveNext And R
                C = DirectCast(IT.Current, Char)
                R = Char.IsLetterOrDigit(C)
            End While
        Else
            R = False
        End If
        Return R
    End Function

#End Region

End Class

Decidir clase en tiempo de ejecución en .NET.

Diciembre 17th, 2008

En ocasiones puede resultarnos imprescindible no determinar la clase de la que un objeto va a ser instancia hasta que estemos en tiempo de ejecución. Para ello podemos valernos de la clase Activator y de su método CreateInstance, que nos permiten pasarle una clase para que nos devuelva un objeto de esa clase.

Podemos por ejemplo crearnos una función sencilla como esta...

'----------------------------------------------------------------
' Creates an instance of specified class and returns that object.
'----------------------------------------------------------------
Public Function CreateInstance(ByVal T As Type) As Object
    Return Activator.CreateInstance(T)
End Function

...que podríamos tener encapsulada como método de alguna clase que nos permitiera hacer determinadas acciones con objetos, esto ya a gusto de cada cual.

Una vez tenemos esta función podemos llamarla de varias maneras. La primera, si tenemos otro objeto de la misma clase de la que ahora queremos obtener una instancia:

Dim OBJ As Object

'Option 1. We already have an object of that class.
Dim C1 As New Class1()
OBJ = Me.CreateInstance(C1.GetType())

La segunda, si no tenemos ningún objeto pero conocemos el nombre de la clase en cuestión:

Dim OBJ As Object

'Option 2. We haven't any object of that class.
OBJ = Me.CreateInstance(Type.GetType("AlbertMata.Class2"))

'But be careful because this would fail:
'OBJ = Me.CreateInstance(Type.GetType("Class2"))

Como aparece en el código, conviene remarcar que el nombre de la clase debe incorporar el espacio de nombres, de lo contrario provocará una excepción.

En ambos casos, si una vez creado el objeto -que si nos fijamos lo habíamos declarado como Object y lo habíamos instanciado a través de un método que también devolvía un Object- consultamos su tipo exacto...

'Checking exact type for object.
Debug.Print(OBJ.GetType.ToString)

...obtendremos...

AlbertMata.Class2

Particularmente este sistema me ha venido bien para alguna travesura que quería hacer con formularios, pero creo que en más ocasiones podrá serme útil.

Distancia de Levenshtein en VisualBasic.NET.

Diciembre 13th, 2008

Hace un rato se me ha planteado un problema que quien más quien menos habrá tenido en alguna ocasión. Necesitaba poder comparar dos cadenas y obtener algún indicador de hasta qué punto podría tratarse de la misma cadena escrita de maneras ligeramente diferentes. He estado dándole vueltas sobre cómo podría jugar con los distintos métodos de la clase String para conseguir algún valor. Me he planteado utilizar análisis de frecuencias, generar números ponderados en función de carácter y posición dentro de la cadena... Pero nada me convencía, así que he insistido un poco más en buscar si ya alguien había desarrollado alguna función parecida y para mi mayúscula sorpresa me he dado de bruces con la Distancia de Levenshtein.

Resulta que esto que andaba buscando es exactamente esta distancia. Y tal como reza la Wikipedia... "se llama Distancia de Levenshtein o distancia de edición al número mínimo de operaciones requeridas para transformar una cadena de caracteres en otra. Se entiende por operación, bien una inserción, eliminación o la substitución de un carácter. Esta distancia recibe ese nombre en honor al científico ruso Vladimir Levenshtein, quien se ocupara de esta distancia en 1965. Es útil en programas que determinan cuán similares son dos cadenas de caracteres, como es el caso de los correctores de ortografía."

Y para mayor regocijo, en la propia página de la Wikipedia aparece el algoritmo en pseudocódigo de la función:

int LevenshteinDistance(char str1[1..lenStr1], char str2[1..lenStr2])
   // d is a table with lenStr1+1 rows and lenStr2+1 columns
   declare int d[0..lenStr1, 0..lenStr2]
   // i and j are used to iterate over str1 and str2
   declare int i, j, cost
 
   for i from 0 to lenStr1
       d[i, 0] := i
   for j from 0 to lenStr2
       d[0, j] := j
 
   for i from 1 to lenStr1
       for j from 1 to lenStr2
           if str1[i] = str2[j] then cost := 0
                                else cost := 1
           d[i, j] := minimum(
                                d[i-1, j] + 1,     // deletion
                                d[i, j-1] + 1,     // insertion
                                d[i-1, j-1] + cost   // substitution
                            )
 
   return d[lenStr1, lenStr2]

Incluso viene la implementación en algunos lenguajes de programación (C++, Java, Perl, Python, Ruby, Delphi y ColdFusion). Lástima que no viniera también en VisualBasic.NET que es el lenguaje en el que yo desarrollo... pero bueno, como codificar un algoritmo que ya nos viene dado en pseudocódigo tampoco es tarea infernal, a ello me he dedicado:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20081212
' Description: Calculates Levenshtein distance between two different 
'              strings.
'--------------------------------------------------------------------
Public Function LevenshteinDistance(ByVal STR1 As String, _
ByVal STR2 As String) As Integer
    'Variables to iterate (i, j) and get cost for any needed 
    'operation.
    Dim i As Integer
    Dim j As Integer
    Dim Cost As Integer

    'Creating array with string's lengths as bounds.
    Dim D(STR1.Length, STR2.Length) As Integer

    'Initializing array's values.
    For i = 0 To STR1.Length
        D(i, 0) = i
    Next
    For j = 0 To STR2.Length
        D(0, j) = j
    Next

    'Calculating Levenshtein distance.
    For i = 1 To STR1.Length
        For j = 1 To STR2.Length
            If STR1(i - 1) = STR2(j - 1) Then
                Cost = 0
            Else
                Cost = 1
            End If
            'First compared element: deletion.
            'Second compared element: insertion.
            'Third compared element: substitution.
            D(i, j) = Math.Min(Math.Min(D(i - 1, j) + 1, _
                                        D(i, j - 1) + 1), _
                                        D(i - 1, j - 1) + Cost)
        Next
    Next

    'Returning result.
    Return D(STR1.Length, STR2.Length)
End Function

Creo que está bien. Y el resultado de algunas pruebas así parece demostrarlo:

Debug.Print(Me.LevenshteinDistance("Albert Mata", "Albert Mata"))
0
Debug.Print(Me.LevenshteinDistance("Albert Mata", "Alber Matas"))
2
Debug.Print(Me.LevenshteinDistance("Albert Mata", "Mr. Albert Mata"))
4

No es ni mucho menos un sistema infalible, pero nos puede resultar muy útil para estimar si dos cadenas algo distintas pueden ser (o pretender ser) en realidad la misma. No obstante hay que utilizarla con cabeza, ya que por ejemplo si comparamos...

Albert Mata
Antoni Pons

...obtendremos una distancia de 9. Y si comparamos...

Berberechos y enlatados del norte, S.A.
Berberechos y enlatd. norte, SA

...obtendremos también una distancia de 9. Sin embargo en el primer caso las personas son claramente distintas y en el segundo caso el cliente parece ser el mismo. Para corregir esto se podrían buscar varias alternativas, pero yo propongo algo tan sencillo como ponderar la distancia entre la longitud de alguna de las cadenas:

Dim S1 As String = "Albert Mata"
Dim S2 As String = "Antoni Pons"
Dim LD As Integer = Me.LevenshteinDistance(S1, S2)
Dim Similar As Double = (S1.Length - LD) / S1.Length

Esto nos da un coeficiente de similitud del 0,181818181818182 (escaso, escaso). En cambio esto otro...

Dim S1 As String = "Berberechos y enlatados del norte, S.A."
Dim S2 As String = "Berberechos y enlatd. norte, SA"
Dim LD As Integer = Me.LevenshteinDistance(S1, S2)
Dim Similar As Double = (S1.Length - LD) / S1.Length

...nos da un coeficiente de similitud del 0,769230769230769 (¡mucho mejor!). Se tratará en cada caso de establecer dónde ponemos el límite.