17

How do I find the seconds to the next hour using date? I know I can do

date -d "next hour"

but that just adds 1 hour to the present time. I want it to show the seconds to the next full hour. For example if the current time is 9:39am I want to find the number of seconds to 10am

broccoli
  • 4,738
  • 10
  • 42
  • 54

5 Answers5

11

The epoch timestamp of right now is

now=$(date '+%s')

That of the next hour is

next=$(date -d $(date -d 'next hour' '+%H:00:00') '+%s')

The number of seconds until the next hour is

echo $(( next - now ))

For a continuous solution, use functions:

now() { date +%s; }
next() { date -d $(date -d "next ${1- hour}" '+%H:00:00') '+%s'; }

And now you have

echo $(( $(next) - $(now) ))

and even

echo $(( $(next day) - $(now) ))

Another way

Another slightly mathier approach still uses the epoch timestamp. We know it started on an hour, so the timestamp mod 3600 only equals zero on the hour. Thus

$(( $(date +%s) % 3600 ))

is the number of seconds since the last hour, and

$(( 3600 - $(date +%s) % 3600 ))

is the number of seconds until the next.

kojiro
  • 74,557
  • 19
  • 143
  • 201
  • 2
    `date -d 'next hour' '+%H:00:00'` is nice, +1 – Gilles Quénot Dec 22 '14 at 02:31
  • 1
    The 3600 math way is good for not having the race conditions. – Robin Hsu Dec 22 '14 at 02:39
  • @RobinHsu the other approach isn't really subject to race conditions either, except when `now ≈ next`. Even if `now ≈ next`, though, it won't be off by more than a second if we do the hour calculation first. – kojiro Dec 22 '14 at 03:04
  • 2
    If `next` is obtained first, the two-dates solution may get negative answer when the system is busy. (e.g. `next` @ 00:59:59, and `now` @ 01:00:01.) – Robin Hsu Dec 22 '14 at 03:35
  • 1
    I'm not fond of relying on the fact that time_t 0 was the top of the hour. In general, it feels like an implementation detail, although admittedly one unlikely to change. But from a practical standpoint, it fails in India and anywhere else the local time is not a whole number of hours offset from UTC. – Mark Reed Dec 22 '14 at 13:41
  • Unless I'm missing something, I believe the `date -d 'next hour' '+%H:00:00'` fails to roll over the date as you approach midnight, yielding a negative number for `$(( next - now ))`. – Mansoor Siddiqui Mar 13 '15 at 04:04
  • @MansoorSiddiqui that format string doesn't indicate the date. In the full context of the answer, no matter how much data you add, you can always fail to roll over at the next largest unit. The solution is in the modulo operator. – kojiro Mar 13 '15 at 13:35
10

Well, there's always the straightforward mathy way:

read min sec <<<$(date +'%M %S')
echo $(( 3600 - 10#$min*60 - 10#$sec ))

EDIT: removed race condition, added explicit radix. Thanks, @rici and @gniourf_gniourf.

Mark Reed
  • 91,912
  • 16
  • 138
  • 175
  • 2
    Once in a while the seconds will wrap around between the two calls to `date` and the value will be off by 60 seconds. – rici Dec 22 '14 at 02:27
  • 3
    here's another way, which is kind of cute: `declare -i "seconds_left=$(date +"3600 - 60*%M - %S")"` – rici Dec 22 '14 at 02:32
  • 1
    @rici that's solid, why not make it an answer so upvotes actually count? :P – kojiro Dec 22 '14 at 02:34
  • I don't generally consider things that reek of `eval` "cute", but to each their own. :) Of course, in the shell it's hard to get away from things that wind up doing `eval` behind the scenes anyway.. – Mark Reed Dec 22 '14 at 02:34
  • 2
    @MarkReed is `declare -i` any more `eval` than `$(( ))`? Or is that what you meant by "hard to get away"? – kojiro Dec 22 '14 at 02:35
  • It's not about `declare -i` vs `$((`...`))`; @rici could have also done `echo $(( $(date +"3600 - 60*%M - %S") ))`, after all. I just prefer to pass around individual parameters that get treated individually, rather than strings that are parsed an extra time through. – Mark Reed Dec 22 '14 at 02:46
  • As always, cuteness is in the eye of the beholder. (I could have used `bc` as well, which would have failed my cuteness filter but maybe would have seemed less eval-like.) – rici Dec 22 '14 at 02:52
  • Cool stuff. I wrapped it in a function for ease of use: `function seconds_left() { echo $(( $(date +"3600 - 60*%M - %S") )) }`. Works like a charm. – Makaze Dec 22 '14 at 02:59
  • 3
    You need `echo $(( 3600 - 10#$min*60 - 10#$sec ))` to ensure that the minutes and the seconds are interpreted in radix 10. Without the `10#`, your command fails if the minute or second is `08` or `09`. – gniourf_gniourf Dec 22 '14 at 13:31
  • Very high level brain-storming, nice shot guys ;) +1 – Gilles Quénot Dec 22 '14 at 18:08
4

With Bash≥4.2 you can use printf with the %(...)T modifier to access dates (current date corresponds to argument -1, or empty since version 4.3):

printf -v left '%(3600-60*10#%M-10#%S)T' -1
echo "$((left))"

Pure Bash and no subshells!

The 10# is here to ensure that Bash's arithmetic (expanded in the $((...))) treats the following number in radix 10. Without this, you'd get an error if the minute or second is 08 or 09.

gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
1

Try doing this :

LANG=C
now=$(date +%s)
next="$(date |
    perl -pe 's/(\d{2}):\d{2}:\d{2}/sprintf "%.2d:00:00", $1 + 1/e')"
next=$(date -d "$next" +%s)
echo $(( next - now ))

OUTPUT :

2422
Gilles Quénot
  • 173,512
  • 41
  • 224
  • 223
-1
#!/usr/bin/env bash
is=`date +%S`
im=`date +%M`
echo $((((60-$im)*60)+(60-$is)))
porto
  • 555
  • 4
  • 7