def calculate_error(tasks, default_estimate=0): def _ok_task(task): ok = True if not task.is_completed: ok = False if 'estimate' not in task.attributes and default_estimate == 0: ok = False if 'spent' not in task.attributes: ok = False return ok categories = {} ok_tasks = [task for task in tasks if _ok_task(task)] error = np.zeros((len(ok_tasks),)) for ti, task in enumerate(ok_tasks): try: if 'estimate' in task.attributes: td_est = parse_duration(task.attributes['estimate'][0]) est = td_est.total_seconds()/3600.0 else: est = default_estimate td_meas = parse_duration(task.attributes['spent'][0]) meas = td_meas.total_seconds()/3600.0 error[ti] = meas - est except: error[ti] = np.nan error = error[np.logical_not(np.isnan(error))] return error
def distribute_projects(all_tasks, default_estimate=0, filter_zero=True): task_distribution = group_projects(all_tasks) spent_distribution = {key:0 for key in task_distribution} estimate_distribution = {key:0 for key in task_distribution} num_distribution = {key:0 for key in task_distribution} for key, tasks in task_distribution.items(): for task in tasks: if not task.is_completed: num_distribution[key] += 1 try: if not task.is_completed: if 'estimate' in task.attributes: td_est = parse_duration(task.attributes['estimate'][0]) if 'spent' in task.attributes: sp_est = parse_duration(task.attributes['spent'][0].replace('min','m')) td_est -= sp_est if td_est < timedelta(seconds=0): td_est = timedelta(seconds=0) estimate_distribution[key] += td_est.total_seconds()/3600.0 else: if default_estimate != 0: estimate_distribution[key] += default_estimate except: pass try: if 'spent' in task.attributes: td_est = parse_duration(task.attributes['spent'][0].replace('min','m')) spent_distribution[key] += td_est.total_seconds()/3600.0 except: pass if filter_zero: spent_distribution = {key:item for key, item in spent_distribution.items() if item > 0} estimate_distribution = {key:item for key, item in estimate_distribution.items() if item > 0} num_distribution = {key:item for key, item in num_distribution.items() if item > 0} return spent_distribution, estimate_distribution, num_distribution
def next_week(search, config, default_estimate, todotxt, show_all): '''Shows the planned work that needs to be completed in the next 7 days distributed across projects, takes "spent" tag into account for non-completed tasks. [SEARCH]: A single pter-type search string, surrounded by quotes. If no SEARCH given, uses 't:+1w duebefore:+1w -@delegate -@milestone'. ''' if len(search) == 0: search = 't:+1w duebefore:+1w -@delegate -@milestone' cfg, todo = prepare(todotxt, config) nominal_day = parse_duration(cfg.get('General', 'work-day-length')) nominal_week = parse_duration(cfg.get('General', 'work-week-length')) w_h = nominal_week.days * (nominal_day.total_seconds() / 3600.0) w_h_ok = w_h * 0.75 tasks = apply_serach(cfg, todo, search) dy = 0.1 rot = 70 spent, est, num = analysis.distribute_projects( tasks, default_estimate=default_estimate, filter_zero=not show_all) fig, ax = plt.subplots() ax.set_title('Total estimated work-time next week') ax.bar([unescape(x) for x in est] + ['Total'], [est[x] for x in est] + [np.sum(est[x] for x in est)]) ax.set_ylabel('Time [h]') ax.set_xticklabels([unescape(x) for x in est] + ['[Total]'], rotation=rot) pos = ax.get_position() pos.y0 += dy ax.set_position(pos) ax.axhline(y=w_h, color='r') ax.axhline(y=w_h_ok, color='g') plt.show()
def test_parse_duration(self): text = '1h10m' then = utils.parse_duration(text) self.assertEqual(then, datetime.timedelta(hours=1, minutes=10))
def calculate_total_activity(tasks_lists, h_per_day, default_estimate=0, default_delay=5, end_date=None, adaptive=True): def _ok_task(task): ok = True if task.is_completed: ok = False if task.creation_date is None: ok = False if not ('due' in task.attributes or 't' in task.attributes): ok = False return ok today = datetime.date.today() today_np = np.array([today]) #create a master task list, remember origin tasks = [] list_index = [] tlst_inds = list(range(len(tasks_lists))) for tind, tlst in enumerate(tasks_lists): tasks += tlst list_index += [tind]*len(tlst) list_index = np.array(list_index, dtype=np.int) list_keep = np.full(list_index.shape, True, dtype=np.bool) start = [] end = [] duration = [] for ti, task in enumerate(tasks): if not _ok_task(task): list_keep[ti] = False continue if 'estimate' in task.attributes: if 'spent' in task.attributes: try: td = parse_duration(task.attributes['estimate'][0]) - parse_duration(task.attributes['spent'][0]) if td < 0: td = timedelta(seconds=0) except: td = parse_duration(task.attributes['estimate'][0]) else: td = parse_duration(task.attributes['estimate'][0]) duration.append(td.total_seconds()/3600.0) else: if default_estimate == 0: list_keep[ti] = False continue else: duration.append(default_estimate) start.append(today) if 't' in task.attributes: due = task.attributes['t'][0].strip() else: due = task.attributes['due'][0].strip() due = due.replace(',','') due = datetime.date.fromisoformat(due) if due < today: due = today + datetime.timedelta(days=default_delay) end.append(due) list_index = list_index[list_keep] duration = np.array(duration) start = np.array(list(np.datetime64(x) for x in start)) end = np.array(list(np.datetime64(x) for x in end)) work_time = np.empty(duration.shape) for ind in range(len(work_time)): work_time[ind] = np.busday_count(start[ind], end[ind])*h_per_day if work_time[ind] == 0: work_time[ind] = h_per_day activity = duration/work_time if end_date is not None: dates = np.arange(today_np[0], end_date) else: dates = np.arange(today_np[0], end.max()) total_activity = np.empty(dates.shape) sub_activites = [np.empty(dates.shape) for tind in tlst_inds] mod_start = start.copy() mod_start[mod_start < today] = today_np[0] mod_activity = activity.copy() for ind, date in enumerate(dates): #Select active tasks select = np.logical_and(date >= mod_start, date <= end) select_inds = np.argwhere(select) #this should never happen assert len(select_inds) > 0, 'what?' #calculate date-activity date_activity = np.sum(mod_activity[select]) sub_acts = [np.sum(mod_activity[np.logical_and(select, list_index==tind)]) for tind in tlst_inds] #push forward start of task to reduce activity < 100% while date_activity > 1.0 and ind < len(dates) and adaptive: break_at_end = False if len(select_inds) <= 1: #we cannot push stuff back anymore, just leave it mv = select_inds[0] mod_start[mv] = start[mv] if mod_start[mv] < today: mod_start[mv] = today_np[0] break_at_end = True else: #select the one we can push the most mv = select_inds[np.argmax(end[select_inds])][0] if date == dates[-2]: #we are at the end, and cant manage, just rest all for mmv in select_inds.flatten(): mod_start[mmv] = start[mmv] if mod_start[mmv] < today: mod_start[mmv] = today_np[0] #re-calculate activity for that task work_time[mmv] = np.busday_count(mod_start[mmv], end[mmv])*h_per_day if work_time[mmv] == 0: work_time[mmv] = h_per_day mod_activity[mmv] = duration[mmv]/work_time[mmv] break_at_end = True elif date == end[mv]: #if we cannot push even that, just reset to original start and move on mod_start[mv] = start[mv] if mod_start[mv] < today: mod_start[mv] = today_np[0] break_at_end = True else: #push task forward mod_start[mv] = dates[ind+1] #re-select (i.e. not the pushed back) select = np.logical_and(date >= mod_start, date <= end) select_inds = np.argwhere(select) #re-calculate activity for that task work_time[mv] = np.busday_count(mod_start[mv], end[mv])*h_per_day if work_time[mv] == 0: work_time[mv] = h_per_day mod_activity[mv] = duration[mv]/work_time[mv] #update activity date_activity = np.sum(mod_activity[select]) sub_acts = [np.sum(mod_activity[np.logical_and(select, list_index==tind)]) for tind in tlst_inds] if break_at_end: break total_activity[ind] = date_activity for tind in tlst_inds: sub_activites[tind][ind] = sub_acts[tind] sub_starts = [mod_start[list_index==tind] for tind in tlst_inds] sub_ends = [end[list_index==tind] for tind in tlst_inds] return dates, sub_activites, sub_starts, sub_ends, total_activity
def burndown_plot(ax, cfg, default_estimate, default_delay, todo, end, search, adaptive): if len(end) > 0: end_date = datetime.date.fromisoformat(end) else: end_date = None nominal_day = parse_duration(cfg.get('General', 'work-day-length')) nominal_week = parse_duration(cfg.get('General', 'work-week-length')) h_per_day = nominal_week.days * (nominal_day.total_seconds() / 3600.0) / 5.0 locator = mdates.AutoDateLocator(minticks=5, maxticks=15) formatter = mdates.ConciseDateFormatter(locator) ax.xaxis.set_major_locator(locator) ax.xaxis.set_major_formatter(formatter) axv_legend = False max_activity = 100 total_activity = None total_dates = None all_tasks = [apply_serach(cfg, todo, sch) for sch in search] dates, all_activity, all_start, all_end, total_activity = analysis.calculate_total_activity( all_tasks, h_per_day, default_estimate=default_estimate, end_date=end_date, default_delay=default_delay, adaptive=adaptive, ) for i, sch in enumerate(search): activity, start, end = all_activity[i], all_start[i], all_end[i] for ind in range(len(start)): if axv_legend: ax.axvline(start[ind], color='g', alpha=0.1) ax.axvline(end[ind], color='r', alpha=0.1) else: ax.axvline(start[ind], color='g', alpha=0.1, label='Task start') ax.axvline(end[ind], color='r', alpha=0.1, label='Task due') axv_legend = True if total_activity.max() * 100 > max_activity: max_activity = total_activity.max() * 100 ax.plot(dates, activity * 100, label=sch) if len(search) > 1: ax.plot(dates, total_activity * 100, '--k', label='Total activity') ax.set_title('Nominal workload task burn-down') ax.set_ylabel('Full-time workload [\%]') ax.set_ylim([0, max_activity]) ax.set_xlim([datetime.datetime.today(), end_date]) ax.legend() return ax