1

I'm trying to write a small function that a continuously-looping script can call to see if the current time is in a certain daily time range. For example, the script might be set to run from 09:00 in the morning to 17:00 in the evening each day, or might be set to run from 19:00 in the evening to 07:00 in the morning.

I've got the beginnings of such a function written, but the problem is that things go crazy when passing into a new day. So, if the script has been told to run from 19:00 to 07:00, at 00:00 it gets confused and stops to wait for 19:00 in that new day to start up again.

What might be a good way to approach this problem? I'm happy for the script to assume that the start time is some time in the future after it has been set running.

I feel as though the function needs to keep track of whether the edges of the time window are start edges or stop edges and I'm not sure how to do this.

#!/usr/bin/env python

"""
################################################################################
#                                                                              #
# cool timin' yea                                                              #
#                                                                              #
################################################################################

usage:
    program [options]

options:
    -h, --help         display help message
    --version          display version and exit
    --dayruntime=TEXT  HHMM--HHMM [default: 1900--0700]
"""

import datetime
import docopt
import time

def main(options):

    day_run_time = options["--dayruntime"]

    while True:

        if in_daily_time_range(time_range = day_run_time):

            print("ok let's do this thing")

            # complex deep learning shit

        else:

            print("i'll wait till it's ma time fam")

            # doin' some other things while chillin'

        time.sleep(5)

def in_daily_time_range(
    time_range = None, # string "HHMM--HHMM" e.g. "1700--1000"
    time_start = None, # string "HHMM"       e.g. "1700"
    time_stop  = None  # string "HHMM"       e.g. "1000"
    ):

    if time_range is not None:
        time_start = time_range.split("--")[0]
        time_stop  = time_range.split("--")[1]

    time_start_datetime = datetime.datetime.combine(
        datetime.datetime.now().date(),
        datetime.datetime.strptime(
            time_start,
            "%H%M"
        ).time()
    )
    time_stop_datetime = datetime.datetime.combine(
        datetime.datetime.now().date(),
        datetime.datetime.strptime(
            time_stop,
            "%H%M"
        ).time()
    )
    if time_stop_datetime < time_start_datetime:
        time_stop_datetime =\
            time_stop_datetime + datetime.timedelta(hours = 24)

    if time_start_datetime      <=\
        datetime.datetime.now() <=\
        time_stop_datetime:
        return True
    else:
        return False

if __name__ == "__main__":
    options = docopt.docopt(__doc__)
    if options["--version"]:
        print(version)
        exit()
    main(options)
BlandCorporation
  • 1,324
  • 1
  • 15
  • 33
  • 5
    you should be using `cron` to do this... – juanpa.arrivillaga Mar 03 '17 at 02:45
  • This is similar to determining if an angle is in a given range http://stackoverflow.com/questions/42133572/create-a-circular-list-by-using-a-range-of-angles-python – PM 2Ring Mar 03 '17 at 02:49
  • 2
    It is really bad for your machine and power consumption to run this nonstop, 24h, with a sleep interval of 5 seconds. Unix `cron` already handles all the corner-cases (midnight, date-changes etc); [`anacron` even handles missing the scheduled event where machine was powered down](http://serverfault.com/questions/52335/job-scheduling-using-crontab-what-will-happen-when-computer-is-shutdown-during). What is the actual sleep interval you need, and do you *really* need to poll it every 5s and perform action every 5s? 5min? once an hour? once a day? Or once in that interval? – smci Mar 03 '17 at 11:05
  • @PM2Ring Yeah, I can see the similarity. Do you suppose that I might just have to bite the bullet and create a global object keeping track of the number of days since the script was set running (or, like, a bool to that effect)? – BlandCorporation Mar 03 '17 at 12:43
  • 2
    @smci Yes, the way this minimal example has been described is pretty inefficient. The reality is that the actual script is far more complex and is dealing with multiple time windows and is doing some pretty intense visual work. The time calculations for this are entirely negligible. Using `cron` would be overly complex for my purposes and I'd rather it all in Python anyway. – BlandCorporation Mar 03 '17 at 12:44
  • I suggest converting the start, stop, and current times to seconds and using the modulus technique shown in my answer that I linked earlier. – PM 2Ring Mar 03 '17 at 13:00
  • @PM2Ring I'm probably just tired, but what do you mean? – BlandCorporation Mar 06 '17 at 23:38

1 Answers1

1

Here is the in_angle_interval function from my linked answer in the comments, modified to work with minutes, since your code uses that resolution. The in_time_interval function itself is quite simple, but I've written a few other functions to make it more convenient to use and to test. The code below also has a simple now_in_minutes function so you can easily test if the current time falls within a specified interval; that function isn't used in my test code.

from datetime import datetime

def hhmm_to_minutes(s):
    h, m = s[:2], s[2:]
    return 60 * int(h) + int(m)

def minutes_to_hhmm(minutes):
    return hour_min_to_hhmm(*divmod(minutes, 60))

def hour_min_to_hhmm(h, m):
    return '{:02d}{:02d}'.format(h, m)

def now_in_minutes():
    now = datetime.now()
    return 60 * now.hour + now.minute

# Test if x falls within the the time interval from a to b, inclusive
# All times are given in minutes since midnight.
minutes_per_day = 1440
def in_time_interval(x, a, b):
    return (x - a) % minutes_per_day <= (b - a) % minutes_per_day

# Find which times fall in the interval t0 - t1, inclusive.
# Test times start at 0000 and increment by delta minutes
def in_test(t0, t1, delta=10):
    print('{} - {}: '.format(t0, t1), end='')
    t0 = hhmm_to_minutes(t0)
    t1 = hhmm_to_minutes(t1)

    passed = [minutes_to_hhmm(nowmin)
        for nowmin in range(0, minutes_per_day, delta)
            if in_time_interval(nowmin, t0, t1)
    ]
    print(', '.join(passed))

# Find which times DON'T fall in the interval t0 - t1, inclusive.
# Test times start at 0000 and increment by delta minutes
def out_test(t0, t1, delta=10):
    print('{} - {}: '.format(t0, t1), end='')
    t0 = hhmm_to_minutes(t0)
    t1 = hhmm_to_minutes(t1)

    failed = [minutes_to_hhmm(nowmin)
        for nowmin in range(0, minutes_per_day, delta)
            if not in_time_interval(nowmin, t0, t1)
    ]
    print(', '.join(failed))

m = 30
print('In tests. The printed times are in the interval at the start of the line')
for h in range(24):
    t0 = hour_min_to_hhmm(h, m)
    t1 = hour_min_to_hhmm((h + 2) % 24, m)
    in_test(t0, t1)

print('\n' + '- ' * 20 + '\n')
print('Out tests. The printed times are outside the interval at the start of the line')
for h in range(24):
    t0 = hour_min_to_hhmm(h, m)
    t1 = hour_min_to_hhmm((h + 2) % 24, m)
    out_test(t1, t0)

output

In tests. The printed times are in the interval at the start of the line
0030 - 0230: 0030, 0040, 0050, 0100, 0110, 0120, 0130, 0140, 0150, 0200, 0210, 0220, 0230
0130 - 0330: 0130, 0140, 0150, 0200, 0210, 0220, 0230, 0240, 0250, 0300, 0310, 0320, 0330
0230 - 0430: 0230, 0240, 0250, 0300, 0310, 0320, 0330, 0340, 0350, 0400, 0410, 0420, 0430
0330 - 0530: 0330, 0340, 0350, 0400, 0410, 0420, 0430, 0440, 0450, 0500, 0510, 0520, 0530
0430 - 0630: 0430, 0440, 0450, 0500, 0510, 0520, 0530, 0540, 0550, 0600, 0610, 0620, 0630
0530 - 0730: 0530, 0540, 0550, 0600, 0610, 0620, 0630, 0640, 0650, 0700, 0710, 0720, 0730
0630 - 0830: 0630, 0640, 0650, 0700, 0710, 0720, 0730, 0740, 0750, 0800, 0810, 0820, 0830
0730 - 0930: 0730, 0740, 0750, 0800, 0810, 0820, 0830, 0840, 0850, 0900, 0910, 0920, 0930
0830 - 1030: 0830, 0840, 0850, 0900, 0910, 0920, 0930, 0940, 0950, 1000, 1010, 1020, 1030
0930 - 1130: 0930, 0940, 0950, 1000, 1010, 1020, 1030, 1040, 1050, 1100, 1110, 1120, 1130
1030 - 1230: 1030, 1040, 1050, 1100, 1110, 1120, 1130, 1140, 1150, 1200, 1210, 1220, 1230
1130 - 1330: 1130, 1140, 1150, 1200, 1210, 1220, 1230, 1240, 1250, 1300, 1310, 1320, 1330
1230 - 1430: 1230, 1240, 1250, 1300, 1310, 1320, 1330, 1340, 1350, 1400, 1410, 1420, 1430
1330 - 1530: 1330, 1340, 1350, 1400, 1410, 1420, 1430, 1440, 1450, 1500, 1510, 1520, 1530
1430 - 1630: 1430, 1440, 1450, 1500, 1510, 1520, 1530, 1540, 1550, 1600, 1610, 1620, 1630
1530 - 1730: 1530, 1540, 1550, 1600, 1610, 1620, 1630, 1640, 1650, 1700, 1710, 1720, 1730
1630 - 1830: 1630, 1640, 1650, 1700, 1710, 1720, 1730, 1740, 1750, 1800, 1810, 1820, 1830
1730 - 1930: 1730, 1740, 1750, 1800, 1810, 1820, 1830, 1840, 1850, 1900, 1910, 1920, 1930
1830 - 2030: 1830, 1840, 1850, 1900, 1910, 1920, 1930, 1940, 1950, 2000, 2010, 2020, 2030
1930 - 2130: 1930, 1940, 1950, 2000, 2010, 2020, 2030, 2040, 2050, 2100, 2110, 2120, 2130
2030 - 2230: 2030, 2040, 2050, 2100, 2110, 2120, 2130, 2140, 2150, 2200, 2210, 2220, 2230
2130 - 2330: 2130, 2140, 2150, 2200, 2210, 2220, 2230, 2240, 2250, 2300, 2310, 2320, 2330
2230 - 0030: 0000, 0010, 0020, 0030, 2230, 2240, 2250, 2300, 2310, 2320, 2330, 2340, 2350
2330 - 0130: 0000, 0010, 0020, 0030, 0040, 0050, 0100, 0110, 0120, 0130, 2330, 2340, 2350

- - - - - - - - - - - - - - - - - - - - 

Out tests. The printed times are outside the interval at the start of the line
0230 - 0030: 0040, 0050, 0100, 0110, 0120, 0130, 0140, 0150, 0200, 0210, 0220
0330 - 0130: 0140, 0150, 0200, 0210, 0220, 0230, 0240, 0250, 0300, 0310, 0320
0430 - 0230: 0240, 0250, 0300, 0310, 0320, 0330, 0340, 0350, 0400, 0410, 0420
0530 - 0330: 0340, 0350, 0400, 0410, 0420, 0430, 0440, 0450, 0500, 0510, 0520
0630 - 0430: 0440, 0450, 0500, 0510, 0520, 0530, 0540, 0550, 0600, 0610, 0620
0730 - 0530: 0540, 0550, 0600, 0610, 0620, 0630, 0640, 0650, 0700, 0710, 0720
0830 - 0630: 0640, 0650, 0700, 0710, 0720, 0730, 0740, 0750, 0800, 0810, 0820
0930 - 0730: 0740, 0750, 0800, 0810, 0820, 0830, 0840, 0850, 0900, 0910, 0920
1030 - 0830: 0840, 0850, 0900, 0910, 0920, 0930, 0940, 0950, 1000, 1010, 1020
1130 - 0930: 0940, 0950, 1000, 1010, 1020, 1030, 1040, 1050, 1100, 1110, 1120
1230 - 1030: 1040, 1050, 1100, 1110, 1120, 1130, 1140, 1150, 1200, 1210, 1220
1330 - 1130: 1140, 1150, 1200, 1210, 1220, 1230, 1240, 1250, 1300, 1310, 1320
1430 - 1230: 1240, 1250, 1300, 1310, 1320, 1330, 1340, 1350, 1400, 1410, 1420
1530 - 1330: 1340, 1350, 1400, 1410, 1420, 1430, 1440, 1450, 1500, 1510, 1520
1630 - 1430: 1440, 1450, 1500, 1510, 1520, 1530, 1540, 1550, 1600, 1610, 1620
1730 - 1530: 1540, 1550, 1600, 1610, 1620, 1630, 1640, 1650, 1700, 1710, 1720
1830 - 1630: 1640, 1650, 1700, 1710, 1720, 1730, 1740, 1750, 1800, 1810, 1820
1930 - 1730: 1740, 1750, 1800, 1810, 1820, 1830, 1840, 1850, 1900, 1910, 1920
2030 - 1830: 1840, 1850, 1900, 1910, 1920, 1930, 1940, 1950, 2000, 2010, 2020
2130 - 1930: 1940, 1950, 2000, 2010, 2020, 2030, 2040, 2050, 2100, 2110, 2120
2230 - 2030: 2040, 2050, 2100, 2110, 2120, 2130, 2140, 2150, 2200, 2210, 2220
2330 - 2130: 2140, 2150, 2200, 2210, 2220, 2230, 2240, 2250, 2300, 2310, 2320
0030 - 2230: 0000, 0010, 0020, 2240, 2250, 2300, 2310, 2320, 2330, 2340, 2350
0130 - 2330: 0000, 0010, 0020, 0030, 0040, 0050, 0100, 0110, 0120, 2340, 2350

Although my now_in_minutes function returns the local time, if you live in a region that uses Daylight Saving Time I strongly advise you to seriously consider working in UTC. Bad Things can happen if you're using local time and you call in_time_interval with a time interval that includes the DST time change.

PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
  • Hey, thank you very much for that. Your solution is detailed and clear. Part of the difficulty I was facing was that I was making the assumption that the script would switch on only on encountering the first edge of the specified time range, that it would not switch on if it found itself launched in the middle of the specified time range, but would wait for the start of the next time range. – BlandCorporation Mar 10 '17 at 01:42