progress-reporters 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 66a446c2c9ee4116e599389eef90ab5f608c4cf1
4
+ data.tar.gz: 9a7249c0fff340985ad787cbbcec102235b3035b
5
+ SHA512:
6
+ metadata.gz: 5a83ed6314162a9005a517ad286d2d5101ddddaa5da23010beabdebef1567eb55b902d8edc4c65c7961395c41fe6ae249f0ec082a7cffd7862d7773fca383741
7
+ data.tar.gz: 1d8ac329f8646b2e94f08000d7bb831088f8acf93b0f789ba5fa14d25542c216d352fe3eb30bd9bbca893fc50810b102399a2e8e5ee7ff59596f4916b10d2da8
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'pry-nav'
7
+ gem 'rake'
8
+ gem 'timecop', '~> 0.7.0'
9
+ end
10
+
11
+ group :test do
12
+ gem 'rspec'
13
+ gem 'rr'
14
+ end
@@ -0,0 +1,3 @@
1
+ == 1.0.0
2
+
3
+ * Birthday!
@@ -0,0 +1,18 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
4
+
5
+ require 'bundler'
6
+ require 'rspec/core/rake_task'
7
+ require 'rubygems/package_task'
8
+
9
+ require './lib/progress-reporters'
10
+
11
+ Bundler::GemHelper.install_tasks
12
+
13
+ task :default => :spec
14
+
15
+ desc 'Run specs'
16
+ RSpec::Core::RakeTask.new do |t|
17
+ t.pattern = './spec/**/*_spec.rb'
18
+ end
@@ -0,0 +1,9 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+ autoload :ProgressReporter, 'progress-reporters/progress_reporter'
5
+ autoload :NilProgressReporter, 'progress-reporters/nil_progress_reporter'
6
+ autoload :StagedProgressReporter, 'progress-reporters/staged_progress_reporter'
7
+ autoload :NilStagedProgressReporter, 'progress-reporters/nil_staged_progress_reporter'
8
+ autoload :MeteredProgressReporter, 'progress-reporters/metered_progress_reporter'
9
+ end
@@ -0,0 +1,117 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+
5
+ class MeteredProgressReporter < ProgressReporter
6
+ CALC_TYPES = [:avg, :moving_avg]
7
+ DEFAULT_MOVING_AVG_WINDOW_SIZE = 5
8
+ DEFAULT_CALC_TYPE = :moving_avg
9
+ DEFAULT_PRECISION = 2
10
+
11
+ attr_reader :calc_type, :window_size, :precision
12
+
13
+ def set_calc_type(calc_type)
14
+ if CALC_TYPES.include?(calc_type)
15
+ @calc_type = calc_type
16
+ self
17
+ else
18
+ raise ArgumentError, "#{calc_type} is not a supported calculation type."
19
+ end
20
+ end
21
+
22
+ def set_window_size(window_size)
23
+ @window_size = window_size
24
+ self
25
+ end
26
+
27
+ def set_precision(precision)
28
+ @precision = precision
29
+ self
30
+ end
31
+
32
+ def reset
33
+ super
34
+ @avg_sum = 0
35
+ @avg_count = 0
36
+ @avg_time_sum = 0
37
+ @avg_window = []
38
+ @avg_time_window = []
39
+ @last_timestamp = nil
40
+ @last_count = nil
41
+ end
42
+
43
+ def window_size
44
+ (@window_size || DEFAULT_MOVING_AVG_WINDOW_SIZE).to_f
45
+ end
46
+
47
+ def calc_type
48
+ @calc_type || DEFAULT_CALC_TYPE
49
+ end
50
+
51
+ def precision
52
+ @precision || DEFAULT_PRECISION
53
+ end
54
+
55
+ protected
56
+
57
+ # this won't get called if on_progress_proc is nil
58
+ def notify_of_progress(count, total)
59
+ on_progress_proc.call(
60
+ count, total, percentage(count, total), rate(count, total)
61
+ )
62
+ end
63
+
64
+ def rate(count, total)
65
+ cur_time = Time.now
66
+
67
+ rate = if @last_timestamp
68
+ time_delta = cur_time - @last_timestamp
69
+ count_delta = count - last_count
70
+ apply_calc_type(time_delta, count_delta)
71
+ else
72
+ 0
73
+ end
74
+
75
+ @last_timestamp = cur_time
76
+ @last_count = count
77
+ rate
78
+ end
79
+
80
+ def apply_calc_type(time_delta, count_delta)
81
+ case calc_type
82
+ when :avg
83
+ apply_avg(time_delta, count_delta)
84
+ when :moving_avg
85
+ apply_moving_avg(time_delta, count_delta)
86
+ end
87
+ end
88
+
89
+ def apply_avg(time_delta, count_delta)
90
+ @avg_sum += count_delta
91
+ @avg_time_sum += time_delta
92
+ @avg_count += 1
93
+
94
+ # (avg items) / (avg time) = avg items per second
95
+ answer = (@avg_sum / @avg_count) / (@avg_time_sum / @avg_count)
96
+ answer.round(precision)
97
+ end
98
+
99
+ def apply_moving_avg(time_delta, count_delta)
100
+ add_window_element(time_delta, count_delta)
101
+ avg_count = @avg_window.inject(&:+) / window_size
102
+ avg_time = @avg_time_window.inject(&:+) / window_size
103
+ (avg_count / avg_time).round(precision)
104
+ end
105
+
106
+ def add_window_element(time_delta, count_delta)
107
+ if @avg_window.size >= window_size
108
+ @avg_window.shift
109
+ @avg_time_window.shift
110
+ end
111
+
112
+ @avg_window << count_delta
113
+ @avg_time_window << time_delta
114
+ end
115
+ end
116
+
117
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+
5
+ class NilProgressReporter
6
+ def self.instance
7
+ @instance ||= new
8
+ end
9
+
10
+ def on_progress(&block)
11
+ self
12
+ end
13
+
14
+ def on_complete(&block)
15
+ self
16
+ end
17
+
18
+ def set_step(step)
19
+ self
20
+ end
21
+
22
+ def report_progress(count, total); end
23
+ def report_complete; end
24
+ def reset; end
25
+ end
26
+
27
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+
5
+ class NilStagedProgressReporter < NilProgressReporter
6
+ def on_stage_changed(&block)
7
+ self
8
+ end
9
+
10
+ def on_stage_finished(&block)
11
+ end
12
+
13
+ def set_stage(stage)
14
+ self
15
+ end
16
+
17
+ def change_stage(new_stage); end
18
+ def report_stage_finished(count, total); end
19
+ end
20
+
21
+ end
@@ -0,0 +1,76 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+
5
+ class ProgressReporter
6
+ DEFAULT_STEP = 1
7
+
8
+ attr_reader :step, :last_count, :on_progress_proc, :on_complete_proc
9
+
10
+ protected :on_progress_proc
11
+ protected :on_complete_proc
12
+
13
+ def initialize
14
+ @step = DEFAULT_STEP
15
+ reset
16
+ end
17
+
18
+ def on_progress(&block)
19
+ @on_progress_proc = block
20
+ self
21
+ end
22
+
23
+ def on_complete(&block)
24
+ @on_complete_proc = block
25
+ self
26
+ end
27
+
28
+ def set_step(step)
29
+ @step = step
30
+ self
31
+ end
32
+
33
+ def report_progress(count, total)
34
+ if on_progress_proc
35
+ notify_of_progress(count, total) if count % step == 0
36
+ end
37
+
38
+ @last_count = count
39
+ end
40
+
41
+ def report_complete
42
+ if on_complete_proc
43
+ notify_of_completion
44
+ end
45
+ end
46
+
47
+ def reset
48
+ @last_count = 0
49
+ end
50
+
51
+ protected
52
+
53
+ # this won't get called if on_progress_proc is nil
54
+ def notify_of_progress(count, total)
55
+ on_progress_proc.call(
56
+ count, total, percentage(count, total)
57
+ )
58
+ end
59
+
60
+ # this won't get called if on_complete_proc is nil
61
+ def notify_of_completion
62
+ on_complete_proc.call
63
+ end
64
+
65
+ private
66
+
67
+ def percentage(count, total, precision = 0)
68
+ if total > 0
69
+ ((count.to_f / total.to_f) * 100).round(precision)
70
+ else
71
+ 0
72
+ end
73
+ end
74
+ end
75
+
76
+ end
@@ -0,0 +1,70 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+
5
+ class StagedProgressReporter < ProgressReporter
6
+ DEFAULT_STAGE = :start
7
+
8
+ attr_reader :stage, :on_stage_changed_proc, :on_stage_finished_proc
9
+ protected :on_stage_changed_proc
10
+ protected :on_stage_finished_proc
11
+
12
+ def initialize
13
+ super
14
+ @stage = DEFAULT_STAGE
15
+ end
16
+
17
+ def set_stage(stage)
18
+ @stage = stage
19
+ self
20
+ end
21
+
22
+ def change_stage(new_stage)
23
+ if on_stage_changed_proc
24
+ notify_of_stage_change(new_stage, stage)
25
+ end
26
+
27
+ @stage = new_stage
28
+ end
29
+
30
+ def report_stage_finished(count, total)
31
+ if on_stage_finished_proc
32
+ notify_of_stage_finish(count, total)
33
+ end
34
+
35
+ @last_count = count
36
+ end
37
+
38
+ def on_stage_changed(&block)
39
+ @on_stage_changed_proc = block
40
+ self
41
+ end
42
+
43
+ def on_stage_finished(&block)
44
+ @on_stage_finished_proc = block
45
+ self
46
+ end
47
+
48
+ protected
49
+
50
+ # this won't get called if on_progress_proc is nil
51
+ def notify_of_progress(count, total)
52
+ on_progress_proc.call(
53
+ count, total, percentage(count, total), stage
54
+ )
55
+ end
56
+
57
+ # this won't get called if on_finished_proc is nil
58
+ def notify_of_stage_finish(count, total)
59
+ on_stage_finished_proc.call(
60
+ count, total, percentage(count, total), stage
61
+ )
62
+ end
63
+
64
+ # this won't get called if on_stage_changed_proc is nil
65
+ def notify_of_stage_change(new_stage, old_stage)
66
+ on_stage_changed_proc.call(new_stage, old_stage)
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+
3
+ module ProgressReporters
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ include ProgressReporters
6
+
7
+ describe ProgressReporters::MeteredProgressReporter do
8
+ before(:each) do
9
+ collector # call to instantiate collector
10
+ end
11
+
12
+ context 'with a metered reporter' do
13
+ let(:reporter) { MeteredProgressReporter.new }
14
+ let(:collector) { MeteredCollector.new(reporter) }
15
+
16
+ context 'with a staged task' do
17
+ let(:task) { MeteredTask.new(reporter) }
18
+
19
+ it 'reports progress and calculates the average rate' do
20
+ reporter.set_calc_type(:avg)
21
+ task.execute(10, 0.5, 0)
22
+
23
+ collector.progress_notifications.each_with_index do |notification, idx|
24
+ expect(notification.quantity).to eq(idx)
25
+ expect(notification.total).to eq(10)
26
+ expect(notification.percentage).to eq(idx * 10)
27
+ expect(notification.rate).to eq(idx == 0 ? 0 : 2.0)
28
+ end
29
+ end
30
+
31
+ it 'reports progress and calculates the moving average rate' do
32
+ reporter.set_calc_type(:moving_avg)
33
+ reporter.set_window_size(2)
34
+ task.execute(10, 1, 1)
35
+
36
+ collector.progress_notifications.each_with_index do |notification, idx|
37
+ expect(notification.quantity).to eq(idx)
38
+ expect(notification.total).to eq(10)
39
+ expect(notification.percentage).to eq(idx * 10)
40
+
41
+ rate = case idx
42
+ when 0 then 0.0
43
+ when 1 then 1.0
44
+ else
45
+ (1 / (0.5 + (idx - 1))).round(2)
46
+ end
47
+
48
+ expect(notification.rate).to eq(rate)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,67 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ include ProgressReporters
6
+
7
+ describe ProgressReporters::ProgressReporter do
8
+ before(:each) do
9
+ collector # call to instantiate collector
10
+ end
11
+
12
+ context 'with a linear reporter' do
13
+ let(:reporter) { ProgressReporter.new }
14
+ let(:collector) { LinearCollector.new(reporter) }
15
+
16
+ context 'with a linear task' do
17
+ let(:task) { LinearTask.new(reporter) }
18
+
19
+ it 'reports progress' do
20
+ task.execute(10)
21
+
22
+ collector.progress_notifications.each_with_index do |notification, idx|
23
+ expect(notification.quantity).to eq(idx)
24
+ expect(notification.total).to eq(10)
25
+ expect(notification.percentage).to eq(10 * idx)
26
+ end
27
+
28
+ expect(collector.complete_notification).to_not be_nil
29
+ end
30
+
31
+ it 'should only report progress every other time when step is set to 2' do
32
+ reporter.set_step(2)
33
+ task.execute(10)
34
+
35
+ expect(collector.progress_notifications.size).to eq(5)
36
+
37
+ collector.progress_notifications.each_with_index do |notification, idx|
38
+ expect(notification.quantity).to eq(idx * 2)
39
+ expect(notification.total).to eq(10)
40
+ expect(notification.percentage).to eq(10 * (idx * 2))
41
+ end
42
+ end
43
+
44
+ it 'should be able to reset itself' do
45
+ task.execute(10)
46
+ expect(reporter.last_count).to eq(9)
47
+ reporter.reset
48
+ expect(reporter.last_count).to eq(0)
49
+ end
50
+ end
51
+
52
+ context 'with a nil reporter' do
53
+ let(:reporter) { NilProgressReporter.new }
54
+ let(:collector) { LinearCollector.new(reporter) }
55
+
56
+ context 'with a linear task' do
57
+ let(:task) { LinearTask.new(reporter) }
58
+
59
+ it 'should not actually report any progress' do
60
+ task.execute(10)
61
+ expect(collector.progress_notifications).to be_empty
62
+ expect(collector.complete_notification).to be_nil
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,127 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rspec'
4
+ require 'progress-reporters'
5
+ require 'timecop'
6
+ require 'pry-nav'
7
+
8
+ RSpec.configure do |config|
9
+ config.mock_with :rr
10
+ end
11
+
12
+ LinearNotification = Struct.new(:quantity, :total, :percentage)
13
+ StagedNotification = Struct.new(:quantity, :total, :percentage, :stage)
14
+ StageChangedNotification = Struct.new(:new_stage, :old_stage)
15
+ MeteredNotification = Struct.new(:quantity, :total, :percentage, :rate)
16
+
17
+ class LinearCollector
18
+ attr_reader :progress_reporter
19
+ attr_reader :progress_notifications, :complete_notification
20
+
21
+ def initialize(progress_reporter)
22
+ @progress_notifications = []
23
+
24
+ @progress_reporter = progress_reporter
25
+ .on_progress { |*args| on_progress(*args) }
26
+ .on_complete { |*args| on_complete(*args) }
27
+
28
+ after_initialize
29
+ end
30
+
31
+ protected
32
+
33
+ def after_initialize
34
+ end
35
+
36
+ def on_progress(*args)
37
+ @progress_notifications << make_notification(*args)
38
+ end
39
+
40
+ def on_complete(*args)
41
+ if @complete_notification
42
+ raise StandardError, 'task already complete'
43
+ else
44
+ @complete_notification = make_notification(*args)
45
+ end
46
+ end
47
+
48
+ def make_notification(*args)
49
+ LinearNotification.new(*args)
50
+ end
51
+ end
52
+
53
+ class StagedCollector < LinearCollector
54
+ attr_reader :stage_changed_notifications
55
+
56
+ protected
57
+
58
+ def after_initialize
59
+ @stage_changed_notifications = []
60
+ progress_reporter.on_stage_changed { |*args| on_stage_changed(*args) }
61
+ end
62
+
63
+ def on_stage_changed(*args)
64
+ @stage_changed_notifications << StageChangedNotification.new(*args)
65
+ end
66
+
67
+ def make_notification(*args)
68
+ StagedNotification.new(*args)
69
+ end
70
+ end
71
+
72
+ class MeteredCollector < LinearCollector
73
+ def make_notification(*args)
74
+ MeteredNotification.new(*args)
75
+ end
76
+ end
77
+
78
+ class Task
79
+ attr_reader :progress_reporter
80
+
81
+ def initialize(progress_reporter)
82
+ @progress_reporter = progress_reporter
83
+ end
84
+ end
85
+
86
+ class LinearTask < Task
87
+ def execute(quantity = 10)
88
+ quantity.times do |i|
89
+ progress_reporter.report_progress(i, quantity)
90
+ end
91
+
92
+ progress_reporter.report_complete
93
+ end
94
+ end
95
+
96
+ class StagedTask < Task
97
+ def execute(stages = 2, quantity_per_stage = 10)
98
+ progress_reporter.set_stage(:stage_1)
99
+
100
+ stages.times do |stage|
101
+ quantity_per_stage.times do |i|
102
+ progress_reporter.report_progress(i, quantity_per_stage)
103
+ end
104
+
105
+ progress_reporter.report_stage_finished(
106
+ quantity_per_stage, quantity_per_stage
107
+ )
108
+
109
+ progress_reporter.change_stage(:"stage_#{stage + 2}")
110
+ end
111
+
112
+ progress_reporter.report_complete
113
+ end
114
+ end
115
+
116
+ class MeteredTask < Task
117
+ def execute(quantity = 10, delay_seconds = 1, rate_of_delay = 1)
118
+ Timecop.freeze do
119
+ quantity.times do |i|
120
+ progress_reporter.report_progress(i, quantity)
121
+ Timecop.travel(Time.now + delay_seconds + (rate_of_delay * i))
122
+ end
123
+
124
+ progress_reporter.report_complete
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,63 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ include ProgressReporters
6
+
7
+ describe ProgressReporters::StagedProgressReporter do
8
+ before(:each) do
9
+ collector # call to instantiate collector
10
+ end
11
+
12
+ context 'with a staged reporter' do
13
+ let(:reporter) { StagedProgressReporter.new }
14
+ let(:collector) { StagedCollector.new(reporter) }
15
+
16
+ context 'with a staged task' do
17
+ let(:task) { StagedTask.new(reporter) }
18
+
19
+ it 'reports progress in stages' do
20
+ task.execute(2, 10)
21
+
22
+ expect(collector.progress_notifications.size).to eq(20)
23
+
24
+ collector.progress_notifications.each_with_index do |notification, idx|
25
+ expect(notification.quantity).to eq(idx % 10)
26
+ expect(notification.total).to eq(10)
27
+ expect(notification.percentage).to eq((idx % 10) * 10)
28
+ expect(notification.stage).to eq(:"stage_#{(idx / 10) + 1}")
29
+ end
30
+
31
+ expect(collector.stage_changed_notifications.size).to eq(2)
32
+
33
+ collector.stage_changed_notifications.tap do |notifications|
34
+ notifications[0].first do |notification|
35
+ expect(notification.old_stage).to_eq(:stage_1)
36
+ expect(notification.new_stage).to eq(:stage_2)
37
+ end
38
+
39
+ notifications[1].first do |notification|
40
+ expect(notification.old_stage).to_eq(:stage_2)
41
+ expect(notification.new_stage).to eq(:stage_3)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'with a nil reporter' do
48
+ let(:reporter) { NilStagedProgressReporter.new }
49
+ let(:collector) { StagedCollector.new(reporter) }
50
+
51
+ context 'with a linear task' do
52
+ let(:task) { StagedTask.new(reporter) }
53
+
54
+ it 'should not actually report any progress' do
55
+ task.execute(10)
56
+ expect(collector.progress_notifications).to be_empty
57
+ expect(collector.stage_changed_notifications).to be_empty
58
+ expect(collector.complete_notification).to be_nil
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: progress-reporters
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cameron Dutro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Callback-oriented way to report the progress of a task.
14
+ email:
15
+ - camertron@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - Gemfile
21
+ - History.txt
22
+ - Rakefile
23
+ - lib/progress-reporters.rb
24
+ - lib/progress-reporters/metered_progress_reporter.rb
25
+ - lib/progress-reporters/nil_progress_reporter.rb
26
+ - lib/progress-reporters/nil_staged_progress_reporter.rb
27
+ - lib/progress-reporters/progress_reporter.rb
28
+ - lib/progress-reporters/staged_progress_reporter.rb
29
+ - lib/progress-reporters/version.rb
30
+ - spec/metered_progress_reporter_spec.rb
31
+ - spec/progress_reporter_spec.rb
32
+ - spec/spec_helper.rb
33
+ - spec/staged_progress_reporter_spec.rb
34
+ homepage: http://github.com/camertron
35
+ licenses: []
36
+ metadata: {}
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 2.2.2
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Callback-oriented way to report the progress of a task.
57
+ test_files: []