28

I would like to write a script or function to tell me how many days from now until a given date in the future. What I'm struggling to work out is how to process the given date and compare it with the current date... I'm imagining something like

read -p "enter the date in the format YYYY-MM-DD "

And then I'm assuming I have a string that's meaningless to the shell and I have to do some evaluations like...?? (This is just an example; I guess bc would be needed)

i=$(($(date +%Y)-${REPLY%%-*}))
j=$(($(date +%m)-${REPLY:5:2}))
k=$(($(date +%d)-${REPLY##*-}))

And then I don't know what to do with those numbers...??

if $i > 1 then assign l=$((i*365)) and else what?? # what about leap years?
Using $j somehow assign m   # confused before I've started
Using $k somehow assign n   # just as bad
echo $((l+m+n))   

I'm surely making it too hard for myself; probably there's a text processing tool that understands dates and can compare them.

How can I do this?

  • Probably no python? Anyway, convert time to epoch time (can be in any format), then it is easy :) – Jacob Vlijm Dec 7 '16 at 20:46
  • @JacobVlijm python solution totally welcome - that will help me when I finally get around to learning python XD and I just want it to work too :) – Zanna Dec 7 '16 at 20:52
  • AHA, a moment... – Jacob Vlijm Dec 7 '16 at 21:04
29

Epoch time

In general, calculations on time are easiest if we first convert time into (Unix) epoch time (seconds from 1-1-1970). In python, we have tools to convert time into epoch time, and back into any date format we prefer.

We can simply set a format, like:

pattern = "%Y-%m-%d"

...and define today:

today = "2016-12-07"

and subsequently write a function to do the job:

def convert_toepoch(pattern, stamp):
    return int(time.mktime(time.strptime(stamp, pattern)))

Then the output of:

nowepoch = convert_toepoch(pattern, today)
print(nowepoch)

> 1481065200

...which is, as mentioned, the number of seconds since 1-1-1970

Calculating the days between two dates

If we do this on both today and our future date, subsequently calculate the difference:

#!/usr/bin/env python3
import time

# set our date pattern
pattern = "%Y-%m-%d" 

def convert_toepoch(pattern, stamp):
    return int(time.mktime(time.strptime(stamp, pattern)))

# automatically get today's date 
today = time.strftime(pattern); future = "2016-12-28"

nowepoch = convert_toepoch(pattern, today)
future_epoch = convert_toepoch(pattern, future)

print(int((future_epoch - nowepoch)/86400))

The output will be calculated by date, since we use the format %Y-%m-%d. Rounding on seconds would possibly give an incorrect date difference, if we are near 24 hrs for example.

Terminal version

#!/usr/bin/env python3
import time

# set our date pattern
pattern = "%Y-%m-%d" 

def convert_toepoch(pattern, stamp):
    return int(time.mktime(time.strptime(stamp, pattern)))

# automatically get today's date 
today = time.strftime(pattern)
# set future date
future = input("Please enter the future date (yyyy-mm-dd): ")
nowepoch = convert_toepoch(pattern, today)
future_epoch = convert_toepoch(pattern, future)
print(int((future_epoch - nowepoch)/86400))

enter image description here

...And the Zenity option

#!/usr/bin/env python3
import time
import subprocess

# set our date pattern
pattern = "%Y-%m-%d" 

def convert_toepoch(pattern, stamp):
    return int(time.mktime(time.strptime(stamp, pattern)))

# automatically get today's date 
today = time.strftime(pattern)
# set future date
try:
    future = subprocess.check_output(
        ["zenity", "--entry", "--text=Enter a date (yyyy-mm-dd)"]
        ).decode("utf-8").strip()
except subprocess.CalledProcessError:
    pass
else:     
    nowepoch = convert_toepoch(pattern, today)
    future_epoch = convert_toepoch(pattern, future)
    subprocess.call(
        ["zenity", "--info",
         "--text="+str(int((future_epoch - nowepoch)/86400))
         ])

enter image description here

enter image description here

And just for fun...

A tiny application. Add it to a shortcut if you use it often.

enter image description here

The script:

#!/usr/bin/env python3
import time
import subprocess
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Pango, Gdk

class OrangDays(Gtk.Window):

    def __init__(self):

        self.pattern = "%Y-%m-%d" 
        self.currdate = time.strftime(self.pattern)
        big_font = "Ubuntu bold 45"
        self.firstchar = True

        Gtk.Window.__init__(self, title="OrangeDays")
        maingrid = Gtk.Grid()
        maingrid.set_border_width(10)
        self.add(maingrid)

        datelabel = Gtk.Label("Enter date")
        maingrid.attach(datelabel, 0, 0, 1, 1)

        self.datentry = Gtk.Entry()
        self.datentry.set_max_width_chars(12)
        self.datentry.set_width_chars(12)
        self.datentry.set_placeholder_text("yyyy-mm-dd")
        maingrid.attach(self.datentry, 2, 0, 1, 1)

        sep1 = Gtk.Grid()
        sep1.set_border_width(10)
        maingrid.attach(sep1, 0, 1, 3, 1)

        buttongrid = Gtk.Grid()
        buttongrid.set_column_homogeneous(True)
        maingrid.attach(buttongrid, 0, 2, 3, 1)

        fakebutton = Gtk.Grid()
        buttongrid.attach(fakebutton, 0, 0, 1, 1)

        calcbutton = Gtk.Button("Calculate")
        calcbutton.connect("clicked", self.showtime)
        calcbutton.set_size_request(80,10)
        buttongrid.attach(calcbutton, 1, 0, 1, 1)

        fakebutton2 = Gtk.Grid()
        buttongrid.attach(fakebutton2, 2, 0, 1, 1)

        sep2 = Gtk.Grid()
        sep2.set_border_width(5)
        buttongrid.attach(sep2, 0, 1, 1, 1)

        self.span = Gtk.Label("0")
        self.span.modify_font(Pango.FontDescription(big_font))
        self.span.set_alignment(xalign=0.5, yalign=0.5)
        self.span.modify_fg(Gtk.StateFlags.NORMAL, Gdk.color_parse("#FF7F2A"))
        maingrid.attach(self.span, 0, 4, 100, 1)

        sep3 = Gtk.Grid()
        sep3.set_border_width(5)
        maingrid.attach(sep3, 0, 5, 1, 1)

        buttonbox = Gtk.Box()
        maingrid.attach(buttonbox, 0, 6, 3, 1)
        quitbutton = Gtk.Button("Quit")
        quitbutton.connect("clicked", Gtk.main_quit)
        quitbutton.set_size_request(80,10)
        buttonbox.pack_end(quitbutton, False, False, 0)

    def convert_toepoch(self, pattern, stamp):
        return int(time.mktime(time.strptime(stamp, self.pattern)))

    def showtime(self, button):
        otherday = self.datentry.get_text()
        try:
            nextepoch = self.convert_toepoch(self.pattern, otherday)
        except ValueError:
            self.span.set_text("?")
        else:
            todayepoch = self.convert_toepoch(self.pattern, self.currdate)
            days = str(int(round((nextepoch-todayepoch)/86400)))
            self.span.set_text(days)


def run_gui():
    window = OrangDays()
    window.connect("delete-event", Gtk.main_quit)
    window.set_resizable(True)
    window.show_all()
    Gtk.main()

run_gui()
  • Copy it into an empty file, save it as orangedays.py
  • Run it:

    python3 /path/to/orangedays.py
    

To wrap it up

Use for the tiny application script above the following .desktop file:

[Desktop Entry]
Exec=/path/to/orangedays.py
Type=Application
Name=Orange Days
Icon=org.gnome.Calendar

enter image description here

  • Copy the code into an empty file, save it as orangedays.desktop in ~/.local/share/applications
  • In the line

    Exec=/path/to/orangedays.py
    

    set the actual path to the script...

23

The GNU date utility is quite good at this sort of thing. It is able to parse a good variety of date formats and then output in another format. Here we use %s to output the number of seconds since the epoch. It is then a simple matter of arithmetic to subtract $now from the $future and divide by 86400 seconds/day:

#!/bin/bash

read -p "enter the date in the format YYYY-MM-DD "

future=$(date -d "$REPLY" "+%s")
now=$(date "+%s")
echo "$(( ( $future / 86400 ) - ( $now / 86400 ) )) days"
  • apart from incorrect rounding (it seems), this works well! I feel silly for doubting the powers of GNU date :) Thanks :) – Zanna Dec 8 '16 at 7:12
  • 1
    @Zanna - I think the solution to the rounding problem is simply to integer-divide both timestamps by 86400, before taking the difference. But there might be some detail I'm missing here. Also do you want the entered date to be local time or UTC? If its UTC, then add the -u parameter to date. – Digital Trauma Dec 8 '16 at 17:57
  • Days which switch between normal time and daylight saving times, might differ for +/- 1 hour and seldomly there are correction seconds placed in certain days. But in practice, this might be unimportant in most of the cases. – user unknown May 14 at 21:58
10

You could try doing something in awk, using the mktime function

awk '{print (mktime($0) - systime())/86400}'

The awk expects to read date from standard input in the format "YYYY MM DD HH MM SS" and then prints the difference between the time specified and the current time in days.

mktime simply converts a time (in the specified format) to the number of seconds from a reference time (1970-01-01 00:00:00 UTC); systime simple specifies the current time in the same format. Subtract one from the other and you get how far apart they are in seconds. Divide by 86400 (24 * 60 * 60) to convert to days.

  • 1
    Nice, There is however one issue: I guess you don't want the number of days as a float, then simply dividing by 86400 will not work, possible rounding as a solution gives an incorrect output if you are near 24 hrs – Jacob Vlijm Dec 7 '16 at 21:33
  • note Awk time functions are not POSIX – Steven Penny Dec 8 '16 at 12:49
10

Here is a Ruby version

require 'date'

puts "Enter a future date in format YYYY-MM-DD"
answer = gets.chomp

difference = (Date.parse(answer) - Date.today).numerator

puts difference > 1 ? "That day will come after #{difference} days" :
  (difference < 0) ? "That day passed #{difference.abs} days ago" :
 "Hey! That is today!"

Example Run:

Example run of the script ruby ./day-difference.rb is given below (assuming you have saved it as day-difference.rb)

With a future date

$ ruby day-difference.rb
Enter a future date in format YYYY-MM-DD
2021-12-30
That day will come after 1848 days

With a passed date

$ ruby day-difference.rb
Enter a future date in format YYYY-MM-DD
2007-11-12
That day passed 3314 days ago

When passed today's date

$ ruby day-difference.rb
Enter a future date in format YYYY-MM-DD
2016-12-8
Hey! That is today!

Here is an nice website to check the differences of date http://www.timeanddate.com/date/duration.html

  • Awesome! So simple and clear. Ruby seems like a great language :) – Zanna Dec 8 '16 at 15:33
  • Greatgreat! Welcome to Ruby :) – Jacob Vlijm Dec 8 '16 at 15:46
  • 1
    @Zanna thanks. It really is. tryruby here if you got 15 mintues. :) – Anwar Dec 8 '16 at 16:36
  • @JacobVlijm Thank your for the encouragement. Though I'm still a student :) – Anwar Dec 8 '16 at 16:36
6

There is a dateutils package which is very convenient for dealing with dates. Read more about it here github:dateutils

Install it by

sudo apt install dateutils

For your problem, simply,

dateutils.ddiff <start date> <end date> -f "%d days"

where the output can be chosen as seconds, minues, hours, days, weeks, months or years. It can be conveniently used in scripts where the output can be used for other tasks.


For example,

dateutils.ddiff 2016-12-26  2017-05-12 -f "%m month and %d days"
4 month and 16 days

dateutils.ddiff 2016-12-26  2017-05-12 -f "%d days"
137 days
  • Excellent :) Good to know about this package. – Zanna Dec 26 '16 at 9:33
2

You can use the awk Velour library:

$ velour -n 'print t_secday(t_utc(2018, 7, 1) - t_now())'
7.16478

Or:

$ velour -n 'print t_secday(t_utc(ARGV[1], ARGV[2], ARGV[3]) - t_now())' 2018 7 1
7.16477
0

A short solution, if both dates belong to the same year, is:

echo $((1$(date -d 2019-04-14 +%j) - 1$(date +%j)))

using the "%j"-format, which returns position of date in days in year, i.e. 135 for the current date. It avoids rounding problems and handles dates in the past, giving negative results.

However, crossing year borders, this will fail. You could add (or subtract) 365 manually for each year or 366 for each leap year, if the last of February is crossed, but that will be nearly as verbose as other solutions.

Here the pure bash solution:

#!/bin/bash
#
# Input sanitizing and asking for user input, if no date was given, is left as an exercise
# Suitable only for dates from 1.1.1970 to 31.12.9999
#
# Get date as parameter (in format yyyy-MM-dd
#
date2=$1
# for testing, more convenient:
# date2=2019-04-14
#
year2=${date2:0:4}
year1=$(date +%Y)
#
# difference in days, ignoring years:
# since %j may lead to values like 080..099, 
# which get interpreted as invalid octal numbers, 
# I prefix them with "1" each (leads to 1080..1099) 
daydiff=$((1$(date -d 1$date2 +%j)- $(date +%j)))
#
yeardiff=$((year2-year1))
# echo yeardiff $yeardiff
#
#
# summarize days per year, except for the last year:
#
daysPerYearFromTo () {
    year1=$1
    year2=$2
    days=0
    for y in $(seq $year1 $((year2-1)))
    do
        ((days+=$(date -d $y-12-31 +"%j")))
    done
    echo $days
}
# summarize days per year in the past, except for the last year:
#
daysPerYearReverse () {
    year1=$1
    year2=$2
    days=0
    for y in $(seq $((year1-1)) -1 $year2)
    do
        ((days+=$(date -d $y-12-31 +"%j")))
    done
    echo $days
}

case $yeardiff in
    0) echo $daydiff
        ;;
    # date in one of previous years:
    -[0-9]*) echo $((daydiff-$(daysPerYearReverse $year1 $year2)))
        ;;
    # date in one of future years:
    [0-9]*) echo $((daydiff+$(daysPerYearFromTo $year1 $year2)))
        ;;
esac

Shellcheck suggests a lot of double quoting, but for days exceeding the year 9999 you should consider a different approach. For the past, it will fail silently for dates before 1970.01.01. Sanitizing user input is left as an exercise to the user.

The two functions can be refactored into one, but that might make it harder to understand.

Please note, that the script needs exhaustive testing for handling leap years in the past correctly. I wouldn't bet it is right.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.