Misceláneas de Chipaca

in English

Matemática entera

Disfruto mucho haciendo que datos tabulados sean fáciles de leer en la terminal. A menudo esto me lleva a lugares extraños y maravillosos.

datos tabulados y escanear y bonito

Cuando uno escribe datos tabulados a una terminal, ‘bonito’ es muy a menudo usado para significar ‘fácil de leer’, ‘fácil de escanear’ eso que uno hace cuando está buscando un elemento de texto en una columna, distinto de leer. En inglés es to scan que es la misma palabra que escanear y escandir. No sé si se le dice escandir a este proceso. , y ‘alineado’. Si hacés bien este último, los otros dos salen casi solos.

Así que si tenés un montón de números (acá estoy hablando de ints y cosas así), querés alinearlos de la manera que el usuario espera, y eso es casi siempre No tengo idea de cómo funciona esto en lenguas donde el texto no fluye de izquierda a derecha, más allá de que existen, y de que es complicado y contextual, así que voy a suponer texto de izquierda a derecha y me encantaría aprender de otras opciones algún día con los números alineados a la derecha.

Year  Units   Net
2021       7    7
2022      14  -23
2023  654386    5

Si estás usando Go, y usando text/tabwriter de su librería estándard, y todo lo que estás imprimiendo son números, y no te importa que los encabezados también se alineen a la derecha, es muy cómodo la opción AlignRight que manda todo para la derecha y ya está:

	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.AlignRight)
	w.Write(data)
	w.Flush()

te da

  Year   Units  Net
  2021       7    7
  2022      14  -23
  2023  654386    5

que está bastante bien.

¿qué ancho tiene ese número?

Sin embargo, a veces tenés otras cosas mezcladas con los números, como una columna de texto ASCII Texto que es otra cosa que ASCII queda para otro artículo. . O quizás tengas el requermiento de alinear los encabezados de las columnas a la izquierda. En estos casos, necesitás alinear los números a la derecha usando algo como fmt.Sprintf("%5d", num), donde a menudo hay una cota superior natural para el ancho de los números a presentar en la terminal (e.g. para años sería 4, o 6 si estás haciendo historia o futurulogía). Pero a veces esa ‘cota superior natural’ es algo muy grande e inusual como 1<<64. ¿Cuánto espacio realmente necesitás para tus números? Tenés que caminar todos los números en la columna, encontrar los extemos, y ver qué tamaño tienen.

¿Cómo ves qué tamaño tienen? Bueno, podrías simplemente mirar el largo de su fmt.Sprint. Fácil!

func lenFmt[V constraints.Unsigned](n V) V {
	return V(len(fmt.Sprint(n)))
}

Pero si sospechás del comportamiento de fmt con respecto a su performance y/o uso de memoria (y probablemente deberías), quizás quieras toquetear esto un poco, aún sin cambiar el ‘algoritmo’:

func lenStrconv[V constraints.Unsigned](n V) V {
	return V(len(strconv.FormatUint(uint64(n), 10)))
}

Ahora, el largo de la representación decimal de un entero u es simplemente ⌊log₁₀u⌋+1 (con un caso especial para el 0). Así que podrías hacer directamente eso,

func lenMath[V constraints.Unsigned](n V) V {
	return V(math.Floor(math.Log10(float64(n)))) + 1
}

o… podrías usar matemática entera para calcular ⌊log₁₀u⌋. Que es lo que he hecho en intmath.Len.

$ go test -short -bench Len -benchmem
goos: linux
goarch: amd64
pkg: chipaca.com/intmath
cpu: 12th Gen Intel(R) Core(TM) i5-12600H
BenchmarkLenFmt-12        	21367644	       49.89 ns/op	     16 B/op	      1 allocs/op
BenchmarkLenStrconv-12    	76015239	       15.01 ns/op	      7 B/op	      0 allocs/op
BenchmarkLenMath-12       	179116516	        6.703 ns/op	      0 B/op	      0 allocs/op
BenchmarkLenInt-12        	1000000000	        0.3558 ns/op	  0 B/op	      0 allocs/op
PASS
ok  	chipaca.com/intmath	5.826s

chipaca.com/intmath está todavía bastante verde, pero la idea es que sea un complemento para math/bits de la librería estándar, para que entre los dos puedas hacer la mayoría de las cosas más obvias de math usando algoritmos que sean más rápidos para enteros.