progressor 0.0.1 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa7eacefe70e6b186f9a72625d6158db19b7f9b45b327c397b7b4402dc56bc47
4
- data.tar.gz: 5cb7a42fffe3d6650a48454e41c73d8f74e7cb72fdf521fb2fdd6e59bb2cae8c
3
+ metadata.gz: 54e39588afa295d9501f32e4a183834d030b536cb902fbc133be2e61d2b97745
4
+ data.tar.gz: 5732a7f82cd05f0ced057785595d2d4724264ddef570c69fca086823b76318fd
5
5
  SHA512:
6
- metadata.gz: 2d11d38113127328cb2d0cd6b6d98652f9e6010816aebdbbd0c44cc27b331ba03c9b961308f2ec71e6abb5f0b0694f4952132214a126cdbecb5d062741e7cbe0
7
- data.tar.gz: cba7008ee6ca3151ba3c89159dc7835d8d76b5cb981da4017d3dffe0fdb14a9bf0c3ef9e18a6f532ab220f4abececc487a8145e86e34b77b226b9a81f9473f13
6
+ metadata.gz: e9339fc8632bdd3b0d1c0ab47d5c95d987e6db3f5383f59541511341735c105ebce68a6544eed20a0614f8ee12974d91f9fc65dfe58f4d5e662d96a02746092a
7
+ data.tar.gz: 5b58b596f4596bccccb352ec2c8ec6949573ad898677b228fcd43073e8b141d561c397b428d5f16dfe53dd7134e2fdb430f2be51c1a0013a1537e95f98d724ae
data/README.md CHANGED
@@ -1,8 +1,34 @@
1
- A very basic library to measure loops in a long-running task.
1
+ [![Build Status](https://travis-ci.org/AndrewRadev/progressor.svg?branch=master)](https://travis-ci.org/AndrewRadev/progressor)
2
+ [![Gem Version](https://badge.fury.io/rb/progressor.svg)](https://badge.fury.io/rb/progressor)
2
3
 
3
- *Note: Very incomplete, so mostly for personal usage. Will hopefully flesh it out, write tests, configuration, etc, at some point (PRs welcome). Until then, a similar library can be found here: https://github.com/mkdynamic/ke*
4
+ Full documentation for the latest released version can be found at: https://www.rubydoc.info/gems/progressor
4
5
 
5
- Example usage:
6
+ ## Basic example
7
+
8
+ Here's an example long-running task:
9
+
10
+ ``` ruby
11
+ Product.find_each do |product|
12
+ next if product.not_something_we_want_to_process?
13
+ product.calculate_interesting_stats
14
+ end
15
+ ```
16
+
17
+ In order to understand how it's progressing, we might add some print statements:
18
+
19
+ ``` ruby
20
+ Product.find_each do |product|
21
+ if product.not_something_we_want_to_process?
22
+ puts "Skipping product: #{product.id}"
23
+ next
24
+ end
25
+
26
+ puts "Working on product: #{product.id}"
27
+ product.calculate_interesting_stats
28
+ end
29
+ ```
30
+
31
+ This gives us some indication of progress, but no idea how much time is left. We could take a count and maintain a manual index, and then eyeball it based on how fast the numbers are adding up. Progressor automates that process:
6
32
 
7
33
  ``` ruby
8
34
  progressor = Progressor.new(total_count: Product.count)
@@ -20,12 +46,79 @@ Product.find_each do |product|
20
46
  end
21
47
  ```
22
48
 
23
- Example output:
49
+ Each invocation of `run` measures how long its block took and records it. The yielded `progress` parameter is an object that can be `to_s`-ed to provide progress information.
50
+
51
+ The output might look like this:
52
+
53
+ ```
54
+ ...
55
+ [0038/1000, (004%), t/i: 0.5s, ETA: 8m:00s] Product 38
56
+ [0039/1000, (004%), t/i: 0.5s, ETA: 7m:58s] Product 39
57
+ [0040/1000, (004%), t/i: 0.5s, ETA: 7m:57s] Product 40
58
+ ...
59
+ ```
60
+
61
+ You can check the documentation for the [Progressor](https://www.rubydoc.info/gems/progressor/Progressor) class for details on the methods you can call to get the individual pieces of data shown in the report.
62
+
63
+ ## Limited and unlimited sequences
64
+
65
+ Initializing a `Progressor` with a provided `total_count:` parameter gives you a limited sequence, which can give you not only a progress report, but an estimation of when it'll be done:
66
+
67
+ ```
68
+ [<current loop>/<total count>, (<progress>%), t/i: <time per iteration>, ETA: <time until it's done>]
69
+ ```
70
+
71
+ The calculation is done by maintaining a list of measurements with a limited size, and a list of averages of those measurements. The average of averages is the "time per iteration" and it's multiplied by the remaining count to produce the estimation.
72
+
73
+ I can't really say how reliable this is, but it seems to provide smoothly changing estimations that seem more or less correct to me, for similarly-sized chunks of work per iteration.
74
+
75
+ **Not** providing a `total_count:` parameter leads to less available information:
76
+
77
+ ``` ruby
78
+ progressor = Progressor.new
79
+
80
+ (1..100).each do |i|
81
+ progressor.run do |progress|
82
+ sleep rand
83
+ puts progress
84
+ end
85
+ end
86
+ ```
87
+
88
+ A sample of output might look like this:
24
89
 
25
90
  ```
26
91
  ...
27
- [0038/1000, (004%), t/i: 0.5s, ETA: 8m:0.27s] Product 38
28
- [0039/1000, (004%), t/i: 0.5s, ETA: 7m:58.47s] Product 39
29
- [0040/1000, (004%), t/i: 0.5s, ETA: 7m:57.08s] Product 40
92
+ 11, t: 5.32s, t/i: 442.39ms
93
+ 12, t: 5.58s, t/i: 446.11ms
30
94
  ...
31
95
  ```
96
+
97
+ The format is:
98
+
99
+ ```
100
+ <current>, t: <time from start>, t/i: <time per iteration>
101
+ ```
102
+
103
+ ## Configuration
104
+
105
+ Apart from `total_count`, which is optional and affects the kind of sequence that will be stored, you can provide `min_samples` and `max_samples`. You can also provide a custom formatter:
106
+
107
+ ``` ruby
108
+ progressor = Progressor.new({
109
+ total_count: 1000,
110
+ min_samples: 5,
111
+ max_samples: 10,
112
+ formatter: -> (p) { p.eta }
113
+ })
114
+ ```
115
+
116
+ The option `min_samples` determines how many loops the tool will wait until trying to produce an estimation. A higher number means no information in the beginning, but no wild fluctuations, either. It needs to be at least 1 and the default is 1.
117
+
118
+ The option `max_samples` is how many measurements will be retained. Those measurements will be averaged, and then those averages averaged to get a time-per-iteration estimate. A smaller number means giving more weight to later events, while a larger one would average over a larger amount of samples. The default is 100.
119
+
120
+ The `formatter` is a callback that gets a progress object as an argument and you can return your own string to output on every loop. Check `LimitedSequence` and `UnlimitedSequence` for the available methods and accessors you can use.
121
+
122
+ ## Related work
123
+
124
+ A very similar tool is the gem [ke](https://github.com/mkdynamic/ke). It provides its estimation by maintaining the median quartile range of the stored measurements, removing outliers. It also automates the output of the progress report, only printing it every N loops. Depending on your needs and preferences, it might be better for your use case.
@@ -0,0 +1,7 @@
1
+ class Progressor
2
+ # A custom error class for targeted catching. All Progressor errors will be
3
+ # wrapped in a Progressor::Error.
4
+ #
5
+ class Error < RuntimeError
6
+ end
7
+ end
@@ -0,0 +1,39 @@
1
+ class Progressor
2
+ module Formatting
3
+ # Formats the given time in seconds to something human readable. Examples:
4
+ #
5
+ # - 1 second: 1.00s
6
+ # - 0.123 seconds: 123.00ms
7
+ # - 100 seconds: 01m:40s
8
+ # - 101.5 seconds: 01m:41s
9
+ # - 3661 seconds: 01h:01m:01s
10
+ def format_time(time)
11
+ return "?s" if time.nil?
12
+
13
+ if time < 1
14
+ "#{format_float((time * 1000).round(2))}ms"
15
+ elsif time < 60
16
+ "#{format_float(time.round(2))}s"
17
+ elsif time < 3600
18
+ minutes = time.to_i / 60
19
+ seconds = (time - minutes * 60).round(2)
20
+ "#{format_int(minutes)}m:#{format_int(seconds)}s"
21
+ else
22
+ hours = time.to_i / 3600
23
+ minutes = (time.to_i % 3600) / 60
24
+ seconds = (time - (hours * 3600 + minutes * 60)).round(2)
25
+ "#{format_int(hours)}h:#{format_int(minutes)}m:#{format_int(seconds)}s"
26
+ end
27
+ end
28
+
29
+ # :nodoc:
30
+ def format_int(value)
31
+ sprintf("%02d", value)
32
+ end
33
+
34
+ # :nodoc:
35
+ def format_float(value)
36
+ sprintf("%0.2f", value)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,134 @@
1
+ class Progressor
2
+ class LimitedSequence
3
+ include Formatting
4
+
5
+ attr_reader :total_count, :min_samples, :max_samples
6
+
7
+ # The current loop index, starts at 1
8
+ attr_reader :current
9
+
10
+ # The time the object was created
11
+ attr_reader :start_time
12
+
13
+ # Creates a new LimitedSequence with the given parameters:
14
+ #
15
+ # - total_count: The expected number of loops.
16
+ #
17
+ # - min_samples: The number of samples to collect before attempting to
18
+ # calculate a time per iteration. Default: 1
19
+ #
20
+ # - max_samples: The maximum number of measurements to collect and average.
21
+ # Default: 100.
22
+ #
23
+ # - formatter: A callable that accepts the sequence object and returns a
24
+ # custom formatted string.
25
+ #
26
+ def initialize(total_count:, min_samples: 1, max_samples: 100, formatter: nil)
27
+ @total_count = total_count
28
+ @min_samples = min_samples
29
+ @max_samples = [max_samples, total_count].min
30
+ @formatter = formatter
31
+
32
+ raise Error.new("min_samples needs to be a positive number") if min_samples <= 0
33
+ raise Error.new("max_samples needs to be larger than min_samples") if max_samples <= min_samples
34
+
35
+ @start_time = Time.now
36
+ @total_count_digits = total_count.to_s.length
37
+ @current = 0
38
+ @measurements = []
39
+ @averages = []
40
+ end
41
+
42
+ # Adds a duration in seconds to the internal storage of samples. Updates
43
+ # averages accordingly.
44
+ #
45
+ def push(duration)
46
+ @current += 1
47
+ @measurements << duration
48
+ # only keep last `max_samples`
49
+ @measurements.shift if @measurements.count > max_samples
50
+
51
+ @averages << average(@measurements)
52
+ @averages = @averages.compact
53
+ # only keep last `max_samples`
54
+ @averages.shift if @averages.count > max_samples
55
+ end
56
+
57
+ # Skips an iteration, updating the total count and ETA
58
+ #
59
+ def skip(n)
60
+ @total_count -= n
61
+ end
62
+
63
+ # Outputs a textual representation of the current state of the
64
+ # LimitedSequence. Shows:
65
+ #
66
+ # - the current number of iterations and the total count
67
+ # - completion level in percentage
68
+ # - how long a single iteration takes
69
+ # - estimated time of arrival (ETA) -- time until it's done
70
+ #
71
+ # A custom `formatter` provided at construction time overrides this default
72
+ # output.
73
+ #
74
+ # If the "current" number of iterations goes over the total count, an ETA
75
+ # can't be shown anymore, so it'll just be the current number over the
76
+ # expected one, and the time per iteration.
77
+ #
78
+ def to_s
79
+ return @formatter.call(self).to_s if @formatter
80
+
81
+ if @current > @total_count
82
+ return [
83
+ "#{@current} (expected #{@total_count})",
84
+ "t/i: #{format_time(per_iteration)}",
85
+ "ETA: ???",
86
+ ].join(', ')
87
+ end
88
+
89
+ [
90
+ "#{@current.to_s.rjust(@total_count_digits, '0')}/#{@total_count}",
91
+ "#{((@current / @total_count.to_f) * 100).round.to_s.rjust(3, '0')}%",
92
+ "t/i: #{format_time(per_iteration)}",
93
+ "ETA: #{format_time(eta)}",
94
+ ].join(', ')
95
+ end
96
+
97
+ # Returns an estimation for the time per single iteration. Implemented as
98
+ # an average of averages to provide a smoother gradient from loop to loop.
99
+ #
100
+ # Returns nil if not enough samples have been collected yet.
101
+ #
102
+ def per_iteration
103
+ return nil if @measurements.count < min_samples
104
+ average(@averages)
105
+ end
106
+
107
+ # Returns an estimation for the Estimated Time of Arrival (time until
108
+ # done).
109
+ #
110
+ # Calculated by multiplying the average time per iteration with the
111
+ # remaining number of loops.
112
+ #
113
+ def eta
114
+ return nil if @measurements.count < min_samples
115
+
116
+ remaining_time = per_iteration * (@total_count - @current)
117
+ remaining_time.round(2)
118
+ end
119
+
120
+ # Returns the time since the object was instantiated, formatted like all
121
+ # the other durations. Useful for a final message to compare initial
122
+ # estimation to actual elapsed time.
123
+ #
124
+ def elapsed_time
125
+ format_time(Time.now - @start_time)
126
+ end
127
+
128
+ private
129
+
130
+ def average(collection)
131
+ collection.inject(&:+) / collection.count.to_f
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,111 @@
1
+ class Progressor
2
+ class UnlimitedSequence
3
+ include Formatting
4
+
5
+ attr_reader :min_samples, :max_samples
6
+
7
+ # The current loop index, starts at 1
8
+ attr_reader :current
9
+
10
+ # The time the object was created
11
+ attr_reader :start_time
12
+
13
+ # Creates a new UnlimitedSequence with the given parameters:
14
+ #
15
+ # - min_samples: The number of samples to collect before attempting to
16
+ # calculate a time per iteration. Default: 1
17
+ #
18
+ # - max_samples: The maximum number of measurements to collect and average.
19
+ # Default: 100.
20
+ #
21
+ # - formatter: A callable that accepts the sequence object and returns a
22
+ # custom formatted string.
23
+ #
24
+ def initialize(min_samples: 1, max_samples: 100, formatter: nil)
25
+ @min_samples = min_samples
26
+ @max_samples = max_samples
27
+ @formatter = formatter
28
+
29
+ raise Error.new("min_samples needs to be a positive number") if min_samples <= 0
30
+ raise Error.new("max_samples needs to be larger than min_samples") if max_samples <= min_samples
31
+
32
+ @start_time = Time.now
33
+ @current = 0
34
+ @measurements = []
35
+ @averages = []
36
+ end
37
+
38
+ # Adds a duration in seconds to the internal storage of samples. Updates
39
+ # averages accordingly.
40
+ #
41
+ def push(duration)
42
+ @current += 1
43
+ @measurements << duration
44
+ # only keep last `max_samples`
45
+ @measurements.shift if @measurements.count > max_samples
46
+
47
+ @averages << average(@measurements)
48
+ @averages = @averages.compact
49
+ # only keep last `max_samples`
50
+ @averages.shift if @averages.count > max_samples
51
+ end
52
+
53
+ # "Skips" an iteration, which, in the context of an UnlimitedSequence is a no-op.
54
+ #
55
+ def skip(_n)
56
+ # Nothing to do
57
+ end
58
+
59
+ # Outputs a textual representation of the current state of the
60
+ # UnlimitedSequence. Shows:
61
+ #
62
+ # - the current (1-indexed) number of iterations
63
+ # - how long since the start time
64
+ # - how long a single iteration takes
65
+ #
66
+ # A custom `formatter` provided at construction time overrides this default
67
+ # output.
68
+ #
69
+ def to_s
70
+ return @formatter.call(self).to_s if @formatter
71
+
72
+ [
73
+ "#{@current + 1}",
74
+ "t: #{elapsed_time}",
75
+ "t/i: #{format_time(per_iteration)}",
76
+ ].join(', ')
77
+ end
78
+
79
+ # Returns an estimation for the time per single iteration. Implemented as
80
+ # an average of averages to provide a smoother gradient from loop to loop.
81
+ #
82
+ # Returns nil if not enough samples have been collected yet.
83
+ #
84
+ def per_iteration
85
+ return nil if @measurements.count < min_samples
86
+ average(@averages)
87
+ end
88
+
89
+ # Is supposed to return an estimation for the Estimated Time of Arrival
90
+ # (time until done).
91
+ #
92
+ # For an UnlimitedSequence, this always returns nil.
93
+ #
94
+ def eta
95
+ # No estimation possible
96
+ end
97
+
98
+ # Returns the time since the object was instantiated, formatted like all
99
+ # the other durations.
100
+ #
101
+ def elapsed_time
102
+ format_time(Time.now - @start_time)
103
+ end
104
+
105
+ private
106
+
107
+ def average(collection)
108
+ collection.inject(&:+) / collection.count.to_f
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,3 @@
1
+ class Progressor
2
+ VERSION = '0.1.2'
3
+ end
data/lib/progressor.rb CHANGED
@@ -1,7 +1,13 @@
1
+ require 'progressor/version'
2
+ require 'progressor/error'
3
+ require 'progressor/formatting'
4
+ require 'progressor/limited_sequence'
5
+ require 'progressor/unlimited_sequence'
6
+
1
7
  require 'benchmark'
2
8
 
3
9
  # Used to measure the running time of parts of a long-running task and output
4
- # an estimation based on the average of the last 10-100 measurements.
10
+ # an estimation based on the average of the last 1-100 measurements.
5
11
  #
6
12
  # Example usage:
7
13
  #
@@ -22,98 +28,75 @@ require 'benchmark'
22
28
  # Example output:
23
29
  #
24
30
  # ...
25
- # [0038/1000, (004%), t/i: 0.5s, ETA: 8m:0.27s] Product 38
26
- # [0039/1000, (004%), t/i: 0.5s, ETA: 7m:58.47s] Product 39
27
- # [0040/1000, (004%), t/i: 0.5s, ETA: 7m:57.08s] Product 40
31
+ # [0038/1000, 004%, t/i: 0.5s, ETA: 8m:00s] Product 38
32
+ # [0039/1000, 004%, t/i: 0.5s, ETA: 7m:58s] Product 39
33
+ # [0040/1000, 004%, t/i: 0.5s, ETA: 7m:57s] Product 40
28
34
  # ...
29
35
  #
30
36
  class Progressor
31
- VERSION = '0.0.1'
37
+ extend Formatting
32
38
 
33
39
  # Utility method to print a message with the time it took to run the contents
34
40
  # of the block.
35
41
  #
36
- # > Progressor.puts("Working on a thing") { thing_work }
42
+ # Progressor.puts("Working on a thing") { thing_work }
43
+ #
44
+ # Output:
37
45
  #
38
- # Working on a thing...
39
- # Working on a thing DONE: 2.1s
46
+ # Working on a thing...
47
+ # Working on a thing DONE: 2.1s
40
48
  #
41
49
  def self.puts(message, &block)
42
50
  Kernel.puts "#{message}..."
43
- measurement = Benchmark.measure { block.call }
51
+ result = nil
52
+ measurement = Benchmark.measure { result = block.call }
44
53
  Kernel.puts "#{message} DONE: #{format_time(measurement.real)}"
54
+ result
45
55
  end
46
56
 
47
- def initialize(total_count:)
48
- @total_count = total_count
49
- @total_count_digits = total_count.to_s.length
50
- @current = 0
51
- @measurements = []
52
- @averages = []
57
+ # Set up a new Progressor instance. Optional parameters:
58
+ #
59
+ # - total_count: If given, the tool will be able to provide an ETA.
60
+ #
61
+ # - min_samples: The number of samples to collect before attempting to
62
+ # calculate a time per iteration. Default: 1
63
+ #
64
+ # - max_samples: The maximum number of measurements to collect and average.
65
+ # Default: 100.
66
+ #
67
+ # - formatter: A callable that accepts a progress object and returns a
68
+ # custom formatted string.
69
+ #
70
+ def initialize(total_count: nil, min_samples: 1, max_samples: 100, formatter: nil)
71
+ params = {
72
+ min_samples: min_samples,
73
+ max_samples: max_samples,
74
+ formatter: formatter,
75
+ }
76
+
77
+ if total_count
78
+ @sequence = LimitedSequence.new(total_count: total_count, **params)
79
+ else
80
+ @sequence = UnlimitedSequence.new(**params)
81
+ end
53
82
  end
54
83
 
84
+ # Run the given block of code, yielding a sequence object that holds progress
85
+ # information.
86
+ #
87
+ # Example usage:
88
+ #
89
+ # progressor.run { |progress| puts progress; long_running_task() }
90
+ #
55
91
  def run
56
- @current += 1
57
-
58
- measurement = Benchmark.measure { yield self }
59
-
60
- @measurements << measurement.real
61
- # only keep last 1000
62
- @measurements.shift if @measurements.count > 1000
63
-
64
- @averages << average(@measurements)
65
- @averages = @averages.compact
66
- # only keep last 100
67
- @averages.shift if @averages.count > 100
92
+ measurement = Benchmark.measure { yield @sequence }
93
+ @sequence.push(measurement.real)
68
94
  end
69
95
 
96
+ # Skips the given number of loops (will likely be 1), updating the
97
+ # estimations appropriately.
98
+ #
70
99
  def skip(n)
71
- @total_count -= n
72
- end
73
-
74
- def to_s
75
- [
76
- "#{@current.to_s.rjust(@total_count_digits, '0')}/#{@total_count}",
77
- "(#{((@current / @total_count.to_f) * 100).round.to_s.rjust(3, '0')}%)",
78
- "t/i: #{self.class.format_time(per_iteration)}",
79
- "ETA: #{self.class.format_time(eta)}",
80
- ].join(', ')
81
- end
82
-
83
- def per_iteration
84
- return nil if @measurements.count < 10
85
- average(@averages)
86
- end
87
-
88
- def eta
89
- return nil if @measurements.count < 10
90
-
91
- remaining_time = per_iteration * (@total_count - @current)
92
- remaining_time.round(2)
93
- end
94
-
95
- private
96
-
97
- def self.format_time(time)
98
- return "?s" if time.nil?
99
-
100
- if time < 0.1
101
- "#{(time * 1000).round(2)}ms"
102
- elsif time < 60
103
- "#{time.round(2)}s"
104
- elsif time < 3600
105
- minutes = time.to_i / 60
106
- seconds = (time - minutes * 60).round(2)
107
- "#{minutes}m:#{seconds}s"
108
- else
109
- hours = time.to_i / 3600
110
- minutes = (time.to_i % 3600) / 60
111
- seconds = (time - (hours * 3600 + minutes * 60)).round(2)
112
- "#{hours}h:#{minutes}m:#{seconds}s"
113
- end
114
- end
115
-
116
- def average(collection)
117
- collection.inject(&:+) / collection.count.to_f
100
+ @sequence.skip(n)
118
101
  end
119
102
  end
metadata CHANGED
@@ -1,57 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: progressor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Radev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-02-20 00:00:00.000000000 Z
11
+ date: 2022-04-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: rake
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.17'
19
+ version: 12.3.3
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.17'
26
+ version: 12.3.3
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '3.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '3.0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rspec
42
+ name: timecop
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.0'
47
+ version: '0.9'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '3.0'
54
+ version: '0.9'
55
55
  description: |
56
56
  Provides a way to measure how long each loop in a task took, outputting a
57
57
  report with an estimated time till the task is done.
@@ -65,11 +65,16 @@ files:
65
65
  - LICENSE
66
66
  - README.md
67
67
  - lib/progressor.rb
68
+ - lib/progressor/error.rb
69
+ - lib/progressor/formatting.rb
70
+ - lib/progressor/limited_sequence.rb
71
+ - lib/progressor/unlimited_sequence.rb
72
+ - lib/progressor/version.rb
68
73
  homepage: https://github.com/AndrewRadev/progressor
69
74
  licenses:
70
75
  - MIT
71
76
  metadata: {}
72
- post_install_message:
77
+ post_install_message:
73
78
  rdoc_options: []
74
79
  require_paths:
75
80
  - lib
@@ -84,8 +89,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
89
  - !ruby/object:Gem::Version
85
90
  version: '0'
86
91
  requirements: []
87
- rubygems_version: 3.0.0
88
- signing_key:
92
+ rubygems_version: 3.1.6
93
+ signing_key:
89
94
  specification_version: 4
90
95
  summary: Measure iterations in a long-running task
91
96
  test_files: []