1

I want to split a period into several sub-periods of predefined size.

Here is an example:

Between 2021-04-11 15:03:00 and 2021-04-11 18:03:00, decompose into hours, quarter-hours and minutes. The expected result is (no specific order):

2021-04-11 15:03:00 (the minute)
2021-04-11 15:04:00 (the minute)
2021-04-11 15:05:00 (the minute)
2021-04-11 15:06:00 (the minute)
2021-04-11 15:07:00 (the minute)
2021-04-11 15:08:00 (the minute)
2021-04-11 15:09:00 (the minute)
2021-04-11 15:10:00 (the minute)
2021-04-11 15:11:00 (the minute)
2021-04-11 15:12:00 (the minute)
2021-04-11 15:13:00 (the minute)
2021-04-11 15:14:00 (the minute)
2021-04-11 15:15:00 (the quarter-hour)
2021-04-11 15:30:00 (the quarter-hour)
2021-04-11 15:45:00 (the quarter-hour)
2021-04-11 16:00:00 (the hour)
2021-04-11 17:00:00 (the hour)
2021-04-11 18:00:00 (the minute)
2021-04-11 18:01:00 (the minute)
2021-04-11 18:02:00 (the minute)

And my current code:

def ceil_dt(dt, delta):
    return dt + (datetime.min - dt) % delta

def floor_dt(dt, delta):
    return dt - (dt - datetime.min) % delta

def list_dt(start, end, subperiod, indice):
    temp = start
    min = ceil_dt(temp, timedelta(minutes=subperiod[indice]))

    print(f'\nstart {start}')
    print(f'end {end}')
    print(f'subperiod {subperiod}')
    print(f'indice {indice}')
    print(f'min {min}')

    while temp + timedelta(minutes=subperiod[indice]) <= floor_dt(end, timedelta(minutes=subperiod[indice])) :
        print(f'result {ceil_dt(temp, timedelta(minutes=subperiod[indice]))}')
        temp = temp + timedelta(minutes=subperiod[indice])
        max = ceil_dt(temp, timedelta(minutes=subperiod[indice]))

    print(f'max {max}')

    if min != start:
        print("other min")
        indice = indice + 1
        list_dt(start, min, subperiod, indice)

#     if max != end:
#         print("other max")
#         indice = indice + 1
#         list_dt(max, end, subperiod, indice)


subperiod = [60, 15, 1]
indice = 0

start = datetime(2021, 4, 11, 15, 3, 0)
end = datetime(2021, 4, 11, 18, 3, 0)
list_dt(start, end, subperiod, indice)

I can't find how to enter correctly in the min and max parts. I'm not sure if we should use recursive. Does anyone have an idea?

halfer
  • 19,824
  • 17
  • 99
  • 186
lovelace63
  • 352
  • 1
  • 6
  • 15

2 Answers2

1

You can use a while loop:

from datetime import datetime, timedelta as td
def list_dt(d1, d2):
  while d1 < d2:
     yield d1
     if not d1.minute and d1+td(hours = 1) <= d2:
        d1 += td(hours = 1)
     elif not d1.minute%15 and d1+td(minutes = 15) <= d2:
        d1 += td(minutes = 15)
     else:
        d1 += td(minutes = 1)

for a, b in [[datetime(2021, 4, 11, 15, 3, 0), datetime(2021, 4, 11, 18, 3, 0)], [datetime(2021, 4, 11, 15, 0, 0), datetime(2021, 4, 11, 18, 0, 0)], [datetime(2021, 4, 11, 15, 15, 0), datetime(2021, 4, 11, 18, 30, 0)]]:
   for i in list_dt(a, b):
      print(str(i))
   print('-'*20)

Output:

2021-04-11 15:03:00
2021-04-11 15:04:00
2021-04-11 15:05:00
2021-04-11 15:06:00
2021-04-11 15:07:00
2021-04-11 15:08:00
2021-04-11 15:09:00
2021-04-11 15:10:00
2021-04-11 15:11:00
2021-04-11 15:12:00
2021-04-11 15:13:00
2021-04-11 15:14:00
2021-04-11 15:15:00
2021-04-11 15:30:00
2021-04-11 15:45:00
2021-04-11 16:00:00
2021-04-11 17:00:00
2021-04-11 18:00:00
2021-04-11 18:01:00
2021-04-11 18:02:00
--------------------
2021-04-11 15:00:00
2021-04-11 16:00:00
2021-04-11 17:00:00
--------------------
2021-04-11 15:15:00
2021-04-11 15:30:00
2021-04-11 15:45:00
2021-04-11 16:00:00
2021-04-11 17:00:00
2021-04-11 18:00:00
2021-04-11 18:15:00
--------------------
Ajax1234
  • 69,937
  • 8
  • 61
  • 102
  • Thanks for your help. I use python 3.7 that does not support assignment expression and I can't switch to 3.8 at the moment. I also need to be able to set the sub-periods. It is not impossible that in the future I will add 30 minutes. What changes should be made? – lovelace63 Apr 14 '21 at 16:45
  • 1
    @lovelace63 I updated the post to remove the assignment expressions. If you want to add other time intervals in the future, than you can just include them as conditionals under the first 15 minute check. – Ajax1234 Apr 14 '21 at 16:49
  • If i try between datetime(2021, 4, 11, 15, 3, 0) and datetime(2021, 4, 11, 18, 3, 0), this should give 15h, 16h and 17h, which is not the case with your code – lovelace63 Apr 14 '21 at 17:01
  • @lovelace63 those are the same datetimes that are used in my post (see `start` and `end`). Can you clarify? – Ajax1234 Apr 14 '21 at 17:13
  • sorry for the bad past. It is between datetime(2021, 4, 11, 15, 0, 0) and datetime(2021, 4, 11, 18, 0, 0) – lovelace63 Apr 14 '21 at 18:06
  • Thank you very much for your help but it is still not the expected result. I just tested for [datetime(2021, 4, 11, 15, 15, 0), datetime(2021, 4, 11, 18, 30, 0)] and I should get 15h15, 15h30, 15h45, 16h, 17h, 18h (the quarter-hour), and 18h15. To add context, in my application, I calculate a data structure every minute, every quarter hour and every hour. For 18h, I have the value 1minute, 15minutes and 1hour. I want to aggregate to have a period while aggregating as little structure as possible. – lovelace63 Apr 15 '21 at 05:45
0

What you are describing is already available through datetime's rrule. As a basic example you can write a function a little like this:

    import dateutil.rrule as rrule
    from datetime import datetime, timedelta
    
    # function to round to nearest n time
    def round_nearest(date, **kwargs):
        secs = timedelta(**kwargs).total_seconds()
        return datetime.fromtimestamp(date.timestamp() + secs - date.timestamp() % secs)
    
    # function to return per OP's post
    def hours_aligned(start, end, inc = True):
        print("Minutes")
        if inc: yield start # remove if you don't require start
        rule = rrule.rrule(rrule.MINUTELY, dtstart=start)
        for x in rule.between(start, end, inc = False):
            yield x
        if inc: yield end # remove if you don't require end
    
        print("Hours")
        if inc: yield start # remove if you don't require start
        rule = rrule.rrule(rrule.HOURLY, dtstart=start)
        for x in rule.between(start, end, inc = False):
            yield x
        if inc: yield end # remove if you don't require end
    
        print("Quarter Hours")
        if inc: yield start # remove if you don't require start
        rule = rrule.rrule(rrule.MINUTELY, interval=15, dtstart=round_nearest(start, minutes=15))
        for x in rule.between(start, end, inc = False):
            yield x
        if inc: yield end # remove if you don't require end
    
    
    start = datetime(2021, 4, 11, 15, 3, 0)
    end = datetime(2021, 4, 11, 18, 3, 0)
    
    for x in hours_aligned(start, end, inc=True):
        print(x)

Minutes
2021-04-11 15:03:00
2021-04-11 15:04:00
2021-04-11 15:05:00
2021-04-11 15:06:00
2021-04-11 15:07:00
2021-04-11 15:08:00
2021-04-11 15:09:00
2021-04-11 15:10:00
2021-04-11 15:11:00
2021-04-11 15:12:00
2021-04-11 15:13:00
2021-04-11 15:14:00
2021-04-11 15:15:00
2021-04-11 15:16:00
2021-04-11 15:17:00
2021-04-11 15:18:00
2021-04-11 15:19:00
2021-04-11 15:20:00
2021-04-11 15:21:00
2021-04-11 15:22:00
2021-04-11 15:23:00
2021-04-11 15:24:00
2021-04-11 15:25:00
2021-04-11 15:26:00
2021-04-11 15:27:00
2021-04-11 15:28:00
2021-04-11 15:29:00
2021-04-11 15:30:00
2021-04-11 15:31:00
2021-04-11 15:32:00
2021-04-11 15:33:00
2021-04-11 15:34:00
2021-04-11 15:35:00
2021-04-11 15:36:00
2021-04-11 15:37:00
2021-04-11 15:38:00
2021-04-11 15:39:00
2021-04-11 15:40:00
2021-04-11 15:41:00
2021-04-11 15:42:00
2021-04-11 15:43:00
2021-04-11 15:44:00
2021-04-11 15:45:00
2021-04-11 15:46:00
2021-04-11 15:47:00
2021-04-11 15:48:00
2021-04-11 15:49:00
2021-04-11 15:50:00
2021-04-11 15:51:00
2021-04-11 15:52:00
2021-04-11 15:53:00
2021-04-11 15:54:00
2021-04-11 15:55:00
2021-04-11 15:56:00
2021-04-11 15:57:00
2021-04-11 15:58:00
2021-04-11 15:59:00
2021-04-11 16:00:00
2021-04-11 16:01:00
2021-04-11 16:02:00
2021-04-11 16:03:00
2021-04-11 16:04:00
2021-04-11 16:05:00
2021-04-11 16:06:00
2021-04-11 16:07:00
2021-04-11 16:08:00
2021-04-11 16:09:00
2021-04-11 16:10:00
2021-04-11 16:11:00
2021-04-11 16:12:00
2021-04-11 16:13:00
2021-04-11 16:14:00
2021-04-11 16:15:00
2021-04-11 16:16:00
2021-04-11 16:17:00
2021-04-11 16:18:00
2021-04-11 16:19:00
2021-04-11 16:20:00
2021-04-11 16:21:00
2021-04-11 16:22:00
2021-04-11 16:23:00
2021-04-11 16:24:00
2021-04-11 16:25:00
2021-04-11 16:26:00
2021-04-11 16:27:00
2021-04-11 16:28:00
2021-04-11 16:29:00
2021-04-11 16:30:00
2021-04-11 16:31:00
2021-04-11 16:32:00
2021-04-11 16:33:00
2021-04-11 16:34:00
2021-04-11 16:35:00
2021-04-11 16:36:00
2021-04-11 16:37:00
2021-04-11 16:38:00
2021-04-11 16:39:00
2021-04-11 16:40:00
2021-04-11 16:41:00
2021-04-11 16:42:00
2021-04-11 16:43:00
2021-04-11 16:44:00
2021-04-11 16:45:00
2021-04-11 16:46:00
2021-04-11 16:47:00
2021-04-11 16:48:00
2021-04-11 16:49:00
2021-04-11 16:50:00
2021-04-11 16:51:00
2021-04-11 16:52:00
2021-04-11 16:53:00
2021-04-11 16:54:00
2021-04-11 16:55:00
2021-04-11 16:56:00
2021-04-11 16:57:00
2021-04-11 16:58:00
2021-04-11 16:59:00
2021-04-11 17:00:00
2021-04-11 17:01:00
2021-04-11 17:02:00
2021-04-11 17:03:00
2021-04-11 17:04:00
2021-04-11 17:05:00
2021-04-11 17:06:00
2021-04-11 17:07:00
2021-04-11 17:08:00
2021-04-11 17:09:00
2021-04-11 17:10:00
2021-04-11 17:11:00
2021-04-11 17:12:00
2021-04-11 17:13:00
2021-04-11 17:14:00
2021-04-11 17:15:00
2021-04-11 17:16:00
2021-04-11 17:17:00
2021-04-11 17:18:00
2021-04-11 17:19:00
2021-04-11 17:20:00
2021-04-11 17:21:00
2021-04-11 17:22:00
2021-04-11 17:23:00
2021-04-11 17:24:00
2021-04-11 17:25:00
2021-04-11 17:26:00
2021-04-11 17:27:00
2021-04-11 17:28:00
2021-04-11 17:29:00
2021-04-11 17:30:00
2021-04-11 17:31:00
2021-04-11 17:32:00
2021-04-11 17:33:00
2021-04-11 17:34:00
2021-04-11 17:35:00
2021-04-11 17:36:00
2021-04-11 17:37:00
2021-04-11 17:38:00
2021-04-11 17:39:00
2021-04-11 17:40:00
2021-04-11 17:41:00
2021-04-11 17:42:00
2021-04-11 17:43:00
2021-04-11 17:44:00
2021-04-11 17:45:00
2021-04-11 17:46:00
2021-04-11 17:47:00
2021-04-11 17:48:00
2021-04-11 17:49:00
2021-04-11 17:50:00
2021-04-11 17:51:00
2021-04-11 17:52:00
2021-04-11 17:53:00
2021-04-11 17:54:00
2021-04-11 17:55:00
2021-04-11 17:56:00
2021-04-11 17:57:00
2021-04-11 17:58:00
2021-04-11 17:59:00
2021-04-11 18:00:00
2021-04-11 18:01:00
2021-04-11 18:02:00
2021-04-11 18:03:00
Hours
2021-04-11 15:03:00
2021-04-11 16:03:00
2021-04-11 17:03:00
2021-04-11 18:03:00
Quarter Hours
2021-04-11 15:03:00
2021-04-11 15:15:00
2021-04-11 15:30:00
2021-04-11 15:45:00
2021-04-11 16:00:00
2021-04-11 16:15:00
2021-04-11 16:30:00
2021-04-11 16:45:00
2021-04-11 17:00:00
2021-04-11 17:15:00
2021-04-11 17:30:00
2021-04-11 17:45:00
2021-04-11 18:00:00
2021-04-11 18:03:00

I've created the 3 rules as by the minute i.e. MINUTELY, by the minute but at 15 minute intervals and then by the hour i.e. HOURLY. Inc is just whether you want the start end included.

Johnny John Boy
  • 3,009
  • 5
  • 26
  • 50
  • Thanks. I didn't know dateutil. I don't see how to combine the rules to get only what I need. – lovelace63 Apr 14 '21 at 16:50
  • Try my updated function.. you just need to remove the start and end's if you don't require them on any or some of the functions.. it was just an example. – Johnny John Boy Apr 15 '21 at 07:25