Skip to content

Commit efd02e8

Browse files
authored
Merge pull request #22 from jhale1805/dev
Improve date handling
2 parents bcbf221 + da9a8e9 commit efd02e8

File tree

4 files changed

+92
-51
lines changed

4 files changed

+92
-51
lines changed

src/github_projects_burndown_chart/chart/burndown.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,42 @@
33

44
from config import config
55
from gh.project import Project
6+
from util.dates import parse_to_local, parse_to_utc
7+
68

79
class BurndownChart:
810

911
def __init__(self, project: Project):
10-
# Initialize important dates
11-
self.start_date = datetime.strptime(
12-
config['settings']['sprint_start_date'],
13-
'%Y-%m-%d')
14-
self.end_date = datetime.strptime(
15-
config['settings']['sprint_end_date'],
16-
'%Y-%m-%d')
17-
self.project = project
18-
12+
self.start_date_utc: datetime = parse_to_utc(
13+
config['settings']['sprint_start_date'])
14+
self.end_date_utc: datetime = parse_to_utc(
15+
config['settings']['sprint_end_date'])
16+
17+
self.project: Project = project
18+
1919
def render(self):
20-
outstanding_points_by_day = self.project.outstanding_points_by_day(
21-
self.start_date,
22-
self.end_date)
20+
outstanding_points_by_day = self.project.outstanding_points_by_date(
21+
self.start_date_utc,
22+
self.end_date_utc)
2323
# Load date dict for priority values with x being range of how many days are in sprint
2424
x = list(range(len(outstanding_points_by_day.keys())))
2525
y = list(outstanding_points_by_day.values())
2626

2727
# Plot point values for sprint along xaxis=range yaxis=points over time
2828
plt.plot(x, y)
2929
plt.axline((x[0], self.project.total_points),
30-
slope=-(self.project.total_points/(len(y)-1)),
31-
color="green",
32-
linestyle=(0, (5, 5)))
30+
slope=-(self.project.total_points/(len(y)-1)),
31+
color="green",
32+
linestyle=(0, (5, 5)))
3333

3434
# Set sprint beginning
3535
plt.ylim(ymin=0)
3636
plt.xlim(xmin=x[0], xmax=x[-1])
3737

3838
# Replace xaxis range for date matching to range value
39-
plt.xticks(x, outstanding_points_by_day.keys())
39+
date_labels = [str(parse_to_local(date))[:10]
40+
for date in outstanding_points_by_day.keys()]
41+
plt.xticks(x, date_labels)
4042
plt.xticks(rotation=90)
4143

4244
# Set titles and labels
Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from datetime import datetime, timedelta
2+
from typing import Dict
3+
from dateutil.parser import isoparse
24

35
from config import config
6+
from util.dates import TODAY_UTC
47

58

69
class Project:
@@ -17,33 +20,52 @@ def __parse_columns(self, project_data):
1720
def total_points(self):
1821
return sum([column.get_total_points() for column in self.columns])
1922

20-
def points_completed_by_date(self, start_date, end_date):
21-
points_completed_by_date = {
22-
str(date)[:10] : 0
23-
for date in [
24-
start_date + timedelta(days=x)
25-
for x in range(0, (end_date - start_date).days + 1)
26-
]
27-
}
28-
for column in self.columns:
29-
for card in column.cards:
30-
if card.closedAt:
31-
date_str = str(card.closedAt)[:10]
32-
points_completed_by_date[date_str] += card.points
23+
def points_completed_by_date(self, start_date: datetime, end_date: datetime) -> Dict[datetime, int]:
24+
"""Computes the number of points completed by date.
25+
Basically the data behind a burnup chart for the given date range.
26+
27+
Args:
28+
start_date (datetime): The start date of the chart in UTC.
29+
end_date (datetime): The end date of the chart in UTC.
30+
31+
Returns:
32+
Dict[datetime, int]: A dictionary of date and points completed.
33+
"""
34+
points_completed_by_date = {}
35+
36+
cards = [card for column in self.columns for card in column.cards]
37+
completed_cards = [card for card in cards if card.closedAt is not None]
38+
sprint_dates = [start_date + timedelta(days=x)
39+
# The +1 includes the end_date in the list
40+
for x in range(0, (end_date - start_date).days + 1)]
41+
for date in sprint_dates:
42+
# Get the issues completed before midnight on the given date.
43+
date_23_59 = date + timedelta(hours=23, minutes=59)
44+
cards_done_by_date = [card for card in completed_cards
45+
if card.closedAt <= date_23_59]
46+
points_completed_by_date[date] = sum([card.points for card
47+
in cards_done_by_date])
3348
return points_completed_by_date
3449

35-
def outstanding_points_by_day(self, start_date, end_date):
36-
outstanding_points_by_day = {}
37-
points_completed = 0
38-
points_completed_by_date = self.points_completed_by_date(start_date, end_date)
39-
current_date = datetime.now()
40-
for date in points_completed_by_date:
41-
points_completed += points_completed_by_date[date]
42-
if datetime.strptime(date, '%Y-%m-%d') < current_date:
43-
outstanding_points_by_day[date] = self.total_points - points_completed
44-
else:
45-
outstanding_points_by_day[date] = None
46-
return outstanding_points_by_day
50+
def outstanding_points_by_date(self, start_date: datetime, end_date: datetime) -> Dict[datetime, int]:
51+
"""Computes the number of points remaining to be completed by date.
52+
Basically the data behind a burndown chart for the given date range.
53+
54+
Args:
55+
start_date (datetime): The start date of the chart in UTC.
56+
end_date (datetime): The end date of the chart in UTC.
57+
58+
Returns:
59+
Dict[datetime, int]: A dictionary of date and points remaining.
60+
"""
61+
points_completed_by_date = self.points_completed_by_date(
62+
start_date, end_date)
63+
today_23_59 = TODAY_UTC + timedelta(hours=23, minutes=59)
64+
return {
65+
date: self.total_points - points_completed_by_date[date]
66+
if date <= today_23_59 else None
67+
for date in points_completed_by_date
68+
}
4769

4870

4971
class Column:
@@ -69,17 +91,13 @@ def __init__(self, card_data):
6991
def __parse_createdAt(self, card_data):
7092
createdAt = None
7193
if card_data.get('createdAt'):
72-
createdAt = datetime.strptime(
73-
card_data['createdAt'][:10],
74-
'%Y-%m-%d')
94+
createdAt = isoparse(card_data['createdAt'])
7595
return createdAt
7696

7797
def __parse_closedAt(self, card_data):
7898
closedAt = None
7999
if card_data.get('closedAt'):
80-
closedAt = datetime.strptime(
81-
card_data['closedAt'][:10],
82-
'%Y-%m-%d')
100+
closedAt = isoparse(card_data['closedAt'])
83101
return closedAt
84102

85103
def __parse_points(self, card_data):
@@ -89,7 +107,7 @@ def __parse_points(self, card_data):
89107
card_points = 1
90108
else:
91109
card_labels = card_data.get('labels', {"nodes": []})['nodes']
92-
for label in card_labels:
93-
if points_label in label['name']:
94-
card_points += int(label['name'][len(points_label):])
95-
return card_points
110+
card_points = sum([int(label['name'][len(points_label):])
111+
for label in card_labels
112+
if points_label in label['name']])
113+
return card_points

src/github_projects_burndown_chart/util/__init__.py

Whitespace-only changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from datetime import datetime, timedelta, timezone
2+
from dateutil import parser
3+
4+
UTC_OFFSET: timedelta = datetime.utcnow() - datetime.now()
5+
"""Local time + UTC_OFFSET = UTC Time"""
6+
7+
def parse_to_utc(date_string: str) -> datetime:
8+
"""
9+
Parse a date string to UTC time.
10+
"""
11+
raw_datetime = parser.parse(date_string) + UTC_OFFSET
12+
datetime_utc = raw_datetime.replace(tzinfo=timezone.utc)
13+
return datetime_utc
14+
15+
def parse_to_local(datetime_utc: datetime) -> datetime:
16+
"""
17+
Parse a datetime object to local time.
18+
"""
19+
return datetime_utc.astimezone()
20+
21+
TODAY_UTC: datetime = parse_to_utc(datetime.today().strftime('%Y-%m-%d'))

0 commit comments

Comments
 (0)