├── .rspec ├── .yardopts ├── Gemfile ├── lib ├── tqdm │ ├── version.rb │ ├── sequel.rb │ ├── printer.rb │ ├── printer │ │ └── default_format.rb │ └── decorator.rb ├── tqdm.rb └── core_ext │ └── enumerable.rb ├── Rakefile ├── .gitignore ├── spec ├── spec_helper.rb ├── sequel_spec.rb └── enumerable_spec.rb ├── LICENSE.txt ├── tqdm.gemspec ├── .github └── workflows │ └── ruby-ci.yml └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --format documentation -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --readme README.md 2 | --charset utf-8 3 | --markup markdown 4 | 'lib/**/*.rb' 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in tqdm.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/tqdm/version.rb: -------------------------------------------------------------------------------- 1 | module Tqdm 2 | # The version of this module and gem by the same name. 3 | VERSION = "0.4.1" 4 | end 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |t| 5 | t.pattern = Dir.glob('spec/**/*_spec.rb') 6 | end 7 | 8 | task :default => :spec -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.expect_with :rspec do |expectations| 3 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 4 | end 5 | 6 | config.mock_with :rspec do |mocks| 7 | mocks.verify_partial_doubles = true 8 | end 9 | end 10 | 11 | def with_stderr(&block) 12 | old_stderr = $stderr 13 | $stderr = StringIO.new 14 | 15 | block.call 16 | 17 | return $stderr.string 18 | ensure 19 | $stderr = old_stderr 20 | end -------------------------------------------------------------------------------- /lib/tqdm.rb: -------------------------------------------------------------------------------- 1 | require 'tqdm/version' 2 | require 'tqdm/decorator' 3 | require 'core_ext/enumerable' 4 | 5 | # Add a progress bar to your loops in a second. 6 | # A port of Python's [tqdm library](https://github.com/tqdm/tqdm), although we're currently 7 | # closer to the feature set of [@noamraph's original release](https://github.com/noamraph/tqdm). 8 | # 9 | # Specifically, `Tqdm` enhances `Enumerable` by printing a progress indicator whenever 10 | # iterating with `#each` or its close relatives. 11 | # 12 | # @author Theodore Pak 13 | # @see https://github.com/tqdm/tqdm 14 | module Tqdm 15 | 16 | class << self 17 | # Upgrades `Sequel::Datasets` with the #with_progress method. 18 | # @see Enumerable#with_progress 19 | def enhance_sequel! 20 | require 'tqdm/sequel' 21 | end 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/tqdm/sequel.rb: -------------------------------------------------------------------------------- 1 | require 'sequel' 2 | require 'tqdm' 3 | 4 | # @see Sequel::Dataset 5 | module Sequel 6 | 7 | # In order to use `Tqdm` with Sequel Datasets, we can simply extend `Sequel::Dataset` 8 | # with the same `#with_progress` method 9 | # 10 | # @see Enumerable#with_progress 11 | # @see http://sequel.jeremyevans.net/ 12 | # @see http://sequel.jeremyevans.net/rdoc/classes/Sequel/Dataset.html 13 | class Dataset 14 | 15 | # Returns a clone of `self` where all calls to `#each` and related methods will print an animated progress bar 16 | # while iterating. 17 | # 18 | # @param options [Hash] options are the same as Enumerable#with_progress 19 | # 20 | # @see Enumerable#with_progress 21 | def with_progress(options = {}) 22 | Tqdm::Decorator.new(self, options).enhance 23 | end 24 | alias :tqdm :with_progress 25 | 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Theodore Pak 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /tqdm.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'tqdm/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "tqdm" 8 | spec.version = Tqdm::VERSION 9 | spec.authors = ["Theodore Pak"] 10 | spec.email = ["theodore.r.pak@gmail.com"] 11 | spec.description = %q{Enhances Enumerables to show progress while iterating. (Port of tqdm for Python.)} 12 | spec.summary = %q{Enhances Enumerables to show progress while iterating.} 13 | spec.homepage = "https://github.com/powerpak/tqdm-ruby" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "sequel" 24 | spec.add_development_dependency "sqlite3" 25 | spec.add_development_dependency "rspec" 26 | spec.add_development_dependency "timecop" 27 | 28 | spec.required_ruby_version = '>= 1.9.2' 29 | end 30 | -------------------------------------------------------------------------------- /.github/workflows/ruby-ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby-CI 9 | 10 | on: 11 | push: 12 | branches: [ "master" ] 13 | pull_request: 14 | branches: [ "master" ] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | test: 21 | 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | ruby-version: ['1.9', '2.0', '2.3', '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2'] 26 | fail-fast: false 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | - name: Set up Ruby 31 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 32 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 33 | # uses: ruby/setup-ruby@v1 34 | uses: ruby/setup-ruby@319066216501fbd5e2d568f14b7d68c19fb67a5d # v1.133.1 35 | with: 36 | ruby-version: ${{ matrix.ruby-version }} 37 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 38 | - name: Run tests 39 | run: bundle exec rake spec 40 | -------------------------------------------------------------------------------- /lib/core_ext/enumerable.rb: -------------------------------------------------------------------------------- 1 | require 'tqdm' 2 | 3 | # We enhance all enumerable objects (e.g. `Array`, `Hash`, `Range`, ...) by extending the `Enumerable` module. 4 | # This mixin is only supposed to be present on objects that provide an `#each` method. 5 | # 6 | # @see http://ruby-doc.org/core-2.2.3/Enumerable.html 7 | module Enumerable 8 | 9 | # Returns a *clone* of `self` where all calls to `#each` and related methods will print an animated progress bar 10 | # while iterating. 11 | # 12 | # @param options [Hash] more options used to control behavior of the progress bar 13 | # @option options [String] :desc a short description added to the beginning of the progress bar 14 | # @option options [Integer] :total (self.size) the expected number of iterations 15 | # @option options [File, IO] :file ($stderr) a file-like object to output the progress bar to 16 | # @option options [Boolean] :leave (false) should the progress bar should stay on screen after it's done? 17 | # @option options [Integer] :min_iters see `:min_interval` 18 | # @option options [Float] :min_interval If less than min_interval seconds or min_iters iterations have passed since 19 | # the last progress meter update, it is not updated again. 20 | # @return [Enumerable] `self` with the `#each` method wrapped as described above 21 | def with_progress(options = {}) 22 | Tqdm::Decorator.new(self, options).enhance 23 | end 24 | alias :tqdm :with_progress 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/tqdm/printer.rb: -------------------------------------------------------------------------------- 1 | require 'tqdm/printer/default_format' 2 | require 'forwardable' 3 | 4 | module Tqdm 5 | 6 | # Prints a status line, handling the deletion of previously printed lines with carriage 7 | # returns as necessary. Instantiated by a `Decorator`. 8 | # 9 | # @private 10 | class Printer 11 | extend Forwardable 12 | 13 | attr_reader :total, :format, :file 14 | 15 | # Initialize a new Printer. 16 | # 17 | # @param options [Hash] the options for the instantiating Tqdm::Decorator 18 | # 19 | # @see Tqdm::Decorator#initialize 20 | def initialize(options) 21 | @total = options[:total] 22 | @format = Printer::DefaultFormat.new(options) 23 | @file = options[:file] || $stderr 24 | @last_printed_length = 0 25 | end 26 | 27 | # Pads a status line so that it is long enough to overwrite the previously written line 28 | # 29 | # @param iteration [Integer] number of iterations, out of the total, that are completed 30 | # @param elapsed_time [Float] number of seconds passed since start 31 | # @return [String] the padded line 32 | def padded_line(iteration, elapsed_time) 33 | meter_line = line(iteration, elapsed_time) 34 | pad_size = [@last_printed_length - meter_line.size, 0].max 35 | @last_printed_length = meter_line.size 36 | meter_line + ' ' * pad_size 37 | end 38 | 39 | # Prints a line of text to @file, after deleting the previously printed line 40 | # 41 | # @param iteration [Integer] number of iterations, out of the total, that are completed 42 | # @param elapsed_time [Float] number of seconds passed since start 43 | def status(iteration, elapsed_time) 44 | file.write("\r" + padded_line(iteration, elapsed_time)) 45 | file.flush 46 | end 47 | 48 | # Prints a line of text to @file, after deleting the previously printed line 49 | # 50 | # @param iteration [Integer] number of iterations, out of the total, that are completed 51 | # @param elapsed_time [Float] number of seconds passed since start 52 | # @param reprint [Boolean] do we need to reprint the line one last time? 53 | def finish(iteration, elapsed_time, reprint) 54 | file.write("\r" + padded_line(iteration, elapsed_time)) if reprint 55 | file.write("\n") 56 | file.flush 57 | end 58 | 59 | # Disappear without a trace. 60 | def null_finish 61 | file.write("\r" + ' ' * @last_printed_length + "\r") 62 | end 63 | 64 | def_delegators :format, :line, :meter 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/sequel_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'timecop' 3 | require_relative '../lib/tqdm' 4 | require_relative '../lib/tqdm/sequel' 5 | 6 | describe 'When enumerating over a Sequel dataset' do 7 | before { Timecop.freeze } 8 | after { Timecop.return } 9 | 10 | def timecop_loop(dataset, options = {}) 11 | dataset.tqdm(options).each do |x| 12 | Timecop.travel 1 13 | end 14 | end 15 | 16 | context 'that has several (five) elements' do 17 | let(:database) do 18 | db = Sequel.sqlite 19 | db.create_table :items do 20 | primary_key :id 21 | Float :price 22 | end 23 | 24 | (0...5).each { db[:items].insert(price: rand * 100) } 25 | 26 | db 27 | end 28 | 29 | context 'with default options' do 30 | it 'displays a progress bar for the first four steps and deletes it' do 31 | final_stderr = with_stderr { timecop_loop(database[:items]) } 32 | 33 | expect(final_stderr).to eq "" \ 34 | "\r|##--------| 1/5 20% [elapsed: 00:01 left: 00:04, 1.00 iters/sec]" \ 35 | "\r|####------| 2/5 40% [elapsed: 00:02 left: 00:03, 1.00 iters/sec]" \ 36 | "\r|######----| 3/5 60% [elapsed: 00:03 left: 00:02, 1.00 iters/sec]" \ 37 | "\r|########--| 4/5 80% [elapsed: 00:04 left: 00:01, 1.00 iters/sec]" \ 38 | "\r " \ 39 | "\r" 40 | end 41 | 42 | it 'returns a re-frozen object' do 43 | enhanced = nil 44 | with_stderr { enhanced = timecop_loop(database[:items]) } 45 | expect(enhanced.frozen?).to be_truthy 46 | end 47 | 48 | it 'returns an object inheriting from Sequel::Dataset' do 49 | enhanced = nil 50 | with_stderr { enhanced = timecop_loop(database[:items]) } 51 | expect(enhanced).to be_kind_of(Sequel::Dataset) 52 | end 53 | end 54 | 55 | context 'with leave: true' do 56 | it 'displays a progress bar with as many steps as elements and leaves it' do 57 | final_stderr = with_stderr { timecop_loop(database[:items], leave: true) } 58 | 59 | expect(final_stderr).to eq "" \ 60 | "\r|##--------| 1/5 20% [elapsed: 00:01 left: 00:04, 1.00 iters/sec]" \ 61 | "\r|####------| 2/5 40% [elapsed: 00:02 left: 00:03, 1.00 iters/sec]" \ 62 | "\r|######----| 3/5 60% [elapsed: 00:03 left: 00:02, 1.00 iters/sec]" \ 63 | "\r|########--| 4/5 80% [elapsed: 00:04 left: 00:01, 1.00 iters/sec]" \ 64 | "\r|##########| 5/5 100% [elapsed: 00:05 left: 00:00, 1.00 iters/sec]" \ 65 | "\n" 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/tqdm/printer/default_format.rb: -------------------------------------------------------------------------------- 1 | module Tqdm 2 | class Printer 3 | class DefaultFormat 4 | PROGRESS_BAR_WIDTH = 10 5 | SPACE = '-' 6 | PROGRESS = '#' 7 | 8 | # Initialize a new DefaultFormat. 9 | # 10 | # @param options [Hash] the options for the Tqdm::Decorator 11 | # 12 | # @see Tqdm::Decorator#initialize 13 | def initialize(options) 14 | @total = options[:total] 15 | @prefix = options[:desc] ? options[:desc] + ': ' : '' 16 | end 17 | 18 | # Formats the prefix, progress bar and meter as a complete line to be printed 19 | # 20 | # @param iteration [Integer] number of finished iterations 21 | # @param elapsed_time [Float] total number of seconds passed since start 22 | # @return [String] the complete line to print 23 | # 24 | # @see #meter 25 | def line(iteration, elapsed_time) 26 | prefix + meter(iteration, total, elapsed_time) 27 | end 28 | 29 | # Formats a count (n) of total items processed + an elapsed time into a 30 | # textual progress bar + meter. 31 | # 32 | # @param n [Integer] number of finished iterations 33 | # @param total [Integer, nil] total number of iterations, or nil 34 | # @param elapsed [Float] number of seconds passed since start 35 | # @return [String] a textual progress bar + meter 36 | def meter(n, total, elapsed) 37 | total = (n > total ? nil : total) if total 38 | 39 | elapsed_str = interval(elapsed) 40 | rate = elapsed && elapsed > 0 ? ('%5.2f' % (n / elapsed)) : '?' 41 | 42 | if total && total > 0 43 | frac = n.to_f / total 44 | 45 | bar_length = (frac * PROGRESS_BAR_WIDTH).to_i 46 | bar = PROGRESS * bar_length + SPACE * (PROGRESS_BAR_WIDTH - bar_length) 47 | 48 | percentage = '%3d%%' % (frac * 100) 49 | 50 | left_str = n > 0 ? (interval(elapsed / n * (total - n))) : '?' 51 | 52 | '|%s| %d/%d %s [elapsed: %s left: %s, %s iters/sec]' % [bar, n, total, 53 | percentage, elapsed_str, left_str, rate] 54 | else 55 | '%d [elapsed: %s, %s iters/sec]' % [n, elapsed_str, rate] 56 | end 57 | end 58 | 59 | private 60 | 61 | attr_reader :total, :prefix 62 | 63 | # Formats a number of seconds into an hh:mm:ss string. 64 | # 65 | # @param seconds [Integer] a number of seconds 66 | # @return [String] an hh:mm:ss string 67 | def interval(seconds) 68 | m, s = seconds.to_i.divmod(60) 69 | h, m = m.divmod(60) 70 | if h > 0 then '%d:%02d:%02d' % [h, m, s]; else '%02d:%02d' % [m, s]; end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/enumerable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'timecop' 3 | require_relative '../lib/tqdm' 4 | 5 | describe 'When enumerating over an object' do 6 | before { Timecop.freeze } 7 | after { Timecop.return } 8 | 9 | def with_stderr(&block) 10 | old_stderr = $stderr 11 | $stderr = StringIO.new 12 | 13 | block.call 14 | 15 | return $stderr.string 16 | ensure 17 | $stderr = old_stderr 18 | end 19 | 20 | def timecop_loop(enumerable, options = {}) 21 | enumerable.tqdm(options).each do |x| 22 | Timecop.travel 1 23 | end 24 | end 25 | 26 | context 'that has zero elements' do 27 | let(:enumerable) { (0...0) } 28 | 29 | context 'with default options' do 30 | it 'never displays a progress bar' do 31 | final_stderr = with_stderr { timecop_loop(enumerable) } 32 | expect(final_stderr).to eq "\r\r" 33 | end 34 | end 35 | 36 | context 'with leave: true' do 37 | it 'never displays a progress bar' do 38 | final_stderr = with_stderr { timecop_loop(enumerable, leave: true) } 39 | expect(final_stderr).to eq "\n" 40 | end 41 | end 42 | end 43 | 44 | context 'that has one element' do 45 | let(:enumerable) { (0...1) } 46 | 47 | context 'with default options' do 48 | it 'never displays a progress bar' do 49 | final_stderr = with_stderr { timecop_loop(enumerable) } 50 | expect(final_stderr).to eq "\r\r" 51 | end 52 | end 53 | 54 | context 'with leave: true' do 55 | it 'displays a progress bar once at 100%' do 56 | final_stderr = with_stderr { timecop_loop(enumerable, leave: true) } 57 | expect(final_stderr).to eq "" \ 58 | "\r|##########| 1/1 100% [elapsed: 00:01 left: 00:00, 1.00 iters/sec]" \ 59 | "\n" 60 | end 61 | end 62 | 63 | end 64 | 65 | context 'that has several (five) elements' do 66 | let(:enumerable) { (0...5) } 67 | 68 | context 'with default options' do 69 | it 'displays a progress bar for the first four steps and deletes it' do 70 | final_stderr = with_stderr { timecop_loop(enumerable) } 71 | 72 | expect(final_stderr).to eq "" \ 73 | "\r|##--------| 1/5 20% [elapsed: 00:01 left: 00:04, 1.00 iters/sec]" \ 74 | "\r|####------| 2/5 40% [elapsed: 00:02 left: 00:03, 1.00 iters/sec]" \ 75 | "\r|######----| 3/5 60% [elapsed: 00:03 left: 00:02, 1.00 iters/sec]" \ 76 | "\r|########--| 4/5 80% [elapsed: 00:04 left: 00:01, 1.00 iters/sec]" \ 77 | "\r " \ 78 | "\r" 79 | end 80 | end 81 | 82 | context 'with leave: true' do 83 | it 'displays a progress bar with as many steps as elements and leaves it' do 84 | final_stderr = with_stderr { timecop_loop(enumerable, leave: true) } 85 | 86 | expect(final_stderr).to eq "" \ 87 | "\r|##--------| 1/5 20% [elapsed: 00:01 left: 00:04, 1.00 iters/sec]" \ 88 | "\r|####------| 2/5 40% [elapsed: 00:02 left: 00:03, 1.00 iters/sec]" \ 89 | "\r|######----| 3/5 60% [elapsed: 00:03 left: 00:02, 1.00 iters/sec]" \ 90 | "\r|########--| 4/5 80% [elapsed: 00:04 left: 00:01, 1.00 iters/sec]" \ 91 | "\r|##########| 5/5 100% [elapsed: 00:05 left: 00:00, 1.00 iters/sec]" \ 92 | "\n" 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tqdm-ruby 2 | [![Build Status](https://github.com/powerpak/tqdm-ruby/actions/workflows/ruby-ci.yml/badge.svg?branch=master&event=push)](https://github.com/powerpak/tqdm-ruby/actions/workflows/ruby-ci.yml?query=event%3Apush+branch%3Amaster) [![Gem Version](https://badge.fury.io/rb/tqdm.svg?version=0.4.1)](https://badge.fury.io/rb/tqdm) 3 | 4 | tqdm-ruby allows you to add a progress indicator to your loops with minimal effort. 5 | 6 | It is a port of the excellent [tqdm library][tqdm] for python. tqdm (read taqadum, تقدّم) means "progress" in Arabic. 7 | 8 | Calling `#tqdm` (or `#with_progress`) on any `Enumerable` returns an enhanced clone that animates a meter during iteration. 9 | 10 | ```ruby 11 | require 'tqdm' 12 | (0...1000).tqdm.each { |x| sleep 0.01 } 13 | ``` 14 | 15 | The default output is sent to `$stderr` and looks like this: 16 | 17 | ![|####------| 492/1000 49% [elapsed: 00:05 left: 00:05, 88.81 iters/sec]](http://i.imgur.com/6y0t7XS.gif) 18 | 19 | It works equally well from within irb, [pry](http://pryrepl.org/), and [iRuby notebooks](https://github.com/SciRuby/iruby) as seen here: 20 | 21 | ![iRuby notebook screencap](http://i.imgur.com/DilrHuX.gif) 22 | 23 | *Why not progressbar, ruby-progressbar, powerbar, or any of the [other gems][]?* These typically have a bucketload of formatting options and you have to manually send updates to the progressbar object to use them. tqdm pleasantly encourages the laziest usage scenario, in that you "set it and forget it". 24 | 25 | [tqdm]: https://github.com/tqdm/tqdm 26 | [other gems]: https://www.ruby-toolbox.com/categories/CLI_Progress_Bars 27 | 28 | ## Install 29 | 30 | Install it globally from [Rubygems](https://rubygems.org/gems/tqdm): 31 | 32 | $ gem install tqdm # (might need sudo for a system ruby) 33 | 34 | *or* add this line to your application's Gemfile: 35 | 36 | gem 'tqdm' 37 | 38 | And then execute: 39 | 40 | $ bundle 41 | 42 | ## Usage 43 | 44 | All `Enumerable` objects gain access to the `#with_progress` method (aliased as `#tqdm`), which returns an enhanced object wherein any iteration (by calling `#each` or any of its relatives: `#each_with_index`, `#map`, `#select`, etc.) produces an animated progress bar on `$stderr`. 45 | 46 | ```ruby 47 | require 'tqdm' 48 | num = 1629241972611353 49 | (2..Math.sqrt(num)).with_progress.reject { |x| num % x > 0 }.map { |x| [x, num/x] } 50 | # ... Animates a progress bar while calculating... 51 | # => [[32599913, 49976881]] 52 | ``` 53 | 54 | Options can be provided as a hash, e.g., `.with_progress(desc: "copying", leave: true)`. The following options are available: 55 | 56 | - `desc`: Short string, describing the progress, added to the beginning of the line 57 | - `total`: Expected number of iterations, if not given, `self.size || self.count` is used 58 | - `file`: A file-like object to output the progress message to, by default, `$stderr` 59 | - `leave`: A boolean (default `false`). Should the progress bar should stay on screen after it's done? 60 | - `min_interval`: Default is `0.5`. If less than `min_interval` seconds or `min_iters` iterations have passed since the last progress meter update, it is not re-printed (decreasing IO thrashing). 61 | - `min_iters`: Default is `1`. See previous. 62 | 63 | [Sequel](http://sequel.jeremyevans.net/) is an amazing database library for Ruby. tqdm can enhance its [`Dataset`](http://sequel.jeremyevans.net/rdoc/classes/Sequel/Dataset.html) objects to show progress while iterating (same options as above): 64 | 65 | ```ruby 66 | require 'tqdm/sequel' # Automatically requires tqdm and sequel 67 | 68 | # In-memory database for demonstration purposes 69 | DB = Sequel.sqlite 70 | DB.create_table :items do 71 | primary_key :id 72 | Float :price 73 | end 74 | 75 | # Show progress during big inserts (this isn't new) 76 | (0..100000).with_progress.each { DB[:items].insert(price: rand * 100) } 77 | 78 | # Show progress during long SELECT queries 79 | DB[:items].where{ price > 10 }.with_progress.each { |row| "do some processing here" } 80 | ``` 81 | 82 | ## TODO 83 | 84 | 1. Performance improvements 85 | 2. Add benchmark suite, expand test coverage 86 | 3. Add smoothing for speed estimates 87 | 4. Support unicode output (smooth blocks) 88 | 5. By default, resize to the apparent width of the output terminal 89 | 90 | ## Contributing 91 | 92 | 1. Fork it 93 | 2. Create your feature branch (`git checkout -b my-new-feature`) 94 | 3. Commit your changes (`git commit -am 'Add some feature'`) 95 | 4. Push to the branch (`git push origin my-new-feature`) 96 | 5. Create new Pull Request 97 | -------------------------------------------------------------------------------- /lib/tqdm/decorator.rb: -------------------------------------------------------------------------------- 1 | require 'tqdm/printer' 2 | 3 | CLONE_UNFROZEN_SUPPORTED = RUBY_VERSION.split('.')[0..1].join('.').to_f >= 2.4 4 | 5 | module Tqdm 6 | 7 | class DecoratorError < StandardError 8 | end 9 | 10 | # Decorates the #each method of an `Enumerable` by wrapping it so that each 11 | # iteration produces a pretty progress bar printed to the console or a file handle. 12 | # 13 | # @note The `Enumerable` is cloned before it is enhanced; it is not modified directly. 14 | # 15 | # @example Enhances `arr` so that an animated progress bar prints while iterating. 16 | # arr = (0...1000) 17 | # arr_tqdm = Decorator.new(arr).enhance 18 | # arr_tqdm.each { |x| sleep 0.01 } 19 | class Decorator 20 | 21 | extend Forwardable 22 | 23 | attr_reader :printer, :enumerable, :iteration, :start_time 24 | 25 | # Initialize a new Decorator. Typically you wouldn't use this object, but 26 | # would immediately call `#enhance` to retrieve the enhanced `Enumerable`. 27 | # 28 | # @param enumerable [Enumerable] the Enumerable object to be enhanced 29 | # @param options [Hash] more options used to control behavior of the progress bar 30 | # @option options [String] :desc a short description added to the beginning of the progress bar 31 | # @option options [Integer] :total (self.size) the expected number of iterations 32 | # @option options [File, IO] :file ($stderr) a file-like object to output the progress bar to 33 | # @option options [Boolean] :leave (false) should the progress bar should stay on screen after it's done? 34 | # @option options [Integer] :min_iters see `:min_interval` 35 | # @option options [Float] :min_interval If less than min_interval seconds or min_iters iterations have passed since 36 | # the last progress meter update, it is not updated again. 37 | # 38 | # @example 39 | # a = (1...1000) 40 | # Decorator.new(a).enhance.each { |x| sleep 0.01 } 41 | # 42 | # @example 43 | # a = [1, 2, 3, 4] 44 | # Decorator.new(a, file: $stdout, leave: true) 45 | def initialize(enumerable, options={}) 46 | @enumerable = enumerable 47 | options.merge!(total: total!) unless options[:total] 48 | @printer = Printer.new(options) 49 | @min_iterations = options[:min_iters] || 1 50 | @min_interval = options[:min_interval] || 0.5 51 | @leave = options[:leave] || false 52 | @force_refreeze = false 53 | end 54 | 55 | # Starts the textual progress bar. 56 | def start! 57 | @iteration = @last_printed_iteration = 0 58 | @start_time = @last_print_time = current_time! 59 | end 60 | 61 | # Called everytime the textual progress bar might need to be updated (i.e. on 62 | # every iteration). We still check whether the update is appropriate to print to 63 | # the progress bar before doing so, according to the `:min_iters` and `:min_interval` 64 | # options. 65 | # 66 | # @see #initialize 67 | def increment! 68 | @iteration += 1 69 | 70 | return unless (iteration - last_printed_iteration) >= @min_iterations 71 | # We check the counter first, to reduce the overhead of Time.now 72 | return unless (current_time! - last_print_time) >= @min_interval 73 | return if iteration == total && !@leave 74 | 75 | printer.status(iteration, elapsed_time!) 76 | @last_printed_iteration = iteration 77 | @last_print_time = current_time 78 | end 79 | 80 | # Prints the final state of the textual progress bar. Based on the `:leave` option, this 81 | # may include deleting it entirely. 82 | def finish! 83 | return printer.null_finish unless @leave 84 | 85 | printer.finish(iteration, elapsed_time!, reprint?) 86 | end 87 | 88 | # Enhances the wrapped `Enumerable`. 89 | # 90 | # @note The `Enumerable` is cloned (shallow copied) before it is enhanced; it is not modified directly. 91 | # 92 | # @return [Enumerable] a clone of Enumerable enhanced so that every call to `#each` animates the 93 | # progress bar. 94 | def enhance 95 | decorate_enumerable_each 96 | enhanced.freeze if @force_refreeze 97 | enhanced 98 | end 99 | 100 | private 101 | 102 | def decorate_enumerable_each 103 | tqdm = self 104 | enhanced.define_singleton_method(:each) do |*args, &block| 105 | tqdm.start! 106 | result = super(*args) do |*items| 107 | block.call *items if block 108 | tqdm.increment! 109 | end 110 | tqdm.finish! 111 | result 112 | end 113 | end 114 | 115 | def enhanced 116 | @enhanced ||= enumerable_unfrozen 117 | end 118 | 119 | # Uses progressively more invasive techniques to return an unfrozen copy of @enumerable 120 | def enumerable_unfrozen 121 | to_unfreeze = CLONE_UNFROZEN_SUPPORTED ? enumerable.clone(freeze: false) : enumerable.clone 122 | return to_unfreeze unless to_unfreeze.frozen? 123 | to_unfreeze = to_unfreeze.dup 124 | return to_unfreeze unless to_unfreeze.frozen? 125 | 126 | # Significantly, for some classes like Sequel::Dataset, both #clone and #dup re-freeze 127 | # the object before returning it, so we have to drop back to Object#clone to avoid this 128 | @force_refreeze = true 129 | if CLONE_UNFROZEN_SUPPORTED 130 | to_unfreeze = Object.instance_method(:clone).bind(enumerable).call(freeze: false) 131 | end 132 | if Object.instance_method(:frozen?).bind(to_unfreeze).call 133 | raise DecoratorError.new("could not create an unfrozen clone") 134 | end 135 | 136 | to_unfreeze 137 | end 138 | 139 | def total! 140 | enumerable.size rescue enumerable.count rescue nil 141 | end 142 | 143 | def last_printed_iteration 144 | @last_printed_iteration ||= iteration 145 | end 146 | 147 | def last_print_time 148 | @last_print_time ||= start_time 149 | end 150 | 151 | def current_time 152 | @current_time ||= current_time! 153 | end 154 | 155 | def current_time! 156 | @current_time = Time.now 157 | end 158 | 159 | def elapsed_time! 160 | current_time! - start_time 161 | end 162 | 163 | def reprint? 164 | last_printed_iteration < iteration 165 | end 166 | 167 | def_delegator :printer, :total 168 | end 169 | end 170 | --------------------------------------------------------------------------------