valodzka
valodzka

Reputation: 5805

Rails number_to_percentage and sub weird behaviour

I'm trying to replace big % numbers to shorters versions (10000% -> 10k%). Generally code works, but if number_to_percentage used it stop working (with TOTALY SAME string).

Loading development environment (Rails 5.1.2)
2.3.1 :001 > "10000.000%".bytes
=> [49, 48, 48, 48, 48, 46, 48, 48, 48, 37]    
2.3.1 :002 > helper.number_to_percentage(10000).bytes
=> [49, 48, 48, 48, 48, 46, 48, 48, 48, 37] # totally same
2.3.1 :003 > helper.number_to_percentage(10000).sub(/(\d\d)\d\d\d(?:[.,]\d+)?\%$/){ "#{$1}k%" }
=> "k%"  # doesn't work
2.3.1 :004 > "10000.000%".sub(/(\d\d)\d\d\d(?:[.,]\d+)?\%$/){ "#{$1}k%" }
=> "10k%" # works

What can cause this? Any ideas?

Upvotes: 2

Views: 210

Answers (2)

Tom Lord
Tom Lord

Reputation: 28305

The key difference is:

"10000.000%".class #=> String
number_to_percentage(10000).class # => ActiveSupport::SafeBuffer

ActiveSupport::SafeBuffer is a subclass of String, and contains the concept of UNSAFE_STRING_METHODS (including sub and gsub). This concept is useful for rails views (which is where number_to_percentage is normally used!), in relation to security; preventing XSS vulnerabilities.

A workaround would be to explicitly convert the variable to a String:

number_to_percentage(10000).to_str.sub(/(\d\d)\d\d\d(?:[.,]\d+)?\%$/){ "#{$1}k%" }
=> "10k%"

(Note that it's to_str, not to_s! to_s just returns self, i.e. an instance of ActiveSupport::SafeBuffer; whereas to_str returns a regular String.)

This article, and this rails issue go into more detail on the issue.

Alternatively, you could write your code like this, and it works as expected:

number_to_percentage(10000).sub(/(\d\d)\d\d\d(?:[.,]\d+)?%$/, '\1k%')
#=> "10k%"

I would actually prefer this approach, since you are no longer relying on modification to the (non-threadsafe) global variable.

Upvotes: 2

Alexis
Alexis

Reputation: 364

Because number_to_percentage returns an ActiveSupport::SafeBuffer and not a String.

helper.number_to_percentage(10000).class # => ActiveSupport::SafeBuffer

ActiveSupport::SafeBuffer (which is a subclass of String) does some magic around unsafe methods like sub. That's why you can have some surprises.

Upvotes: 3

Related Questions