martes, febrero 28, 2017

Los peligros de SET COLLATE

Se me ha presentado una situación que no veo descripta claramente en la documentación de Visual FoxPro y que hasta ahora creía que se aplicaba solamente a los índices de las tablas.

Como es sabido, el comando SET COLLATE (que por defecto es "MACHINE") permite indicar el algoritmo de comparación a usar para los índices y funciones relacionadas con ellos (búsquedas, ordenamientos, etc), de forma que si se definen unos datos así:

CREATE CURSOR test (palabra C(30))
INSERT INTO test VALUES ("Comparación")
INSERT INTO test VALUES ("Comparación 2")
INSERT INTO test VALUES ("Comparación 3")


Con SET EXACT OFF y SET COLLATE TO "MACHINE" las comparaciones son exactas a nivel de mayúsculas/minúsculas (capitalización), pero teniendo en cuenta cómo comienzan las cadenas:

LIST all for palabra = "Comparación"

Record#  PALABRA                      
      1  Comparación                  
      2  Comparación 2                
      3  Comparación 3                


Lo siguiente no retorna resultados:

LIST all for palabra = "COMPARACIÓN"

Record#  PALABRA                      

Cambiando a SET COLLATE TO "GENERAL", las comparaciones cambian y ya no distinguen entre minúsculas/mayúsculas:

LIST all for palabra = "Comparación"

Record#  PALABRA                      
      1  Comparación                  
      2  Comparación 2                
      3  Comparación 3                


LIST all for palabra = "COMPARACIÓN"

Record#  PALABRA                      
      1  Comparación                  
      2  Comparación 2                
      3  Comparación 3                



Hasta aquí, todo esto es de conocimiento público.

Algo importante a tener en cuenta es que el COLLATE también afecta a las comparaciones lógicas que no tengan que ver con tablas ni datos de las mismas, lo que abarca cualquier tipo de comparación ASCII, o sea, de caracteres y cadenas.

Por ejemplo, cuando comparamos caracteres, internamente lo que se compara son los valores ASCII, de modo que A < Z porque el ASCII de A es 65 y el de Z es 90 y la comparación que realmente se hace es 65 < 90:

? "A" < "Z" && En ASCII: 65 < 90
.T.

? "a" < "z" && En ASCII: 97 < 122.T.

? BETWEEN("F", "a", "Z") && En ASCII: 70 está entre 97 y 90
.F.

? "a" < "Z" && En ASCII: 97 < 90
.F.

Lo anterior es cierto cuando el COLLATE es "MACHINE", pero cuando es "GENERAL" las cosas cambian:

? "A" < "Z" && En ASCII: 65 < 90
.T.


? "a" < "z" && En ASCII: 97 < 122
.T.

? BETWEEN("F", "a", "Z") && En ASCII: 70 está entre 97 y 90
.T.

? "a" < "Z" && En ASCII: 97 < 90
.T.

Estos dos últimos casos son los que pueden resultar más confusos, ya que va contra toda lógica... a menos que se sepa cuál es la lógica a aplicar, y es que cuando se evalúan las opciones con COLLATE "GENERAL", lo que realmente se hace es comparar todo en la misma capitalización (que supongo que es en mayúsculas, aunque no puedo confirmarlo).

Bajo esta lógica, si normalizamos primero la capitalización de la última comparación, realmente sería lo mismo que hacerla así:

? "A" < "Z" && En ASCII: 65 < 90
.T.


? BETWEEN("F", "A", "Z") && En ASCII: 70 está entre 65 y 90
.T.

Y lo mismo se aplica a las demás funciones de comparación de cadenas.

Este último caso es el que se me presentó recientemente, y realmente puede resultar algo perturbador ver algo como esto sin lógica aparente:


Lo que se ve en la imagen es el depurador abierto con una aplicación antigua en ejecución (parte superior) y la ventana de comandos en la parte inferior. Como se puede observar, la aplicación hace una comparación que en definitiva hace esto:

IF BETWEEN(caracter, "a", "Z")
   lnCuantos = lnCuantos + 1
ELSE
   EXIT
ENDIF

A primera vista, esa comparación no tiene sentido porque jamás un valor puede estar "entre 97 y 90" (comparando en ASCII) y la ventana de comandos lo confirma, pero resulta que la aplicación tiene sesión de datos privada y COLLATE "GENERAL", por lo que realmente está comparando todo con la misma capitalización de mayúsculas/minúsculas, y por eso funciona.



Resumen:


Este es uno de esos casos donde se vuelve a comprobar la importancia de usar buenas prácticas de programación. En este caso algunas formas correctas de escribir esta condición sería:

IF BETWEEN(carac, "A", "z") && carac entre 65 y 122

o bien:

IF BETWEEN(UPPER(carac), "A", "Z") && carac entre 65 y 90


Y de esa forma independizarse del valor del SET COLLATE.

Espero que esto les evite algunos problemas,

Hasta la próxima! :D