La Coctelera

Luis Villa (aka <maguisso/>, aka <grancomo/>, aka <dispersiontotal/>)

"El testing es para cobardes"

Hace una semana Luis vino hasta mi puesto y me dijo que se iba. Mentiría si dijese que me sorprendió la noticia. No es que me la esperase de él concretamente, pero es que a estas alturas del partido ya me la espero de cualquiera.

Mientras subíamos por las escaleras hacia la cocina le dije que para mí nos dejaba un "mítico", a lo que como no podía ser de otra forma me respondíó:

"¿Mítico? Los mitos suenan a gente que se muere y [...]"

Charlando en la cocina sobre los principios, sobre los éxitos y los fracasos en el camino, recordé que este blog surgió como un experimento en el que las personas podían tener su post. Y en estos días me he dado cuenta de que no puedo dejar de retomar aquella costumbre para dedicarle uno a él, al gran "Luis Bill" :)

Embajador de La Ruby Room por donde quiera que fuese, ha sido desde el principio (aunque le llevó un tiempo entrar "en plantilla", su apoyo y su Dispersión Total estuvieron en The Cocktail desde que yo recuerdo) una cucharilla que nos agitaba a todos para que nos sintiésemos parte del mismo cóctel.

Mi Motivación de Base estos días está un poquito tocada, pero tal y como intentaba contar en mi pasada charla en el Wild Congress, esa es, en el fondo, muy buena señal. Es señal de que he tenido la suerte de trabajar y disfrutar de un Gran Compañero.

Parámetros opcionales en una definición de paso de Cucumber

Hoy un amigo me ha preguntado cómo podía completar una definición de paso (step definition) ya existente con una finalización opcional. Aunque la solución es simple cuando se conoce, si buscas no es fácil encontrarla en la red.

La clave está en conocer que las expresiones regulares tienen un tipo de agrupación que agrupa sin capturar: son los llamados "grouping-only parentheses".

En la expresión regular de una step definition cualquier parte entre paréntesis pasa a ser un parámetro del método que define el paso. Usando los paréntesis grouping-only evitamos este comportamiento, y si al final de ellos colocamos una interrogación (?) indicando que dicha agrupación es opcional, que puede aparecer o no, lo tenemos casi todo para tener un parámetro opcional.

¿Qué nos falta? Pues nos falta simplemente tener capturas dentro de esa sólo-agrupación-opcional.

El ejemplo, que siempre ayuda. Tenemos la siguiente definición de paso:


 Dado /^que creo un post cuyo título es (.+)$/ do |title|
   [...]
 end
 

Y queremos que, opcionalmente podamos indicar también los tags que asociamos al post, completando la frase con algo como "... tageado con 'cucumber optional parameters'".


 Dado /^que creo un post cuyo título es (.+)(?: tageado con (.+))?$/ do |title, tags|
   [...]
 end
 

Si en la feature sólo indicamos el título del post la regexp macheará y tendremos un nulo en el parámetro tags.

Esta técnica es perfecta si los parámetros opcionales están al final de la frase. La cosa cambia si están al principio o entre medias, ya que esa situación tendremos que añadir lógica dentro del paso para descubrir qué parámetros son los que nos han llegado, si es que es posible averiguarlo.

Los grouping-only sirven también para hacer los pasos más abiertos, posibilitando expresarlos de distintas formas igualmente válidas. Si en el ejemplo anterior queremos aceptar también "artículo" además de "post" podríamos escribir la definición de esta forma:


 Dado /^que creo un (?:artículo|post) cuyo título es (.+)(?: tageado con (.+))?$/ do |title, tags|
   [...]
 end
 

Desde hace mucho tiempo he querido escribir un post sobre dos features de las expresiones regulares que me permitieron disfrutar como un enano del desarrollo de MundoPepino. Una de ellas es esta que he contado.

La otra es el "negative lookahead". A ver si encuentro un caso de uso chulo para dedicarle post y termino de cumplir mi deseo.

NULL de SQL y las búsquedas "por exclusión"

Hay un comportamiento de SQL al que no termino de acostumbrarme. No sé si se puede etiquetar como gotcha, pero lo que está claro es que si andas un poco despistado puede llegar a confundir bastante.

Por definición en SQL toda comparación con un nulo evalua a falso.

Lo habitual cuando hacemos búsquedas es que sean por inclusión, es decir, buscando registros que son iguales, parecidos, mayores o menores que un patrón dado. En estas situaciones que la comparación de nuestro patrón con nulo evalue a falso no crea ninguna confusión: si el valor es nulo entonces no es igual, ni parecido, ni mayor, ni menor que mi patrón.

La confusión se genera cuando lo que buscamos es algo distinto de un patrón, es decir algo que no es igual que nuestro patrón (campo <> patron) o que no se parece a nuestro patrón (campo not like patron). Aquí en principio podríamos pensar que un nulo no coincide con nuestro patrón, y que por lo tanto debería formar parte de los resultados obtenidos. Pero no, no es así, porque toda comparación con un nulo evalua a falso.

Con un ejemplo rápido se ve claro. Pongamos que no tenemos claro el color de la oveja Ovi:


 mysql> select * from sheeps;
 +----+-------+-------+
 | id | name  | color |
 +----+-------+-------+
 | 1  | Molly | black |
 | 2  | Dolly | white |
 | 3  | Ovi   | NULL  |
 +----+-------+-------+
 3 rows in set (0.00 sec)
 
 mysql> select * from sheeps where color <> 'black';
 +----+-------+-------+
 | id | name  | color |
 +----+-------+-------+
 | 2  | Dolly | white |
 +----+-------+-------+
 1 row in set (0.00 sec)
 

Cuando recuperamos todas las ovejas que no son negras Ovi no está entre ellas. Y lo mismo nos ocurre cuando queremos recuperar todas las ovejas cuyo color no comienza por white:


 mysql> select * from sheeps where color not like 'white%';
 +----+-------+-------+
 | id | name  | color |
 +----+-------+-------+
 | 1  | Molly | black |
 +----+-------+-------+
 1 row in set (0.01 sec)
 

Si queremos que aparezca Ovi tenemos que abrir la condición con un or color is null:


 mysql> select * from sheeps where color not like 'white%' or color is null;
 +----+-------+-------+
 | id | name  | color |
 +----+-------+-------+
 | 1  | Molly | black |
 | 3  | Ovi   | NULL  |
 +----+-------+-------+
 1 row in set (0.01 sec)
 

A ver si con suerte, gracias a este post, no lo vuelvo a olvidar :)

Moraleja:
Por omisión las migraciones de Rails generan campos cuyo valor por defecto es nulo. En algunas ocasiones puede resultar interesante impedir que un campo acepte nulos (con la opción :null => false).

Truncado por palabras en ruby utilizando una expresión regular

Buscando como truncar un texto por palabras me he encontrado con este post de Paul Sturgess en el que como en casi todo buen post lo mejor está en los comentarios. En estos se propone y desarrolla una expresión regular que finalmente queda así:


 texto.gsub(/^(.{100}[\w.]*)(.*)/m) {$2.empty? ? $1 : $1 + '…'}
 

Esto, en teoría, nos cortaría el texto en el final de la palabra que se encuentre a partir del carácter número 100.

Sin embargo, casualidades de la vida, en el ejemplo que yo estaba probando esta expresión regular me cortaba la palabra "infomática" por la "á", dejándome "[...] no es que la informá...". El problema de siempre...

Fácil de resolver por otro lado, cambiando el "[\w.]*" por "[^\s]*" (literalmente "todo lo que no sea un espacio") que se aproxima mejor a lo que se pretende:


 texto.gsub(/^(.{100}[^\s]*)(.*)/m) {$2.empty? ? $1 : $1 + '…'}
 

Búsqueda "aproximada" de un texto en Javascript

Hoy me ha tocado bregar con algo que no tenía nada claro como resolver. Al final me ha resultado más sencillo de lo que pensaba, pero lo voy a dejar aquí en un post por si me toca volver a hacer algo similar.

Se trata de, teniendo ya en el cliente un conjunto más o menos grande de elementos (como opciones de una lista de selección, por ejemplo), cuando en un campo de texto se teclee los elementos de dicho conjunto deben reducirse sólo a aquellos que contienen el texto tecleado.

Además, no deberán tenerse en cuenta mayúsculas/minúsculas y tampoco, y aquí es donde viene la gracia del post, las siempre delicadas tildes en las vocales.

Seguro que existen mejores formas de resolverlo, pero como primera "aproximación" esto es lo que he hecho:

       var re_text = text.toLowerCase().
         replace(/[aáäâà]/g,'[aáäâà]').
         replace(/[eéëêè]/g,'[eéëêè]').
         replace(/[iíïîì]/g,'[iíïîì]').
         replace(/[oóöôò]/g,'[oóöôò]').
         replace(/[uúüûù]/g,'[uúüûù]');
       
       var re = new RegExp(re_text, 'i');
 
       collection.each(function(elem){
         if(re.test(elem)) alert(elem+' is in the house!');
       });
 

Sacar los meses en los que hay chicha sin sufrir demasiado

No, no se trata de una dieta milagrosa. Se trata de una situación en la que me he encontrado más de una vez y que nunca resuelvo de la misma forma. Esta que he aplicado hoy me parece lo suficientemente buena como para ponerla por aquí y así no volver a darle más vueltas a esto si me toca hacerlo de nuevo.

Se trata del clásico "Archivo" que se muestra habitualmente en la columna de un blog de tal forma que muestre un enlace a todos los meses en los que existe algún contenido publicado. Algo tal que:

2010 Enero

2009 Diciembre, Octubre, Febrero

Y así sucesivamente. Los enlaces nos llevan a un listado con los contenidos que se publicaron dicho mes.

El problema es que si hay muchos contenidos hacerlo tirando del modelo en cuestión puede resultar una operación muy pesada.

Si por el contrario dejamos el trabajo duro de agrupar por año y mes al motor de BBDD la cosa se agiliza notablemente. En mi caso, detrás tengo MySQL y el siguiente helper me ha solucionado el problema:

 def archive
   yearandmonths = ActiveRecord::Base.connection.select_rows(<<QUERY)
     SELECT DISTINCT(DATE_FORMAT(published_at, "%Y-%c"))
     FROM highlights
     WHERE published_at IS NOT NULL
     ORDER BY published_at DESC
 QUERY
   yearandmonths.map do |yearandmonth|
     year, month = yearandmonth.first.split('-')
     Struct.new(:year, :month).new(year, month.to_i)
   end
 end

La tabla asociada a los contenidos que pretendo mostrar es "highlights" y el campo con la fecha de publicación "published_at". No costaría mucho que la función recibiese esto parametrizado pero en este caso sólo necesito el helper en esta parte de la aplicación.

Aquí asumo que cualquier contenido que tiene fecha de publicación ("published_at") está publicado (es decir, que no existe otro campo que determine si puede verse dicho contenido). Idealmente, para que la consulta fuese lo más rápida posible debería existir un índice en dicho campo.

Después, en la vista, uso el helper llevando su salida al "group_by" quedando la cosa tal que así:

 <% archive.group_by(&:year).each do |year, year_months| %>
 <strong><%= year %></strong>
 <% year_months.each do |m| %>
 <%= link_to t('date.month_names')[m.month],
                 highlights_path(:year => year, :month => m.month) %>
 <% end %>
 

Y eso es todo. Un posete, que ya iba tocando para que no se me quede el mes de febrero fuera del "archivo". :)

Directorio para ficheros temporales con Ruby

Aquí va uno curioso. El método de tmpdir de la clase Dir nos devuelve el directorio de ficheros temporales del sistema.

Pues bien dicho método no está dentro de la implementación principal de la clase, si no en otro fichero, tmpdir.rb, que tenemos que requerir si deseamos utilizarlo.

Unas órdenes desde la consola dicen más que mil palabras:


 irb(main):001:0> Dir.tmpdir
 NoMethodError: undefined method `tmpdir' for Dir:Class
         from (irb):1
 irb(main):002:0> require 'tmpdir'
 => true
 irb(main):003:0> Dir.tmpdir
 => "/tmp"
 irb(main):004:0>
 

No quiero meter ruido en Ruby-Talk con esta tontá, pero imagino que alguna razón habrá para que esto sea así.

Como borrar desde bash todos los ficheros cuyo nombre sigua un patrón

Si el patrón es, por ejemplo, ._* (todos los ficheros que empiecen por un punto seguido de un guión bajo), sería:

find . -name '._*' -delete

Si por alguna razón nos interesa hacerlo con xargs:

find . -name '._*' -print0 | xargs -0 rm

Con la opción -print0 del find le decimos que separe los ficheros con bytes nulos (null bytes). La opción -0 de xargs es su pareja de baile: hace que este espere dicha separación por byte nulo entre entradas. De esta forma los nombres de los archivos pueden tener nombres con espacios y cosas raras (¡hasta retornos de carro!).

Jugando con el MacBook de Pantulis nos hemos dado cuenta de que el find de BSD que trae su MacOS necesita la ruta en la que se quiere realizar la búsqueda. Sin embargo el find de las GNU findutils utiliza el directorio actual si no indicamos ruta.

Resumiendo, que en GNU/Linux podríamos ahorrarnos el puntico y dejarlo en:

find -name '._*' -delete

Eso sí, dada su potencia se recomienda evitar el consumo de alcohol y sustancias psicotrópicas antes de su utilización, no vaya a ser que al final se líe parda.

Créditos:

SegFault hizo aumentar notablemente el nivel de este post al comentarme la existencia de los parámetros -print0 y -0 del find y xargs respectivamente. ¡Gracias!