Class: Central::Support::IterationService

Inherits:
Object
  • Object
show all
Defined in:
lib/central/support/iteration_service.rb

Constant Summary collapse

DAYS_IN_WEEK =
(1.week / 1.day)
VELOCITY_ITERATIONS =
3

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(project, since = nil) ⇒ IterationService

Returns a new instance of IterationService.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/central/support/iteration_service.rb', line 14

def initialize(project, since = nil)
  @project = project

  relation = project.stories.includes(:owned_by)
  relation = relation.where('accepted_at > ? or accepted_at is null', since) if since
  @stories = relation.to_a

  @accepted_stories = @stories.
    select { |story| story.column == '#done' }.
    select { |story| story.accepted_at < iteration_start_date(Time.current) }

  calculate_iterations!
  fix_owner!

  @stories.each { |s| s.iteration_service = self }
  @backlog = ( @stories - @accepted_stories.select { |s| s.column == '#done' } ).sort_by(&:position)
end

Instance Attribute Details

#projectObject (readonly)

Returns the value of attribute project.



7
8
9
# File 'lib/central/support/iteration_service.rb', line 7

def project
  @project
end

Instance Method Details

#backlog_iterations(velocity_value = velocity) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/central/support/iteration_service.rb', line 153

def backlog_iterations(velocity_value = velocity)
  velocity_value = 1 if velocity_value < 1
  @backlog_iterations ||= {}
  # mimics the project.js rebuildIteration() function
  @backlog_iterations[velocity_value] ||= begin
    current_iteration = Iteration.new(self, current_iteration_number, velocity_value)
    backlog_iteration = Iteration.new(self, current_iteration_number + 1, velocity_value)
    iterations = [current_iteration, backlog_iteration]
    @backlog.
      select { |story| story.column != '#chilly_bin' }.
      each do |story|
      if current_iteration.can_take_story?(story)
        current_iteration << story
      else
        if !backlog_iteration.can_take_story?(story)
          # Iterations sometimes 'overflow', i.e. an iteration may contain a
          # 5 point story but the project velocity is 1.  In this case, the
          # next iteration that can have a story added is the current + 4.
          next_number       = backlog_iteration.number + 1 + (backlog_iteration.overflows_by / velocity_value).ceil
          backlog_iteration = Iteration.new(self, next_number, velocity_value)
          iterations << backlog_iteration
        end
        backlog_iteration << story
      end
    end
    iterations
  end
end

#bugs_impact(stories) ⇒ Object



101
102
103
104
105
106
107
108
109
# File 'lib/central/support/iteration_service.rb', line 101

def bugs_impact(stories)
  stories.map do |story|
    if Story::ESTIMABLE_TYPES.include? story.story_type
      0
    else
      1
    end
  end
end

#calculate_iterations!Object



60
61
62
63
64
65
66
67
# File 'lib/central/support/iteration_service.rb', line 60

def calculate_iterations!
  @accepted_stories.each do |record|
    iteration_number            = iteration_number_for_date(record.accepted_at)
    iteration_start_date        = date_for_iteration_number(iteration_number)
    record.iteration_number     = iteration_number
    record.iteration_start_date = iteration_start_date
  end
end

#current_iteration_detailsObject



182
183
184
185
186
187
188
189
# File 'lib/central/support/iteration_service.rb', line 182

def current_iteration_details
  current_iteration = backlog_iterations.first
  %w(started finished delivered accepted rejected).reduce({}) do |data, state|
    data.merge(state => current_iteration.
               select { |story| story.state == state }.
               reduce(0) { |points, story| points + (story.estimate || 0) } )
  end
end

#current_iteration_numberObject



56
57
58
# File 'lib/central/support/iteration_service.rb', line 56

def current_iteration_number
  iteration_number_for_date(Time.current)
end

#date_for_iteration_number(iteration_number) ⇒ Object



51
52
53
54
# File 'lib/central/support/iteration_service.rb', line 51

def date_for_iteration_number(iteration_number)
  difference = (iteration_length * DAYS_IN_WEEK) * (iteration_number - 1)
  iteration_start_date + difference.days
end

#fix_owner!Object

FIXME must figure out why the Story allows a nil owner in delivered states



70
71
72
73
74
75
# File 'lib/central/support/iteration_service.rb', line 70

def fix_owner!
  @dummy_user ||= User.find_or_create_by!(username: "dummy", email: "[email protected]", name: "Dummy", initials: "XX")
  @accepted_stories.
    select { |record| record.owned_by.nil? }.
    each   { |record| record.owned_by = @dummy_user }
end

#group_by_bugsObject



111
112
113
114
115
116
117
118
119
120
# File 'lib/central/support/iteration_service.rb', line 111

def group_by_bugs
  @group_by_bugs ||=  @accepted_stories.
    group_by { |story| story.iteration_number }.
    reduce({}) do |group, iteration|
      group.merge(iteration.first => bugs_impact(iteration.last))
    end.
    reduce({}) do |group, iteration|
      group.merge(iteration.first => iteration.last.reduce(&:+))
    end
end

#group_by_developerObject



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/central/support/iteration_service.rb', line 135

def group_by_developer
  @group_by_developer ||= begin
    min_iteration = @accepted_stories.map(&:iteration_number).min
    max_iteration = @accepted_stories.map(&:iteration_number).max
    @accepted_stories.
      group_by { |story| story.owned_by.name }.
      map do |owner|
        # all multiple series must have all the same keys or they will mess the graph
        data = (min_iteration..max_iteration).reduce({}) { |group, key| group.merge(key => 0)}
        owner.last.group_by { |story| story.iteration_number }.
          each do |iteration|
            data[iteration.first] = stories_estimates(iteration.last).reduce(&:+)
          end
        { name: owner.first, data: data }
      end
  end
end

#group_by_iterationObject



77
78
79
80
81
82
83
# File 'lib/central/support/iteration_service.rb', line 77

def group_by_iteration
  @group_by_iteration ||= @accepted_stories.
    group_by { |story| story.iteration_number }.
    reduce({}) do |group, iteration|
      group.merge(iteration.first => stories_estimates(iteration.last))
    end
end

#group_by_velocityObject



95
96
97
98
99
# File 'lib/central/support/iteration_service.rb', line 95

def group_by_velocity
  @group_by_velocity ||= group_by_iteration.reduce({}) do |group, iteration|
    group.merge(iteration.first => iteration.last.reduce(&:+))
  end
end

#iteration_number_for_date(compare_date) ⇒ Object



44
45
46
47
48
49
# File 'lib/central/support/iteration_service.rb', line 44

def iteration_number_for_date(compare_date)
  compare_date      = compare_date.to_time if compare_date.is_a?(Date)
  days_apart        = ( compare_date - iteration_start_date ) / 1.day
  days_in_iteration = iteration_length * DAYS_IN_WEEK
  ( days_apart / days_in_iteration ).floor + 1
end

#iteration_start_date(date = nil) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
# File 'lib/central/support/iteration_service.rb', line 32

def iteration_start_date(date = nil)
  date = start_date if date.nil?
  iteration_start_date = date.beginning_of_day
  if start_date.wday != iteration_start_day
    day_difference = start_date.wday - iteration_start_day
    day_difference += DAYS_IN_WEEK if day_difference < 0

    iteration_start_date -= day_difference.days
  end
  iteration_start_date
end

#standard_deviation(groups = [], sample = false) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
# File 'lib/central/support/iteration_service.rb', line 191

def standard_deviation(groups = [], sample = false)
  return 0 if groups.empty?
  # algorithm: https://www.mathsisfun.com/data/standard-deviation-formulas.html
  #
  mean            = groups.sum.to_f / groups.size.to_f
  differences_sqr = groups.map { |velocity| (velocity.to_f - mean) ** 2 }
  count = sample ? (groups.size - 1) : groups.size
  variance        = differences_sqr.sum / count.to_f

  Math.sqrt(variance)
end

#stories_estimates(stories) ⇒ Object



85
86
87
88
89
90
91
92
93
# File 'lib/central/support/iteration_service.rb', line 85

def stories_estimates(stories)
  stories.map do |story|
    if Story::ESTIMABLE_TYPES.include? story.story_type
      story.estimate || 0
    else
      0
    end
  end
end

#velocity(number_of_iterations = VELOCITY_ITERATIONS) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/central/support/iteration_service.rb', line 122

def velocity(number_of_iterations = VELOCITY_ITERATIONS)
  @velocity ||= {}
  @velocity[number_of_iterations] ||= begin
    number_of_iterations = group_by_iteration.size if number_of_iterations > group_by_iteration.size
    return 1 if number_of_iterations.zero?

    sum = group_by_velocity.values.slice((-1 * number_of_iterations)..-1).sum

    velocity = (sum / number_of_iterations).floor
    velocity < 1 ? 1 : velocity
  end
end

#volatility(number_of_iterations = VELOCITY_ITERATIONS) ⇒ Object



203
204
205
206
207
208
209
210
211
212
# File 'lib/central/support/iteration_service.rb', line 203

def volatility(number_of_iterations = VELOCITY_ITERATIONS)
  number_of_iterations = group_by_velocity.size if number_of_iterations > group_by_velocity.size

  is_sample       = number_of_iterations != group_by_velocity.size
  last_iterations = group_by_velocity.values.reverse.take(number_of_iterations)
  std_dev         = standard_deviation(last_iterations, is_sample)
  velocity_value  = velocity(number_of_iterations)

  ( std_dev / velocity_value )
end