Skip to content

Instantly share code, notes, and snippets.

@brendancol
Created December 13, 2023 09:24
Show Gist options
  • Save brendancol/fdbb2c3b5ace71193ffaa4f5a40dd81f to your computer and use it in GitHub Desktop.
Save brendancol/fdbb2c3b5ace71193ffaa4f5a40dd81f to your computer and use it in GitHub Desktop.
from datetime import datetime
from enum import Enum
from dateutil.relativedelta import relativedelta
from typing import Tuple, List
def get_today() -> datetime:
tz = pytz.timezone('America/New_York')
return datetime.now(tz)
class RelativeTimeFrame(str, Enum):
CURRENT = 'c'
LAST = 'l'
NEXT = 'n'
PLUS = '+'
MINUS = '-'
TODAY = 't'
class TimeFrame(str, Enum):
DAY = 'd'
WEEK = 'w'
MONTH = 'm'
QUARTER = 'q'
YEAR = 'y'
class Day(str, Enum):
MONDAY = '1'
TUESDAY = '2'
WEDNESDAY = '3'
THURSDAY = '4'
FRIDAY = '5'
SATURDAY = '6'
SUNDAY = '7'
class Quarter(str, Enum):
FIRST = '1'
SECOND = '2'
THIRD = '3'
FOURTH = '4'
class Operator(str, Enum):
BACKWARD = '-'
FORWARD = '+'
LAST = 'l'
NEXT = 'n'
START_OF_PERIOD = '^'
END_OF_PERIOD = '$'
class TimeUnit(str, Enum):
DAY = 'd'
WEEK = 'w'
MONTH = 'm'
QUARTER = 'q'
YEAR = 'y'
class SpecialCommand(str, Enum):
TODAY = 't'
TOMORROW = 'T'
YESTERDAY = 'Y'
THIS_WEEK = 'W'
THIS_MONTH = 'M'
THIS_QUARTER = 'Q'
THIS_YEAR = 'YY'
VISUAL_MODE = 'v'
START_MODE = '['
END_MODE = ']'
SEARCH = '/'
class ValueChar(str, Enum):
VALUE_0 = '0'
VALUE_1 = '1'
VALUE_2 = '2'
VALUE_3 = '3'
VALUE_4 = '4'
VALUE_5 = '5'
VALUE_6 = '6'
VALUE_7 = '7'
VALUE_8 = '8'
VALUE_9 = '9'
class Snap(str, Enum):
START = '^'
END = '$'
class SubCommand(object):
def __init__(self):
self.operator = None
self.value = None
self.unit = None
self.special = None
def __repr__(self):
return f'SubCommand<operator={self.operator}, value={self.value}, unit={self.unit}, special={self.special}>'
@property
def is_visual_mode(self):
return self.special == SpecialCommand.VISUAL_MODE
@property
def is_start_mode(self):
return self.special == SpecialCommand.START_MODE
@property
def is_end_mode(self):
return self.special == SpecialCommand.END_MODE
class DateTimeRangeTransformer(object):
def __init__(self, command_str):
self._command_str = command_str
self.commands = self._interpret_subcommands(command_str)
def __repr__(self):
return self._command_str
@property
def first(self):
return self.commands[0]
def __call__(self, start_date: datetime,
end_date: datetime) -> Tuple[datetime, datetime]:
for cmd in self.start_date_modifiers:
start_date += self._get_relativedelta(start_date,
cmd,
snap=Snap.START)
for cmd in self.end_date_modifiers:
end_date += self._get_relativedelta(end_date,
cmd,
snap=Snap.END)
return start_date, end_date
def _visual_index(self):
for i, c in enumerate(self.commands):
if c.special == SpecialCommand.VISUAL_MODE:
return i
return None
@property
def start_date_modifiers(self):
if self._visual_index() is not None:
cmds = self.commands[:self._visual_index()]
else:
cmds = self.commands
for c in cmds:
if c.is_end_mode:
continue
else:
yield c
@property
def end_date_modifiers(self):
if self._visual_index() is not None:
cmds = self.commands[self._visual_index()+1:]
else:
cmds = self.commands
for c in cmds:
if c.is_start_mode:
continue
else:
yield c
def _interpret_subcommands(self, cmd_str: str) -> List[SubCommand]:
cmds = cmd_str.split(' ')
for i in range(len(cmds)):
group = cmds[i]
cmd = SubCommand()
if group == 'YY':
cmd.special = SpecialCommand(group)
else:
numerics = ''
for char in group:
if char in Operator.__members__.values():
cmd.operator = Operator(char)
elif char in TimeUnit.__members__.values():
cmd.unit = TimeUnit(char)
elif char in SpecialCommand.__members__.values():
cmd.special = SpecialCommand(char)
elif char in ValueChar.__members__.values():
numerics += char
# cast numeric if it exists
if len(numerics):
cmd.value = int(numerics)
if cmd.special in (SpecialCommand.TODAY,
SpecialCommand.YESTERDAY,
SpecialCommand.TOMORROW):
cmd.value = 0
cmd.unit = TimeUnit.DAY
elif cmd.special == SpecialCommand.THIS_WEEK:
cmd.value = 0
cmd.unit = TimeUnit.WEEK
elif cmd.special == SpecialCommand.THIS_MONTH:
cmd.value = 0
cmd.unit = TimeUnit.MONTH
elif cmd.special == SpecialCommand.THIS_QUARTER:
cmd.value = 0
cmd.unit = TimeUnit.QUARTER
elif cmd.special == SpecialCommand.THIS_YEAR:
cmd.value = 0
cmd.unit = TimeUnit.YEAR
cmds[i] = cmd
return cmds
def snap_datetime_to_unit_start(self, d: datetime, unit: TimeUnit) -> datetime:
if unit == TimeUnit.DAY:
return d.replace(hour=0, minute=0, second=0, microsecond=0)
elif unit == TimeUnit.WEEK:
return d - relativedelta(days=d.weekday())
elif unit == TimeUnit.MONTH:
return d.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
elif unit == TimeUnit.QUARTER:
if d.month in [1, 2, 3]:
return d.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
elif d.month in [4, 5, 6]:
return d.replace(month=4, day=1, hour=0, minute=0, second=0, microsecond=0)
elif d.month in [7, 8, 9]:
return d.replace(month=7, day=1, hour=0, minute=0, second=0, microsecond=0)
elif d.month in [10, 11, 12]:
return d.replace(month=10, day=1, hour=0, minute=0, second=0, microsecond=0)
elif unit == TimeUnit.YEAR:
return d.replace(day=1, month=1, hour=0, minute=0, second=0, microsecond=0)
def snap_datetime_to_unit_end(self, d: datetime, unit: TimeUnit) -> datetime:
if unit == TimeUnit.DAY:
return d.replace(hour=23, minute=59, second=59, microsecond=999999)
elif unit == TimeUnit.WEEK:
return d + relativedelta(days=6-d.weekday())
elif unit == TimeUnit.MONTH:
return d.replace(day=1, hour=23, minute=59, second=59, microsecond=999999) + relativedelta(months=1, days=-1)
elif unit == TimeUnit.QUARTER:
if d.month in [1, 2, 3]:
return d.replace(month=3, day=31, hour=23, minute=59, second=59, microsecond=999999)
elif d.month in [4, 5, 6]:
return d.replace(month=6, day=30, hour=23, minute=59, second=59, microsecond=999999)
elif d.month in [7, 8, 9]:
return d.replace(month=9, day=30, hour=23, minute=59, second=59, microsecond=999999)
elif d.month in [10, 11, 12]:
return d.replace(month=12, day=31, hour=23, minute=59, second=59, microsecond=999999)
elif unit == TimeUnit.YEAR:
return d.replace(day=1, month=1, hour=23, minute=59, second=59, microsecond=999999) + relativedelta(years=1, days=-1)
def _compute_relative_delta(self,
anchor: datetime,
cmd: SubCommand,
snap: Snap) -> relativedelta:
ruff = None
# LAST
if cmd.unit == TimeUnit.DAY and cmd.operator == Operator.LAST:
ruff = anchor - relativedelta(days=1)
elif cmd.unit == TimeUnit.WEEK and cmd.operator == Operator.LAST:
ruff = anchor - relativedelta(days=7)
elif cmd.unit == TimeUnit.MONTH and cmd.operator == Operator.LAST:
ruff = anchor - relativedelta(months=1)
elif cmd.unit == TimeUnit.QUARTER and cmd.operator == Operator.LAST:
ruff = anchor - relativedelta(months=3)
elif cmd.unit == TimeUnit.YEAR and cmd.operator == Operator.LAST:
ruff = anchor - relativedelta(years=1)
# NEXT
elif cmd.unit == TimeUnit.DAY and cmd.operator == Operator.NEXT:
ruff = anchor + relativedelta(days=1)
elif cmd.unit == TimeUnit.WEEK and cmd.operator == Operator.NEXT:
ruff = anchor + relativedelta(days=7)
elif cmd.unit == TimeUnit.MONTH and cmd.operator == Operator.NEXT:
ruff = anchor + relativedelta(months=1)
elif cmd.unit == TimeUnit.QUARTER and cmd.operator == Operator.NEXT:
ruff = anchor + relativedelta(months=3)
elif cmd.unit == TimeUnit.YEAR and cmd.operator == Operator.NEXT:
ruff = anchor + relativedelta(years=1)
# PREDEFINED
elif cmd.special in (SpecialCommand.TODAY,
SpecialCommand.THIS_WEEK,
SpecialCommand.THIS_MONTH,
SpecialCommand.THIS_QUARTER,
SpecialCommand.THIS_YEAR):
ruff = get_today()
elif cmd.special == SpecialCommand.YESTERDAY:
ruff = get_today() - relativedelta(days=1)
elif cmd.special == SpecialCommand.TOMORROW:
ruff = get_today() + relativedelta(days=1)
if snap == Snap.START:
final_date = self.snap_datetime_to_unit_start(ruff, cmd.unit)
elif snap == Snap.END:
final_date = self.snap_datetime_to_unit_end(ruff, cmd.unit)
return relativedelta(final_date, anchor)
def _compute_absolute_delta(self, cmd: SubCommand) -> relativedelta:
value = cmd.value
if cmd.operator in (Operator.BACKWARD):
value *= -1
if cmd.unit == TimeUnit.DAY:
return relativedelta(days=value)
elif cmd.unit == TimeUnit.WEEK:
return relativedelta(weeks=value)
elif cmd.unit == TimeUnit.MONTH:
return relativedelta(months=value)
elif cmd.unit == TimeUnit.QUARTER:
return relativedelta(months=3*value)
elif cmd.unit == TimeUnit.YEAR:
return relativedelta(years=value)
else:
raise ValueError(f'Unknown unit: {cmd.unit}')
def _get_relativedelta(self,
anchor: datetime,
cmd: SubCommand,
snap: Snap) -> relativedelta:
if cmd.operator in (Operator.BACKWARD, Operator.FORWARD):
return self._compute_absolute_delta(cmd)
else:
return self._compute_relative_delta(anchor, cmd, snap)
if __name__ == '__main__':
import pytz
import pandas as pd
pd.set_option('display.max_columns', 50)
tz = pytz.timezone('America/New_York')
today = datetime.now(tz)
start_date = today.replace(hour=0, minute=0, second=0, microsecond=0)
end_date = today.replace(hour=23, minute=59, second=59, microsecond=999999)
cmds = [
DateTimeRangeTransformer("-1d v +2d"),
DateTimeRangeTransformer("-1d"),
DateTimeRangeTransformer("-1m v +2w"),
DateTimeRangeTransformer("-2y v -2w"),
DateTimeRangeTransformer("-1y v -1m"),
DateTimeRangeTransformer("-1y +1m v -1m"),
DateTimeRangeTransformer("[-1y"),
DateTimeRangeTransformer("]+1y"),
DateTimeRangeTransformer("lw"),
DateTimeRangeTransformer("nw"),
DateTimeRangeTransformer("lm"),
DateTimeRangeTransformer("nm"),
DateTimeRangeTransformer("lq"),
DateTimeRangeTransformer("nq"),
DateTimeRangeTransformer("ly"),
DateTimeRangeTransformer("ny"),
DateTimeRangeTransformer("lw [-2d"),
DateTimeRangeTransformer("lw ]+2d"),
DateTimeRangeTransformer("t"),
DateTimeRangeTransformer("T"),
DateTimeRangeTransformer("W"),
DateTimeRangeTransformer("M"),
DateTimeRangeTransformer("Q"),
DateTimeRangeTransformer("Y"),
DateTimeRangeTransformer("YY"),
]
results = []
for c in cmds:
obj = {}
obj['cmd'] = c
result = c(start_date, end_date)
obj['start_date'] = result[0].strftime('%a %Y-%m-%d %H:%M:%S')
obj['end_date'] = result[1].strftime('%a %Y-%m-%d %H:%M:%S')
obj['delta'] = result[1] - result[0]
obj['start_delta'] = result[0] - start_date
obj['end_delta'] = result[1] - end_date
results.append(obj)
df = pd.DataFrame.from_records(results)
print(df)
@brendancol
Copy link
Author

              cmd               start_date                 end_date  \
0       -1d v +2d  Tue 2023-12-12 00:00:00  Fri 2023-12-15 23:59:59
1             -1d  Tue 2023-12-12 00:00:00  Tue 2023-12-12 23:59:59
2       -1m v +2w  Mon 2023-11-13 00:00:00  Wed 2023-12-27 23:59:59
3       -2y v -2w  Mon 2021-12-13 00:00:00  Wed 2023-11-29 23:59:59
4       -1y v -1m  Tue 2022-12-13 00:00:00  Mon 2023-11-13 23:59:59
5   -1y +1m v -1m  Fri 2023-01-13 00:00:00  Mon 2023-11-13 23:59:59
6            [-1y  Tue 2022-12-13 00:00:00  Wed 2023-12-13 23:59:59
7            ]+1y  Wed 2023-12-13 00:00:00  Fri 2024-12-13 23:59:59
8              lw  Mon 2023-12-04 00:00:00  Sun 2023-12-10 23:59:59
9              nw  Mon 2023-12-18 00:00:00  Sun 2023-12-24 23:59:59
10             lm  Wed 2023-11-01 00:00:00  Thu 2023-11-30 23:59:59
11             nm  Mon 2024-01-01 00:00:00  Wed 2024-01-31 23:59:59
12             lq  Sat 2023-07-01 00:00:00  Sat 2023-09-30 23:59:59
13             nq  Mon 2024-01-01 00:00:00  Sun 2024-03-31 23:59:59
14             ly  Sat 2022-01-01 00:00:00  Sat 2022-12-31 23:59:59
15             ny  Mon 2024-01-01 00:00:00  Tue 2024-12-31 23:59:59
16        lw [-2d  Sat 2023-12-02 00:00:00  Sun 2023-12-10 23:59:59
17        lw ]+2d  Mon 2023-12-04 00:00:00  Tue 2023-12-12 23:59:59
18              t  Wed 2023-12-13 00:00:00  Wed 2023-12-13 23:59:59
19              T  Thu 2023-12-14 00:00:00  Thu 2023-12-14 23:59:59
20              W  Mon 2023-12-11 04:21:04  Sun 2023-12-17 04:21:04
21              M  Fri 2023-12-01 00:00:00  Sun 2023-12-31 23:59:59
22              Q  Sun 2023-10-01 00:00:00  Sun 2023-12-31 23:59:59
23              Y  Tue 2023-12-12 00:00:00  Tue 2023-12-12 23:59:59
24             YY  Sun 2023-01-01 00:00:00  Sun 2023-12-31 23:59:59

                      delta              start_delta              end_delta
0    3 days 23:59:59.999999        -1 days +00:00:00        2 days 00:00:00
1    0 days 23:59:59.999999        -1 days +00:00:00      -1 days +00:00:00
2   44 days 23:59:59.999999       -30 days +00:00:00       14 days 00:00:00
3  716 days 23:59:59.999999      -730 days +00:00:00     -14 days +00:00:00
4  335 days 23:59:59.999999      -365 days +00:00:00     -30 days +00:00:00
5  304 days 23:59:59.999999      -334 days +00:00:00     -30 days +00:00:00
6  365 days 23:59:59.999999      -365 days +00:00:00        0 days 00:00:00
7  366 days 23:59:59.999999          0 days 00:00:00      366 days 00:00:00
8    6 days 23:59:59.999999        -9 days +00:00:00      -3 days +00:00:00
9    6 days 23:59:59.999999          5 days 00:00:00       11 days 00:00:00
10  29 days 23:59:59.999999       -42 days +00:00:00     -13 days +00:00:00
11  30 days 23:59:59.999999         19 days 00:00:00       49 days 00:00:00
12  91 days 23:59:59.999999      -165 days +00:00:00     -74 days +00:00:00
13  90 days 23:59:59.999999         19 days 00:00:00      109 days 00:00:00
14 364 days 23:59:59.999999      -711 days +00:00:00    -347 days +00:00:00
15 365 days 23:59:59.999999         19 days 00:00:00      384 days 00:00:00
16   8 days 23:59:59.999999       -11 days +00:00:00      -3 days +00:00:00
17   8 days 23:59:59.999999        -9 days +00:00:00      -1 days +00:00:00
18   0 days 23:59:59.999999          0 days 00:00:00        0 days 00:00:00
19   0 days 23:59:59.999999          1 days 00:00:00        1 days 00:00:00
20   6 days 00:00:00.000051 -2 days +04:21:04.517694 3 days 04:21:04.517746
21  30 days 23:59:59.999999       -12 days +00:00:00       18 days 00:00:00
22  91 days 23:59:59.999999       -73 days +00:00:00       18 days 00:00:00
23   0 days 23:59:59.999999        -1 days +00:00:00      -1 days +00:00:00
24 364 days 23:59:59.999999      -346 days +00:00:00       18 days 00:00:00

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment