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
-
#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
51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 51 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
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 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 66 def ages_of_issues_when_leaving_column column_index:, today: 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.board.cycletime.started_stopped_times(issue) # 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 = case # rubocop:disable Style/EmptyCaseCondition when next_column_start.nil? # If this is the last column then base age against today today when 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
104 105 106 107 108 109 110 111 112 113 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 104 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
119 120 121 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 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 119 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
115 116 117 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 115 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 24 25 26 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 12 def moves_backwards? issue started, stopped = issue.board.cycletime.started_stopped_times(issue) return false unless started previous_column = nil issue.status_changes.each do |change| column = board.visible_columns.index { |c| c.status_ids.include?(change.value_id) } next if change.time < started next if column.nil? # It disappeared from the board for a bit return true if previous_column && column && column < previous_column previous_column = column end false end |
#stack_data(data_list) ⇒ Object
36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 36 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
28 29 30 31 32 33 34 |
# File 'lib/jirametrics/board_movement_calculator.rb', line 28 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 |