add streak-releted basic functionality
This commit is contained in:
parent
98c9c1faee
commit
5eee7176d4
|
@ -1,8 +1,10 @@
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from common.utils import generate_split_ranges
|
||||||
|
|
||||||
dateformat: str = "%d/%m/%Y"
|
dateformat: str = "%d/%m/%Y"
|
||||||
datetimeformat: str = "%d/%m/%Y %H:%M"
|
datetimeformat: str = "%d/%m/%Y %H:%M"
|
||||||
timeformat: str = "%H:%M"
|
timeformat: str = "%H:%M"
|
||||||
|
@ -84,3 +86,80 @@ def local_strftime(datetime: datetime, format: str = datetimeformat) -> str:
|
||||||
return timezone.localtime(datetime).strftime(format)
|
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}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
from datetime import date
|
||||||
from random import choices
|
from random import choices
|
||||||
from string import ascii_lowercase
|
from string import ascii_lowercase
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable, Generator, TypeVar
|
||||||
|
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
|
@ -145,3 +146,23 @@ def truncate_with_popover(input_string: str) -> str:
|
||||||
|
|
||||||
def randomid(seed: str = "", length: int = 10) -> str:
|
def randomid(seed: str = "", length: int = 10) -> str:
|
||||||
return seed + "".join(choices(ascii_lowercase, k=length))
|
return seed + "".join(choices(ascii_lowercase, k=length))
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", str, int, date)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_split_ranges(
|
||||||
|
value_list: list[T], split_points: list[T]
|
||||||
|
) -> Generator[tuple[T, T], None, None]:
|
||||||
|
for x in range(0, len(split_points) + 1):
|
||||||
|
if x == 0:
|
||||||
|
start = 0
|
||||||
|
elif x >= len(split_points):
|
||||||
|
start = value_list.index(split_points[x - 1]) + 1
|
||||||
|
else:
|
||||||
|
start = value_list.index(split_points[x - 1]) + 1
|
||||||
|
try:
|
||||||
|
end = value_list.index(split_points[x])
|
||||||
|
except IndexError:
|
||||||
|
end = len(value_list)
|
||||||
|
yield (value_list[start], value_list[end - 1])
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from common.time import streak_bruteforce
|
||||||
|
from games.models import Session
|
||||||
|
|
||||||
|
all = Session.objects.filter(timestamp_start__gt="1970-01-01")
|
||||||
|
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for session in all:
|
||||||
|
current = session.timestamp_start
|
||||||
|
data.append(current.date())
|
||||||
|
|
||||||
|
start = time.time_ns()
|
||||||
|
start_cpu = time.process_time_ns()
|
||||||
|
print(streak_bruteforce(data))
|
||||||
|
end = time.time_ns()
|
||||||
|
end_cpu = time.process_time_ns()
|
||||||
|
print(
|
||||||
|
f"Processed {all.count()} items in {((end - start)/ 1_000_000_000):.10f} seconds and {((end_cpu - start_cpu)/ 1_000_000_000):.10f} seconds of process time."
|
||||||
|
)
|
|
@ -0,0 +1,60 @@
|
||||||
|
import unittest
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from common.time import daterange, streak_bruteforce
|
||||||
|
|
||||||
|
|
||||||
|
class StreakTest(unittest.TestCase):
|
||||||
|
streak = streak_bruteforce
|
||||||
|
|
||||||
|
def test_daterange_exclusive(self):
|
||||||
|
d = daterange(date(2024, 8, 1), date(2024, 8, 3))
|
||||||
|
self.assertEqual(
|
||||||
|
d,
|
||||||
|
[date(2024, 8, 1), date(2024, 8, 2)],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_daterange_inclusive(self):
|
||||||
|
d = daterange(date(2024, 8, 1), date(2024, 8, 3), end_inclusive=True)
|
||||||
|
self.assertEqual(
|
||||||
|
d,
|
||||||
|
[date(2024, 8, 1), date(2024, 8, 2), date(2024, 8, 3)],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_1day_streak(self):
|
||||||
|
self.assertEqual(streak([date(2024, 8, 1)])["days"], 1)
|
||||||
|
|
||||||
|
def test_2day_streak(self):
|
||||||
|
self.assertEqual(streak([date(2024, 8, 1), date(2024, 8, 2)])["days"], 2)
|
||||||
|
|
||||||
|
def test_31day_streak(self):
|
||||||
|
self.assertEqual(
|
||||||
|
streak(daterange(date(2024, 8, 1), date(2024, 8, 31), end_inclusive=True))[
|
||||||
|
"days"
|
||||||
|
],
|
||||||
|
31,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_5day_streak_in_10_days(self):
|
||||||
|
d = daterange(
|
||||||
|
date(2024, 8, 1), date(2024, 8, 5), end_inclusive=True
|
||||||
|
) + daterange(date(2024, 8, 7), date(2024, 8, 10), end_inclusive=True)
|
||||||
|
self.assertEqual(streak(d)["days"], 5)
|
||||||
|
|
||||||
|
def test_10day_streak_in_31_days(self):
|
||||||
|
d = daterange(date(2024, 8, 1), date(2024, 8, 31), end_inclusive=True)
|
||||||
|
d.remove(date(2024, 8, 8))
|
||||||
|
d.remove(date(2024, 8, 15))
|
||||||
|
d.remove(date(2024, 8, 21))
|
||||||
|
self.assertEqual(streak(d)["days"], 10)
|
||||||
|
|
||||||
|
def test_10day_streak_in_31_days_with_consecutive_missing(self):
|
||||||
|
d = daterange(date(2024, 8, 1), date(2024, 8, 31), end_inclusive=True)
|
||||||
|
d.remove(date(2024, 8, 4))
|
||||||
|
d.remove(date(2024, 8, 5))
|
||||||
|
d.remove(date(2024, 8, 6))
|
||||||
|
d.remove(date(2024, 8, 7))
|
||||||
|
d.remove(date(2024, 8, 8))
|
||||||
|
d.remove(date(2024, 8, 15))
|
||||||
|
d.remove(date(2024, 8, 21))
|
||||||
|
self.assertEqual(streak(d)["days"], 10)
|
Loading…
Reference in New Issue