Error if "complete" is Unicode
esotericpig opened this issue · comments
Using Unicode in the complete
option breaks it with a negative array size
error.
# encoding: UTF-8
# frozen_string_literal: true
require 'tty-progressbar'
p = TTY::ProgressBar.new(':title [:bar] :percent :eta',
incomplete: '〜',head: '語',complete: '本',total: 100,width: 33)
p.start()
0.upto(5) do
sleep(1)
p.advance(3)
end
p.finish()
It will produce this error:
Traceback (most recent call last):
...
9: from ~/.gem/ruby/2.7.0/gems/tty-progressbar-0.17.0/lib/tty/progressbar.rb:360:in `finish'
8: from ~/.gem/ruby/2.7.0/gems/tty-progressbar-0.17.0/lib/tty/progressbar.rb:284:in `render'
7: from ~/.rubies/ruby-2.7.0/lib/ruby/2.7.0/forwardable.rb:235:in `decorate'
6: from ~/.gem/ruby/2.7.0/gems/tty-progressbar-0.17.0/lib/tty/progressbar/pipeline.rb:34:in `decorate'
5: from ~/.gem/ruby/2.7.0/gems/tty-progressbar-0.17.0/lib/tty/progressbar/pipeline.rb:34:in `inject'
4: from ~/.gem/ruby/2.7.0/gems/tty-progressbar-0.17.0/lib/tty/progressbar/pipeline.rb:34:in `each'
3: from ~/.gem/ruby/2.7.0/gems/tty-progressbar-0.17.0/lib/tty/progressbar/pipeline.rb:37:in `block in decorate'
2: from ~/.gem/ruby/2.7.0/gems/tty-progressbar-0.17.0/lib/tty/progressbar/formatter/bar.rb:48:in `format'
1: from ~/.gem/ruby/2.7.0/gems/tty-progressbar-0.17.0/lib/tty/progressbar/formatter/bar.rb:48:in `new'
~/.gem/ruby/2.7.0/gems/tty-progressbar-0.17.0/lib/tty/progressbar/formatter/bar.rb:48:in `initialize': negative array size (ArgumentError)
If you change complete
to English, it works fine.
Environment
- Ruby 2.7.0p0 (2019-12-25 revision 647ee6f091)
- TTY::ProgressBar v0.17.0
Looks like this is actually a scaling (width) issue. If you remove the width: 33
it works. For example, this code produces the same error (on ruby 2.6.5):
# encoding: UTF-8
# frozen_string_literal: true
require 'tty-progressbar'
p = TTY::ProgressBar.new(':title [:bar] :percent :eta',
incomplete: 'yy',head: 'xx',complete: 'zz',total: 100, width: 33)
p.start()
0.upto(5) do
sleep(1)
p.advance(3)
end
p.finish()
What seems to be happening is in BarFormatter
, it's calculating by "length" of incomplete/complete chars and comes up with a negative number of incomplete items due to the odd-numbered width. It calculates the width of each character to be 2 (for Unicode & my example), and calculates the "complete_items" for 100% complete to be 17 (33/2 = 16.5, rounded). Then, it calculates the incomplete items:
(33 width
- ( 17 complete items
* 2 complete_char_length
) ) / 2 incomplete_char_length
... = -1
I think clamping incomplete_items
to >=0 is likely a safe solution to this, but I can't guarantee it won't have any edge cases. The only other thing I can think of is overriding the user-provided width
if it won't fit the character spacing.. but that seems less ideal than just accounting for it in the formatter.
diff --git a/lib/tty/progressbar/formatter/bar.rb b/lib/tty/progressbar/formatter/bar.rb
index 71b1070..b5928a6 100644
--- a/lib/tty/progressbar/formatter/bar.rb
+++ b/lib/tty/progressbar/formatter/bar.rb
@@ -42,7 +42,7 @@ module TTY
# decimal number of items only when unicode chars are used
# otherwise it has no effect on regular ascii chars
complete_items = (complete_bar_length / complete_char_length.to_f).round
- incomplete_items = (width - complete_items * complete_char_length) / incomplete_char_length
+ incomplete_items = [(width - complete_items * complete_char_length) / incomplete_char_length, 0].max
complete = Array.new(complete_items, @progress.complete)
incomplete = Array.new(incomplete_items, @progress.incomplete)
@slowbro Thanks for investigating this issue. I appreciate it a lot. Your fix suggestion seems reasonable. On the other hand, I'm thinking of making the complete_items
to be more conservative and always floor
the calculation? The reason why I'm leaning towards this approach is:
- We don't want to exceed a specified width, in this instance I see 33 as a hard limit. This is especially true for terminal display where every character matters.
- When displaying Unicode characters it's probably better to err on the side of caution and show the maximum amount we can fit in the given width, in this instance, 16 complete characters.
Any thoughts?
Agreed, I think that taking the given width as "absolute maximum" makes things less confusing; if it's a little shorter to fit within the constraints, it makes more sense than going over the constraint.
The problem I run in to with floor
is on the first iteration where you have some progress (i.e. complete_bar_length > 0
), `complete_items is still 0 and you get a:
tty-progressbar/lib/tty/progressbar/formatter/bar.rb:50:in `format': index -1 too small for array; minimum: 0 (IndexError)
In addition to this, it gets more complex when the head
character is of a different length than the complete
character - since we're essentially swapping the last 'complete' char for a 'head' char. In order to not have the bar vary in size, the 'head' character would need to be right-padded with space(s) to account for the difference - or, in the case that the 'head' char was longer than the 'complete' char, take this in to account for calculating the number of complete chars.
I'm trying to find a nice way to calculate all of this. I'm thinking we'd need to find the least common multiple of the complete
, head
, and incomplete
chars that fits within the given width
, and then clamp the actual width to that - padding right-padding the head char as needed to maintain spacing.
I'll continue working on it, but right now my brain is not cooperating.
@slowbro This proved to be much more involved than anticipated. I added tests for all possible permutations for complete
, incomplete
and head
options. In the complete and incomplete case, I chose to see how many of the opposing characters 'can fit' in. This is based on the observation that a bar at the start or finish is full of either complete or incomplete characters that need to balance out within the given width at every rendering step. I also covered the head
edge case that you described - it's awkward but works. I admit this is far from optimal solution and there may be a much simpler way but it's late and I cannot think of anything better. It'd be great to have another pair of eyes look at this.
I like it, but it seemed to "jump" around for me in my test cases. I am testing, specifically (in order of the image below): 1-char complete/incomplete w/ 2-char head, 2-char complete/incomplete w/ 1-char head, and all unicode (2-char length). I modified it like so and it produced consistent output:
diff --git a/lib/tty/progressbar/formatter/bar.rb b/lib/tty/progressbar/formatter/bar.rb
index b03f66e..4f9f4d1 100644
--- a/lib/tty/progressbar/formatter/bar.rb
+++ b/lib/tty/progressbar/formatter/bar.rb
@@ -59,14 +59,15 @@ module TTY
complete = Array.new(complete_items, @progress.complete)
incomplete = Array.new(incomplete_items, @progress.incomplete)
- if complete.size > 0 && head_char_length > 0
- extra_space = width - complete_width -
- incomplete_items * incomplete_char_length
- if 0 < extra_space && (head_char_length == extra_space &&
- complete_char_length == extra_space)
- complete << @progress.head
- else
+ if complete.size > 0 && head_char_length > 0 && incomplete.size > 0
+ if head_char_length == complete_char_length
complete[-1] = @progress.head
+ else
+ multiple = complete_char_length * head_char_length
+ extra_space = multiple - head_char_length
+ complete.pop( head_char_length )
+ complete << @progress.head
+ complete << " " * extra_space
end
end
I modified the BarFormatter to print a newline to show each step. This is 0..50% by 10s then a jump to 100% w/ width: 33. Also tested with an even width.
Good catch! Inspired by your code, I implemented a version that handles any size head. Added test to demonstrate behaviour with a 3-characters-long head that uses the incomplete space when required. One thing that I need to sleep on is whether the head should disappear when the bar is done. This is breaking change so didn't want to rush into any decisions yet. Any thoughts?
I have generally been operating that way, but I guess that's not how it 'usually is' - somehow I had gotten that mixed up. For example, pv
doesn't get rid of it:
100MiB 0:00:00 [ 306MiB/s] [=======================================================================>] 100%
I'm indifferent. I think it looks good both ways, to be honest. Maybe it could be an option?
@slowbro Added :clear_head
option which by default keeps the head character when progress is done. Thank you for the reviews and feedback! ❤️
Thank you very much!