why doesn't my refactored ruby using inject work? - ruby

I tried to do some refactoring to convert an each block into an inject, but it didn't work and I don't understand why.
Here's the code that works before refactoring:
class String
# Build the word profile for the given word. The word profile is an array of
# 26 integers -- each integer is a count of the number of times each letter
# appears in the word.
#
def profile
profile = Array.new(26) { 0 }
self.downcase.split(//).each do |letter|
# only process letters a-z
profile[letter.ord - 'a'.ord] += 1 unless letter.ord > 'z'.ord
end
profile
end
end
and here's my refactor that doesn't work:
class String
# Build the word profile for the given word. The word profile is an array of
# 26 integers -- each integer is a count of the number of times each letter
# appears in the word.
#
def profile
self.downcase.split(//).inject(Array.new(26) {0}) do |profile, letter|
# only process letters a-z
profile[letter.ord - 'a'.ord] += 1 unless letter.ord > 'z'.ord
end
end
end
When I try and execute the refactored method I'm getting
`block in profile': undefined method `[]=' for 1:Fixnum (NoMethodError)
If I understand that correctly, it's doesn't like the array reference operator on the profile object in my refactored version, which implies that the initialiser passed to inject isn't working. Is that understanding correct? And if so, why not?
Thanks!

The []= method returns the assigned value, so the value of profile in the next iteration will be 1 (since it's the value of the last iteration). In order to get the behavior you want, you'll have to do:
self.downcase.split(//).inject(Array.new(26) {0}) do |profile, letter|
# only process letters a-z
profile[letter.ord - 'a'.ord] += 1 unless letter.ord > 'z'.ord
profile
end
or
self.downcase.split(//).inject(Array.new(26) {0}) do |profile, letter|
# only process letters a-z
profile.tap { profile[letter.ord - 'a'.ord] += 1 unless letter.ord > 'z'.ord }
end

Related

calculates highest count words across lines to be will, it, really (FAILED - 1) ruby rspec

I've asked this question before and maybe I wasn't clear enough (or didn't) put all the details of the my question.
It's been weeks now frustration is killing me on this assignment as I continue to switch sessions and the assignment itself seems enigmatic to everybody on Coursera since nobody has really answered the question in all the posts I've read.
Here are the assignment questions
Meanwhile this is my code:
#Implement all parts of this assignment within (this) module2_assignment2.rb file
#Implement a class called LineAnalyzer.
class LineAnalyzer
#Implement the following read-only attributes in the LineAnalyzer class.
#* highest_wf_count - a number with maximum number of occurrences for a single word (calculated)
#* highest_wf_words - an array of words with the maximum number of occurrences (calculated)
#* content - the string analyzed (provided)
#* line_number - the line number analyzed (provided)
#Add the following methods in the LineAnalyzer class.
#* initialize() - taking a line of text (content) and a line number
#* calculate_word_frequency() - calculates result
#Implement the initialize() method to:
#* take in a line of text and line number
#* initialize the content and line_number attributes
#* call the calculate_word_frequency() method.
#Implement the calculate_word_frequency() method to:
#* calculate the maximum number of times a single word appears within
# provided content and store that in the highest_wf_count attribute.
#* identify the words that were used the maximum number of times and
# store that in the highest_wf_words attribute.
attr_accessor :highest_wf_count, :highest_wf_words, :content, :line_number
def initialize(content, line)
#content = content
#line_number = line
#highest_wf_count=0
calculate_word_frequency()
end
def calculate_word_frequency()
#highest_wf_words = Hash.new
words = #content.split
words.each { |w|
if #highest_wf_words.has_key?(w)
#highest_wf_words[w] += 1
else
#highest_wf_words[w] = 1
end
}
#highest_wf_words.sort_by { |word, count| count }
#highest_wf_words.each do |key, value|
if value > #highest_wf_count
#highest_wf_count = value
end
end
end
def highest_wf_count= (number)
#highest_wf_count = number
end
end
# Implement a class called Solution.
class Solution
# Implement the following read-only attributes in the Solution class.
#* analyzers - an array of LineAnalyzer objects for each line in the file
#* highest_count_across_lines - a number with the maximum value for highest_wf_words attribute in the analyzers array.
#* highest_count_words_across_lines - a filtered array of LineAnalyzer objects with the highest_wf_words attribute
# equal to the highest_count_across_lines determined previously.
# Implement the following methods in the Solution class.
#* analyze_file() - processes 'test.txt' into an array of LineAnalyzers and stores them in analyzers.
#* calculate_line_with_highest_frequency() - determines the highest_count_across_lines and
# highest_count_words_across_lines attribute values
#* print_highest_word_frequency_across_lines() - prints the values of LineAnalyzer objects in
# highest_count_words_across_lines in the specified format
# Implement the analyze_file() method() to:
#* Read the 'test.txt' file in lines
#* Create an array of LineAnalyzers for each line in the file
# Implement the calculate_line_with_highest_frequency() method to:
#* calculate the maximum value for highest_wf_count contained by the LineAnalyzer objects in analyzers array
# and stores this result in the highest_count_across_lines attribute.
#* identifies the LineAnalyzer objects in the analyzers array that have highest_wf_count equal to highest_count_across_lines
# attribute value determined previously and stores them in highest_count_words_across_lines.
#Implement the print_highest_word_frequency_across_lines() method to
#* print the values of objects in highest_count_words_across_lines in the specified format
attr_reader :analyzers, :highest_count_across_lines, :highest_count_words_across_lines
def initialize()
#analyzers = Array.new
#highest_count_across_lines = nil
#highest_count_words_across_lines = nil
end
def analyze_file()
File.foreach('test.txt').with_index(1) do |content, line|
line_analyzer = LineAnalyzer.new(content, line)
#analyzers << line_analyzer
end
end
def calculate_line_with_highest_frequency()
#highest_count_across_lines = 0
#highest_count_words_across_lines = Array.new
#analyzers.each do |analyzer|
if analyzer.highest_wf_count > #highest_count_across_lines
#highest_count_across_lines = analyzer.highest_wf_count
end
end
end
def print_highest_word_frequency_across_lines()
#highest_count_words_across_lines = Array.new
puts "The following words have the highest word frequency per line:"
end
end
This is the rspec file for class = solution:
require_relative "../module2_assignment"
require 'rspec'
describe Solution do
subject(:solution) { Solution.new }
it { is_expected.to respond_to(:analyze_file) }
it { is_expected.to respond_to(:calculate_line_with_highest_frequency) }
it { is_expected.to respond_to(:print_highest_word_frequency_across_lines) }
context "#analyze_file" do
it "creates 3 line analyzers" do
expect(solution.analyzers.length).to eq 0
solution.analyze_file
expect(solution.analyzers.length).to eq 3
end
end
context "#calculate_line_with_highest_frequency" do
it "calculates highest count across lines to be 4" do
solution.analyze_file
expect(solution.highest_count_across_lines).to be nil
solution.calculate_line_with_highest_frequency
expect(solution.highest_count_across_lines).to be 4
end
it "calculates highest count words across lines to be will, it, really" do
solution.analyze_file
expect(solution.highest_count_words_across_lines).to be nil
solution.calculate_line_with_highest_frequency
words_found = solution.highest_count_words_across_lines.map(&:highest_wf_words).flatten
expect(words_found).to match_array ["will", "it", "really"]
end
end
end
The test.txt file:
This is a really really really cool experiment really
Cute little experiment
Will it work maybe it will work do you think it will it will
And this is the rspec results I've had so far:
LineAnalyzer
has accessor for highest_wf_count
has accessor for highest_wf_words
has accessor for content
has accessor for line_number
has method calculate_word_frequency
calls calculate_word_frequency when created
attributes and values
has attributes content and line_number
content attribute should have value "test"
line_number attribute should have value 1
#calculate_word_frequency
highest_wf_count value is 3
highest_wf_words will include "really" and "you"
content attribute will have value "This is a really really really cool cool you you you"
line_number attribute will have value 2
Solution
should respond to #analyze_file
should respond to #calculate_line_with_highest_frequency
should respond to #print_highest_word_frequency_across_lines
#analyze_file
creates 3 line analyzers
#calculate_line_with_highest_frequency
calculates highest count across lines to be 4
calculates highest count words across lines to be will, it, really (FAILED - 1)
Failures:
1) Solution#calculate_line_with_highest_frequency calculates highest count words across lines to be will, it, really
Failure/Error: expect(words_found).to match_array ["will", "it", "really"]
expected collection contained: ["it", "really", "will"]
actual collection contained: []
the missing elements were: ["it", "really", "will"]
# ./spec/solution_spec.rb:39:in `block (3 levels) in <top (required)>'
Finished in 0.13939 seconds (files took 0.39711 seconds to load)
19 examples, 1 failure
Failed examples:
rspec ./spec/solution_spec.rb:31 # Solution#calculate_line_with_highest_frequency calculates highest count words across lines to be will, it, really
I really can't figure out where the problem is.
Is it a typo problem or I'm doing something wrong somewhere?
If someone could help then it'll be very great,please.
Thanks in advance.
Brice
As I said above, your immediate problem is that the #highest_count_words_across_lines is only ever set to an empty array - so the final result will inevitably also be empty.
You could build a list of all words containing the maximum number of repetitions as follows:
#highest_count_words_across_lines = analyzers.map do |analyzer|
analyzer.highest_wf_words.select do |word, count|
count == highest_count_across_lines
end
end.flat_map(&:keys)
You may wish to move some of that logic into the LineAnalyzer class, e.g. in a LineAnalyzer#words_of_length(length) method.
Note that this will only return two values ["really", "it"]. This is because your code is currently considering the first "Will" to be a different word as it is capitalised. If this is not desired, then you should downcase all words when analysing the file.
Thanks very much #Tom for your help. After some efforts the code below passed but not before applying the downcase at the LineAnalyser class level as you suggested:
#highest_count_across_lines = analyzers.sort_by { |analyzer| analyzer.highest_wf_count }.reverse.first.highest_wf_count
#highest_count_words_across_lines = analyzers.select { |analyzer| #highest_count_across_lines == analyzer.highest_wf_count }
Many thanks again.

Ruby: undefined method `digits' for 3212:Fixnum (NoMethodError)

The code below is meant to take in an integer, square each digit, and return the integer with the squared digits.
However, I kept having this error:
`square_digits': undefined method `digits' for 3212:Fixnum (NoMethodError)
from `
'
I don't understand why I have this error as the .digits method is an included method in ruby and I'm using it on an integer, yet it gives me a NoMethodError.
def square_digits(digit)
puts digit
puts digit.inspect
puts digit.class
if digit <= 0
return 0
else
#converts the digit into digits in array
split_digit = digit.digits
puts "split digit class and inspect"
puts split_digit.inspect
puts split_digit.class
# multiples each digit by itself
squared = split_digit.map{ |a| a * a}
squared.reverse!
# makes digits into string
string = squared.join('')
# converts string into integer
string.to_i
end
end
Does anyone know what is going on??
I guess you are using older version of Ruby than 2.4.0. If so then this method will not be available. It is added in 2.4.0. See this link
To add support for you old ruby version you can just add below code before your method definition.
class Integer
def digits(base: 10)
quotient, remainder = divmod(base)
quotient == 0 ? [remainder] : [*quotient.digits(base: base), remainder]
end
end
After adding this code snippet you should be able to run your method.

Undefined method 'scan' for nil:NilClass (NoMethodError)

Stuck on this one, this layout is for a chef inspec test but leveraging ruby to grab the contents of a file. However with this test I'm not actually testing against a file, so I'm trying to understand how to account for that, heres the code:
%w(/etc/bashrc /etc/profile).each do |path|
file(path).content.scan(/^\s*umask\s+(\d{3})\b/).flatten.each do |umask|
BASELINE = '0027'
(1..3).each do |i| # leading char is '0' octal indicator
describe umask[i].to_i do
it { should be <= BASELINE[i].to_i }
end
end
end
end
end
Here is the line giving me trouble
file(path).content.scan(/^\s*umask\s+(\d{3})\b/).flatten.each do |umask|
As far as the error is concerned, i.e., "Undefined method 'scan' for nil:NilClass", this error would only come up, while doing inspec run, if the files, which are being passed, are either not present or not readable on the file system.
Also, The information provided is not complete, as it is unclear that what is the umask set in both files, i.e, is it 3 digits or 4 digits ones?
Because while doing scan you are looking for 3 digit umask "scan(/^\sumask\s+(\d{3})\b/)*" and you have set "BASELINE = '0027'" which is 4 digit one. So, it would definitely going to have a problem.
If you have "umask 027" in the files, then, it should be:
Check BASELINE = '027', searching 3 digit umask
%w(/etc/bashrc /etc/profile).each do |path|
file(path).content.scan(/^\s*umask\s+(\d{3})\b/).flatten.each do |umask|
BASELINE = '027'
(1..3).each do |i| # leading char is '0' octal indicator
describe umask[i].to_i do
it { should be <= BASELINE[i].to_i }
end
end
end
end
Else you have "umask 0027" in the files, then, it should be:
Check scan(/^\s*umask\s+(\d{4})\b/), searching 4 digit umask
%w(/etc/bashrc /etc/profile).each do |path|
file(path).content.scan(/^\s*umask\s+(\d{4})\b/).flatten.each do |umask|
BASELINE = '027'
(1..3).each do |i| # leading char is '0' octal indicator
describe umask[i].to_i do
it { should be <= BASELINE[i].to_i }
end
end
end
end
You can change file(path).content to a string that matches whatever the file content is.
"Sample_string".scan(/^\s*umask\s+(\d{3})\b/).flatten.each do |umask|
The reason is file(path).content returns nil if you are not testing against a real file. And nil does not have the scan method, which is why you are getting the error.

A way to specify and initialize the type of a map's values?

I want to count all the words in a line of text. I'm using a map to do this, with the words for keys and integers for values. I don't know how to tell Ruby that all the values will be integers. It forces me to put an ugly branching inside my iterator's block:
# in the constructor
#individual_words = {}
def count_words_from( text_line )
text_line.each do |line|
line.scan(/\p{Word}+/)
.reject{ |string| string =~ /\d/ }
.each do |word|
if #individual_words[ word ] == nil then # This is ugly
#individual_words[ word ] = 1 # This is ugly as well
else
#individual_words[ word ] += 1
end
end
end
end
In simple, I'd like to do something like this Java line:
Map<String, Integer> individualWords;
to avoid having to change the type of the first occurence of a word from Nil to Integer.
You can set a default value in your hash like this:
individual_words = Hash.new(0)
Then when you come across a word, whether its key is in the hash or not, all you have to do is:
individual_words[word] += 1
You can also do something like this
#individual_words[word] ||= 0
#individual_words[word] += 1
||= ensures that the value gets set if it's not truthy (ie. nil)

Explaining a Ruby code snippet

I'm in that uncomfortable position again, where somebody has left me with a code snippet in a language I don't know and I have to maintain it. While I haven't introduced Ruby to myself some parts of it are quite simple, but I'd like to hear your explanations nonetheless.
Here goes:
words = File.open("lengths.txt") {|f| f.read }.split # read all lines of a file in 'words'?
values = Array.new(0)
words.each { |value| values << value.to_i } # looked this one up, it's supposed to convert to an array of integers, right?
values.sort!
values.uniq!
diffs = Array.new(0) # this looks unused, unless I'm missing something obvious
sum = 0
s = 0 # another unused variable
# this looks like it's computing the sum of differences between successive
# elements, but that sum also remains unused, or does it?
values.each_index { |index| if index.to_i < values.length-1 then sum += values.at(index.to_i + 1) - values.at(index.to_i) end } # could you also explain the syntax here?
puts "delta has the value of\n"
# this will eventually print the minimum of the original values divided by 2
puts values.at(0) / 2
The above script was supposed to figure out the average of the differences between every two successive elements (integers, essentially) in a list. Am I right in saying this is nowhere near what it actually does, or am I missing something fundamental, which is likely considering I have no Ruby knowledge?
Explanation + refactor (non used variables removed, functional approach, each_cons):
# Read integer numbers from file, sort them ASC and remove duplicates
values = File.read("lengths.txt").split.map(&:to_i).sort.uniq
# Take pairwise combinations and get the total sum of partial differences
partial_diffs = values.each_cons(2).map { |a, b| b - a }.inject(0, :+)
That guy surely didn't grasp Ruby himself. I wonder why he chose to use that language.
Here's an annotated explanation:
# Yes, it reads all lines of a file in words (an array)
words = File.open("lengths.txt") {|f| f.read }.split
values = Array.new(0)
# Yes, to_i convert string into integer
words.each { |value| values << value.to_i }
values.sort!
values.uniq!
# diffs and s seem unused
diffs = Array.new(0)
sum = 0
s = 0
# The immediate line below can be read as `for(int index = 0; index < values.length; index++)`
values.each_index { |index|
# index is integer, to_i is unnecessary
if index.to_i < values.length-1 then
# The `sum` variable is used here
# Following can be rewritten as sum += values[i-1] - values[i]
sum += values.at(index.to_i + 1) - values.at(index.to_i)
end
}
puts "delta has the value of\n"
# Yes, this will eventually print the minimal of the original values divided by 2
puts values.at(0) / 2
To help you get a better grasp of what "real" (idiomatic) Ruby looks like, I've written what you wanted, with some annotations
values = open("lengths.txt") do |f|
# Read it like this:
#
# Take the list of all lines in a file,
# apply a function to each line
# The function is stripping the line and turning it
# into an integer
# (This means the resultant list is a list of integers)
#
# And then sort it and unique the resultant list
#
# The eventual resultant list is assigned to `values`
# by being the return value of this "block"
f.lines.map { |l| l.strip.to_i }.sort.uniq
end
# Assign `diffs` to an empty array (instead of using Array.new())
diffs = []
values.each_index do |i|
# Syntactic sugar for `if`
# It applies the 1st part if the 2nd part is true
diffs << (values[i+1] - values[i]) if i < values.length - 1
end
# You can almost read it like this:
#
# Take the list `diffs`, put all the elements in a sentence, like this
# 10 20 30 40 50
#
# We want to inject the function `plus` in between every element,
# so it becomes
# 10 + 20 + 30 + 40 + 50
#
# The colon `:+` is used to refer to the function `plus` as a symbol
#
# Take the result of the above summation, divided by length,
# which gives us average
delta = diffs.inject(:+) / diffs.length
# `delta` should now contains the "average of differences" between
# the original `values`
# String formatting using the % operator
# No \n needed since `puts` already add one for us
puts "delta has the value of %d" % delta
That is by no means pushing the true power of Ruby, but you see why Rubyists get so enthusiastic about expressiveness and stuffs :P
values.each_index { |index| if index.to_i < values.length-1 then sum += values.at(index.to_i + 1) - values.at(index.to_i) end }
The above line sums the differences between consecutive values. the test index.to_i < values.length-1 is to not access the array out of bounds, because of values.at(index.to_i + 1).
You are right, this code does not do much thing. it only prints half of the minimum value from the file.

Resources