Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Random function with Fixes #1062

Merged
merged 5 commits into from Feb 1, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 49 additions & 3 deletions lib/sass/script/functions.rb
Expand Up @@ -401,6 +401,24 @@ def self.signature(method_name, arg_arity, kwarg_arity)
@signatures[method_name].first
end

# Sets the random seed used by Sass's internal random number generator.
#
# This can be used to ensure consistent random number sequences which
# allows for consistent results when testing, etc.
#
# @param seed [Integer]
# @return [Integer] The same seed.
def self.random_seed=(seed)
@random_number_generator = Random.new(seed)
end

# Get Sass's internal random number generator.
#
# @return [Random]
def self.random_number_generator
@random_number_generator ||= Random.new
end

# The context in which methods in {Script::Functions} are evaluated.
# That means that all instance methods of {EvaluationContext}
# are available to use in functions.
Expand Down Expand Up @@ -2018,9 +2036,9 @@ def if(condition, if_true, if_false)
# @overload unique_id()
# @return [Sass::Script::Value::String]
def unique_id
Thread.current[:sass_last_unique_id] ||= rand(36**8)
Thread.current[:sass_last_unique_id] ||= generator.rand(36**8)
# avoid the temptation of trying to guess the next unique value.
value = (Thread.current[:sass_last_unique_id] += (rand(10) + 1))
value = (Thread.current[:sass_last_unique_id] += (generator.rand(10) + 1))
# the u makes this a legal identifier if it would otherwise start with a number.
identifier("u" + value.to_s(36).rjust(8, '0'))
end
Expand Down Expand Up @@ -2157,7 +2175,6 @@ def mixin_exists(name)
end
declare :mixin_exists, [:name]


# Return a string containing the value as its Sass representation.
#
# @param value [Sass::Script::Value::Base] The value to inspect.
Expand All @@ -2168,6 +2185,28 @@ def inspect(value)
end
declare :inspect, [:value]

# @overload random()
# Return a decimal between 0 and 1, inclusive of 0, but not 1.
# @return [Sass::Script::Number] A decimal value.
# @overload random($limit)
# Return an integer between 1 and `$limit` inclusive.
# @param $limit [Sass::Script::Value::Number] The maximum of the random integer to be returned, a positive integer.
# @return [Sass::Script::Number] An integer.
# @raise [ArgumentError] if the `$limit` is not 1 or greater
def random(limit = nil)
if limit
assert_integer limit, "limit"
if limit.value < 1
raise ArgumentError.new("Expected the limit to be 1 or greater")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, this message should be "$limit #{limit} must be greater than or equal to 1".

end
number(1 + generator.rand(limit.value))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that this is inclusive of limit; for consistency with random(), it should probably be exclusive instead.

else
number(generator.rand)
end
end
declare :random, []
declare :random, [:limit]

private

# This method implements the pattern of transforming a numeric value into
Expand Down Expand Up @@ -2205,5 +2244,12 @@ def to_h(obj)
WARNING
obj.to_h
end

# Convenient access to Sass's internal random number generator.
#
# @return [Random]
def generator
Sass::Script::Functions.random_number_generator
end
end
end
1 change: 1 addition & 0 deletions lib/sass/util.rb
Expand Up @@ -1090,3 +1090,4 @@ def lcs_backtrace(c, x, y, i, j, &block)

require 'sass/util/multibyte_string_scanner'
require 'sass/util/normalized_map'
require 'sass/util/random'
15 changes: 15 additions & 0 deletions lib/sass/util/random.rb
@@ -0,0 +1,15 @@
# In Ruby 1.9+, there is a built-in Random class, which is globally defined.
# In case we are running on Ruby 1.8.7 (detected, by having no Random class defined),
# this method defines a mock Random class.
unless defined?(::Random)
# Shim for ruby 1.8.7 support.
class Random
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name this CrossPlatformRandom or something and put it within the Sass::Util namespace. We don't want to cause conflicts if someone else defines a top-level Random class on 1.8.

def initialize(seed = nil)
srand(seed) if seed
end

def rand(*args)
Kernel.rand(*args)
end
end
end
49 changes: 49 additions & 0 deletions test/sass/functions_test.rb
Expand Up @@ -1574,6 +1574,55 @@ def test_inspect
assert_equal "(a: 1, b: 2)", evaluate("inspect((a: 1, b: 2))")
end

def test_random
Sass::Script::Functions.random_seed = 1
assert_equal "0.41702", evaluate("random()")
assert_equal "13", evaluate("random(100)")
end

def test_random_works_without_a_seed
if Sass::Script::Functions.instance_variable_defined?("@random_number_generator")
Sass::Script::Functions.send(:remove_instance_variable, "@random_number_generator")
end
assert_nothing_raised do
result = perform("random()")
assert_kind_of Sass::Script::Number, result
assert result.value >= 0, "Random number was below 0"
assert result.value <= 1, "Random number was above 1"
end
end

def test_random_works_with_limit
if Sass::Script::Functions.instance_variable_defined?("@random_number_generator")
Sass::Script::Functions.send(:remove_instance_variable, "@random_number_generator")
end
assert_nothing_raised do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary, since assert will produce an appropriate error.

# Passing 1 as the limit should always return 1, since limit calls return
# integers from 1 to the argument, so when the argument is 1, its a predicatble
# outcome
assert "1", evaluate("random(1)")
end

# Limit must be 1 or greater
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer these be separate tests with descriptive names.

assert_raises Sass::SyntaxError do
evaluate("random(0)")
end

# Limit cannot be negative
assert_raises Sass::SyntaxError do
evaluate("random(-1)")
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should also be a test for a non-integer limit.

end

# This could *possibly* fail, but exceedingly unlikely
def test_random_is_semi_unique
if Sass::Script::Functions.instance_variable_defined?("@random_number_generator")
Sass::Script::Functions.send(:remove_instance_variable, "@random_number_generator")
end
assert_nothing_raised do
assert_not_equal evaluate("random()"), evaluate("random()")
end
end

## Regression Tests

Expand Down