how to present seconds as duration in jq

87 Views Asked by At

I want to display some amount of seconds that correspond to dime duration rather than regular date in some human-readable form. say sth like:

[
  30300,
  120300,
  296700,
  31922700,
  63458700
]

and what i would like to get

[
  "0d, 08:25",
  "1d, 09:25",
  "3d, 10:25",
  "1y 4d, 11:25",
  "2y 4d, 11:25"
]

seems there is no "easy" (built in) way to get this. found some lib that does almost what i need:

https://github.com/fearphage/jq-duration

but it has a fixed output format, which doesnt quite fit my needs.

i was able to achieve what i want with strftime, basically this is "almost a date", right? i can get hours and minutes, i can get "day # of year" (&j), i can get year (%Y). but there are some caveats: by default year starts at 1970, and according to the doc: day of year is 1-366.

what if i just subtract 1970 from "years" to get years counting from 0, and do same for "days" (subtract one) ?

so we would end up with by below jq filter:

gmtime | (.[0] -= 1970) | (.[7] -= 1) | strftime ("%Yy %jd, %H:%M") | ltrimstr("0y ") | sub("0+(?<ds>[0-9]+d)";"\(.ds)")

the only thing left is to ltrim excessive zeros (%j produces a fixed width (3chars))

kind of hacky, but gets the job done - what do you think?

https://jqplay.org/s/JGUUnI8q1zK

maybe sbd has a better way to approach such problem?

btw - i checked also on bigger values (exceeding 4 years) , and it seems that "my way" returns the same as javascript moment.js + https://github.com/jsmreese/moment-duration-format lib:

2

There are 2 best solutions below

0
oguz ismail On

Here is a way that doesn't use time builtins:

map(
  [foreach (
    [60, "seconds"],
    [60, "minutes"],
    [24, "hours"],
    [365, "days"],
    [false, "years"]
  ) as [$unit, $key] (
    {in: .};
    if $unit then
      {in: (.in/$unit | floor), out: {($key): (.in%$unit)}}
    else
      {out: {($key): .in}}
    end;
    .out
  )] | add | [
    (.years | if . > 0 then "\(.)y" else empty end),
    "\(.days)d,",
    ([.hours, .minutes] | map(if . < 10 then "0\(.)" else . end) | join(":"))
  ] | join(" ")
)

Online demo

0
peak On

Here is a solution written for flexibility and using a generic seconds2object function:

# Emit a JSON object of the form {years, days, hours, minutes, seconds}
# assuming a 365-day year
def seconds2object:
  def divrem($x; $y):
    [$x/$y|floor, $x % $y];

  def units:
    { min: 60}
      | .hour =  60 * .min
      | .day  =  24 * .hour
      | .year = 365 * .day;

  units as $units
  | divrem( .; $units.year) as [$years, $r]
  | divrem($r; $units.day)  as [$days, $r]
  | divrem($r; $units.hour) as [$hours, $r]
  | divrem($r; $units.min)  as [$mins, $secs]
  | {$years, $days, $hours, $mins, $secs} ;

# Emit a string in Yy Dd HH:MM format,
# ignoring residual seconds and omitting 0y
def seconds2human:
  def lpad($len): tostring | ($len - length) as $l | ("0" * $l) + .;
  seconds2object
  | if .years == 0 then "" else "\(.years)y " end
    + "\(.days)d, \(.hours|lpad(2)):\(.mins|lpad(2))" ;
 
 
[
  30300,
  120300,
  296700,
  31922700,
  63458700
] 
| map(seconds2human)