Skip to content

Commit 1e0831b

Browse files
committed
v0.0.2
1 parent a3a1f4a commit 1e0831b

File tree

7 files changed

+317
-91
lines changed

7 files changed

+317
-91
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/_build
22
/cover
33
/deps
4+
/doc
5+
/docs
46
erl_crash.dump
57
*.ez

.travis.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
language: elixir
2+
elixir:
3+
- 1.2.5
4+
otp_release:
5+
- 18.3
6+
sudo: false
7+
env:
8+
- MIX_ENV=test
9+
- MIX_ENV=test ELIXIR_ERL_OPTIONS="+T 9"
10+
before_script:
11+
- mix compile --warnings-as-errors
12+
- mix dialyzer.plt
13+
script:
14+
- mix coveralls.travis
15+
- mix dialyzer --halt-exit-status
16+
- mix credo --strict
17+
after_script:
18+
- mix deps.get --only docs
19+
- MIX_ENV=docs mix inch.report
20+
cache:
21+
directories:
22+
- .dialyxir
23+
- _build
24+
- deps

README.md

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,43 @@
1-
# DOCS IN PROGRESS!
1+
[![Build Status](https://travis-ci.org/antipax/tqdm_elixir.svg?branch=master)](https://travis-ci.org/antipax/tqdm_elixir) [![Coverage Status](https://coveralls.io/repos/github/antipax/tqdm_elixir/badge.svg?branch=master)](https://coveralls.io/github/antipax/tqdm_elixir?branch=master) [![Inline docs](http://inch-ci.org/github/antipax/tqdm_elixir.svg?branch=master)](http://inch-ci.org/github/antipax/tqdm_elixir)
22

33
# Tqdm
44

5-
Add a progress bar to your enumerables in a second.
5+
Tqdm easily adds a CLI progress bar to any enumerable.
66

77
A (partial) port of Python's [tqdm](https://github.com/tqdm/tqdm) to Elixir. Thanks noamraph and all other contributors for the original library!
88

9-
Wrap your favorite enumerables (i.e. Lists, Maps, Streams, anything that implements Enumerable!) with tqdm, and see progress in detail!
10-
11-
For example:
9+
Just wrap Lists, Maps, Streams, or anything else that implements Enumerable with `Tqdm.tqdm`:
1210

1311
```elixir
1412
for _ <- Tqdm.tqdm(1..1000) do
1513
:timer.sleep(10)
1614
end
17-
```
1815

19-
will give you a nice progress bar like:
16+
# or
2017

21-
```
22-
|######----| 665/1000 67.0% [elapsed: 00:00:10.522247 left: 00:00:05, 63.2 iters/sec]
18+
1..1000
19+
|> Tqdm.tqdm()
20+
|> Enum.map(fn _ -> :timer.sleep(10) end)
21+
22+
# or even...
23+
24+
1..1000
25+
|> Stream.map(fn -> :timer.sleep(10) end)
26+
|> Tqdm.tqdm()
27+
|> Stream.run()
28+
29+
# |###-------| 392/1000 39.0% [elapsed: 00:00:04.627479 left: 00:00:07, 84.71 iters/sec]
2330
```
2431

2532
## Installation
2633

27-
If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
28-
2934
1. Add tqdm to your list of dependencies in `mix.exs`:
3035

3136
def deps do
32-
[{:tqdm, "~> 0.0.1"}]
37+
[{:tqdm, "~> 0.0.2"}]
3338
end
3439

35-
2. Ensure tqdm is started before your application:
40+
2. Ensure tqdm is added to your list of applications:
3641

3742
def application do
3843
[applications: [:tqdm]]

lib/tqdm.ex

Lines changed: 166 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,110 @@
11
defmodule Tqdm do
2+
@moduledoc """
3+
Tqdm easily adds a CLI progress bar to any enumerable.
24
3-
@num_bars 10
5+
Just wrap Lists, Maps, Streams, or anything else that implements Enumerable
6+
with `Tqdm.tqdm`:
47
5-
def tqdm(enumerable, opts \\ []) do
6-
now = :erlang.monotonic_time()
8+
for _ <- Tqdm.tqdm(1..1000) do
9+
:timer.sleep(10)
10+
end
11+
12+
# or
13+
14+
1..1000
15+
|> Tqdm.tqdm()
16+
|> Enum.map(fn _ -> :timer.sleep(10) end)
17+
18+
# or even...
19+
20+
1..1000
21+
|> Stream.map(fn -> :timer.sleep(10) end)
22+
|> Tqdm.tqdm()
23+
|> Stream.run()
24+
25+
# |###-------| 392/1000 39.0% [elapsed: 00:00:04.627479 \
26+
left: 00:00:07, 84.71 iters/sec]
27+
"""
28+
29+
30+
@type option ::
31+
{:description, String.t} |
32+
{:total, non_neg_integer} |
33+
{:clear, boolean} |
34+
{:device, IO.device} |
35+
{:min_interval, non_neg_integer} |
36+
{:min_iterations, non_neg_integer} |
37+
{:total_segments, non_neg_integer}
38+
39+
@type options :: [option]
40+
41+
@doc """
42+
Wrap the given `enumerable` and print a CLI progress bar.
43+
44+
`options` may be provided:
45+
46+
* `:description` - a short string that is displayed on the progress bar.
47+
For example, if the string `"Processing values"` is provided for this
48+
option:
49+
50+
# Processing values: |###-------| 349/1000 35.0% [elapsed: \
51+
00:00:06.501472 left: 00:00:12, 53.68 iters/sec]
52+
53+
* `:total` - by default, `Tdqm` will use `Enum.count` to count how many
54+
elements are in the given `enumerable`. For large amounts of data, or
55+
streams, this may not be appropriate. You can provide your own total with
56+
this option. You may provide an estimate, and if the actual count
57+
exceeds this value, the progress bar will change to an indeterminate mode:
58+
59+
# 296 [elapsed: 00:00:03.500038, 84.57 iters/sec]
60+
61+
You can also force the indeterminate mode by passing `0`.
62+
63+
* `:clear` - by default, `Tqdm` will clear the progress bar after the
64+
enumeration is complete. If you pass `false` for this option, the progress
65+
bar will persist, instead.
66+
67+
* `:device` - by default, `Tqdm` writes to `:stderr`. You can provide any
68+
`IO.device` to this option to use it instead of the default.
69+
70+
* `:min_interval` - by default, `Tqdm` will only print progress updates
71+
every 100ms. You can increase or decrease this value using this option.
72+
73+
* `:min_iterations` - by default, `Tqdm` will check if the `:min_interval`
74+
has passed for every iteration. Passing a value for this option will skip
75+
this check until at least `:min_iterations` iterations have passed.
76+
77+
* `:total_segments` - by default, `Tqdm` will split its progress bar into 10
78+
segments. You can customize this by passing a different value for this
79+
option.
80+
"""
81+
@spec tqdm(Enumerable.t, options) :: Enumerable.t
82+
def tqdm(enumerable, options \\ []) do
83+
start_fun = fn ->
84+
now = :erlang.monotonic_time()
85+
86+
get_total = fn -> Enum.count(enumerable) end
87+
88+
%{
89+
n: 0,
90+
last_print_n: 0,
91+
start_time: now,
92+
last_print_time: now,
93+
last_printed_length: 0,
94+
prefix: options |> Keyword.get(:description, "") |> prefix(),
95+
total: Keyword.get_lazy(options, :total, get_total),
96+
clear: Keyword.get(options, :clear, true),
97+
device: Keyword.get(options, :device, :stderr),
98+
min_interval:
99+
options
100+
|> Keyword.get(:min_interval, 100)
101+
|> :erlang.convert_time_unit(:milli_seconds, :native),
102+
min_iterations: Keyword.get(options, :min_iterations, 1),
103+
total_segments: Keyword.get(options, :total_segments, 10)
104+
}
105+
end
7106

8-
state = %{
9-
n: 0,
10-
last_print_n: 0,
11-
start_time: now,
12-
last_print_time: now,
13-
last_printed_length: 0,
14-
prefix: Keyword.get(opts, :description, "") |> prefix(),
15-
total: Keyword.get_lazy(opts, :total, fn -> Enum.count(enumerable) end),
16-
clear: Keyword.get(opts, :clear, true),
17-
device: Keyword.get(opts, :device, :stderr),
18-
min_interval: Keyword.get(opts, :min_interval, 100),
19-
min_iterations: Keyword.get(opts, :min_iterations, 1)
20-
}
21-
22-
Stream.transform(enumerable, fn -> state end, &do_tqdm/2, &do_tqdm_after/1)
107+
Stream.transform(enumerable, start_fun, &do_tqdm/2, &do_tqdm_after/1)
23108
end
24109

25110
defp prefix(""), do: ""
@@ -29,26 +114,40 @@ defmodule Tqdm do
29114
{[element], %{print_status(state, :erlang.monotonic_time()) | n: 1}}
30115
end
31116

32-
defp do_tqdm(element, %{n: n, last_print_n: last_print_n, min_iterations: min_iterations} = state)
33-
when n - last_print_n < min_iterations,
117+
defp do_tqdm(
118+
element,
119+
%{n: n, last_print_n: last_print_n, min_iterations: min_iterations} = state
120+
) when n - last_print_n < min_iterations,
34121
do: {[element], %{state | n: n + 1}}
35122

36-
defp do_tqdm(element, %{n: n, last_print_time: last_print_time, min_interval: min_interval} = state) do
123+
defp do_tqdm(element, state) do
37124
now = :erlang.monotonic_time()
38125

39-
if :erlang.convert_time_unit(now - last_print_time, :native, :milli_seconds) >= min_interval do
40-
state = %{print_status(state, now) | last_print_n: n, last_print_time: :erlang.monotonic_time()}
41-
end
126+
time_diff =
127+
now - state.last_print_time
128+
129+
state =
130+
if time_diff >= state.min_interval do
131+
Map.merge(print_status(state, now), %{
132+
last_print_n: state.n,
133+
last_print_time: :erlang.monotonic_time()
134+
})
135+
else
136+
state
137+
end
42138

43-
{[element], %{state | n: n + 1}}
139+
{[element], %{state | n: state.n + 1}}
44140
end
45141

46142
defp do_tqdm_after(state) do
47143
state = print_status(state, :erlang.monotonic_time())
48144

49145
finish =
50146
if state.clear do
51-
"\r" <> String.duplicate(" ", String.length(state.prefix) + state.last_printed_length) <> "\r"
147+
prefix_length = String.length(state.prefix)
148+
total_bar_chars = prefix_length + state.last_printed_length
149+
150+
"\r" <> String.duplicate(" ", total_bar_chars) <> "\r"
52151
else
53152
"\n"
54153
end
@@ -60,48 +159,71 @@ defmodule Tqdm do
60159
status = format_status(state, now)
61160
status_length = String.length(status)
62161

63-
padding = String.duplicate(" ", max(state.last_printed_length - status_length, 0))
162+
num_padding_chars = max(state.last_printed_length - status_length, 0)
163+
padding = String.duplicate(" ", num_padding_chars)
64164

65165
IO.write(state.device, "\r#{state.prefix}#{status}#{padding}")
66166

67167
%{state | last_printed_length: status_length}
68168
end
69169

70-
defp format_status(%{n: n, total: total, start_time: start_time}, now) do
71-
elapsed = :erlang.convert_time_unit(now - start_time, :native, :micro_seconds)
72-
73-
total = if n <= total, do: total
170+
defp format_status(state, now) do
171+
elapsed =
172+
:erlang.convert_time_unit(now - state.start_time, :native, :micro_seconds)
74173

75174
elapsed_str = format_interval(elapsed, false)
76175

77-
rate = if elapsed > 0, do: Float.round(n / (elapsed / 1_000_000), 2), else: "?"
176+
rate = format_rate(elapsed, state.n)
177+
178+
format_status(state, elapsed, rate, elapsed_str)
179+
end
78180

79-
if total do
181+
defp format_status(state, elapsed, rate, elapsed_str) do
182+
n = state.n
183+
total = state.total
184+
total_segments = state.total_segments
185+
186+
if n <= total and total != 0 do
80187
progress = n / total
81188

82-
num_bars = trunc(progress * @num_bars)
83-
bar = String.duplicate("#", num_bars) <> String.duplicate("-", @num_bars - num_bars)
189+
num_segments = trunc(progress * total_segments)
190+
191+
bar = format_bar(num_segments, total_segments)
84192

85193
percentage = "#{Float.round(progress * 100)}%"
86194

87-
left_str = if n > 0, do: format_interval(elapsed / n * (total - n), true), else: "?"
195+
left = format_left(n, elapsed, total)
88196

89-
"|#{bar}| #{n}/#{total} #{percentage} [elapsed: #{elapsed_str} left: #{left_str}, #{rate} iters/sec]"
197+
"|#{bar}| #{n}/#{total} #{percentage} " <>
198+
"[elapsed: #{elapsed_str} left: #{left}, #{rate} iters/sec]"
90199
else
91200
"#{n} [elapsed: #{elapsed_str}, #{rate} iters/sec]"
92201
end
93202
end
94203

204+
defp format_rate(elapsed, n) when elapsed > 0,
205+
do: Float.round(n / (elapsed / 1_000_000), 2)
206+
defp format_rate(_elapsed, _n),
207+
do: "?"
208+
209+
defp format_bar(num_segments, total_segments) do
210+
String.duplicate("#", num_segments) <>
211+
String.duplicate("-", total_segments - num_segments)
212+
end
213+
214+
defp format_left(n, elapsed, total) when n > 0,
215+
do: format_interval(elapsed / n * (total - n), true)
216+
defp format_left(_n, _elapsed, _total),
217+
do: "?"
218+
95219
defp format_interval(elapsed, trunc_seconds) do
96220
minutes = trunc(elapsed / 60_000_000)
97221
hours = div(minutes, 60)
98222
rem_minutes = minutes - hours * 60
99223
micro_seconds = elapsed - minutes * 60_000_000
100224
seconds = micro_seconds / 1_000_000
101225

102-
if trunc_seconds do
103-
seconds = trunc(seconds)
104-
end
226+
seconds = if trunc_seconds, do: trunc(seconds), else: seconds
105227

106228
hours_str = format_time_component(hours)
107229
minutes_str = format_time_component(rem_minutes)
@@ -110,13 +232,8 @@ defmodule Tqdm do
110232
"#{hours_str}:#{minutes_str}:#{seconds_str}"
111233
end
112234

113-
defp format_time_component(time) do
114-
time_string = to_string(time)
115-
116-
if time < 10 do
117-
"0" <> time_string
118-
else
119-
time_string
120-
end
121-
end
235+
defp format_time_component(time) when time < 10,
236+
do: "0#{time}"
237+
defp format_time_component(time),
238+
do: to_string(time)
122239
end

0 commit comments

Comments
 (0)