1. Introduction
Although it has limitations and drawbacks, the UNIX epoch time, or just UNIX time, is still widely used for timestamps in many standards, environments, and applications. Because of this, knowing how to generate, parse, and handle such a timestamp is still relevant and often important. In fact, converting dates to and from the UNIX format can enable easier computations without having to consider time boundaries.
In this tutorial, we cover ways to perform arithmetic on UNIX timestamps in the shell. First, we go over the basic UNIX time format. Next, we turn to adding and subtracting fixed time units. After that, we calculate date differences and perform unit conversions. Finally, we explore converting any date-time to a UNIX timestamp as a way to handle more complex formats.
For brevity and clarity, we use the term timestamp as any date-time representation, while the RFC-3339 date format in Coordinated Universal Time (UTC) is our main reference.
We tested the code in this tutorial on Debian 11 (Bullseye) with GNU Bash 5.1.4. It should work in most POSIX-compliant environments unless otherwise specified.
2. UNIX Epoch Timestamps
Despite its historical context, the current UNIX epoch is 1 January 1970.
So, to represent a UNIX epoch timestamp, we get the number of seconds since 1970-01-01T00:00:00+00:00:
$ date +%s
1685606660
$ date --iso-8601=seconds --universal --date=@1685606660
2023-06-01T08:04:20+00:00
Here, we use the date command to first get a UNIX epoch [+%s]seconds timestamp and then convert it to an RFC-3339-compatible ISO-8601 –date (-d). As a result, we see that 1685606660 seconds have passed since UNIX epoch until 1 June 2023 at 08:04:20 UTC.
Further, we can reach [%N]anosecond precision, but it won’t comply with the original intent of the format. In fact, the UNIX time format is 32-bit, which only allows it to contain times between 13 December 1901 and 19 January 2038:
$ date --iso-8601=seconds --universal --date=@-$((2**31))
1901-12-13T20:45:52+00:00
$ date --iso-8601=seconds --universal --date=@$((2**31))
2038-01-19T03:14:08+00:00
To show this, we use $(()) arithmetic expansion with the ** exponent operator.
In any case, the UNIX timestamp format is a basic quantity that we can manipulate as such. Because of this, although we can employ libraries, tools, and programming languages like Perl, Python, and Ruby, we leverage the shell alone for our needs.
3. Add and Subtract Basic Time Units
When it comes to basic time units, we can perform $(()) arithmetic operations with a UNIX timestamp directly.
Conveniently, due to its simplicity, the format automatically handles transitions between minutes, hours, and dates. In all examples, we use the 1685606660 (2023-06-01T08:04:20+00:00) timestamp and basic Bash arithmetic.
3.1. Seconds
Since the UNIX timestamp is in seconds, we can directly add or remove values in this unit:
$ date --iso-8601=seconds --universal --date=@$((1685606660 - 20))
2023-06-01T08:04:00+00:00
$ date --iso-8601=seconds --universal --date=@$((1685606660 + 20))
2023-06-01T08:04:40+00:00
In this example, we perform subtraction and addition of 20 seconds with 1685606660.
3.2. Minutes
Minutes contain 60 seconds, so we can either convert them to the respective number of seconds or create expressions to perform the calculation:
$ date --iso-8601=seconds --universal --date=@$((1685606660 - 2*60))
2023-06-01T08:02:20+00:00
$ date --iso-8601=seconds --universal --date=@$((1685606660 + 2*60))
2023-06-01T08:06:20+00:00
Here, we subtract and add 2 minutes (60 seconds each). Thus, we get more readable commands and expressions.
3.3. Hours
When it comes to hours, we have 60 minutes with 60 seconds for a total of 3600 seconds. Of course, we can again convert the total number to seconds, but legibility would usually suffer at this point. Because of this, we’ll chain our previous calculations:
$ date --iso-8601=seconds --universal --date=@$((1685606660 - 3*60*60))
2023-06-01T05:04:20+00:00
$ date --iso-8601=seconds --universal --date=@$((1685606660 + 3*60*60))
2023-06-01T11:04:20+00:00
Removing and adding 3 hours, we see the expected results.
3.4. Days
With 24-hour days, 60 minutes per hour, and 60 seconds per minute, we can again accumulate the times as necessary instead of using 86400 seconds:
$ date --iso-8601=seconds --universal --date=@$((1685606660 - 2*24*60*60))
2023-05-30T08:04:20+00:00
$ date --iso-8601=seconds --universal --date=@$((1685606660 + 2*24*60*60))
2023-06-03T08:04:20+00:00
When removing two days, we cross a month boundary, but that’s not a problem for the format.
3.5. Weeks
Since weeks are 604800 seconds but can also be represented as 7 days with 24 60-minute hours, each consisting of 60 seconds, we can still employ our previous approach:
$ date --iso-8601=seconds --universal --date=@$((1685606660 - 4*7*24*60*60))
2023-05-04T08:04:20+00:00
$ date --iso-8601=seconds --universal --date=@$((1685606660 + 4*7*24*60*60))
2023-06-29T08:04:20+00:00
Although the usual notion is that a month is four (4) weeks, we see that’s not actually the case. In fact, when it comes to months and years, we need a more complex approach to time, even with the UNIX timestamp.
4. Date Differences and Comparisons
Again, due to the simplicity of UNIX timestamps, we can subtract dates directly:
$ timestamp1=1685606660
$ timestamp2=1685500000
$ echo $(($timestamp1 - $timestamp2))
106660
This way, we get the number of seconds between the two dates:
- positive differences mean timestamp1 is after timestamp2
- negative differences mean timestamp1 is before timestamp2
Thus, we can deduce whether the current date is before or after an arbitrary timestamp via a basic comparison and a logical shorthand:
$ [[ $(date +%s) -gt $timestamp ]] \
&& echo "$timestamp is in the past." \
|| echo "$timestamp is in the future."
Using the rules from earlier in reverse, we can convert the difference from seconds to any time unit:
$ timestamp1=1685606660
$ timestamp2=1685600000
$ diff=$(($timestamp1 - $timestamp2)) # 106660 seconds total =
$
$ seconds= $diff # 106660 seconds total ~
$ minutes=$(( $diff / (60) )) # 1777 minutes total ~
$ hours=$(( $diff / (60*60) )) # 29 hours total ~
$ days=$(( $diff / (60*60*24) )) # 1 days total ~
$ weeks=$(( $diff / (60*60*24*7) )) # 0 weeks total
In fact, by doing these calculations step by step, we get the composition instead of the total in each time unit:
$ timestamp1=1685606660
$ timestamp2=1685500000
$ diff=$(($timestamp1 - $timestamp2)) # 106660 seconds total =
$
$ weeks=$(( $diff / (60*60*24*7) )) # 0 weeks +
$ days=$(( $diff % (60*60*24*7) / (60*60*24) )) # 1 day +
$ hours=$(( $diff % (60*60*24) / (60*60) )) # 5 hours +
$ minutes=$(( $diff % (60*60) / (60) )) # 37 minutes +
$ seconds=$(( $diff % (60) )) # 40 seconds
In our case, that means 106660 seconds are equal to 0 weeks, 1 day, 5 hours, 37 minutes, and 40 seconds.
Armed with this knowledge, we can use the Bash comparison operators to know whether a given number of time units has passed. For example, we can compare session times, timeouts, and other similar values.
5. Date Conversion
As mentioned, we can leverage the simplicity of UNIX timestamps via date conversion when working with more complex formats, even when they include timezones:
$ datetime='2023-05-05T00:05:10+04:00'
$ timestamp=$(date +%s --date="$datetime")
$ TZ=Europe/Sofia date --iso-8601=seconds --date=@$(($timestamp - 2*60*60 + 5*60))
2023-05-04T21:10:10+03:00
Here, we removed an hour and 55 minutes (subtracted 2 hours and added 5 minutes) from our original datetime and even changed the timezone of the output.
Using this method, we can also subtract and compare dates in arbitrary formats as long as the date command or another tool of choice recognizes them.
6. Summary
In this article, we talked about the UNIX epoch timestamp, how to do arithmetic with the format, and how to use it to our advantage.
In conclusion, although it may seem cryptic at first, the UNIX time format can be a very convenient way to handle timestamps without having to do complex parsing.