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.

Más notas sobre importar posts

Minientrada

Retomo el tema importar entradas en WordPress de forma masiva.

Recientemente volví a tirar de ese script para traerme un JSON con tweets viejos a danirod.es. Ni siquiera los he hecho públicos. Por el momento sólo los he hecho privados porque lo que me interesa es tenerlos consolidados. Tampoco son tweets recientes; estamos hablando de tweets que tienen más de una década. Todos los enlaces externos dan HTTP 404, están fatal escritos y, en general, es contenido poco interesante para internet. Pero me apetecía tenerlos a mano.

Uso el plugin Syndication Links para conectar entradas que sean importadas de otras redes sociales con su URL de referencia; las sindicaciones de Hacker News son un ejemplo. En el caso de tweets de una cuenta que ya ni siquiera existe, es bastante irrelevante plantar un u-syndication. Aun así, me apetecía ponerlo.

En el caso del plugin Syndication Links, la metadata sobre sindicación se guarda como un campo personalizado de WordPress llamado mf2_syndication. Con la API XML-RPC de WordPress es posible crear campos personalizados rellenando el array custom_fields. Hay que tener en cuenta que no es un clave-valor simple, sino que es un array de objetos, con su campo key para el título del campo, y su campo value para el valor del campo. En el módulo de npm, los campos se dan usando la key customFields en la llamada a newPost:

client.newPost({
  title: '...',
  content: '...',
  customFields: [
    { key: 'field-1', value: 'value value' },
    { key: 'field-2', value: 'more value more value' }
  ]
}, (e, id) => console.log(e, id));

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

Good and Bad Elixir ➜

Enlace

Super buenos consejos para escribir código Elixir más manejable.

Las funciones con side-effects suelen devolver resultados tipo {:ok, term()} | {:error, term()}. Si estás trabajando con funciones que tienen side-effects, no las conectes con la siguiente función con un pipe. Es mejor tratar los resultados con un case o con un with.

Super de acuerdo en esto. Recuerdo que hace no demasiado me pareció «superapropiado» hacer un fmap monádico en Elixir para poder escribir luego código fancy a lo Haskell:

# Just don't. Not a very smart idea despite what it looks like
def fmap({:ok, x}, f), do: {:ok, f.(x)}
def fmap(whatever, _), do: whatever

def readline(path),
  do:
    File.read(path)
    |> fmap(&String.split(&1, "\n"))
    |> fmap(&Enum.filter(&1, fn line -> line != "" end))

Pero desde que conozco el with, no sólo no me parece necesario sino que oh dios mío por qué pude pensar que esto era buena idea. En caso de que deje de cumplirse con la secuencia de condiciones, with evalúa el bloque completo al valor devuelto por la primera función que salió mal, dejandolo todo más limpio.

def readline(path),
  do:
    with {:ok, contents} <- File.read(path),
         {:ok, lines} <- String.split(contents, "\n"),
         {:ok, trimmed} <- Enum.filter(lines, fn line -> line != "" end) do
      trimmed
    end

Supongo que no será la única epifanía ocurrida en este mundo al conocer la palabra clave with.

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.

Importando de redes sociales con XML-RPC

Minientrada

Una de las cosas bonitas de la GDPR es que casi obliga a las redes sociales y sitios similares a tener un sistema de exportado de datos. Y casi siempre lo hacen con algún formato estructurado como CSV, JSON o XML, fácil de procesar por un ordenador.

Paralelamente, WordPress tiene el viejo confiable XML-RPC para crear posts de forma programática. Si no tienes mucha idea, recomiendan cerrar ese endpoint al exterior para evitar problemas. Pero en verdad, si sabes protegerlo, resulta muy práctico para crear posts a golpe de petición HTTP. (En teoría existe la API REST, pero bueno…)

Con la ayuda de un cliente XML-RPC para WordPress que hay en NPM y de la API de Hacker News, hice en Node.js un script de un solo uso que crease una entrada por cada comentario y enlace enviado para enlazar hacia el comentario o la historia, siguiendo el modelo de sindicación PESOS de IndieWeb (Publish Elsewhere, Syndicate to Own Site). Le he puesto la etiqueta hn-import a la colección. ¿Cuál será el próximo archivo que importe?

En sí la librería no es muy complicada de utilizar. Una vez tienes un cliente conectado es muy fácil chutarle un array de posts a crear (por ejemplo, procesar fila a fila un CSV o item a item un JSON o un XML). Primero se crea un cliente:

const { createClient } = require("wordpress")

const client = createClient({
  url: "https://example.com",
  username: "mi user",
  password: "mi password"
})

Luego definiendo el payload. Aquí es donde encuentro más cómodo crear un objeto JSON que declarar todo el chorizo XML de XML-RPC:

const payload = {
  title: "El título de mi entrada",
  content: `
    <p>El contenido de mi entrada.</p>
    <blockquote>Aceptamos HTML.</blockquote>
    <p><a href="https://www.example.com">Y enlaces</a></p>
  `.trim(),
  status: 'publish', // podría ser 'draft' o 'private'
  termNames: {
    category: ['Categoría'],
    post_tag: ['tag 1', 'tag 2', 'tag 3'],
  },
  date: '2006-01-02 15:04:05',
  format: 'link' // podría ser 'aside', 'status', 'photo', ...
}

Y ya postear usando newPost. La librería es previa a promesas así que su único punto malo es que está orientada a callbacks.

client.newPost(payload, (err, id) => {
  if (err) {
    console.error(err)
  } else {
    console.log(`ID del nuevo post: ${id}`)
  }
})

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.