Search code examples
linuxbashdatetimedst

Bash - number of hours in given day


Using Bash shell on Linux, and given a datetime, how can I determine how many hours there are on that particular day?

The datetime pertains to some time zone with daylight saving, e.g. MET.


Solution

  • In order to completely account for all scenarios, you need to consider a few things:

    • Not every local day has a midnight, and the date command will fail if you pass a date on one of these days, unless you also pass a time and an offset from UTC. This primarily occurs on the spring-forward transition days. For example:

      $ TZ=America/Sao_Paulo date -d '2016-10-16'
      date: invalid date '2016-10-16'
      
    • Not every DST transition is 1 hour. America/Lord_Howe switches by 30 minutes. Bash only performs integer division, so you have to use one of these techniques if you want decimals.

    Here is a function that accounts for these:

    seconds_in_day() {
      # Copy input date to local variable
      date=$1
    
      # Start with the offset at noon on the given date.
      # Noon will almost always exist (except Samoa on 2011-12-30)
      offset1=$(date -d "$date 12:00" +%z)
    
      # Next get the offset for midnight.  If it doesn't exist, the time will jump back to 23:00 and we'll get a different offset.
      offset1=$(date -d "$date 00:00 $offset1" +%z)
    
      # Next get the offset for the next day at midnight.  Again, if it doesn't exist, it will jump back an hour.
      offset2=$(date -d "$date 00:00 $offset1 + 1 day" +%z)
    
      # Get the unix timestamps for both the current date and the next one, at midnight with their respective offsets.
      unixtime1=$(date -d "$date 00:00 $offset1" +%s)
      unixtime2=$(date -d "$date 00:00 $offset2 + 1 day" +%s)
    
      # Calculate the difference in seconds and hours.  Use awk for decimal math.
      seconds=$((unixtime2 - unixtime1))
      hours=$(awk -v seconds=$seconds 'BEGIN { print seconds / 3600 }')
    
      # Print the output
      echo "$date had $seconds secs in $TZ, or $hours hours."
    }
    

    Examples:

    $ TZ=America/Los_Angeles seconds_in_day 2016-03-12
    2016-03-12 had 86400 secs in America/Los_Angeles, or 24 hours.
    $ TZ=America/Los_Angeles seconds_in_day 2016-03-13
    2016-03-13 had 82800 secs in America/Los_Angeles, or 23 hours.
    $ TZ=America/Los_Angeles seconds_in_day 2016-03-14
    2016-03-14 had 86400 secs in America/Los_Angeles, or 24 hours.
    
    $ TZ=America/Los_Angeles seconds_in_day 2016-11-05
    2016-11-05 had 86400 secs in America/Los_Angeles, or 24 hours.
    $ TZ=America/Los_Angeles seconds_in_day 2016-11-06
    2016-11-06 had 90000 secs in America/Los_Angeles, or 25 hours.
    $ TZ=America/Los_Angeles seconds_in_day 2016-11-07
    2016-11-07 had 86400 secs in America/Los_Angeles, or 24 hours.
    
    $ TZ=America/Sao_Paulo seconds_in_day 2016-02-19
    2016-02-19 had 86400 secs in America/Sao_Paulo, or 24 hours.
    $ TZ=America/Sao_Paulo seconds_in_day 2016-02-20
    2016-02-20 had 90000 secs in America/Sao_Paulo, or 25 hours.
    $ TZ=America/Sao_Paulo seconds_in_day 2016-02-21
    2016-02-21 had 86400 secs in America/Sao_Paulo, or 24 hours.
    
    $ TZ=America/Sao_Paulo seconds_in_day 2016-10-15
    2016-10-15 had 86400 secs in America/Sao_Paulo, or 24 hours.
    $ TZ=America/Sao_Paulo seconds_in_day 2016-10-16
    2016-10-16 had 82800 secs in America/Sao_Paulo, or 23 hours.
    $ TZ=America/Sao_Paulo seconds_in_day 2016-10-17
    2016-10-17 had 86400 secs in America/Sao_Paulo, or 24 hours.
    
    $ TZ=Australia/Lord_Howe seconds_in_day 2016-04-02
    2016-04-02 had 86400 secs in Australia/Lord_Howe, or 24 hours.
    $ TZ=Australia/Lord_Howe seconds_in_day 2016-04-03
    2016-04-03 had 88200 secs in Australia/Lord_Howe, or 24.5 hours.
    $ TZ=Australia/Lord_Howe seconds_in_day 2016-04-04
    2016-04-04 had 86400 secs in Australia/Lord_Howe, or 24 hours.
    
    $ TZ=Australia/Lord_Howe seconds_in_day 2016-10-01
    2016-10-01 had 86400 secs in Australia/Lord_Howe, or 24 hours.
    $ TZ=Australia/Lord_Howe seconds_in_day 2016-10-02
    2016-10-02 had 84600 secs in Australia/Lord_Howe, or 23.5 hours.
    $ TZ=Australia/Lord_Howe seconds_in_day 2016-10-03
    2016-10-03 had 86400 secs in Australia/Lord_Howe, or 24 hours.