170 lines
6.2 KiB
Python
170 lines
6.2 KiB
Python
import re
|
|
from datetime import date, datetime, timedelta
|
|
|
|
from django.utils import timezone
|
|
|
|
from common.utils import generate_split_ranges
|
|
|
|
dateformat: str = "%d/%m/%Y"
|
|
datetimeformat: str = "%d/%m/%Y %H:%M"
|
|
timeformat: str = "%H:%M"
|
|
durationformat: str = "%2.1H hours"
|
|
durationformat_manual: str = "%H hours"
|
|
|
|
|
|
def _safe_timedelta(duration: timedelta | int | None):
|
|
if duration == None:
|
|
return timedelta(0)
|
|
elif isinstance(duration, int):
|
|
return timedelta(seconds=duration)
|
|
elif isinstance(duration, timedelta):
|
|
return duration
|
|
|
|
|
|
def format_duration(
|
|
duration: timedelta | int | float | None, format_string: str = "%H hours"
|
|
) -> str:
|
|
"""
|
|
Format timedelta into the specified format_string.
|
|
Valid format variables:
|
|
- %H hours
|
|
- %m minutes
|
|
- %s seconds
|
|
- %r total seconds
|
|
Values don't change into higher units if those units are missing
|
|
from the formatting string. For example:
|
|
- 61 seconds as "%s" = 61 seconds
|
|
- 61 seconds as "%m %s" = 1 minutes 1 seconds"
|
|
Format specifiers can include width and precision options:
|
|
- %5.2H: hours formatted with width 5 and 2 decimal places (padded with zeros)
|
|
"""
|
|
minute_seconds = 60
|
|
hour_seconds = 60 * minute_seconds
|
|
day_seconds = 24 * hour_seconds
|
|
safe_duration = _safe_timedelta(duration)
|
|
# we don't need float
|
|
seconds_total = int(safe_duration.total_seconds())
|
|
# timestamps where end is before start
|
|
if seconds_total < 0:
|
|
seconds_total = 0
|
|
days = hours = hours_float = minutes = seconds = 0
|
|
remainder = seconds = seconds_total
|
|
if "%d" in format_string:
|
|
days, remainder = divmod(seconds_total, day_seconds)
|
|
if re.search(r"%\d*\.?\d*H", format_string):
|
|
hours_float, remainder = divmod(remainder, hour_seconds)
|
|
hours = float(hours_float) + remainder / hour_seconds
|
|
if re.search(r"%\d*\.?\d*m", format_string):
|
|
minutes, seconds = divmod(remainder, minute_seconds)
|
|
literals = {
|
|
"d": str(days),
|
|
"H": str(hours) if "m" not in format_string else str(hours_float),
|
|
"m": str(minutes),
|
|
"s": str(seconds),
|
|
"r": str(seconds_total),
|
|
}
|
|
formatted_string = format_string
|
|
for pattern, replacement in literals.items():
|
|
# Match format specifiers with optional width and precision
|
|
match = re.search(rf"%(\d*\.?\d*){pattern}", formatted_string)
|
|
if match:
|
|
format_spec = match.group(1)
|
|
if "." in format_spec:
|
|
# Format the number as float if precision is specified
|
|
replacement = f"{float(replacement):{format_spec}f}"
|
|
else:
|
|
# Format the number as integer if no precision is specified
|
|
replacement = f"{int(float(replacement)):>{format_spec}}"
|
|
# Replace the format specifier with the formatted number
|
|
formatted_string = re.sub(
|
|
rf"%\d*\.?\d*{pattern}", replacement, formatted_string
|
|
)
|
|
return formatted_string
|
|
|
|
|
|
def local_strftime(datetime: datetime, format: str = datetimeformat) -> str:
|
|
return timezone.localtime(datetime).strftime(format)
|
|
|
|
|
|
def daterange(start: date, end: date, end_inclusive: bool = False) -> list[date]:
|
|
time_between: timedelta = end - start
|
|
if (days_between := time_between.days) < 1:
|
|
raise ValueError("start and end have to be at least 1 day apart.")
|
|
if end_inclusive:
|
|
print(f"{end_inclusive=}")
|
|
print(f"{days_between=}")
|
|
days_between += 1
|
|
print(f"{days_between=}")
|
|
return [start + timedelta(x) for x in range(days_between)]
|
|
|
|
|
|
def streak(datelist: list[date]) -> dict[str, int | tuple[date, date]]:
|
|
if len(datelist) == 1:
|
|
return {"days": 1, "dates": (datelist[0], datelist[0])}
|
|
else:
|
|
print(f"Processing {len(datelist)} dates.")
|
|
missing = sorted(
|
|
set(
|
|
datelist[0] + timedelta(x)
|
|
for x in range((datelist[-1] - datelist[0]).days)
|
|
)
|
|
- set(datelist)
|
|
)
|
|
print(f"{len(missing)} days missing.")
|
|
datelist_with_missing = sorted(datelist + missing)
|
|
ranges = list(generate_split_ranges(datelist_with_missing, missing))
|
|
print(f"{len(ranges)} ranges calculated.")
|
|
longest_consecutive_days = timedelta(0)
|
|
longest_range: tuple[date, date] = (date(1970, 1, 1), date(1970, 1, 1))
|
|
for start, end in ranges:
|
|
if (current_streak := end - start) > longest_consecutive_days:
|
|
longest_consecutive_days = current_streak
|
|
longest_range = (start, end)
|
|
return {"days": longest_consecutive_days.days + 1, "dates": longest_range}
|
|
|
|
|
|
def streak_bruteforce(datelist: list[date]) -> dict[str, int | tuple[date, date]]:
|
|
if (datelist_length := len(datelist)) == 0:
|
|
raise ValueError("Number of dates in the list is 0.")
|
|
datelist.sort()
|
|
current_streak = 1
|
|
current_start = datelist[0]
|
|
current_end = datelist[0]
|
|
current_date = datelist[0]
|
|
highest_streak = 1
|
|
highest_streak_daterange = (current_start, current_end)
|
|
|
|
def update_highest_streak():
|
|
nonlocal highest_streak, highest_streak_daterange
|
|
if current_streak > highest_streak:
|
|
highest_streak = current_streak
|
|
highest_streak_daterange = (current_start, current_end)
|
|
|
|
def reset_streak():
|
|
nonlocal current_start, current_end, current_streak
|
|
current_start = current_end = current_date
|
|
current_streak = 1
|
|
|
|
def increment_streak():
|
|
nonlocal current_end, current_streak
|
|
current_end = current_date
|
|
current_streak += 1
|
|
|
|
for i, datelist_item in enumerate(datelist, start=1):
|
|
current_date = datelist_item
|
|
if current_date == current_start or current_date == current_end:
|
|
continue
|
|
if current_date - timedelta(1) != current_end and i != datelist_length:
|
|
update_highest_streak()
|
|
reset_streak()
|
|
elif current_date - timedelta(1) == current_end and i == datelist_length:
|
|
increment_streak()
|
|
update_highest_streak()
|
|
else:
|
|
increment_streak()
|
|
return {"days": highest_streak, "dates": highest_streak_daterange}
|
|
|
|
|
|
def available_stats_year_range():
|
|
return range(datetime.now().year, 1999, -1)
|