domingo, 28 de diciembre de 2014

Cuando la explicación correcta es la obvia pero no lo parece

Un mal día


Un día como otro cualquiera estaba en la oficina programando en Java, utilizando una versión del Netbeans que había actualizado recientemente.

Tengo por costumbre utilizar muy a menudo el botón Clean and build (Limpiar y generar) porque estoy harto de que me pasen cosas raras con versiones desfasadas de ficheros compilados.

En una de tantas veces, recompilé y arranqué el programa. Al llegar a la pestaña en la que estaba trabajando me saltó un error NoClassDefFoundError indicando que no podía encontrarse una inner class que tenía como oyente de un botón. La documentación de dicho error dice:

Thrown if the Java Virtual Machine or a ClassLoader instance tries to load in the definition of a class (as part of a normal method call or as part of creating a new instance using the new expression) and no definition of the class could be found.
The searched-for class definition existed when the currently executing class was compiled, but the definition can no longer be found.

En ese momento me dije: eso no puede ser, porque si acabo de compilar sin errores de compilación, y dicha clase es parte del programa, la clase existe y es correcta. Y yo no manipulo para nada el class-path.

Fue uno de esos momentos en los que piensas que el IDE se ha corrompido. Cerré y reabrí el Netbeans, pero seguía fallando. Limpié la caché, pero más de lo mismo. Y pensé, ¿acaso no estará compilando esta clase por alguna extraña razón?
  • ¿el nombre de la inner class será demasiado largo?
  • ¿se me habrá colado algún caracter extraño y habrá problemas de encoding?
Hice pruebas como renombrar la clase, revisar el encoding del fichero y cosas así, y en una de tantas, funcionó y me olvidé del tema. Una de esas veces que te dices: ¡A saber lo que estaría pasando!.

Pero al siguiente día, el mismo error. Y esta vez pasaba el tiempo y era incapaz de conseguir solventarlo. Así que me dije: ¿puede ser que ese error se produzca por alguna otra razón? ¿Tal vez alguna excepción previa esté siendo silenciada?

Y así descubrí que ese error se puede producir si en algún trozo de código static se produce algún ExceptionInInitializerError que impida cargar la clase. O sea, en ese caso el error debería llamarse ClassLoadingError o algo similar, pero se reutiliza la misma clase Throwable.

Me cercioré que no había ningún ExceptionInInitializerError en el stack trace, incluso desactivé el UncaughtExceptionHandler que utilizo por si yo mismo estaba silenciando ese error. Pero nada.

Y lo peor de todo, es que esporádicamente, conseguía evitar el Runtime Error. Y no dejaba de preguntarme: ¿Cómo es posible? ¿Qué he cambiado? ¿Por qué unas veces sí y otras no?

Como diría Sherlock Holmes


Como diría Sherlock Holmes: "una vez descartado lo imposible, lo que quede, por improbable que parezca, tiene que ser la verdad".

Tras el enésimo Clean and build busqué el fichero ".class" en el subdirectorio build del proyecto, y efectivamente no estaba. Abrí el fichero JAR resultante con un manejador de ficheros comprimido, y efectivamente, allí tampoco estaba. Conclusión: ¡el error se produce porque la clase no existe!

Yo no manipulaba el class-path, pero por alguna razón, el fichero ".class" no estaba donde debería estar.

Esta vez, probé a compilar únicamente el fichero comflictivo y apareció el fichero ".class". En esas condiciones, funcionaba la aplicación. Sin embargo, tras un Build desaparecía la clase.

Finalmente, tras ponerle un nombre totalmente diferente a la inner class, todo fue como la seda. Con la moral por los suelos y la cabeza a punto de estallar, me dije que sería algún fallo estrambótico del ant con el nombre que le tenía puesto a la inner class.

Pero al contarle la batallita a un compañero me hizo un comentario que al fin, me abrió los ojos. Hacía mucho tiempo, había modificado el fichero build.xml para borrar del subdirectorio build (y por tanto, no incluir en el JAR) todo lo que estuviese por debajo un package llamado xtra. Al revisar la regla, me dí cuenta de que se me había colado un * de más en el patrón. Como el nombre que usaba para la inner class contenía la cadena Extra, encajaba en el erróneo patrón y su fichero ".class" era borrado.

Conclusión


El error lanzado por la JavaVM siempre me había indicado correctamente lo que estaba pasando y la culpa, en el fondo, siempre había sido mía. La explicación correcta siempre fue la obvia, pero me parecía tan imposible que lo fuese, que removí cielo y tierra para buscar otra explicación.

Greer's Third Law: A computer program does what you tell it to do, not what you want it to do. ¿Anónimo?

If you don't know anything about computers, just remember that they are machines that do exactly what you tell them but often surprise you in the result. Richard Dawkins, The Blind Watchmaker (1986)

Y como no hay mal que por bien no venga, además de lo que aprendí, corregí el fichero build.xml que era donde realmente estaba el problema.

Enlaces


Enlaces de interés relacionados con este artículo:

(Actualizado 29/12/2014)

domingo, 31 de agosto de 2014

Capturar un Component de Swing/AWT (renderizar a una imagen)

Introducción


A veces, programáticamente interesa guardar una imagen de la apariencia visual (captura) de un progrma Java. Una posibilidad muy sencilla es utilizar el método:

Pero... ¿y si lo que se desea capturar es solamente un componente?

¿Y si se desea capturar un componente que no cabe en pantalla? Por ejemplo, un componente que esté envuelto en un JScrollPane, todas las filas de una tabla o una ventana sobredimensionada.

¿Y si se desea capturar una componente que está parcialmente o totalmente fuera de la pantalla? Por ejemplo, una ventana que se ha desplazado parcialmente fuera de la pantalla.

Solución


La clase Component de Java tiene el método:

La clase BufferedImage tiene el método:

La clave está en combinar ambos métodos:
  1. Construir una imagen del tamaño del componente.
  2. Pintar el componente sobre el contexto gráfico de la imagen.
Con la imagen luego se pueden hacer procesos posteriores como guardarla en un fichero.

Este mecanismo se puede utilizar con cualquier Component, incluidas las ventanas y los diálogos, incluso aunque no estén total o parcialmente en la pantalla.

Ejemplo


En el siguiente enlace está la pequeña clase CaptureTest que permite guardar cualquier componente en un fichero PNG (Portable Network Graphics):

El método CaptureTest#capture(Component, Color) realiza la tarea descrita en este artículo, incluyendo algunos detalles técnicos como el color de fondo para componentes con partes transparentes o el ajuste del tamaño de ventanas descontando las decoraciones (la barrita con los botones de maximizar, minimizar, etc. que incluye el manejador de ventanas).

Al probarla, hay que tener en cuenta que los ficheros con las capturas se intentan crear en el directorio desde donde se invoca la máquina virtual Java.

Conclusión


Este artículo demuestra un uso muy sencillo y práctico de algunas funcionalidades disponibles en la la API de Swing/AWT y referencia la clase CaptureTest, cuyo código se puede utilizar para realizar la tarea descrita en él.

Enlaces


Enlaces de interés relacionados con este artículo:

(Actualizado 31/08/2014)

Source code: CaptureTest

Código fuente del programa CaptureTest

Ver la entrada correspondiente en el siguiente enlace:


Capturar un Component de Swing (renderizar a una imagen)



package pruebas;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;

public class CaptureTest {

    public static RenderedImage capture(Component component, Color background) {
        //Se crea una imagen de las dimensiones del componente.
        int width= component.getWidth();
        int height= component.getHeight();

        //El tamaño de las ventanas incluye las decoraciones si las hay, que no se pintan en el método paint(Graphics).
        if (component instanceof Window) {
            Window window= (Window) component;
            Insets insets= window.getInsets();

            if (insets != null) {
                //Hay que restar el ancho y el alto del Insets (Border y decoración).
                width= width - (insets.left + insets.right);
                height= height - (insets.top + insets.bottom);
            }
        }

        int imageType= BufferedImage.TYPE_INT_ARGB;
        BufferedImage bufImage= new BufferedImage(width, height, imageType);

        //Se pinta el componente sobre la imagen creada.
        Graphics graphics= bufImage.getGraphics();

        //El tamaño de las ventanas incluye las decoraciones si las hay, que no se pintan en el método paint(Graphics).
        if (component instanceof Window) {
            Window window= (Window) component;
            Insets insets= window.getInsets();

            if (insets != null) {
                //Hay que mover el origen.
                graphics.translate(
                    0 - insets.left,
                    0 - insets.top
                );
            }
        }

        //Si el componente no es opaco y se ha indicado un color de fondo.
        if (component.isOpaque() == false) {
            if (background != null) {
                graphics.setColor(background);
                graphics.fillRect(0, 0, width, height);
            }
        }

        component.paint(graphics);

        graphics.dispose();

        return bufImage;
    }


    private static File doCapture(Component component, String fileName) {
        try {
            RenderedImage image= CaptureTest.capture(component, null);
            File file= new File(fileName);
            boolean wasOK= ImageIO.write(image, "PNG", file);

            return file;
        }
        catch (IOException ex) {
            ex.printStackTrace(System.err);
            System.exit(1);
            return null;//Necesario para que compile.
        }
    }


    public static void test() {
        final JTextArea ta= new JTextArea();
        ta.setText("This is a very looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog line.");
        ta.setCaretPosition(0);

        JScrollPane scroll= new JScrollPane(ta);

        JButton bCaptureTA= new JButton("Capture Text Area");

        JButton bCaptureWin= new JButton("Capture Window");

        JPanel panel= new JPanel( new BorderLayout() );
        panel.add(scroll, BorderLayout.CENTER);
        panel.add(bCaptureTA, BorderLayout.PAGE_START);
        panel.add(bCaptureWin, BorderLayout.PAGE_END);

        final JFrame frame= new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setContentPane(panel);
        frame.setSize(300, 200);

        bCaptureTA.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                File file= CaptureTest.doCapture(ta, "ta.png");
                String msg= String.format("Saved to file <%s>.", file.getAbsolutePath());
                JOptionPane.showMessageDialog(frame, msg);
            }
        });

        bCaptureWin.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //Se usa invokeLater para que el botón no se capture pulsado.
                SwingUtilities.invokeLater(new Runnable() { public void run() {
                    File file= CaptureTest.doCapture(frame, "win.png");
                    String msg= String.format("Saved to file <%s>.", file.getAbsolutePath());
                    JOptionPane.showMessageDialog(frame, msg);
                }});
            }
        });

        frame.setVisible(true);
    }


    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() { public void run() {
            CaptureTest.test();
        }});
    }

}

martes, 29 de julio de 2014

¿Dónde se ubica este script?

Introducción


Al escribir un script a veces interesa saber en qué directorio está ubicado, por ejemplo, para acceder a algún recurso (un fichero de datos o de configuración asociado.)

Muchos scripts se alojan en un directorio propiedad del usuario y se ejecutan desde el propio directorio con la ruta "./". En tal caso, se podría acceder a un recurso con esa misma ruta. Sin embargo, nada impide ejecutarlo desde otro directorio, o incluso invocarlo desde otro script, con lo que esa forma de referenciar los recursos fallaría.

Aproximaciones


Algunas soluciones burdas, pero efectivas en muchos casos, son tan simples como:
  • Alojar el script y sus recursos en el mismo directorio.
  • Alojar los recursos en determinado directorio fijo y poner toda la ruta cada vez que se referencien desde el script (es mucho mejor poner una variable, claro).
Sin embargo, si deseamos reubicar el script o sus recursos, esas soluciones no sirven o implican al menos editar el script.

Solución


El comando readlink permite obtener el nombre canónico (con su ruta completa) de un fichero.
El comando which permite encontrar los ejecutables que coinciden con el argumento indicado. Por defecto, sólo muestra la ruta completa del primero que se encuentra recorriendo las rutas de la variable de entorno PATH.
En un script la variable $0 corresponde al nombre del propio script.
El comando dirname permite eliminar el sufijo no correspondiente al directorio de una ruta (normalmente el nombre de un fichero junto a su extensión). Es decir, extraer hasta el último directorio de una ruta.

Combinando todo lo anterior, es posible obtener el nombre real del fichero que se está ejecutando y a partir de él el directorio donde está ubicado:

MYSELF="$(which $(readlink -f ${0}))"
MY_PATH="$(dirname $(which $(readlink -f ${0})))"

La variable MYSELF contendrá el nombre canónico del script y la variable MY_PATH el directorio donde está alojado.

Ejemplo


Si quieres probar que funciona en varios casos, puedes hacer lo siguiente:

#!/bin/bash

MYSELF="$(which $(readlink -f ${0}))"
MY_PATH="$(dirname $(which $(readlink -f ${0})))"
echo "\$0=${0}"
echo "\$MYSELF=   ${MYSELF}"
echo "\$MY_PATH=  ${MY_PATH}"

  • Crear un script con las líneas que anteriores llamado "cat".
  • Poner el script en un directorio cualquiera de tu home.
  • Ir a /tmp/ y crear un enlace simbólico al script ejecutando
    $ ln -s /home/_your_user/.../cat
  • Alterar la variable PATH en la sesión actual:
    $ PATH=/tmp/:$PATH
  • Acceder a cualquier directorio y ejecutar
    $ cat something
En lugar de ejecutarse el comando cat del sistema, se ejecutará el script, mostrando su nombre canónico y el directorio donde está, aunque en realidad, se esté ejecutando el enlace simbólico que se puso en el directorio /tmp/.

Conclusión


Probablemente haya alguna solución mejor, más elegante o más precisa, pero hasta la fecha, a mi me ha bastado con esta.

Enlaces


Enlaces de interés relacionados con este artículo:

(Actualizado 29/07/2014)

domingo, 29 de junio de 2014

Cómo mejorar scripts en bash

Introducción


Si has escrito scripts en algún intérprete de comandos como bash probablemente hayas tenido errores porque al haber escrito mal el nombre de una variable, el intérprete la define en ese momento vacía.

También se te habrá dado el caso de querer interrumpir la ejecución desde que algún comando falle, ya que por defecto, la ejecución de un script no se interrumpe aunque falle algún comando. Por ejemplo, si cambias de directorio (cd) y luego borras ficheros (rm), si falla el comando cd el resultado del comando rm puede ser desastroso.

Parámetros de configuración


El bash se puede configurar para abortar la ejecución de un script si se utiliza una variable que no ha sido inicializada:

#Expresiones equivalentes
set -o nounset
set -u

Esa opción es muy útil y puede evitarte muchos accidentes y quebraderos de cabeza.

También se puede configurar que se interrumpa la ejecución del script si falla algún comando:

#Expresiones equivalentes
set -o errexit
set -e

OJO, se considera fallar que el comando devuelva un valor distinto de 0.

Hay más parámetros que te pueden interesar como "pipefail" por ejemplo.

Ejemplos


Con este pequeño script, activando o desactivando los parámetros anteriores, puedes comprobar tú mismo como cambia el comportamiento del bash:

set -o nounset
set -o errexit
###############################################################################

#echo "Using mode: nounset"
echo "This command is executed: OK."
echo "This command fails because this variable has not been initialized <${VAR_UNDEF}>."
echo "This command is not executed because previous command failed."

#echo "Using mode: errexit"
echo "This command is executed: OK."
#Next command fails because the file doesn't exist.
cat "/tmp/unexistent.file"
echo "This command is not executed because previous command failed."

Si quieres hacer pruebas on-line, puedes usar este enlace:


Observaciones


Respecto al modo "errexit" hay mucha controversia (más abajo hay algunos enlaces si te interesa el tema). Hay que tener muy en cuenta que se aborta cuando un comando devuelve algo distinto de 0, lo cual no tiene por qué significar necesariamente que haya fallado. De hecho, hay comandos que intencionadamente devuelven valores distintos de 0 sin tratarse de errores.

Otra cosa que se comenta por ahí, es que algunas de las características (como "pipefail") van cambiando de una versión de bash a otra.
 

Conclusión


En mi humilde opinión, el modo "nounset" debería ser el comportamiento por defecto, pero bueno, para gustos los colores.

Yo considero que el modo "errexit" es una herramienta muy útil que el programador debe saber cuando usar y cuando no.

A veces interesa buscar si el comportamiento de alguna herramienta es configurable, porque muchas veces, el comportamiento por defecto no nos conviene.

Enlaces


Enlaces de interés relacionados con este artículo:

(Actualizado 29/06/2014)

sábado, 31 de mayo de 2014

Mejor no dar algo por sentado

Introducción


Sin darnos cuenta, nos acostumbramos a utilizar herramientas de cierto modo, y cuando utilizamos una similiar damos por hecho que funciona exactamente igual.

Por ejemplo, solemos dar por sentado que ciertas operaciones funcionan igual en un lenguaje de programación que en otro. Normalmente no hay problema porque suele ser así, pero es importante ser concientes de que no tiene por qué serlo:

a veces el funcionamiento de alguna característica de un lenguaje depende de la convención que se siguiera al diseñarlo y puede ser diferente de la convención seguida al diseñar otro lenguaje.

Un ejemplo real


Una vez escribí un programa en Ruby y al hacer pruebas obtenía resultados que no coincidían con lo esperaba.

Depurándolo acoté la zona del programa donde se realizaba mal un cálculo, pero por mucho que miraba el código no entendía lo que ocurría.

Hasta que descubrí que la aritmética en Ruby no funciona como yo suponía...

Estaba tan acostumbrado a utilizar la aritmética de C/C++ o Java, que no me esperaba que algo tan básico como dividir un entero entre otro no funcionase igual en Ruby. Pero vaya, así es.

Cuando se divide un número entero entre otro y el resultado no es un número entero, sino uno real (con decimales), hay que realizar un redondeo (o más bien un truncamiento). Pues resulta que no todos los lenguajes siguen la misma convención.

Al dividir un número negativo entre uno positivo o viceversa, en C/C++ y Java se trunca el resultado hacia cero (arriba), de modo que el valor absoluto del truncamiento de una división entera no depende del signo de los operandos. Por ejemplo:

OperaciónResultadoComentario
+5/3+1Resultado de truncar +1.667 hacia cero o -inf (abajo)
-5/3-1Resultado de truncar -1.667 hacia cero o +inf (arriba)

Sin embargo, en Ruby el truncamiento siempre es hacia abajo, variando el valor absoluto en función del signo de los operandos:

OperaciónResultadoComentario
+5/3+1Resultado de truncar +1.667 hacia cero o -inf (abajo)
-5/3-2Resultado de truncar -1.667 hacia -inf (abajo)

Ruby sigue una convenión distinta a la que siguen C++ y Java, por lo que se obtienen resultados diferentes.

Compruébalo tu mismo


Aquí hay unos pequeños programas que demuestran lo comentado en el apartado anterior: en Java, en C++ y en Ruby. Cada programa se puede copiar, pegar y probar online en el enlace indicado debajo.

public class RoundTest {

    public static void main(String[] args) {
        int a= -5;
        int b= 3;
        System.out.format("(%s/%s)=%s%n", a, b, (a/b));
        a= 5;
        b= 3;
        System.out.format("(%s/%s)=%s%n", a, b, (a/b));
    }
}

#include <cstdio>

using namespace std;

int main() {
    int a= -5;
    int b= 3;
    printf("(%d/%d)=%d\n", a, b, (a/b));
    a= 5;
    b= 3;
    printf("(%d/%d)=%d\n", a, b, (a/b));

    return 0;
}

#!/usr/local/bin/ruby -w

Integer a= -5;
Integer b= 3;
puts "(#{a}/#{b})=#{(a/b)}";
a= 5;
b= 3;
puts "(#{a}/#{b})=#{(a/b)}";

Con estos pequeños programas es fácil comprobar que los resultados en Ruby difieren de los obtenidos en C++ y Java.

Recuerda que hay más casos


Hay más lenguajes que implementan la división de forma similar a Ruby: Python, Tcl... También varía la implementación de la operacion Módulo (Modulus).

Un ejemplo mucho más típico y conocido son las sentencias condicionales multivía (case, when, switch...). Unos lenguajes, por ejemplo Pascal, tratan de forma exclusiva cada caso, de modo que se ejecuta exclusivamente el caso por el que se entra, mientras que otros, como C/C++ proporcionan un punto de entrada, pero debe ser el programador quien indique explícitamente donde interrumpir el flujo. de ejecución.

No sólo entre hay diferencia entre lenguajes de programación. Hay otras características que varían de unos sistemas a otros, como por ejemplo, el tratamiento que hace Oracle de las Strings vacías: las asimila a NULL.

Conclusión


Conviene tener el citado ejemplo en cuenta para recordar que al cambiar de un lenguaje de programación a otro, no se debe asumir que todo funciona exactamente igual.

Nunca está de más hacer alguna pequeña prueba para cerciorarse de que el programa se comporta como es de esperar.

Enlaces


Enlaces de interés relacionados con este artículo:

(Actualizado 31/05/2014)

Recapitulando: año 1

Repasando los artículos del blog escritos a lo largo de este año, puedes descubrir:


Si has leído alguno, muchas gracias y espero que te haya gustado.

Y sobre todo, agradezco especialmente a aquellos que han hecho algún comentario. El feedback siempre es bienvenido.

(Actualizado 31/05/2014)



domingo, 27 de abril de 2014

¿Cómo obtener la ocurrencia más corta usando expresiones regulares?

Introducción


Alguna vez habrás usado expresiones regulares (regular expression), ya sea programando, en la línea de comandos o en algún editor de texto.

¿No te ha pasado que al usar algún comodín de repetición como el * en vez de obtener la ocurrencia más corta obtienes la más larga (que contiene ocurrencias más cortas)?

Por ejemplo, al analizar la cadena XML:

<a>1111</a><a>2222</a><a>3333</a><a>4444</a>

con la expresión regular siguiente para locazizar los elementos (tags) a:

<a>.*</a>

obtienes toda la cadena en vez de únicamente el primer elemento <a>1111</a>.

Ups, hay ambigüedad: encajan varios trozos de la cadena y además encaja toda la cadena. ¿Qué resultado se elige? 

Greediness vs Laziness


El problema es que al diseñar un motor de expresiones regulares, hay que resolver la ambigüedad. Hay dos comportamientos:
  1. Greediness (avaricia, codicia): el comodín se expande consumiento el máximo número de caracteres posible. En la documentación se suele denominar modo greedy.
  2. Laziness (pereza): el comodín se expande consumiendo el mínimo número de caracteres posible. En la documentación se suele denominar modo lazy, non-greedy o reluctant.
Algunos motores de búsqueda permiten configurar el comportamiento, otros no.

Si te interesa mucho el tema, en estos enlaces se explica (en inglés) muy bien como funcionan los motores de expresiones regulares y sus comportamientos:

Cómo capturar la ocurrencia más corta (non-greedy)


Algunos motores de expresiones regulares han resuelto la ambigüedad utilizando operadores diferentes para los modos greedy y non-greedy.

El operador para 0 o N ocurrencias más conocido es el greedy * mientras que el non-greedy suele ser una variación suya *? en la mayoría de los casos.

A continuación hay ejemplos del operador non-greedy en varios entornos. Para ello, se utiliza la cadena y la expresión regular de la introducción.

En lenguajes interpretados se muestra la lista de ocurrencias, por ser fácil y muy ilustrativo.

En lenguajes compilados, por simplicidad, se reemplazan todas las ocurencias por la palabra Replaced, de modo que mirando el resultado se sabe si el comportamiento fue greedy (Replaced aparecerá sólo una vez) o non-greedy (Replaced aparecerá varias veces).

grep (hay que activar el modo Perl)
echo "<a>1111</a><a>2222</a><a>3333</a><a>4444</a>" | grep -P -o "<a>.*?</a>"

Vim (este caso difiere mucho del resto)
<a>.\{-}<\/a>
Para detectar tags cuyo contenido incluya saltos de línea:
<a>\_.\{-}<\/a>

Perl (ejecutable en línea de comandos)
perl -e 'my @matches= "<a>1111</a><a>2222</a><a>3333</a><a>4444</a>" =~ /<a>.*?<\/a>/g; print (join("\n", @matches), "\n");'

Ruby (ejecutable en línea de comandos)
ruby -e 'puts "<a>1111</a><a>2222</a><a>3333</a><a>4444</a>".scan( /<a>.*?<\/a>/ ).to_a'

PostgreSQL
SELECT regexp_matches('<a>1111</a><a>2222</a><a>3333</a><a>4444</a>', '<a>.*?<\/a>', 'g');

PHP (ejecutable en línea de comandos)
php -r '$matches= array(); $search= preg_match_all("/<a>.*?<\/a>/", "<a>1111</a><a>2222</a><a>3333</a><a>4444</a>", $matches); print_r($matches);'

Java
"<a>1111</a><a>2222</a><a>3333</a><a>4444</a>".replaceAll("<a>.*?</a>", "Replaced")

Javascript
function doReplace() {
  var str= "<a>1111</a><a>2222</a><a>3333</a><a>4444</a>"
  str= str.replace(new RegExp("<a>.*?</a>", "g"), "Replaced");
  alert(str);
}

C++
Para menejar expresiones regulares en C++ haría falta un compilador que soporte por completo C++11 (GCC 4.9) o bibliotecas alternativa como boost o clang.

sed (este comando siempre es greedy)
http://stackoverflow.com/questions/1103149/non-greedy-regex-matching-in-sed

bash (sus funciones/operadores de expresiones regulares como =~ siempre son greedy)
Al parecer, las expresiones regulares POSIX sólo soportan modo greedy.
http://stackoverflow.com/questions/18738576/regex-in-bash-expression

Prúebalo tu mismo


Aquí hay enlaces a varias paǵinas web para probar algunas de las expresiones anteriores directamente on-line:

Enlaces


Enlaces de interés relacionados con este artículo:


(Actualizado 10/07/2014)


domingo, 16 de marzo de 2014

Antipatterns en programación

La experiencia es una profesora muy muy dura,
porque primero examina, y luego enseña la lección.
Vernon Law
¿Conoces el término antipattern en el ámbito de la programación?

El término antipattern se refiere a "malas prácticas" de programación muy comunes que conllevan el peligro de provocar errores o mal funcionamiento de los programas y que para colmo suelen ser de muy difícil detección.

Por ejemplo, en Java muchos programadores inexpertos suelen comparar String con el operador == en vez de hacerlo con String#equals(Object). No es que esté mal usar el operador ==, pero probablemente no saben que la semántica de ambos casos es distinta y claro, cuando no les cuadran los resultados consultan a un compañero más experimentado que se tira de los pelos cuando ve el código fuente.

Dichas "malas prácticas" no sólo se deben a la inexperiencia, "poca cualificación" o "desidia/comodidad" del programador, sino a otras muchas razones como desconocimiento del funcionamiento interno de una API, de un compilador, de una máquina vitual... vamos, berenjenales en los que un muy buen programador no tendría por qué meterse.

Aunque hay reglas generales, cada lenguaje y API tiene sus propios antipatterns y hay multitud de libros (bueno, de C/C++ deben ser enclopedias, jejeje) y webs hablando de ellos.

Me voy a centrar en Java. Personalmente la experiencia me ha hecho toparme con varios casos y llegar a soluciones muy parecidas a las que he encontrado por ahí. Por ejemplo, cómo concatenar Strings y cómo asegurar que se liberan recursos cuando hay excepciones.

Aquí dejo algunas webs que he leído recientemente y en las que he visto y aprendido cosas muy interesantes (y muy sorprendentes en algunos casos):


Siempre se aprenden cosas muy intesantes de la experiencia de los demás. A mí jamás se me hubiese ocurrido que java.net.URL usara resolución de nombres dentro del método URL#equals(Object) o que medir tiempos con System#currentTimeMillis() no sea seguro.

Hoy en día, los compiladores e incluso los editores de los IDE suelen avisar cuando detectan algunas "anomalías", pero también hay algunas herramientas independientes. Yo usé durante años esta, con su plugin integrado en versiones antiguas del NetBeans:


martes, 25 de febrero de 2014

Tool Tip multilínea en Java Swing

Introducción


Si has programado en Java Swing estarás familiarizado con el método JComponent#setToolTipText(String) para mostrar Tool Tips (esos pequeños mensajes emergentes que suelen ser descripciones o sugerencias). Si no es así, aquí está el tutorial oficial:

En principio, todo el texto se muestra en una única línea, eliminándose todos los saltos de línea (tabuladores también) presentes en la String.

¿Y si necesitas mostrar varias líneas de texto en un Tool Tip?

HTML al rescate


Diversos componentes de Swing permiten mostrar HTML 3.2 estático con cierto soporte de CSS. Aprovechando dicha característica, si en vez de pasar una String directamente, se envuelve en un elemento PRE de HTML se obtiene fácilmente el comportamiento multilínea. Por ejemplo:


String toolTipHTML= ""
    + "<html><body><pre>"
    + "This is a\nmultiline tooltip."
    + "</pre></body></html>"
;

En el siguiente enlace hay un pequeño programa para demostrar esta característica.

Conclusiones


La limitación de una única línea en los Tool Tips se puede evitar de un modo muy simple añadiendo muy poco código. Hay otras alternativas, pero aquí se muestra una efectiva y muy sencilla de implementar.
El soporte de HTML incrustado en Swing tiene importantes limitaciones, pero para algunas cosas resulta muy útil.


Enlaces


Enlaces de interés relacionados con este artículo:


(Actualizado 28/02/2014)