FromNow — Time difference from now in different formats (Bash package)
Below you find the latest version of (1) the package's README and (2) its main source file.
You may also want to read:
For the git repository and issue tracker, see the project's page on sr.ht.
For more packages, see Software.
Overview
FromNow calculates the difference between two moments in time and then displays it in one of a few available formats.
When a second moment is not given, it's assumed to be "now".
You can customize how you want to display a time difference: long or short, exact or fuzzy, how many and which units to use, etc.
Usage
The docstring of main() will tell you all about it.
You can see it:
- Online, at the package's page.
- Or by running
fromnow -h.
Examples
The output will depend on the current date and time, unless you:
- pass
-nto use something other thannow, or - pass as input a relative date in a format understood by
coreutils'date(e.g.'42 days ago').
Regular
Examples of output for (relative) input date '31708842 seconds ago':
| Arguments | Description | Output |
|---|---|---|
| -S0 | no sign, long | 1 year, 1 day, 18 hours, 42 seconds |
| -S0 -b | no sign, brief | 1Y1D18h42s |
| -S0 -@ | no sign, values | 1 0 0 1 18 0 42 |
| long (default) | 1 year, 1 day, 18 hours, 42 seconds ago | |
| -b | brief | -1Y1D18h42s |
| -@ | values | - 1 0 0 1 18 0 42 |
| -c2 | l, ≤ 2 units | 1 year, 2 days ago |
| -bc2 | b, ≤ 2 units | -1Y2D |
| -c3 -uYhs | l, ≤ 3 units, Yhs | 1 year, 42 hours, 42 seconds ago |
| -bc3 -uYhs | b, ≤ 3 units, Yhs | -1Y42h42s |
| -c2 -uYhs | l, ≤ 2 units, Yhs | 1 year, 42 hours ago |
| -bc2 -uYhs | b, ≤ 2 units, Yhs | -1Y42h |
| -c2 -uDm | l, ≤ 2 units, Dm | 367 days, 1 minute ago |
| -bc2 -uDm | b, ≤ 2 units, Yhs | -367D1m |
| -us | long, only secs | 31708842 seconds ago |
| -bus | brief, only secs | -31708842s |
So note that:
- Since this input date is relative, the difference between
itandnowis constant, and the results will always be the same. - But if instead you pass an absolute date such as
2042-12-12, the difference betweenitandnowwill keep changing. - And if you pass
2042-12-12with-n 2041-11-11, then "the now will have become fixed", so the difference will again be constant, producing the same output every time you run it.
You can see more examples by running:
chmod +x dev/examples.sh dev/examples.sh
There's no option to output just the number of seconds — but if you need that, you can trivially get it with tr -d s.
For example:
# Brief output, using only seconds as units fromnow '42 min' -bus #⇒ +2520s fromnow '42 min' -bus | tr -d s #⇒ +2520 fromnow '42 min' -bus | echo "$(($(tr -d s) - 2400))" #⇒ 120 (2 minutes) fromnow '42 min' -bus | tr -d s | { read s; echo "$((s-2400))" ;} #⇒ 120 (2 minutes) read s < <(fromnow '42 min' -bus | tr -d s) echo "$((s-2400))" #⇒ 120 (2 minutes) read s < <(fromnow '42 min' -bus | tr -d s) ((s-=2400)); echo "$s" #⇒ 120 (2 minutes)
Installation
See my page Software for the most up-to-date instructions on how to download and install my packages.
Contributing
See my page Software for information about how to contribute to my packages.
See also
Other packages
Awhile is my Emacs Lisp implementation of the same idea — a close enough translation that I wrote soon after I finished this one. It has some additional features.
License
This project follows the REUSE Specification (FAQ), which in turn is built upon SPDX.
Therefore, license and copyright information can be found in:
- each file's comment header, or
- an adjacent file of the same name with the additional extension
.license, or - the
.reuse/dep5file
The full text of the licenses can be found in the LICENSES subdirectory.
fromnow
Structure
## FromNow --- Time difference from now in different formats ## Commentary ### See the README for more information ## Code ### Settings ### Global constants ### Global variables ### Functions #### Main #### Display #### Convert #### Collect #### Utilities #### Get help ### Run it and exit ## FromNow ends here
Contents
#!/usr/bin/env bash ## FromNow --- Time difference from now in different formats # SPDX-FileCopyrightText: © flandrew <https://flandrew.srht.site/listful> # SPDX-License-Identifier: GPL-3.0-or-later #---------------------# # Author: flandrew # # Created: 2025-11-29 # # Updated: 2025-12-17 # #---------------------# # Version: 0.2.0 # #---------------------# ## Commentary # # FromNow calculates the difference between two moments in time and then # displays it in one of a few available formats. When a second moment is not # given, it's assumed to be "now". # # You can customize how you want to display a time difference: long or short, # exact or fuzzy, which units to use, etc. # # For help, run: fromnow -h # ### See the README for more information # # A local README.org should be available in the same directory as this file. # # You can also read it online: # https://flandrew.srht.site/listful/sw-bash-fromnow.html # ############################################################################# #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ## Code ### Settings LC_ALL=C set -eo pipefail require() { hash "$@" || exit 127 ;} require getopt sed #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ### Global constants # Number of seconds in each unit. ((_s=1, _m=60, _h=60*_m, _D=24*_h, _W=7*_D, _M=(365*4+1)*_D/48, _Y=_M*12)) # Note that the sizes of "year" and "month" are averages over a 4-year period. #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ### Global variables AUS=YMWDhms # All time units available #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ### Functions #### Main main() { : _options _dorf1 _dorf2 _dorf3 … <<_ FromNow --- Time difference from now in different formats Usage: fromnow [-h] fromnow [-l|-b|-@[AKkQ]] [-S[1|0]] [-uUS] [-cMAX] [-nDATE] [-(r|R)RATE] DORF1 [DORF2 …] Options: | Flag | Type | Description | Default | |--------+---------+------------------------------------------+---------| | -l | display | long and descriptive (can be omitted) | -l | | -b | display | brief, uses letters | | | -@X | display | array, where optional X is one of [AKkQ] | | |--------+---------+------------------------------------------+---------| | -S | sign | whether to include reference to the sign | 1 (yes) | |--------+---------+------------------------------------------+---------| | -uUS | values | a string of valid units to use | YMWDhms | |--------+---------+------------------------------------------+---------| | -cMAX | values | maximum number of units to use | 7 (all) | |--------+---------+------------------------------------------+---------| | -nDATE | now | result will be relative to DATE | now | |--------+---------+------------------------------------------+---------| | -rRATE | display | repeatedly calculate difference: replace | | | -RRATE | display | repeatedly calculate difference: regular | | |--------+---------+------------------------------------------+---------| | DORFn | dates | date or file-with-one-date-per-line | | |--------+---------+------------------------------------------+---------| | | help | help (just usage) | | | -h | help | help (all this) | | _____________________________________________________________________________ The defaults flags are: -l -S1 -uYMWDhms -c7 -nnow Flags are processed in order. If they conflict, the one given last wins. In repeated display ("countdown mode"), the rate is given in seconds. _____________________________________________________________________________ If you want to process the output, you may like the array options. Here's what you can do: -@A: array declaration that you can reuse by passing it to eval. -@K: array keys and values, quoted -@k: array keys and values, unquoted -@Q: array values, quoted (keys: Y W S M D s m h: reverse alpha) -@: array values, unquoted (keys: S Y M W D h m s: regular order) Long or brief makes no difference for these. _____________________________________________________________________________ Examples of output for (relative) input date '31708842 seconds ago': | Arguments | Description | Output | |------------+-------------------+-----------------------------------------| | -S0 | no sign, long | 1 year, 1 day, 18 hours, 42 seconds | | -S0 -b | no sign, brief | 1Y1D18h42s | | -S0 -@ | no sign, values | 1 0 0 1 18 0 42 | |------------+-------------------+-----------------------------------------| | | long (default) | 1 year, 1 day, 18 hours, 42 seconds ago | | -b | brief | -1Y1D18h42s | | -@ | values | - 1 0 0 1 18 0 42 | |------------+-------------------+-----------------------------------------| | -c2 | l, ≤ 2 units | 1 year, 2 days ago | | -bc2 | b, ≤ 2 units | -1Y2D | |------------+-------------------+-----------------------------------------| | -c3 -uYhs | l, ≤ 3 units, Yhs | 1 year, 42 hours, 42 seconds ago | | -bc3 -uYhs | b, ≤ 3 units, Yhs | -1Y42h42s | |------------+-------------------+-----------------------------------------| | -c2 -uYhs | l, ≤ 2 units, Yhs | 1 year, 42 hours ago | | -bc2 -uYhs | b, ≤ 2 units, Yhs | -1Y42h | |------------+-------------------+-----------------------------------------| | -c2 -uDm | l, ≤ 2 units, Dm | 367 days, 1 minute ago | | -bc2 -uDm | b, ≤ 2 units, Yhs | -367D1m | |------------+-------------------+-----------------------------------------| | -us | long, only secs | 31708842 seconds ago | | -bus | brief, only secs | -31708842s | Note that: - Input is one or more dates and/or files. - If none is directly given, we read from stdin. - Dates may be in any format accepted by coreutil's date's -d flag. - Files are assumed to contain no more than one date per line. - You may mix dates and files-containing-dates in your input. - Input may contain comments and empty lines. - Output order will match the input order. _____________________________________________________________________________ About units. Minutes, hours, days, and weeks are precise units. We can agree that: - 1 minute = 60 seconds - 1 hour = 60 minutes - 1 day = 24 hours - 1 week = 7 days Months and years are trickier. - Months vary from 28 to 31 days. - Years usually have 365 days. Except when they have 366. How to deal with these? One way is to follow the date. From 2042-05-07 to 2042-06-07 is one month, and from 2042-06-07 to 2042-07-07 is one month again, and to 2043-07-07 is one year. When you run: $ date -d '1 month ago' you see that /usr/bin/date decreases the month and keeps the day and year. The advantage of this approach is that it matches our intuition about dates. The disadvantage is that we end up with time units that are not constant, as they depend on context: "2 months ago" when in March of a non-leap year would mean 59 days ago, whereas "2 months ago" when in September would mean 62 days. This is unsatisfactory. FromNow averages the units over a four-year period: - 1 year = (365×4 + 1) ÷ 4 = 365.25 days = 365 days, 6 hours - 1 month = (365×4 + 1) ÷ 48 = 30.4375 days = 30 days, 10 hours, 30 minutes Twelve months exactly fit into one year. With this, fuzzy relative times will be more precise over longer periods. And whenever you see "year" or "month", the elapsed time that they represent will be predictably the same as above. Think "average year" and "average month". If you dislike this, avoid these two units. Use -uWDhms or -uDhms instead. _____________________________________________________________________________ Development options: fromnow -hFUN # Documentation of internal function FUN fromnow -=FUN [PARAMS] # Execute function FUN on its (optional) PARAMS Examples of these: fromnow -=collect 2042-12-12 now fromnow -=normalize 2042-12-12 now echo 2042-12-12 | fromnow -=normalize fromnow -hcollect fromnow -hnormalize _____________________________________________________________________________ _ local dp cnt sign now rr \ values opt rt \ S Y M W D h m s us \ defaults=(-l -S1 -u"$AUS" -c"${#AUS}" -nnow) opt=$(getopt -n fromnow -o 'lbS:u:c:n:r:R:@::h::=:' -- "${defaults[@]}" "$@") eval set -- "$opt" while :; do case "$1" in -[lb]) dp="${1:1}" ;; -@) dp="@$2" ; shift ;; -u) us="$2" ; shift ;; -c) cnt="$2" ; shift ;; -n) now="$2" ; shift ;; -S) sign="$2" ; shift ;; -R) rr="$2"; rt='\n' ; shift ;; -r) rr="$2"; rt='\r' ; shift ;; -h) doc "${2:-main}" ; exit 0 ;; -=) "$2" "${@:4}" ; exit "$?" ;; --) shift ; break ;; * ) echo >&2 "Error" ; exit 1 ;; esac; shift ; continue done # If no input at all, show brief help and quit if (("$#"==0)) && ! piped; then usage; exit 0; fi # Collect input dates; process them; display them if [[ "$rr" ]] then display-one-repeat "$dp" "$sign" "$us" "$cnt" "$now" "$rr" "$rt" "$1" else display-many-once "$dp" "$sign" "$us" "$cnt" "$now" "$@" fi ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### Display display-many-once() { : dp sign us cnt now _dof1 _dof2 _dof3 … <<_ Display the difference between all input dates and now. For the meaning of DP, SIGN, US, CNT, see display(). NOW is the reference date, which defaults to "now". Each DOFn is either a date or a file containing one date per line. If none is given, read from stdin. _ local n dp sign us cnt now read -r dp sign us cnt now _ <<<"$@" now=$(date +%s -d "$now") while read -r n do display "$((n-now))" "$dp" "$sign" "$us" "$cnt" done < <(normalize "${@:6}") ;} display-one-repeat() { : dp sign us cnt now rr rt _dof <<_ Repeatedly display the difference between input date and now. For the meaning of DP, SIGN, US, CNT, see display(). NOW is the reference date, which defaults to "now". RR is the repeat rate in seconds. RT is the refresh type: '\r' or '\n'. DOF is either a date or a file containing a date. If not given, read from stdin. _ trap 'echo; exit 0' 2 local n dp sign us cnt now rr rt dof str read -r dp sign us cnt now rr rt dof <<<"$@" read -r n < <(normalize "$dof") case "$rt" in '\n') : "" ;; '\r') : "-${COLUMNS:=$(tput cols)}" ;; esac; str="%${_}s$rt" while : do : "$(date +%s -d "$now")" : "$((n-_))" printf "$str" "$(display "$_" "$dp" "$sign" "$us" "$cnt")" sleep "$rr" done ;} display() { : n dp sign us cnt <<_ Display using common units a date difference of N seconds. SIGN is whether to display sign information: 1 is yes, 0 is no. DP is the display option: l, b, or @{,A,K,K,Q}. US is a string of possible units to use. CNT is a maximum number of units to use. _ local n="$1" dp="$2" sign="$3" us="$4" cnt="$5" values values=$(distribute "$n" "$cnt" "$us") local S Y M W D h m s read -r S Y M W D h m s <<< "$values" # Our display options are: case "$dp" in # Long l ) if ((n)) then declare -A long=([Y]=year [M]=month [W]=week [D]=day [h]=hour [m]=minute [s]=second) ((n<0)) && : " ago" || : " from now" ((sign)) && S="$_" || S="" local U out="" for u in Y M W D h m s do U="${!u}" ((U>1)) && : "s" || : "" ((U<1)) || out+="$U ${long[$u]}$_, " done; echo "${out/%, /$S}" else ((sign)) && echo "now" || echo "0 seconds" fi ;; # Brief b ) ((sign)) || S="" for u in Y M W D h m s do : "${!u}"; ((_)) && S+="$_$u" done; ((n)) || S+="0s" echo "$S" ;; # Array @[AKkQ]) declare -A fromnow=([Y]="$Y" [M]="$M" [W]="$W" [D]="$D" [h]="$h" [m]="$m" [s]="$s") ((sign)) && fromnow+=([S]="$S") || true eval echo '${fromnow[@]'"$dp"'}' ;; @ ) ((sign)) && : 0 || : 2; echo "${values:$_}" ;; # No display: some error @*) echo "Try one of:" @{,A,K,k,Q} >&2 ; exit 1 ;; * ) echo "Internal error: v=$dp?" >&2 ; exit 1 ;; esac ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### Convert normalize() { : _dof1 _dof2 _dof3 … <<_ Convert all input dates to seconds since the Epoch. Each DOFn is either a date or a file containing one date per line. If no argument is given, read from stdin. _ collect "$@" | date +%s -f - ;} distribute() { : n _cnt _us <<_ Distribute N seconds into common units. We start with the highest unit, and fill up to a maximum of CNT units. Optional argument US is a string with units to try. If not given, default to the value of global variable AUS. If this is somehow unavailable, use its expected value: "YMWDhms". The output order is a string with the amounts in these variables: S Y M W D h m s where S is the sign. _ local n="$1" cnt="${2:-7}" us="${3:-${AUS:-YMWDhms}}" local Y=0 M=0 W=0 D=0 h=0 m=0 s=0 i=0 _u last ((n<0 ? (S=0, n=-n) : (S=1))) us=$(parse-us "$us") last=$(tail -n1 <<< "$us") while read -r _u do declare -n U="${_u:1}" ((U=n/_u, n%=_u, U==0 || ++i)) if ((i==cnt)) || [[ "$_u" == "$last" ]] then ((2*n < _u || ++U)) # round it break fi done <<<"$us" ((S)) && : + || : - echo "$_ $Y $M $W $D $h $m $s" ;} parse-us() { : us <<_ Parse units string US. Each letter gets its own line and is prefixed with _. The list is uniq-ified, and then sorted from highest to lowest unit. _ for u in Y M W D h m s do [[ "$1" =~ $u ]] && printf "_%s\n" "${BASH_REMATCH[0]}" done ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### Collect collect() { : _dof1 _dof2 _dof3 … <<_ Collect all input dates. Each DOFn is either a date or a file containing one date per line. If no argument is given, read from stdin. _ if piped then cat - else for x # regular, readable file? do if [[ -f "$x" && -r "$x" ]] then cat "$x" else printf "%s\n" "$x" fi done fi | no-comment ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### Utilities no-comment() { : <<_ Remove comments and empty lines from piped input. _ sed -E 's/[[:blank:]]*#.*// ; /^[[:blank:]]*$/d' ;} piped() { : <<_ Is the input coming from stdin? Am I being piped to? _ [[ ! -t 0 ]] ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### Get help doc() { : fun <<_ Output the docstring of FUN. _ : "${1:-doc}"; type "$_" | sed -n "/<<_$/,/^_$/p" | sed "1 {s/ <<_.*/)/ ; s/[^:]*: */$_ (/}; $ d; 1a\ " | less -F ;} usage() { : <<_ Show just usage. _ doc main | sed -n '/^Usage:/,/^$/p' | sed '$ d' ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ### Run it and exit main "$@" exit 0 # Local Variables: # coding: utf-8 # indent-tabs-mode: nil # sentence-end-double-space: nil # outline-regexp: "###* " # End: ## FromNow ends here
📆 2025-W51-4📆 2025-12-18