11defmodule 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 )
122239end
0 commit comments