Reputation: 1105
I'm looking to randomly change the case of a string to upcase or downcase. There is a similar question on here for it except it doesn't guarantee that the string gets changed.
I am currently using:
def random_case_changes(string_to_change: string, change_case: 'downcase')
raise ArgumentError, "Param is #{string_to_change.class}, not a string" unless string_to_change.is_a? String
string_to_change.chars.map { |char| (rand 0..2).zero? ? char : char.send(change_case) }.join
end
random_case_changes(string_to_change: 'Hello', change_case: 'upcase')
There is a chance that this will just return 'Hello
.
I've looked at using .sample
on the array of chars, but it jumbles the order and I haven't found a way to put the string back into it's original order after a change has been made.
Is there a way to guarantee that a change will take place?
Upvotes: 1
Views: 322
Reputation: 1105
Thank you all for your input. These answers pushed me down a rabbit hole of learning and investigation.
Here is the solution I went with:
def random_case_change(string, options = {})
new_case = options[:new_case].nil? ? 'upcase' : options[:new_case]
# don't attempt to change if string is ONLY whitespace
unless string.match?(/\A\s*\z/)
# only word characters will be changed,
string.chars.each_with_index.map { |c, idx| c.match?(/\w/) ? idx : nil }.reject(&:nil?)
.then { |indicies| indicies.sample(rand(1..indicies.size)) }
.inject(string) { |memo, idx| memo[idx] = memo[idx].send(new_case); memo }
end
string
end
I took it further to ignore the string if it is only whitespace. It will also ignore any characters in the string that are not word characters. I also added a default option to the kind of I wish to use. This will allow, upcase, downcase and swapcase.
Upvotes: 0
Reputation: 110745
Here are three ways that could be done that ensure pseudo-randomness.
All use the string
str = "WoLf"
for demonstration and the method
def change_possible?(str, to_case)
str.count(to_case == :downcase ? 'a-z' : 'A-Z') < str.size
end
to determine if the randomization procedure can produce a string that is different than the given string, str
.
change_possible?("WoLf", :downcase) #=> true
change_possible?("WoLf", :upcase) #=> true
change_possible?('wolf', :downcase) #=> false
change_possible?('WOLF', :upcase) #=> false
The latter two approaches use the method
def char_case(c)
c.downcase == c ? :downcase : :upcase
end
char_case('a') #=> :downcase
char_case('A') #=> :upcase
Change the case of one randomly-selected character then randomly determine whether to set the case of each of the remaining characters to the specified case1
def scramble(str, to_case)
return nil unless change_possible?(str, to_case)
i = str.size.times.reject do |i|
char_case(str[i]) == to_case
end.sample
c = str[i].public_send(to_case)
str.gsub(/./) { |s| [s, s.public_send(to_case)].sample }.
tap { |s| s[i] = c }
end
str #=> "WoLf"
scramble(str, :downcase) #=> "Wolf"
scramble(str, :downcase) #=> "woLf"
scramble(str, :downcase) #=> "wolf"
scramble("wolf", :downcase) #=> nil
scramble(str, :upcase) #=> "WOLF"
scramble(str, :upcase) #=> "WOLf"
scramble(str, :upcase) #=> "WOLF"
scramble("WOLF", :upcase) #=> nil
Randomly determine whether to set the case of each character to the specified case, repeating until the resulting string differs from the given string
def scramble(str, to_case)
return nil unless change_possible?(str, to_case)
loop do
s = str.gsub(/./) { |c| [c, c.public_send(to_case)].sample }
break s unless s == str
end
end
str #=> "WoLf"
scramble(str, :downcase) #=> "Wolf"
scramble(str, :downcase) #=> "woLf"
scramble(str, :downcase) #=> "wolf"
scramble("wolf", :downcase) #=> nil
scramble(str, :upcase) #=> "WOLF"
scramble(str, :upcase) #=> "WoLF"
scramble(str, :upcase) #=> "WoLF"
scramble("WOLF", :upcase) #=> nil
Note that c.public_send(to_case)
may equal c
in [c, c.public_send(to_case)].sample
.
Construct the sample space then select a member at random
def scramble(str, to_case)
return nil unless change_possible?(str, to_case)
sample_space(str, to_case).sample
end
def sample_space(str, to_case)
fixed_idx = str.size.times.select do |i|
char_case(str[i]) == to_case
end
len = str.size
(0..2**str.size - 1).map do |n|
len.times.with_object('') do |i,s|
s << (n[i] == 1 ? str[i].upcase : str[i].downcase)
end
end.reject do |s|
s == str || fixed_idx.any? { |i| s[i] != str[i] }
end
end
str #=> "WoLf"
scramble(str, :downcase) #=> "wolf"
scramble(str, :downcase) #=> "woLf"
scramble(str, :downcase) #=> "Wolf"
scramble("wolf", :downcase) #=> nil
scramble(str, :upcase) #=> "WOLF"
scramble(str, :upcase) #=> "WOLF"
scramble(str, :upcase) #=> "WoLF"
scramble("WOLF", :upcase) #=> nil
For :downcase
the sample space was found to be
["wolf", "Wolf", "woLf"]
For :upcase
,
["WOLf", "WoLF", "WOLF"]
The sample space initially contains 2**str.size #=> 16
elements, there being a 1-1 map between each element of the sample space and one of the integers between 0
and 2**str.size - 1
. The mapping corresponds to the value of the integer's bits (padded with leading zeroes, when necessary, to str.size
bits), 0
corresponding to lowercase, 1
to uppercase. Invalid strings are then removed from the sample space.
See Integer#[] and note that n[i]
can equal 0
, 1
or nil
, with nil
corresponding to a leading 0
.
I make no claims about the efficiency of this approach.
1. This is a variant of Stefan's approach.
Upvotes: 0
Reputation: 114248
Is there a way to guarantee that a change will take place?
You could get the indices of the character that could change, e.g. the lowercase ones:
string_to_change = 'Hello'
indices = string_to_change.enum_for(:scan, /[[:lower:]]/).map { $~.begin(0) }
#=> [1, 2, 3, 4]
And out of this array pick between 1 and all elements that will change: (the order doesn't matter)
indices_to_be_changed = indices.sample(rand(1..indices.size))
#=> [4, 2]
Now all you have to do is swap the case of the corresponding characters: (upper to lower or vice-versa)
indices_to_be_changed.each do |i|
string_to_change[i] = string_to_change[i].swapcase
end
string_to_change
#=> "HeLlO"
Upvotes: 2
Reputation: 107077
I would do something like this:
def random_case_changes(string: string, case: 'downcase')
if string.is_a?(String)
string.chars.map { |char| [char, char.public_send(change_case)].sample }.join
else
raise ArgumentError, "Param is #{string.class}, not a string"
end
end
Upvotes: 0