2

All of the tons of articles I have found so far all seemed to be focused on obtaining a resulting date, which is still useful, but not what I want in this case.

Example link: Unix & Linux SE - Quickly calculate date differences.

With datediff in that other Q&A, this is more of what I want referring to a date/time duration result, and it essentially uses Unix timestamp math with those preceding conversions, but I want my granularity down to the second, which is why I don't divide by 86400.

With the duration of seconds available, I now want to format it to something like using the GNU date command this way: date -d "@69600" "+ %Y years %m months %e days %H:%M:%S"

I realize everything is essentially a date/time duration from the Unix epoch, but now I have the problem with the day, month, and year numbers not starting from 0, which would not correctly represent the duration value I want.

I could get into cutting this format and applying more expr commands to essentially subtract 1 or 1970 from each value, but I'm wondering if there is any other easier and simpler way to deal with date/time calculation to achieve a duration result besides chaining math and formatting steps together. Maybe there is some other option in GNU date that I could take advantage of, or another tool that would accept 2 date/time arguments and immediately give me the result I am looking for.

Having it be available in Homebrew for Macs would be a plus :).

Simple date math link: Walker News - Date Arithmetic In Linux Shell Scripts

Random date formatting links:

2
  • echo $(($(date +%s -d "3 days ago")-$(date +%s -d "4 weeks 2 hours 1 minutes 33 seconds ago"))) | sed 's/-//'
    – DopeGhoti
    May 18, 2017 at 18:10
  • I've even wondered if there are any convenient functions to take advantage of in a script by, say, using SQL. Trying to avoid a server, maybe one could use SQLite instead. Just a thought as I vaguely remember some date calculation operations being available there.
    – Pysis
    May 18, 2017 at 20:07

3 Answers 3

3

With dateutils's datediff (not GNU sorry), (formerly ddiff, dateutils.ddiff on Debian):

$ dateutils.ddiff -f '%Y years, %m months, %d days, %H:%0M:%0S' \
    '2012-01-23 15:23:01' '2017-06-01 09:24:00'
5 years, 4 months, 8 days, 18:00:59

(dates taken as UTC, add something like --from-zone=Europe/London for the dates to be taken as local time in the corresponding time zone. --from-zone=localtime may work for the default timezone in the system; --from-zone="${TZ#:}" would work as long as $TZ specifies the path of a IANA timezone file (like TZ=:Europe/London, not TZ=GMT0BST,M3.5.0/1:00:00,M10.5.0/2:00:00 POSIX-style TZ specification)).

With ast-open date, so still not with GNU tools sorry (if using ksh93 as your shell, that date may be available as a builtin though) you can use date -E to get the difference between two dates in a format that gives 2 numbers with units:

$ date -E 1970-01-01 2017-06-01
47Y04M
$ date -E 2017-01-01 2017-06-01
4M29d
$ date -E 12:12:01 23:01:43
10h49m

Note that anything above hour is ambiguous, as days have a varying number of hours in locales with DST (23, 24 or 25) and months and years have a varying number of days, so it may not make much sense to have more precision than those two units above.

For instance, there's exactly one year in between 2015-01-01 00:00:00 and 2016-01-01 00:00:00 or between 2016-01-01 00:00:00 and 2017-01-01 00:00:00, but that's not the same duration (365*24 hours in one case, 366*24 hours in the other)

There's exactly one day between 2017-03-24 12:00:00 and 2017-03-25 12:00:00 or between 2017-03-25 12:00:00 and 2017-03-26 12:00:00, but when those are local time in European countries (that switched to summer time on 2017-03-26), that one day is 24 hours in one case and 23 hours in the other.

In other words a duration that mentions years, months, weeks or days is only meaningful when associated to one of the boundaries it's meant to be applied to (and the timezone). So you cannot convert a number of seconds to such a duration without knowing what time (from or until) it's meant to be applied to (unless you want to use approximate definitions of day (24 hour), month (30*24 hours) or year (365*24 hours)).

To some extent, even a duration in seconds is ambiguous. For simplification, the seconds in the Unix epoch time are defined as the 86400th part of a given Earth day1. Those seconds are getting longer and longer as Earth spins down.

So something like (with GNU date):

d1='2016-01-01 00:00:00' d2='2017-01-01 00:00:00'
eval "$(date -d "@$(($(date -d "$d2" +%s) - $(date -d "$d1" +%s)))" +'
  delta="$((%-Y - 1970)) years, $((%-m - 1)) months, $((%-d - 1)) days, %T"')"
echo "$delta"

Or in fish:

date -d@(expr (date -d $d2 +%s) - (date -d $d1 +%s)) +'%Y %-m %-d %T' | \
  awk '{printf "%s years, %s months, %s days, %s\n", $1-1970, $2-1, $3-1, $4, $5}'

Would generally not give you something that is correct as the time delta is applied to 1970 instead of the actual start date 2016.

For instance, above, that gives me (in a Europe/London timezone):

1 years, 0 months, 1 days, 01:00:00

instead of

1 years, 0 months, 0 days, 00:00:00

(that may still be a good enough approximation for you).


1 Technically, while a day is 86400 Unix seconds long, network-connected systems typically synchronise their clock to atomic clocks using SI seconds. When an atomic clock says 12:00:00.00, those Unix systems also say 12:00:00.00. The exception is only upon the introduction of leap seconds, where either there's one Unix second that lasts 2 seconds or a few seconds that last a bit longer to smear that extra second upon a longer period.

So, it is possible to know the exact duration (in terms of atomic seconds) in between two Unix time stamps (issued by systems synchronised to atomic clock): get the difference of Unix epoch time between the two timestamps and add the number of leap seconds that have been added in between.

datediff also has a -f %rS to get the number of real seconds in between two dates:

$ dateutils.ddiff -f %S '1990-01-01 00:00:00' '2010-01-01 00:00:00'
631152000
$ dateutils.ddiff -f %rS '1990-01-01 00:00:00' '2010-01-01 00:00:00'
631152009

The difference is for the 9 leap seconds that have been added in between 1990 and 2010.

Now, that number of real seconds is not going to help you to calculate the number of days as days don't have a constant number of real seconds. There are exactly 20 years between those 2 dates and those 9 leaps seconds rather get in the way to get to that value. Also note that ddiff's %rS only works when not combined with other format specifiers. Also, future leap seconds are not known long in advance, but also information about recent leap seconds may not be available. For instance, my system doesn't know about the ones after 2012-07-01.

9
  • So this is AST eh? unix.stackexchange.com/questions/296902/… github.com/att/ast Although in the GitHub link I did not find date at a glance. If I need it bad enough, I may put the tools on my system.
    – Pysis
    May 18, 2017 at 20:02
  • @Pysis, see edits May 19, 2017 at 6:54
  • Yes, I had written a script that stopped at whether I want to do the manual math to fix the offset for the date values, and I did write my date expectation in a comment above already, but it is nice to highlight that the difference when taking a duration based from 1970 instead of from my input start date can not be reused completely as such. Also I probably should have mentioned I am using the fish shell for some reason, and am trying to avoid the $(( ... )) Bash computation syntax in favor of expr, but in a script I guess I have either interpreter available anyway.
    – Pysis
    May 19, 2017 at 13:18
  • Also, it's interesting how you seemed to have mixed that computation syntax, and pass it through inside of the date format parameter, later to be eval'ed. That's the clever bit of code I was hoping for at a point in my script but couldn't come up with.
    – Pysis
    May 19, 2017 at 13:19
  • 1
    @Pysis, sorry, I didn't mean to imply that fish was POSIX, just that you could use your system's sh without having to install bash (though if you have GNU utilities, you're likely to have bash as well as bash is the GNU shell). See edit for a fish alternative. May 19, 2017 at 14:08
1

Okay, so let's say you have two date-compatible strings defining two points in time you want to calculate the duration in between:

echo $(($(date +%s -d "3 days ago")-$(date +%s -d "4 weeks 2 hours 1 minutes 33 seconds ago"))) | sed 's/-//'

But let's say further that you want it more readably:

convertsecs() {
    hours=$(($1/3600))
    minutes=$(($1%3600/60))
    seconds=$(($1%60))
}
totalduration="$(($(date +%s -d "3 days ago")-$(date +%s -d "4 weeks 2 hours 1 minutes 33 seconds ago"))) | sed 's/-//'"
convertsecs "$totalduration"
echo "Duration was ${hours} hours, ${minutes} minutes, and ${seconds} seconds."
3
  • So you just have as far as I made it? No more insightful or targeted methods, just doing some manual division per unit range, and this example only goes up to hours, which is ok in some of my cases.
    – Pysis
    May 18, 2017 at 18:44
  • 2
    It seemed to me that your beef was not getting to-the-second granulairity and not wanting the "duration" to be a random date in the distant past; I showed you how to get it? If this isn't what you're looking for, perhaps make your question more clear about what you want with respect to given input and desired output.
    – DopeGhoti
    May 18, 2017 at 18:50
  • I expressed that datediff from the other answer gave me an answer in days, where I want more unit in addition to that without truncating. I have the seconds value as demonstrated in that answer, and now I am curious if there are other ways to format the duration more naturally rather than having more manual steps of calculations to split the values apart, as per my final paragraph.
    – Pysis
    May 18, 2017 at 18:56
0

Time is a complicated thing, and it's important to manage your expectations while calculating it. I don't believe it's technically possible to do what you ask both accurately and precisely in the general case, since a given number of seconds might correspond to a different number of minutes once you consider things like leap seconds, which themselves don't occur on a wholly-predictable basis.

Put another way, while we say a day is 24 hours, not every day is precisely 24 hours, so if you want to know how many days plus minutes plus seconds a given duration was, you need to anchor that duration somewhere in real time so as to know whether or not there was a leap second in there, or even whether or not April 29th existed somewhere in the duration.

Even more exotic, not every month has the same number of days.

If that's not a concern, simply run your count of seconds through basic arithmetic; use date to convert to seconds-since-epoch, divide by 86400 to get days, the remainder by 3600 to get hours, the remainder to get minutes, and so on.

2
  • I had the expectation, and still do, that if I use a tool that understands dates and time in order to calculate with them, it would understand that months have a differing amount of total days in each, leap years exist every 4 years, and a leap second here or there, etc. The same would go for when someone sees a statement indicating how many years, months, and so on have passed, and if that doesn't come to mind, then hopefully the purpose for the duration statement could handle the error being negligible. My uses are casual, so it should be fine, but I did have this in mind already.
    – Pysis
    May 19, 2017 at 13:11
  • 3
    As far as the Unix epoch time is concerned (the output of date +%s), one day is always 86400 seconds. IOW, the Unix second (as opposed to the SI second) has a varying length (in absolute terms) like the Earth day. That makes date manipulation much easier. You don't have to worry about leap seconds. May 19, 2017 at 13:52

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .