SQL: Suma acumulativa

Minientrada

Tengo una tabla como la siguiente:

datetime               points
2021-07-05 10:00:00    1
2021-07-05 10:05:00    1
2021-07-05 10:08:00    -1
2021-07-05 10:10:00    1
2021-07-05 10:11:00    -1

Y quiero sacar las sumas acumulativas. Es decir, no quiero sacar simplemente un SUM(points) y que me devuelva 1, sino que quiero ir viendo, para cada fila, la suma parcial de todas las filas que hay hasta llegar a esa desde el principio.

Por lo que veo en Stack Overflow, una forma universal de hacerlo aunque poco eficiente con tablas muy grandes, es utilizar INNER JOINs aplicados sobre la misma tabla que parcialmente vayan agrupando los resultados por fecha. Me hice algo como lo siguiente en mi caso usando un WITH para filtrar sólo aquellos resultados que realmente quiero sumar y así tratar de hacerlo más eficiente.

WITH points AS (
  SELECT datetime, points
    FROM data
   WHERE target_id = 12345
)
SELECT a.datetime, a.points, SUM(b.points)
  FROM points a
       INNER JOIN points b
               ON b.datetime <= a.datetime;

jq: filtrar índice inverso por valor de un subcampo

Minientrada

Amo jq, pero a la vez daría lo que fuera por poder aprender del todo su lenguaje de consulta porque a veces se me atraganta.

Sería muy fácil con jq hacer una búsqueda en un array de objetos en función de lo que vale uno de los campos de cada objeto, ¿verdad? Hablo de un array como este:

[
  {
    "nombre": "Carmen",
    "departamento": "Ventas",
    "superior": "Ana"
  }
]

Pero ¿qué pasa si en vez de un array tengo un objeto a modo de índice inverso? Me refiero a la clásica de fabricar con este array un objeto donde como valores tengo cada uno de los valores del array, pero como clave tengo algún identificador concreto de cada objeto. Al fin y al cabo, buscar items en una lista tiene complejidad O(n), pero hacerlo en un mapa tiene complejidad O(1). Es más rápido hacer lista["Ana"] que hacer lista.find(i => i.name == "Ana").

{
  "Carmen": {
    "nombre": "Carmen",
    "departamento": "Ventas",
    "superior": "Ana"
  }
}

El secreto está en usar to_entries y from_entries para convertir entre objeto y array de entries. jq 'to_entries | from_entries' < input.json se anula entre sí. Entre ambas sentencias podemos plantar un select para hacer un filtro de aquellos elementos del nuevo array generado por to_entries que filtre por una condición concreta para .[].value.whatever.

jq 'to_entries | select(.[].value.superior == "Ana") | from_entries' < empleados.json

La salida de to_entries es un array, por lo que cuando se hace el select para filtrar elementos que cumplan un criterio, hay que empezar la query con un .[], para poder hacer introspección sobre cada elemento del array. Realmente no es muy complejo, pero me lo apunto porque me ha costado un poco de ensayo y error dar con el orden correcto en el que tengo que poner los puntos y los corchetes.

Ordenar archivos por una columna específica en Bash

Minientrada

Es posible especificarle al comando sort la columna por la que se quiere ordenar un archivo, en vez de utilizar el criterio por defecto, el de ordenar al fabéticamente por el contenido de toda la fila. Esto resulta útil, por ejemplo, para ordenar archivos de log tabulares por una columna concreta que no sea la primera. Hay que usar la opción -k, que tiene un comportamiento similar a la opción -f de cut cuando se delimita por espacio.

cat prueba.txt
3 miércoles
1 lunes
2 martes
4 jueves

sort prueba.txt
1 lunes
2 martes
3 miércoles
4 jueves

sort -k2 prueba.txt
4 jueves
1 lunes
2 martes
3 miércoles

Acentos en la consola de FreeBSD

Minientrada

Tenía problemas para escribir acentos y eñes una vez enciendo mi ordenador en FreeBSD. Tengo configurado mi ordenador para arrancar manualmente el entorno gráfico, por lo que cuando enciendo el sistema lo primero que veo es una consola de vt pidiéndome el usuario y la contraseña. En este punto, todavía puedo teclear la letra ñ en el campo de nombre de usuario, pero después de hacer login, soy incapaz. Tampoco logro hacerlo si arranco X11 y abro terminales.

La razón de esto parece estar en que no se activa el soporte para caracteres extranjeros si no está establecida la variable de entorno $LANG. Aquí un hilo en el foro donde dicen que la solución es esa. Si tal cual en tcsh ejecuto setenv LANG es_ES.UTF-8, a partir de ese momento puedo escribir caracteres españoles en la consola.

Para persistirlo entre sesión y sesión, dentro del archivo ~/.login_conf introduje lo siguiente:

me:\
	:charset=UTF-8:\
	:lang=en_US.UTF-8:

Con esto queda establecido el charset a UTF-8 y la variable de entorno LANG la dejo preparada con el valor en_US.UTF-8. Si quisiese que mi ordenador estuviese en español podría utilizar es_ES.UTF-8 en su lugar, pero considero que es más valioso poder buscar el mensaje exacto en inglés que una traducción.

Por cierto, el archivo .login_conf no puede ser un enlace simbólico. Tuve la genial idea de tratar de mover ese archivo a mi repositorio de dotfiles una vez lo dejé funcionando para poder incluirlo en mi copia de seguridad, pero se conoce que, por cuestiones de seguridad, este es uno de esos archivos en el que el sistema no resuelve los enlaces simbólicos cuando se pretende utilizar a través de la API del sistema operativo, para evitar posibles problemas de seguridad. Por eso también lo subo íntegro a este post: aparte de para ayudar a posibles almas perdidas desorientadas con esto, para hacer backup en alguna parte, visto que no se integra bien con el resto de mi repo de dotfiles todavía.

GNU Guile

Minientrada

Al hilo del programa que escribí hace poco para visualizar el fractal de Mandelbrot, puede resultar interesante a simple vista que haya escogido Racket como lenguaje de programación. Fundamentalmente lo elegí porque quería hacer programación visual, y en Racket esto resulta fácil de hacer desde su GUI de forma interactiva. Se puede hacer un (require racket/draw), y ya se pueden usar funciones para dibujar sobre mapas de bits que hasta se pueden visualizar dentro del propio DrRacket. Tengo un curso de Racket en mi canal de YouTube, pero está a medias.

Racket es una implementación de Scheme, pero existen muchas otras. GNU Emacs es conocido por tener una implementación de Scheme (Emacs Lisp), con la que se pueden hacer hasta plugins; como una alternativa al Vimscript. También se puede activar el modo scratch y programar en Emacs Lisp desde dentro de Emacs, para darle más lore al meme de que GNU Emacs es un propio sistema operativo dentro de otro.

Sin embargo, GNU tiene otra implementación alternativa de Scheme llamada GNU Guile, que también tiene características interesantes. Guile es como Lua. Se puede incrustar soporte para Guile en otro programa y utilizarlo como una herramienta para fabricar plugins o extensiones. Se puede pedir a código C que interactúe con Guile.

Además, también Guile tiene un modo independiente para usarlo como una herramienta de línea de comandos más, e incluso para interpretar programas escritos en archivos de código fuente. Lo normal es que tengan la extension .scm (aunque, como siempre, en un entorno GNU esto suele dar igual), y que se invoque GNU Guile en este caso con guile -s codigo.scm.

~ $ guile
GNU Guile 2.2.4
Copyright (C) 1995-2017 Free Software Foundation, Inc.

Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.

Enter `,help' for help.
scheme@(guile-user)> (+ 2 2)
$1 = 4
scheme@(guile-user)>

Existen algunas cosas que me gustan de GNU Guile viniendo de Racket. La mayoría de palabras clave funcionan igual. En GNU Guile existe la primitiva define y la primitiva let. No tienen nombres especiales o diferentes. También existen algunas carencias en GNU Guile, pero son fáciles de subsanar. Por ejemplo, muchas de las funciones para trabajar con listas (foldl o reduce) están declaradas en la librería SRFI-1, por lo que tienen que ser importadas antes de poder usarse con un (use-modules (srfi srfi-1)).

~ $ guile
GNU Guile 2.2.4
Copyright (C) 1995-2017 Free Software Foundation, Inc.

Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.

Enter `,help' for help.
scheme@(guile-user)> (use-modules (srfi srfi-1))
scheme@(guile-user)> (reduce (lambda (x a) (+ x a)) 0 '(1 2 3 4 5))
$1 = 15

Notas sobre el conjunto de Mandelbrot

Minientrada

Ayer cerré un stream en Twitch en el que intenté programar la típica representación gráfica del conjunto de Mandelbrot. No lo terminé porque se me alargó, pero lo continuaré. Dejo aquí algunas notas para cuando haga la segunda parte.

El conjunto de Mandelbrot es el conjunto de números complejos donde se cumple que la evolución de la serie definida por la función f(c, n) = f(c, n-1)^2 + c, siendo f(c, 0) = 0, no tiende al infinito. Por ejemplo, para c=1 se obtiene la serie 0,1,2,5,26…, no acotada, pero para c=-1 se obtiene la serie 0,-1,0,-1,…, que sí está acotada. (Por supuesto, c=1 y c=-1 son ejemplos muy simples, pero esta fórmula se usará con números con parte imaginaria como 0.2265+0.331i.)

Para facilitar las cosas, lo normal es asumir que si la magnitud del número complejo supera en algún punto de la serie el valor 4, entonces con seguridad no se acota. Como no podemos pedirle al ordenador precisión infinita, si después de un número máximo de iteraciones sigue sin tender al infinito, podemos asumir que sí se acota.

O sea, que al final en un programa de ordenador repetiremos la función hasta que se superen 50, 100, 1000 iteraciones (lo cual nos diría que está acotada), o hasta obtener algún valor con un absoluto mayor a 4, lo que nos deja interrumpir la ejecución asumiendo que no se acota. Cuantas más iteraciones apliquemos, más precisa será la evaluación, ya que puede ocurrir que una serie para un complejo tarde más tiempo en divergir, aunque también tomará más tiempo.

En cuanto a la clásica imagen del fractal generado a partir del conjunto de Mandelbrot, que seguramente muchos habremos visto alguna vez, lo que vemos es la representación en un sistema de coordenadas 2D del valor de esta función para todos los números complejos. El eje X representa la parte real del complejo y el eje Y, la parte imaginaria.

En el programa de ordenador generaríamos la imagen transformando del sistema de coordenadas de la imagen (por ejemplo píxeles del (0,0) al (640,480)) a una interpolación más aceptable en el rango de los complejos que vayamos a comprobar (como (-1,-1) a (1,1), aunque podríamos reducir el área para hacer zoom), y luego consultando si ese número complejo está en el conjunto o no. Si está en el conjunto, lo podemos representar de negro. Si no está en el conjunto, lo típico es crear algún tipo de paleta de colores para representar con un color diferente aquellos complejos que escapen antes al infinito de aquellos que escapan más tarde.

 Representación del conjunto de Mandelbrot
La clásica foto del conjunto de Mandelbrot tomada de Wikipedia.

Borrando ramas locales de Git que ya no existen en remoto

Minientrada

En la mayoría de mis repositorios Git, correr git branch suele suponer abrir un cubo de basura bien grande. Cuando una rama de Git desaparece en el remoto (por ejemplo, en GitHub cuando se borra desde la interfaz web automáticamente), luego te tienes que acordar en local de borrar también tu rama. De lo contrario, vas a acabar con ramas stale que son aquellas de las que se hizo git push para abrir PR y que quedan ahí.

Una forma de identificar estas ramas es hacer un git fetch --prune, manteniendo ese prune para que se ocupe de detectar qué ramas han desaparecido del remoto, seguido de git branch -vv | grep gone. En el modo verbose de branch, las ramas locales que hacen tracking de un remoto que ya no está se identifican porque aparece [gone] en su línea de terminal. Por lo que esta pipeline lista únicamente esas ramas locales que han desaparecido del repositorio remoto.

Cortando la primera columna (mejor con awk '{ print $1 }' aunque con cut también se pueda hacer), puedes listar únicamente los nombres de las ramas. Y si estás de acuerdo con la salida de git branch -vv | grep gone | awk '{ print $1 }', (y sólo si estás de acuerdo, porque ya sabes, no refunds), entonces puedes envolver todo en un git branch -D $(git branch -vv | grep gone | awk '{ print $1 }') para cargarte todas esas ramas de un plumazo.

Follow-up sobre el portapapeles de Vim

Minientrada

Compartí por el Discord de mi comunidad de YouTube mi post del otro día en el que compartía un atajo para copiar y pegar de Vim al portapapeles. Aparentemente no a todo el mundo le funcionó a la primera.

Para poder utilizar compartir el portapapeles, Vim tiene que haber sido compilado con soporte para el mismo. Esto se puede saber ejecutando vim --version y comprobando que en la salida del comando aparece la opción +clipboard (tiene que ser un +, porque un -clipoboard precisamente avisa que no). Las versiones de Vim que trae Homebrew en macOS lo suelen tener. En otros UNIX y en GNU/Linux, es posible que haya que cambiar el paquete vim por vim-x11 o por una versión de Vim más completa.

Probando en mi instalación de FreeBSD con X11, funciona tanto para el portapapeles CLIPBOARD ("+) como para el portapapeles PRIMARY (el del botón central del ratón, "*).

¿Qué hacemos cuando hay conflicto en el Gemfile.lock?

Minientrada

Pues esta es fácil pero como todo en Git nunca aparente. Va a pasar cuando haga cambios al Gemfile en dos ramas a la vez.

git checkout HEAD -- Gemfile.lock

Esto es para traerme el Gemfile.lock que había antes de obtener mi conflicto. Lo importante es que podemos confirmar con un git status que se preserva el Gemfile, así que los cambios en las versiones o librerías siguen ahí.

Si he metido o quitado una dependencia del Gemfile, ahora ejecuto bundle para que se descargue o se retire del lock por encima de la última versión que había en la rama en la que estoy intentando hacer el merge.

Si he actualizado la versión de una dependencia del Gemfile, ahora ejecuto bundle update para que se vuelva a reflejar ese cambio en el lock por encima de la última versión que había en la rama en la que estoy intentando hacer el merge.