Appending strings using << does not work as expected, but using + does - ruby

I have the following code which is causing me problems around the line I've marked.
arr = 'I wish I may I wish I might'.split
dictionary = Hash.new
arr.each_with_index do |word, index|
break if arr[index + 2] == nil
key = word << " " << arr[index + 1] #This is the problem line
value = arr[index + 2]
dictionary.merge!( { key => value } ) { |key, v1, v2| [v1] << v2 }
end
puts dictionary
Running this code, I would expect the following output:
{"I wish"=>["I", "I"], "wish I"=>["may", "might"], "I may"=>"I", "may I"=>"wish"}
However, what I instead get is
{"I wish"=>["I may", "I"], "wish I"=>["may I", "might"], "I may"=>"I wish", "may I"=>"wish I"}
I've found that if I replace the problem line with
key = word + " " + arr[index + 1]
Everything works as expected. What is it about the first version of my line that was causing the unexpected behaviour?

The String#<< method modifies the original object on which it is called.
Here that is the object referred to by your word variable which is just
another reference to one of the Strings in the arr Array. You can see this
effect with the code:
a = 'Hello'
b = a << ' ' << 'World'
puts a.__id__
puts b.__id__
So when you use that method in one pass through the iterator it affects the
following passes as well.
On the other hand the String#+ method creates a new String object to hold
the combined strings. With this method one pass through the iterator has no
effect on other passes.

key = word << " " << arr[index + 1]
The problem is that String#<< performs an in-place operation so the string is modified the next time it's used. On the other hand String#+ returns a new copy.
You have been bitten by an imperative side-effect (which is not unusual since side-effects are a huge source of bugs. Unless there are very compelling performance reasons, a functional approach yields better code). For example, that's how it could be written using each_cons and map_by from Facets:
words = 'I wish I may I wish I might'.split
dictionary = words.each_cons(3).map_by do |word1, word2, word3|
["#{word1} #{word2}", word3]
end

Related

Inserting variables into arrays

I want to generate text and insert them into an array. Please help.
new_vrms = Array.new[3] {"placeholder"}
puts "How many registration marks do you require?"
how_many = gets.chomp!()
i = 0
while i < how_many.to_i do
prefix =('a'..'z').to_a.shuffle.first(2).join
year = 68
suffix =('a'..'z').to_a.shuffle.first(3).join
aVRM = (prefix.to_s + year.to_s + suffix.to_s)
aVRM = aVRM.upcase!
puts ("#{aVRM} added to index #{i}")
#new_vrms.insert(0, 1) <-Array.rb:14:in `<main>': undefined method `insert' for nil:NilClass (NoMethodError)
#new_vrms.push << #aVRM <-Array.rb:15:in `<main>': undefined method `push' for nil:NilClass (NoMethodError)
#new_vrms[i] = ("#{aVRM}") <- Array.rb:16:in `<main>': undefined method `[]=' for nil:NilClass (NoMethodError)
i += 1
end
puts ("Succesfully generated "+ i.to_s + " registration marks")
The error is in the array initialization. What you have there (Array.new[3]) is seen by ruby as
(Array.new)[3]
You want to pass 3 to new as an argument.
Array.new(3)
This is an extended comment about the OPs code in general, so no upvotes please (downvotes are OK).
You begin with
new_vrms = Array.new[3] {"placeholder"}
#Sergio has identified your problem here, but beyond that there is no need to initialize the value of each element of the array ("placeholder") or even fix the size of the array. Indeed, you evidently wish to return the array with how_many elements and how_many is not yet know. Therefore, you should simply create an empty array here:
new_vrms = Array.new(0)
which is the same as
new_vrms = Array.new
which is more commonly written
new_vrms = []
Next you ask the user how many elements should be in the array:
how_many = gets.chomp!()
If the user enters "3", gets will return "3\n" and gets.chomp will return "3". Notice there is no need to end a method (here chomp) with () when it has no arguments. Also, chomp! is not incorrect but the non-mutating version, chomp, is generally used. You want how_many to be an integer, however, not a string (gets always returns a string). You therefore need to convert it to an integer:
how_many = gets.chomp.to_i
If you look at the doc for String#to_i, however, you will see that "123abc".to_i #=> 123, so "123\n".to_i #=> 123, meaning that we don't need chomp when converting a string to an integer:
how_any = gets.to_i
We now know the number of times we wish to repeat the loop (how_many), so you will want to use an iterator and block rather than a while loop:
how_many.times do |i|
...<your code to add elements to new_vrms>
end
new_vrms # return the computed value
See Integer#times.
You are repeating code in calculating prefix and suffix, so let's make that a separate method:
LETTERS = ('A'..'Z').to_a
#=> ["A", "B", "C",...,"X", "Y", "Z"]
def random_3_letters
LETTERS.sample(3).join
end
See Array#sample, which provides a more direct way of computing the random triples of letters. (Also, we may draw from an array of uppercase letters so we don't need to convert the samples to uppercase later.1 I created the constant LETTERS so that we don't need to create the array each time the method is called. We can now write the block.
YEAR = "68"
how_many.times do |i|
prefix = random_3_letters
suffix = random_3_letters
aVRM = prefix + YEAR + SUFFIX
puts "#{aVRM} added to index #{i}")
new_vrms.push(aVRM)
end
new_vrms
There is, in fact, no reason to define the variables prefix and suffix, as we can simplify as follows.
YEAR = "68"
how_many.times do |i|
aVRM = random_3_letters + YEAR + random_3_letters
puts "#{aVRM} added to index #{i}")
new_vrms.push(aVRM)
end
new_vrms
If you wish to print the value of each element of aVRM it's best to do that from outside the loop--more generally from outside a method you will wrap around the code. If the statement puts "#{aVRM} added to index #{i}") is extracted from the block the block variable i is no longer used, so it can be omitted:
YEAR = "68"
def doit
new_vrms = []
gets.to_i.times do
new_vrms << random_3_letters + YEAR + random_3_letters
end
new_vrms
end
Notice that I've changed Array#push to the more-commonly used Array#<< and substituted out the variables how_many and aVRS.
An experienced Rubyist might tighten this up even more (though input values would also be checked for validity in real code):
LETTERS = ('A'..'Z').to_a
YEAR = "68"
def doit
gets.to_i.times.with_object([]) do |_,new_vrms|
new_vrms << (random_3_letters + YEAR + random_3_letters)
end
end
def random_3_letters
LETTERS.sample(3).join
end
doit # entering "4"
#=> ["ZPI68LWY", "PQV68HLD", "IZG68JCH", "EAC68WLY"]
Notice that by using Enumerator#with_object we eliminate the statement new_vrms = [] and new_vrms at the end, the latter because with_object causes the block to return the "object", the value of new_vrms.
1 Note that you should never write str = str.upcase! because str.upcase! returns nil if str is already upcased (and therefore no changes are made to str). See String#upcase!. Many "bang" methods ...! return nil when no change is made to the receiver.

Getting position from multiple characters in a string [duplicate]

I am trying to return the index's to all occurrences of a specific character in a string using Ruby. A example string is "a#asg#sdfg#d##" and the expected return is [1,5,10,12,13] when searching for # characters. The following code does the job but there must be a simpler way of doing this?
def occurances (line)
index = 0
all_index = []
line.each_byte do |x|
if x == '#'[0] then
all_index << index
end
index += 1
end
all_index
end
s = "a#asg#sdfg#d##"
a = (0 ... s.length).find_all { |i| s[i,1] == '#' }
require 'enumerator' # Needed in 1.8.6 only
"1#3#a#".enum_for(:scan,/#/).map { Regexp.last_match.begin(0) }
#=> [1, 3, 5]
ETA: This works by creating an Enumerator that uses scan(/#/) as its each method.
scan yields each occurence of the specified pattern (in this case /#/) and inside the block you can call Regexp.last_match to access the MatchData object for the match.
MatchData#begin(0) returns the index where the match begins and since we used map on the enumerator, we get an array of those indices back.
Here's a less-fancy way:
i = -1
all = []
while i = x.index('#',i+1)
all << i
end
all
In a quick speed test this was about 3.3x faster than FM's find_all method, and about 2.5x faster than sepp2k's enum_for method.
Here's a long method chain:
"a#asg#sdfg#d##".
each_char.
each_with_index.
inject([]) do |indices, (char, idx)|
indices << idx if char == "#"
indices
end
# => [1, 5, 10, 12, 13]
requires 1.8.7+
Another solution derived from FMc's answer:
s = "a#asg#sdfg#d##"
q = []
s.length.times {|i| q << i if s[i,1] == '#'}
I love that Ruby never has only one way of doing something!
Here's a solution for massive strings. I'm doing text finds on 4.5MB text strings and the other solutions grind to a halt. This takes advantage of the fact that ruby .split is very efficient compared to string comparisions.
def indices_of_matches(str, target)
cuts = (str + (target.hash.to_s.gsub(target,''))).split(target)[0..-2]
indicies = []
loc = 0
cuts.each do |cut|
loc = loc + cut.size
indicies << loc
loc = loc + target.size
end
return indicies
end
It's basically using the horsepower behind the .split method, then using the separate parts and the length of the searched string to work out locations. I've gone from 30 seconds using various methods to instantaneous on extremely large strings.
I'm sure there's a better way to do it, but:
(str + (target.hash.to_s.gsub(target,'')))
adds something to the end of the string in case the target is at the end (and the way split works), but have to also make sure that the "random" addition doesn't contain the target itself.
indices_of_matches("a#asg#sdfg#d##","#")
=> [1, 5, 10, 12, 13]

Reversing a Ruby String, without .reverse method

I am working on this coding challenge, and I have found that I am stuck. I thought it was possible to call the .string method on an argument that was passed in, but now I'm not sure. Everything I've found in the Ruby documentation suggests otherwise. I'd really like to figure this out without looking at the solution. Can someone help give me a push in the right direction?
# Write a method that will take a string as input, and return a new
# string with the same letters in reverse order.
# Don't use String's reverse method; that would be too simple.
# Difficulty: easy.
def reverse(string)
string_array = []
string.split()
string_array.push(string)
string_array.sort! { |x,y| y <=> x}
end
# These are tests to check that your code is working. After writing
# your solution, they should all print true.
puts(
'reverse("abc") == "cba": ' + (reverse("abc") == "cba").to_s
)
puts(
'reverse("a") == "a": ' + (reverse("a") == "a").to_s
)
puts(
'reverse("") == "": ' + (reverse("") == "").to_s
)
This is the simplest one line solution, for reversing a string without using #reverse, that I have come across -
"string".chars.reduce { |x, y| y + x } # => "gnirts"
Additionally, I have never heard of the #string method, I think you might try #to_s.
Easiest way to reverse a string
s = "chetan barawkar"
b = s.length - 1
while b >= 0
print s[b]
b=b-1
end
You need to stop the search for alternative or clever methods, such as altering things so you can .sort them. It is over-thinking the problem, or in some ways avoiding thinking about the core problem you have been asked to solve.
What this test is trying to get you you to do, is understand the internals of a String, and maybe get an appreciation of how String#reverse might be implemented using the most basic string operations.
One of the most basic String operations is to get a specific character from the string. You can get the first character by calling string[0], and in general you can get the nth character (zero-indexed) by calling string[n].
In addition you can combine or build longer strings by adding them together, e.g. if you had a="hell" and b="o", then c = a + b would store "hello" in the variable c.
Using this knowledge, find a way to loop through the original string and use that to build the reverse string, one character at a time. You may also need to look up how to get the length of a string (another basic string method, which you will find in any language's string library), and how to loop through numbers in sequence.
You're on the right track converting it to an array.
def reverse(str)
str.chars.sort_by.with_index { |_, i| -i }.join
end
Here is a solution I used to reverse a string without using .reverse method :
#string = "abcde"
#l = #string.length
#string_reversed = ""
i = #l-1
while i >=0 do
#string_reversed << #string[i]
i = i-1
end
return #string_reversed
Lol, I am going through the same challenge. It may not be the elegant solution, but it works and easy to understand:
puts("Write is a string that you want to print in reverse")
#taking a string from the user
string = gets.to_s #getting input and converting into string
def reverse(string)
i = 0
abc = [] # creating empty array
while i < string.length
abc.unshift(string[i]) #populating empty array in reverse
i = i + 1
end
return abc.join
end
puts ("In reverse: " + reverse(string))
Thought i'd contribute my rookie version.
def string_reverse(string)
new_array = []
formatted_string = string.chars
new_array << formatted_string.pop until formatted_string.empty?
new_array.join
end
def reverse_str(string)
# split a string to create an array
string_arr = string.split('')
result_arr = []
i = string_arr.length - 1
# run the loop in reverse
while i >=0
result_arr.push(string_arr[i])
i -= 1
end
# join the reverse array and return as a string
result_arr.join
end

Capitalizing words in an array, Ruby

I'm going through App Academy's Ruby Prep questions, and I want to know why this solution works. It appears that the words array is never altered and yet the method works. Is this a glitch in the matrix, or is it right under my nose?
def capitalize_words(string)
words = string.split(" ")
idx = 0
while idx < words.length
word = words[idx]
word[0] = word[0].upcase
idx += 1
end
return words.join(" ")
end
The method works because word contains a reference to the array position. So when you assign:
word = words[idx]
You're just using word as a shorthand to operate on that array element, which gets modified by:
word[0] = word[0].upcase
--
Also, if you'd like to come back to this answer after learning some Ruby, here's a simplified version of the method:
def capitalize_words(string)
string.split.map(&:capitalize).join(' ')
end
String#[]= is a mutating operation. To illustrate using a concise, contained excerpt from your code:
word = "foo"
word[0] = word[0].upcase # <-- verbatim from your code
word #=> "Foo"
word is still the same exact object contained in the array words (arrays simply contain references to objects, not the data within them), but it has been mutated in-place. It’s generally best to avoid mutations whenever possible as it makes it non-obvious what is happening (as you can see).
Your code could also be more concisely written using map & capitalize (and without any mutations):
string.split(' ').map(&:capitalize).join(' ')
word = word[idx] creates a copy of your data. It will then modify that copy instead of the words in the original array.
Simple solution would be:
def capitalize_words(string)
words = string.split(" ")
idx = 0
while idx < words.length
words[idx][0] = words[idx][0].upcase
idx += 1
end
return words.join(" ")
end

Ruby-How to build a multivalued hash?

Here is my code snippet:
something_1.each do |i|
something_2.each do |j|
Data.each do |data|
date = data.attribute('TIME_PERIOD').text
value = data.attribute('OBS_VALUE').text
date_value_hash[date] = value
end
end
end
I want to capture all the values in a single date. date is the key of my hash and it may have multiple values for a single date. How can I accomplish that here? When I am using this line:
date_value_hash[date] = value
values are getting replaced each time the loop iterates. But, I want to accumulate all the values in my date_value_hash for each dates i.e. I want to build the values dynamically.
Currently I am getting this:
{"1990"=>"1", "1994"=>"2", "1998"=>"0"}
But, I want something like this:
{"1990"=>"1,2,3,4,5,6", "1994"=>"1,2,3,4,5,6", "1998"=>"1,2,3,4,5,6"}
Anyone have any idea how can I accomplish that?
Like this
magic = Hash.new{|h,k|h[k]=[]}
magic["1990"] << "A"
magic["1990"] << "B"
magic["1994"] << "C"
magic["1998"] << "D"
magic["1994"] << "F"
after which magic is
{"1998"=>["D"], "1994"=>["C", "F"], "1990"=>["A", "B"]}
and if you need the values as comma separated string (as indicated by your sample data), you'll just access them as
magic['1990'].join(',')
which yields
"A,B"
if later you want to pass magic around and preventing it from automagically creating keys, just wrap it as follows
hash = Hash.new.update(magic)
Hope that helps!
Another approach of building multi-valued hash in Ruby:
h = {}
(h[:key] ||= []) << "value 1"
(h[:key] ||= []) << "value 2"
puts h

Resources