Class: BoardMovementCalculator
- Inherits:
-
Object
- Object
- BoardMovementCalculator
- Defined in:
- lib/jirametrics/board_movement_calculator.rb
Instance Attribute Summary collapse
-
#board ⇒ Object
readonly
Returns the value of attribute board.
-
#issues ⇒ Object
readonly
Returns the value of attribute issues.
-
#today ⇒ Object
readonly
Returns the value of attribute today.
Instance Method Summary collapse
- #age_data_for(percentage:) ⇒ Object
-
#ages_of_issues_when_leaving_column(column_index:, today:) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity.
-
#find_current_column_and_entry_time_in_column(issue) ⇒ Object
Figure out what column this is issue is currently in and what time it entered that column.
- #forecasted_days_remaining_and_message(issue:, today:) ⇒ Object
-
#initialize(board:, issues:, today:) ⇒ BoardMovementCalculator
constructor
A new instance of BoardMovementCalculator.
- #label_days(days) ⇒ Object
- #moves_backwards?(issue) ⇒ Boolean
- #stack_data(data_list) ⇒ Object
- #stacked_age_data_for(percentages:) ⇒ Object
Constructor Details
#initialize(board:, issues:, today:) ⇒ BoardMovementCalculator
Returns a new instance of BoardMovementCalculator.
6 7 8 9 10 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 6 def initialize board:, issues:, today: @board = board @issues = issues.select { |issue| issue.board == board && issue.done? && !moves_backwards?(issue) } @today = today end |
Instance Attribute Details
#board ⇒ Object (readonly)
Returns the value of attribute board.
4 5 6 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 4 def board @board end |
#issues ⇒ Object (readonly)
Returns the value of attribute issues.
4 5 6 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 4 def issues @issues end |
#today ⇒ Object (readonly)
Returns the value of attribute today.
4 5 6 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 4 def today @today end |
Instance Method Details
#age_data_for(percentage:) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 48 def age_data_for percentage: data = [] board.visible_columns.each_with_index do |_column, column_index| ages = ages_of_issues_when_leaving_column column_index: column_index, today: today if ages.empty? data << 0 else index = ((ages.size - 1) * percentage / 100).to_i data << ages[index] end end data end |
#ages_of_issues_when_leaving_column(column_index:, today:) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 64 def ages_of_issues_when_leaving_column column_index:, today: # Why are complexity warnings disabled? This is an inherently sequential run of guard clauses # feeding a single age calculation. Pulling pieces out (the skip rules, the end-date case) would # scatter the linear "compute the column times -> apply the skip rules -> pick the end date -> age" # story across single-use methods and read worse, so we keep it whole. this_column = board.visible_columns[column_index] next_column = board.visible_columns[column_index + 1] @issues.filter_map do |issue| this_column_start = issue.first_time_in_or_right_of_column(this_column.name)&.time next_column_start = next_column.nil? ? nil : issue.first_time_in_or_right_of_column(next_column.name)&.time issue_start, issue_done = issue.started_stopped_times # Skip if we can't tell when it started. next if issue_start.nil? # Skip if it never entered this column next if this_column_start.nil? # Skip if it left this column before the item is considered started. next 0 if next_column_start && next_column_start <= issue_start # Skip if it was already done by the time it got to this column or it became done when it got to this column next if issue_done && issue_done <= this_column_start end_date = if next_column_start.nil? # If this is the last column then base age against today today elsif issue_done && issue_done < next_column_start # it completed while in this column issue_done.to_date else # It passed through this whole column next_column_start.to_date end (end_date - issue_start.to_date).to_i + 1 end.sort end |
#find_current_column_and_entry_time_in_column(issue) ⇒ Object
Figure out what column this is issue is currently in and what time it entered that column. We need this for aging and forecasting purposes
107 108 109 110 111 112 113 114 115 116 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 107 def find_current_column_and_entry_time_in_column issue column = board.visible_columns.find { |c| c.status_ids.include?(issue.status.id) } return [] if column.nil? # This issue isn't visible on the board status_ids = column.status_ids entry_at = issue.changes.reverse.find { |change| change.status? && status_ids.include?(change.value_id) }&.time [column.name, entry_at] end |
#forecasted_days_remaining_and_message(issue:, today:) ⇒ Object
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 122 def issue:, today: return [nil, 'Already done'] if issue.done? likely_age_data = age_data_for percentage: 85 column_name, entry_time = find_current_column_and_entry_time_in_column issue return [nil, 'This issue is not visible on the board. No way to predict when it will be done.'] if column_name.nil? # This condition has been reported in production so we have a check for it. Having said that, we have no # idea what conditions might make this possible and so there is no test for it. if entry_time.nil? = "Unable to determine the time this issue entered column #{column_name.inspect} so no way to " \ 'predict when it will be done' return [nil, ] end age_in_column = (today - entry_time.to_date).to_i + 1 = nil column_index = board.visible_columns.index { |c| c.name == column_name } last_non_zero_datapoint = likely_age_data.reverse.find { |d| !d.zero? } return [nil, 'There is no historical data for this board. No forecast can be made.'] if last_non_zero_datapoint.nil? remaining_in_current_column = likely_age_data[column_index] - age_in_column if remaining_in_current_column.negative? = "This item is an outlier at #{label_days issue.board.cycletime.age(issue, today: today)} " \ "in the #{column_name.inspect} column. Most items on this board have left this column in " \ "#{label_days likely_age_data[column_index]} or less, so we cannot forecast when it will be done." remaining_in_current_column = 0 return [nil, ] end forecasted_days = last_non_zero_datapoint - likely_age_data[column_index] + remaining_in_current_column [forecasted_days, ] end |
#label_days(days) ⇒ Object
118 119 120 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 118 def label_days days "#{days} day#{'s' unless days == 1}" end |
#moves_backwards?(issue) ⇒ Boolean
12 13 14 15 16 17 18 19 20 21 22 23 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 12 def moves_backwards? issue started, = issue.started_stopped_times return false unless started # filter_map drops any status that isn't on a visible column (it disappeared from the board for a bit), # so a dip either side of that gap is still seen as consecutive columns. columns = issue.status_changes .select { |change| change.time >= started } .filter_map { |change| board.visible_columns.index { |c| c.status_ids.include?(change.value_id) } } columns.each_cons(2).any? { |previous, current| current < previous } end |
#stack_data(data_list) ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 33 def stack_data data_list remainder = nil data_list.collect do |percentage, data| unless remainder.nil? data = (0...data.length).collect do |i| data[i] - remainder[i] end end remainder = data [percentage, data] end end |
#stacked_age_data_for(percentages:) ⇒ Object
25 26 27 28 29 30 31 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 25 def stacked_age_data_for percentages: data_list = percentages.sort.collect do |percentage| [percentage, age_data_for(percentage: percentage)] end stack_data data_list end |