
Tengo un intervalo, en segundos con un decimal, que me gustaría mostrar en formato legible por humanos (H:MM:SS.SSS). Por ejemplo, 16633.284 debería mostrarse como 4:37:13.284. Todos estos tiempos son inferiores a 24 horas, pero si fueran superiores, seguirían siendo solo horas.
Algunos ejemplos más:
0 → 0:00:00.000
60.394 → 0:01:00.394
8944.77 → 2:29:04.770
Tenga en cuenta que tiene un ancho fijo por menos de 10 horas. Por supuesto, eso se puede hacer con bastante facilidad printf
.
Estoy publicando mi solución como respuesta, pero parece que debe haber una mejor manera de hacerlo, así que pregunto: ¿cuáles son las otras formas? Estoy abierto a formas que sean bashismos, zshismos, etc.
Nota: Esto está relacionado con¿Mostrar segundos como días/horas/minutos/segundos?pero todos esos enfoques no funcionan porque la aritmética en bash es solo de números enteros.
Respuesta1
La parte fraccionaria no contribuye en nada al número de horas, minutos o segundos, por lo que simplemente puedes dejarla a un lado, hacer los cálculos sin ella y volver a agregarla al final. Algo como esto:
#!/bin/sh
seconds=16633.284
f=${seconds##*.}
if [ "$f" = "$seconds" ]; then f=0; fi
t=${seconds%.*}
s=$((t%60))
m=$((t/60%60))
h=$((t/60/60))
printf '%d:%02d:%06.3f\n' $h $m $s.$f
Producción:
0 → 0:00:00.000
33. → 0:00:33.000
.21 → 0:00:00.210
60.394 → 0:01:00.394
8944.77 → 2:29:04.770
Esto funciona en cualquier shell tipo sh (excepto algunos antiguos anteriores a POSIX).
Respuesta2
Mi enfoque es este, usando dc
para hacer los cálculos:
seconds=16633.284
printf '%i:%02i:%06.3f\n' \
$(dc -e "$seconds d 3600 / n [ ] n d 60 / 60 % n [ ] n 60 % f")
Es de esperar que printf sea bastante familiar, pero si no, %i
significa un número entero. %02i
significa rellenar el número entero con un 0 inicial si es de un solo dígito. %06.3f
es el más extraño, pero significa 3 dígitos después del punto decimal y rellenar con ceros a la izquierda si la longitud total es inferior a 6 (incluido el punto decimal).
La sustitución intencionalmente no citada $(…)
proporciona esos tres parámetros, usando dc
:
Primero, empuje $seconds
(expandido, por lo que el número real) en la pila; duplicarlo; luego divida (truncando) por 3600 para obtener horas. Pop e imprime eso, luego presiona la barra espaciadora e imprímelo ( [ ] n
).
Eso deja sólo los segundos en la pila. Duplícalo, divídelo por 60 (nuevamente truncando). Eso da minutos; llévalo mod 60 para tirar las horas. Imprímelo, nuevamente con el espacio. Dejando solo los segundos en la pila.
Finalmente, el mod 60 proporciona solo el campo de segundos, y en CC, el módulo se adapta perfectamente a los decimales y conserva la precisión. f
luego imprime la pila más una nueva línea.
Respuesta3
Puedes hacerlo fácilmente en sh simple.. Puedes hacer el código un poco más corto con la fecha GNU (espero que no te importe redondear los milisegundos hacia abajo, redondear al más cercano sería un poco más largo):
hours=$((${seconds%.*} / 3600))
min_s_nano=$(TZ=GMT date -d @$seconds +%M:%S.%N)
time_string=$hours:${min_s_nano%??????}
Respuesta4
Invocamos dc
y colocamos la cantidad de segundos en su pila y realizamos los cálculos (segundos->HH/MM/SS.MSEC), formateamos y mostramos los resultados.
seconds=16633.284
dc <<DC
# rearrange stack for number < 1hr
[r ldx q]sb
# rearrange stack for number < 1min
[rd ldxq]sc
# min++, sec-=60 hrs++, min-=60
[r1+r 60-d 60!>a]sa
# display result as h:mm:ss.sss
[n 58an 2lpx 58an 2lpx 46an 3lpx 10an]sd
# perform the conversion of seconds -> hours, minutes, seconds
[d60 >c lax r0rd60 >b lax r ldx]si
[
d1000* 1000% 1/r # fractional portion
1000* 1000/ 1/0r # integer portion
lix
]sh
# left zero-pad a number
[lk1+d sk 0n le >g ]sg
[sedZd sk le >gn]sp
# setup the number on the stack and begin computation
$seconds lhx
DC
Resultados
0 --> 0:00:00.000
60.394 --> 0:01:00.394
8944.77 --> 2:29:04.770
86399.99 --> 23:59:59.990
59.9999 --> 0:00:59.999
599.9999 --> 0:09:59.999
16633.284 --> 4:37:13.284
33 --> 0:00:33.000
.21 --> 0:00:00.210
123456789.123456 --> 34293:33:09.123