I have a model spec that throws an unexpected error (doesn't build the object right).
When I move the same code into a previous test, it runs smoothly.
Here's the problematic expectation:
expect(title.full_title.length).to be <= 140
When adding the line to the first test, it passes, while the second test fails:
describe "generates a title" do
let(:collection) { FactoryBot.create(:collection) }
let(:items) { FactoryBot.create_list(:item, 10, collection: collection, chosen: true) }
let(:title) { Title.create_title(collection) }
context "happy path" do
it "assigns keywords by score" do
array = []
items.each do |i|
array << [i.search.term, i.search.score]
end
array.sort! { |a, b| a[1] <=> b[1] }
split_title = title.full_title.split(', ')
remaining_chars = 140
i = 0
split_title.each do |s|
if remaining_chars - s.length >= 0
expect(s).to eq(array[i][0])
i += 1
remaining_chars -= s.length
end
expect(title.full_title.length).to be <= 140
end
end
it "does not exceed 140 characters" do
expect(title.full_title.length).to be <= 140
end
end
Here is the error message, it doesn't create the object:
1) Title generates a title happy path does not exceed 140 characters
Failure/Error: remaining_chars = 140 - keywords[0].length
NoMethodError:
undefined method `length' for nil:NilClass
TIA!
This is a long shot, but let's try if I'm lucky today.
Guess: Title.full_title implementation (which you didn't share) depends somehow on the existence of the Item's related to the collection which is used to initialize the Title object.
If that assumption is correct, different behavior comes from the lazy nature of let.
In the first it, you actually call items (e.g. items.each) so the let(:items) is evaluated, the rows in DB are created and title.full_title does not return nil.
There a few possible many ways to fix it:
Improve the implementation of full_title to always return string.
call items before in the second scenario
it "does not exceed 140 characters" do
items
expect(title.full_title.length).to be <= 140
end
use let!(:items) which is evaluated immediately (not lazy)
Related
I am working on a coding problem where I have 3 lines of text and I have to calculate the words that appear the most in those lines. The answer is: ['it','really','will'] because the text is:
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
Everything works in the code below except the highest_count_words_across_lines method. It's supposed to return ['it','really','will'] but instead returns 2 hashes inside an array:[{"a"=>1, "cool"=>1, "experiment"=>1, "is"=>1, "really"=>4, "this"=>1}, {"cute"=>1, "experiment"=>1, "little"=>1}, {"do"=>1, "it"=>4, "maybe"=>1, "think"=>1, "will"=>4, "work"=>2, "you"=>1}].
I've tried iterating through a hash with multiple select statements to no avail.
This is my full code so far:
class LineAnalyzer
attr_accessor :highest_wf_count, :highest_wf_words, :content, :line_number #Implement the following read-only attributes in the LineAnalyzer class.
def initialize(content, line)
#content = content #* initialize the content and line_number attributes
#line_number = line
#highest_wf_count = 0
calculate_word_frequency()
end
def calculate_word_frequency()
#highest_wf_words = Hash.new
words = #content.downcase.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
class Solution
attr_reader :analyzers, :highest_count_across_lines, :highest_count_words_across_lines # Implement the following read-only attributes in the Solution class.
def initialize()
#analyzers = []
highest_count_across_lines = nil
highest_count_words_across_lines = []
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 = analyzers.map(&:highest_wf_count).max
#highest_count_words_across_lines = analyzers.select { |k,v| v = #highest_count_across_lines }
end
def print_highest_word_frequency_across_lines()
"The following words have the highest frequency per line: \n #{highest_count_words_across_lines} (appears in line #{line_num} \n"
end
end
This is the error message I get:
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: [{"a"=>1, "cool"=>1, "experiment"=>1, "is"=>1, "really"=>4, "this"=>1}, {"cute"=>1, "experiment"=>1, "little"=>1}, {"do"=>1, "it"=>4, "maybe"=>1, "think"=>1, "will"=>4, "work"=>2, "you"=>1}]
the missing elements were: ["it", "really", "will"]
the extra elements were: [{"a"=>1, "cool"=>1, "experiment"=>1, "is"=>1, "really"=>4, "this"=>1}, {"cute"=>1, "experiment"=>1, "little"=>1}, {"do"=>1, "it"=>4, "maybe"=>1, "think"=>1, "will"=>4, "work"=>2, "you"=>1}]
# ./spec/solution_spec.rb:39:in `block (3 levels) in <top (required)>'
Finished in 0.26418 seconds (files took 0.38 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've tried iterating through the hashes within an array but kept getting an error message. I am trying to find the keys where the values (counts) are equal to the highest count (4). So the final answer should be ["it","really","will"]. Any suggestions?
Step 1
Merge the array of hash into one single hash:
lets say, your array of hash is arrayOfHash
hash = arrayOfHash.inject(:merge)
Step 2
collect the keys which contains the maximum values in that single hash we created in step 1:
result = arrayOfHash.collect{|k, v| k if v == arrayOfHash.values.max}.compact
I have a method that looks like this:
def self.average_top_level_comments_leaders
top_level_comment_count = CrucibleComment.group(:user_id).where(parent_comment_id: nil).order('count_all DESC').count
code_review_assigned_count = Reviewer.group(:user_id).order('count_all DESC').count
division_result = top_level_comment_count.inject({}) do |result, item|
id = item.first #id =12
count = item.last #value = 57
if (count && code_review_assigned_count[id])
result[id] = (count/ code_review_assigned_count[id]).round(2)
#result[12] = 57/12 = 3.3, => {12, 3.3}
end
result
end
end
This method returns a hash with the IDs as keys and the results of the division as the values.
I have successfully tested top_level_comment_count and code_review_assigned count, but I am having trouble figuring out how I can test the 4 other things that are in the do block:
.first, .last, .round(2), result
I am trying to test .first and this is what I have so far:
describe '#average_top_level_comments_leaders' do
subject { User.average_top_level_comments_leaders}
let(:avg_top_level_comments) { double }
let(:code_review_count) { double }
let(:item) { double( {id: 12}) }
context 'when getting the comment count succeeds ' do
before do
allow(CrucibleComment).to receive(:group).with(:user_id).and_return(avg_top_level_comments)
allow(avg_top_level_comments).to receive(:where).with(parent_comment_id: nil).and_return(avg_top_level_comments)
allow(avg_top_level_comments).to receive(:order).with('count_all DESC').and_return(avg_top_level_comments)
allow(avg_top_level_comments).to receive(:count).and_return(avg_top_level_comments)
allow(avg_top_level_comments).to receive(:inject).and_return(avg_top_level_comments)
allow(item).to receive(:first).and_return(item)
allow(Reviewer).to receive(:group).with(:user_id).and_return(code_review_count)
allow(code_review_count).to receive(:order).with('count_all DESC').and_return(code_review_count)
allow(code_review_count).to receive(:count).and_return(code_review_count)
allow(code_review_count).to receive(:round).with(2).and_return(code_review_count)
end
it 'and the correct parameters are called' do
expect(CrucibleComment).to receive(:group).with(:user_id)
subject
end
it 'and comment count is calling descending correctly' do
expect(avg_top_level_comments).to receive(:order).with('count_all DESC')
subject
end
it 'item gets the first result' do
expect(item).to receive(:first)
subject
end
end
end
I cannot get the last it statement to pass. I am trying to expect(item).to receive(:first), but it says this in the error:
Failure/Error: expect(item).to receive(:first)
(Double).first(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Any idea why this is not passing? The other two its are passing
The item double is never used in the test, so when it reaches:
expect(item).to receive(:first)
it fails.
If you were expecting the item double to be used within the inject block here:
division_result = top_level_comment_count.inject({}) do |result, item|
merely by virtue of it having the same name, it doesn't work that way. You'd need to define a method on the avg_top_level_comments double that returns the item double when inject is called.
But, you shouldn't do that. Throw all of this out and use real model instances for the test. It will be much easier to read and maintain.
I have an array of objects.
I want to find an object in the array based on some property of the object.
I can do
array.detect {|x| x.name=="some name"}
or I could do
ind=array.index {|x| x.name=="some name"}
array[ind] unless ind.nil?
Is there any reason to choose one over the other?
If you aren't interested in finding the index value of the object you're searching for, I would suggest detect. It'll save you from having to do that nil check before accessing the array.
From a performance standpoint, I imagine it's relatively comparable, but that could help your decision too. That would require benchmarking as Niels B. mentioned in his comment.
If you want to find an element in a collection, it's important to use collections made for fast retrieval. Arrays are not made for that, nor are they particularly convenient unless you are making a stack or a queue.
Here's some code to show ways to improve the storage/retrieval speed over what you can get using find, detect or other normal array-based methods:
require 'fruity'
require 'digest'
class Foo
attr_reader :var1, :var2
def initialize(var1, var2)
#var1, #var2 = var1, var2
end
end
START_INT = 1
START_CHAR = 'a'
END_INT = 10
END_CHAR = 'z'
START_MD5 = Digest::MD5.hexdigest(START_INT.to_s + START_CHAR)
END_MD5 = Digest::MD5.hexdigest(END_INT.to_s + END_CHAR)
ary = []
hsh = {}
hsh2 = {}
START_INT.upto(END_INT) do |i|
(START_CHAR .. END_CHAR).each do |j|
foo = Foo.new(i, j)
ary << foo
hsh[[i, j]] = foo
hsh2[Digest::MD5.hexdigest(i.to_s + j)] = foo
end
end
compare do
array_find {
ary.find { |a| (a.var1 == START_INT) && (a.var2 == START_CHAR) }
ary.find { |a| (a.var1 == END_INT) && (a.var2 == END_CHAR) }
}
hash_access_with_array {
hsh[[START_INT, START_CHAR]]
hsh[[END_INT, END_CHAR]]
}
hash_access_with_digest {
hsh2[START_MD5]
hsh2[END_MD5]
}
end
Which results in:
Running each test 16384 times. Test will take about 17 seconds.
hash_access_with_digest is faster than hash_access_with_array by 10x ± 1.0
hash_access_with_array is faster than array_find by 16x ± 1.0
There are three different tests, and I'm looking for the first, and last elements in the array ary, and the corresponding objects in the hashes. The result of looking for the first and last elements in the array will be an average time for that search. For comparison I'm searching for the same objects in the hashes.
If we had some advance knowledge of which array index the object is in, retrieving the object from the array would be faster, but that's the problem, and making another container to keep track of that information would be slower than using the hash.
See for yourself!
require 'benchmark'
array = (1..1000000).to_a
Benchmark.bmbm do |x|
x.report("#index for 1") {
array.index(1)
}
x.report("#detect 1") {
array.detect { |i| i == 1 }
}
x.report("#index for 500k") {
array.index(500000)
}
x.report("#detect 500k") {
array.detect { |i| i == 500000 }
}
x.report("#index for 1m") {
array.index(1000000)
}
x.report("#detect 1m") {
array.detect { |i| i == 1000000 }
}
end
Put the code above in a file and execute it from the console with ruby <file>
Ignore the top block, that is rehearsal, the bottom block should look something like this:
user system total real
#index for 1 0.000005 0.000002 0.000007 ( 0.000004)
#detect 1 0.000007 0.000002 0.000009 ( 0.000006)
#index for 500k 0.003274 0.000049 0.003323 ( 0.003388)
#detect 500k 0.029870 0.000200 0.030070 ( 0.030872)
#index for 1m 0.005866 0.000009 0.005875 ( 0.005880)
#detect 1m 0.059819 0.000520 0.060339 ( 0.061340)
Running on my mac and Ruby 2.5.0, the numbers seem to suggest that #detect is an order of magnitude slower than #index.
I have a function that generates random output (string).
I need to call that function until I get 3 different outputs (strings).
What is the most elegant way to generate array with 3 unique strings by calling the function, with the limit how many times the function can be called if the output is not generated in specified number of attempts?
Here's what I currently have:
output = []
limit_calls = 5
limit_calls.times do |i|
str = generate_output_function
output.push str
break if output.uniq.size > 2
end
Can this be beautified / shortened to 1 line? I'm pretty sure in ruby.. :)
Thanks
Using a set makes it (a bit) easier:
require 'set'
output = Set.new
limit_calls = 5
call_count = 0
while output.size < 3 and call_count < limit_calls
output << generate_output_function
call_count += 1
end
output
or with an array
output = []
limit_calls = 5
while output.size < limit_calls and output.uniq.size < 3
output << generate_output_function
end
output.uniq
UPDATE with the call limit. Seems like the Array version wins! Thanks Iain!
Will also ponder a version using inject.
UPDATE 2 - with inject:
5.times.inject([]) { |a, el| a.uniq.size < 3 ? a << generate_output_function : a }
there is your oneliner. I am not sure I prefer it cause it is a bit hard to follow.....
Froderik's answer missed out the call_limit requirement. What about a function like...
def unique_string_array(call_limit)
output = []
calls = 0
until (output.size == 3 || calls == call_limit) do
(output << generate_output_function).uniq! && calls+=1
end
output
end
It isn't a one-liner but it is readable... with this implementation, you may end up with arrays less than size 3. The most important thing is that you have a test that asserts the behaviour you want! (in order to test this thoroughly you'll have to stub out the call to generate_output_function)
I would like to analyse data in my database to find out how many times certain words appear.
Ideally I would like a list of the top 20 words used in a particular column.
What would be the easiest way of going about this.
Create an autovivified hash and then loop through the rows populating the hash and incrementing the value each time you get the same key (word). Then sort the hash by value.
A word counter...
I wasn't sure if you were asking how to get rails to work on this or how to count words, but I went ahead and did a column-oriented ruby wordcounter anyway.
(BTW, at first I did try the autovivified hash, what a cool trick.)
# col: a column name or number
# strings: a String, Array of Strings, Array of Array of Strings, etc.
def count(col, *strings)
(#h ||= {})[col = col.to_s] ||= {}
[*strings].flatten.each { |s|
s.split.each { |s|
#h[col][s] ||= 0
#h[col][s] += 1
}
}
end
def formatOneCol a
limit = 2
a.sort { |e1,e2| e2[1]<=>e1[1] }.each { |results|
printf("%9d %s\n", results[1], results[0])
return unless (limit -= 1) > 0
}
end
def formatAllCols
#h.sort.each { |a|
printf("\n%9s\n", "Col " + a[0])
formatOneCol a[1]
}
end
count(1,"how now")
count(1,["how", "now", "brown"])
count(1,[["how", "now"], ["brown", "cow"]])
count(2,["you see", "see you",["how", "now"], ["brown", "cow"]])
count(2,["see", ["see", ["see"]]])
count("A_Name Instead","how now alpha alpha alpha")
formatAllCols
$ ruby count.rb
Col 1
3 how
3 now
Col 2
5 see
2 you
Col A_Name Instead
3 alpha
1 how
$
digitalross answer looks too verbose to me, also, as you tag ruby-on-rails and said you use DB.. i'm assuming you need an activerecord model so i'm giving you a full solution
in your model:
def self.top_strs(column_symbol, top_num)
h = Hash.new(0)
find(:all, :select => column_symbol).each do |obj|
obj.send(column_symbol).split.each do |word|
h[word] += 1
end
end
h.map.sort_by(&:second).reverse[0..top_num]
end
for example, model Comment, column body:
Comment.top_strs(:body, 20)