Ruby Map Method edits the original array? - ruby

def removal(arr)
letters ="i"
p arr
new_array = arr.map do |c_word|
c_word.each_char.with_index do |char, index|
if letters.include?(char)
c_word[index] = "*"
end
end
end
p arr #the original array is getting edited? why?
p new_array
end
removal(["hiiiiiigh","git", "training"])
In this code, the original array (arr) in the map method keeps getting edited. I thought that map does not edit the original array. If I needed to edit the original, then I would use .map!
I believe it has something to do with the nested enumerator or a variable reference that I am not seeing. Instead of each_char.with_index, I used a while loop and map would still edit the original array. Why is the original array being edited?

You are actually wrong in (at least) two places:
map is not editing the original array
in fact, the original array is not being edited at all
If you look closely, the array hasn't changed, only the strings inside the array have changed. And it is not map that is doing this, it is String#[]=, which you are calling here:
c_word[index] = "*"
So, you are calling a method that edits strings, and you should not be surprised that your strings are edited!

Think of using:
map as saying "I want to create new data based on existing data"
each as saying "I either want to not change any data, or change existing data"
Having this in mind, what you are doing is using map with array to create new array based on existing one, and then using each to modify characters in existing strings. This is why the strings in the original array end up modified.
To fix this use map twice, first to "create new array based on existing array", and then the second time to "create new string based on existing string". This way the original strings won't get modified.
def removal(arr)
letters ="i"
p arr
new_array = arr.map do |word|
word.chars.map do |char|
letters.include?(char) ? '*' : char
end.join
end
p arr
p new_array
end
removal(["hiiiiiigh","git", "training"]) #=> ["hiiiiiigh", "git", "training"]
# ["hiiiiiigh", "git", "training"]
# ["h******gh", "g*t", "tra*n*ng"]
More practical solution to this problem would be something like this:
def censor(strings, forbidden_chars_string, censor_char = '*')
re = Regexp.union(forbidden_chars_string.chars)
strings.map {|str| str.gsub(re, censor_char) }
end
p ["hiiiiiigh","git", "training"] #=> ["hiiiiiigh", "git", "training"]
p censor(["hiiiiiigh","git", "training"], "i") #=> ["h******gh", "g*t", "tra*n*ng"]
p censor(["hiiiiiigh","git", "training"], "gn", '_') #=> ["hiiiiii_h", "_it", "trai_i__"]

This is happening because inside the map block you are doing some processing on each word of arr and not on each word new_array. If you want to copy the words of arr and change it in new_array then create a copy, change it and return the word.
Checkout these 2 codes and you will get my point
Code 1
def removal(arr)
letters ="i"
p arr
new_array = arr.map do |c_word|
c_word.each_char.with_index do |char, index|
if letters.include?(char)
c_word[index] = "*"
end
end
c_word
end
p arr
p new_array
end
removal(["hiiiiiigh","git", "training"])
Here you are changing words of arr and copying it to new_array
Code 2
def removal(arr)
letters ="i"
p arr
new_array = arr.map do |c_word|
n_word = c_word.dup
n_word.each_char.with_index do |char, index|
if letters.include?(char)
n_word[index] = "*"
end
end
n_word
end
p arr
p new_array
end
removal(["hiiiiiigh","git", "training"])
Here you are copying words of arr, changing it and adding them to new_array

If you don't want to change the array's elements, you should not change them. Your problem is in this line:
c_word[index] = "*"
So just use methods that do not affect the recipient, e.g.:
def removal(array)
letter = 'i'
array.map { |word| word.gsub(letter, '*') }
end

Related

Build list of substrings created by separating a string by a match

I have a string:
"a_b_c_d_e"
I would like to build a list of substrings that result from removing everything after a single "_" from the string. The resulting list would look like:
['a_b_c_d', 'a_b_c', 'a_b', 'a']
What is the most rubyish way to achieve this?
s = "a_b_c_d_e"
a = []
s.scan("_"){a << $`} #`
a # => ["a", "a_b", "a_b_c", "a_b_c_d"]
You can split the string on the underscore character into an Array. Then discard the last element of the array and collect the remaining elements in another array joined by underscores. Like this:
str = "a_b_c_d_e"
str_ary = str.split("_") # will yield ["a","b","c","d","e"]
str_ary.pop # throw out the last element in str_ary
result_ary = [] # an empty array where you will collect your results
until str_ary.empty?
result_ary << str_ary.join("_") #collect the remaining elements of str_ary joined by underscores
str_ary.pop
end
# result_ary = ["a_b_c_d","a_b_c","a_b","a"]
Hope this helps.
I am not sure about “most rubyish”, my solutions would be:
str = 'a_b_c_d_e'
(items = str.split('_')).map.with_index do |_, i|
items.take(i + 1).join('_')
end.reverse
########################################################
(items = str.split('_')).size.downto(1).map do |e|
items.take(e).join('_')
end
########################################################
str.split('_').inject([]) do |memo, l|
memo << [memo.last, l].compact.join('_')
end.reverse
########################################################
([items]*items.size).map.with_index(&:take).map do |e|
e.join('_')
end.reject(&:empty?).reverse
My fave:
([str]*str.count('_')).map.with_index do |s, i|
s[/\A([^_]+_){#{i + 1}}/][0...-1]
end.reverse
Ruby ships with a module for abbreviation.
require "abbrev"
puts ["a_b_c_d_e".tr("_","")].abbrev.keys[1..-1].map{|a| a.chars*"_"}
# => ["a_b_c_d", "a_b_c", "a_b", "a"]
It works on an Array with words - just one in this case. Most work is removing and re-placing the underscores.

concatenate with splat ruby

say I have
arr = [1,2,3]
How can I change this method so it adds each argument to the array?
def add(*number)
arr << *number
end
So add(4,5,6) produces:
arr #=> [1,2,3,4,5,6]
When accepting arguments via splat, they will always be an array. So you can simply add the two arrays together.
def add(*numbers)
arr + numbers
end
Use concat:
def add(*nums)
arr.concat nums
end
Or +:
def add(*nums)
arr + nums
end
$arr = [1,2,3]
def add(*number)
$arr.concat number
end
add(4,5,6)
$arr #=> [1,2,3,4,5,6]
Note: concat modifies the object it operates on ($arr). Plus (+) does not.
As the Tin Man mentions, you don't want to use a global to do this. It is better to simply do
arr.concat [4,5,6]
outside of a function call. Better yet:
arr += [4,5,6]

How to sort array of words into arrays of anagrams in Ruby?

http://spark-university.s3.amazonaws.com/berkeley-saas/homework/hw1.pdf
Trying to part 3 of this assignment. The following code does not seem to work, namely for the parameter ['HeLLo', 'hello'], returning [["hello"], ["HeLLo"]] instead of [["HeLLo", "hello"]]
def combine_anagrams(words)
#iterate through words, make hashmap with the sorted version
hash = {}
words.each do |x|
hash[x.chars.sort.join.downcase.gsub /\W/, ""] = []
end
#iterate through words, access hashmap and append curr to array
words.each do |x|
hash[x.chars.sort.join.downcase.gsub /\W/, ""] << x
end
hash.values #return array of values
end
Any help would be appreciated. (I'm new to Ruby)
You could easily do it like this:
def combine_anagrams(words)
anagrams={}
words.each do |word|
anagrams[word.downcase.split('').sort.join] ||=[]
anagrams[word.downcase.split('').sort.join] << word
end
anagrams.values
end

array modified in the loop causes bug with the output ruby

I have a code that places anagrams into an array of arrays. (which contain anagrams)
but somewhere i made a bug and the first values do not output as arrays but just as strings
I am using the << operator to push one array into the other
the code is not that complicated but i cannot find a bug
def combine_anagrams(words)
indexes = []
anagrams = []
words.each{|word|
if(word.is_a? String )
first_word = word.downcase.chars.sort.join
words.each{|second_word|
if(second_word.is_a? String)
if(first_word == second_word.downcase.chars.sort.join)
indexes << words.index(second_word)
end
end
}
indexes.each{|index| anagrams << words[index] }
words.reject!.with_index {|el, idx| indexes.include?(idx)}
words << anagrams # i replaced words with an array all_anagrams
indexes = []
anagrams = []
end
}
return words
end
puts combine_anagrams([ 'cars','for', 'potatoes', 'racs', 'four','scar', 'creams', 'scream'] ).inspect
outputs
["for", "four", ["cars", "racs", "scar"], ["potatoes"], ["creams", "scream"]]
if i switch the order of "cars" and "for" in the input i get
["cars", "racs", "scar", ["for"], ["potatoes"], ["four"], ["creams", "scream"]]
Whats going on here
Sorry for the messy code im just begging to learn ruby
I created an additional variable all_anagrams = [] to store the array of all anagrams
when i output the array onto the sreen i get all the values except the "for" and "four" for some reason those never get send to all_anagrams
probably because i shorten the array when i am in the loop and those values get skipped over?
However i dont know how to deal with this problem.
the output of all_anagrams is
[["cars", "racs", "scar"], ["potatoes"], ["creams", "scream"]]
What you need is introduce a new array to store anagrams before you blank it, lets call it valid_anagrams. Right now you're pushing that in words. And as Fredrick pointed out you're modifying words while iterating over it. Its not good and to avoid that you keep a clone of words called words_clone and reject items from it instead. Following code should work -
def combine_anagrams(words)
indexes, anagrams, valid_anagrams = [], [], []
words_clone = words.clone # creating a clone of words
words.each do |word|
if(word.is_a? String )
first_word = word.downcase.chars.sort.join
words.each do |second_word|
if(second_word.is_a? String)
if(first_word == second_word.downcase.chars.sort.join)
indexes << words.index(second_word)
end
end
end
indexes.each{|index| anagrams << words[index] }
# reject from words_cloned instead of words
words_clone.reject!.with_index {|el, idx| indexes.include?(idx)}
# insert anagrams into valid_anagrams array. In your code you inserted it in words array
valid_anagrams << anagrams unless valid_anagrams.include?(anagrams)
indexes, anagrams = [], []
end
end
# return valid_anagrams array
return valid_anagrams
end

How to make sure certain elements not get into arrays in Ruby

I have an array lets say
array1 = ["abc", "a", "wxyz", "ab",......]
How do I make sure neither for example "a" (any 1 character), "ab" (any 2 characters), "abc" (any 3 characters), nor words like "that", "this", "what" etc nor any of the foul words are saved in array1?
This removes elements with less than 4 characters and words like this, that, what from array1 (if I got it right):
array1.reject! do |el|
el.length < 4 || ['this', 'that', 'what'].include?(el)
end
This changes array1. If you use reject (without !), it'll return the result and not change array1
You can open and add a new interface to the Array class which will disallow certain words. Example:
class Array
def add(ele)
unless rejects.include?(ele)
self.push ele
end
end
def rejects
['this', 'that', 'what']
end
end
arr = []
arr.add "one"
puts arr
arr.add "this"
puts arr
arr.add "aslam"
puts arr
Output would be:
one one one aslam
And notice the word "this" was not added.
You could create a stop list. Using a hash for this would be more efficient than an array as lookup time will be consistant with a hash. With an array the lookup time is proportional to the number of elements in the array. If you are going to check for stop words a lot, I suggest using a hash that contains all the stop words. Using your code, you could do the following
badwords_a = ["abc", "a", "wxyz", "ab"] # Your array of bad words
badwords_h = {} # Initialize and empty hash
badwords_a.each{|word| badwords_h[word] = nil} # Fill the hash
goodwords = []
words_to_process = ["abc","a","Foo","Bar"] # a list of words you want to process
words_to_process.each do |word| # Process new words
if badwords_h.key?(word)
else
goodwords << word # Add the word if it did not match the bad list
end
end
puts goodwords.join(", ")

Resources