Ruby, iterating over a multi-value Hash using if/else, trying to return key/value pairs, fails when value is not found - ruby

I am trying to scan a string of raw input from a user and return a sentence that's composed of an array of arrays with the (TOKEN, WORD) pairings. If a word isn't part of the lexicon, then it should still return the WORD but set the TOKEN to an error token.
Inside the method "##dictionary.each do |type, list|" the initial if statement works fine at building a key/value array of found words, as long as the else statement is set to return nil. However, when I try and place error/words pairs into the array for the words not contained in the ##dictionary hash (i.e. those that fall into the else part of the code), I receive 5 separate pairs in the array for each word that the user entered, one for each iteration over each key for each word entered.
Does anybody have an idea how to return just one error/value pair to the array, instead of one for each of the five iterations for every word?
class Lexicon
##dictionary = {
'direction' => ['north', 'south', 'east', 'west', 'down', 'up', 'left', 'right', 'back'],
'verbs' => ['go', 'stop', 'kill', 'eat'],
'stop words' => ['the', 'in', 'of', 'on', 'at', 'it'],
'nouns' => ['door', 'bear', 'princess', 'cabinet'],
'numbers' => [0..9]
}
stuff = $stdin.gets.chomp
##words = stuff.split(' ')
def self.scan
result = []
##words.each do |text_element|
categorized = []
##dictionary.each do |type, list|
if
list.include?(text_element.downcase)
categorized = [type, text_element]
result.push(categorized)
else
nil
#categorized = ["ERROR", text_element]
#result.push(categorized)
end
end
end
print result
end
Lexicon.scan
end

It happens because of each iterates over all elements and it is true once or never.
This reduction of your code should help you understand what happen:
dictionary = {
'direction' => ['north', 'south'],
'verbs' => ['go', 'stop', 'kill', 'eat'],
'whathever' => ['blah']
}
text = 'go'
dictionary.each do |type, list|
if p list.include?(text) # added p
then
p text
else
p 'error'
end
end
It returns:
# false
# "error"
# true
# "go"
# false
# "error"
You need a different approach, for example:
text = 'nothing'
result = dictionary.find { |_, v| v.include? text }
result ? [result.keys, text] : "Error"

While it may feel organize to have the dictionary categorized by lists, this would be both simplified and much faster were the dictionary to be flattened and have a default set to the 'ERROR' token.
For example:
##dictionary = {
'direction' => ['north', 'south', 'east', 'west', 'down', 'up', 'left', 'right', 'back'],
'verbs' => ['go', 'stop', 'kill', 'eat'],
...
Becomes this:
##dictionary = {
'north' => 'direction',
'south' => 'direction',
...
'go' => 'verbs',
'stop' => 'verbs',
...
}
##dictionary.default = 'ERROR'
This way, your lookup becomes linear and without unnecessary boolean logic, like so.
def scan
result = stuff.split(' ').map do |word|
[##dictionary[word.downcase], word]
end
print result
end

This has worked for me. Thanks to Sebastian Scholl for the idea of simplifying the dictionary.
class Lexicon
##dictionary = {
'direction' => ['north', 'south', 'east', 'west', 'down', 'up', 'left', 'right', 'back'],
'verbs' => ['go', 'stop', 'kill', 'eat'],
'stop words' => ['the', 'in', 'of', 'on', 'at', 'it'],
'nouns' => ['door', 'bear', 'princess', 'cabinet'],
'numbers' => [0..9]
}
stuff = $stdin.gets.chomp
##words = stuff.downcase.split(' ')
def self.scan
result = []
values = []
##dictionary.each do |key, value|
values << value
end
value_list = values.flatten.uniq
##words.each do |text_element|
if value_list.include?(text_element)
##dictionary.each do |key, value|
if value.include?(text_element)
categorized = [key, text_element]
result.push(categorized)
else
nil
end
end
else
result.push(["Error, #{text_element}"])
end
end
print result
end
Lexicon.scan
end

Related

How do I assign multiple symbols to the same value in Ruby?

The idea of what I am trying to do is to clump synonym symbols to the same value, without having to redefine the same value over and over. Basically turn this:
fruits = { orange: "Citrus", grapefruit: "Citrus", tangerine: "Citrus" }
Into this:
fruits = { orange:, grapefruit:, tangerine: => "Citrus" }
What is the proper syntax for accomplishing this?
Thanks
Use a hash, in order to access the type of fruit using the fruit name. For example:
fruits = %i{ orange grapefruit tangerine apple }
citrus_fruits = %i{ orange grapefruit tangerine }
fruit_type = citrus_fruits.zip([:citrus] * citrus_fruits.length).to_h
fruit_type[:apple] = :rosaceae
puts fruit_type
# {:orange=>:citrus, :grapefruit=>:citrus, :tangerine=>:citrus, :apple=>:rosaceae}
Here, zip and to_h are used to simplify the hash creation and avoid repetitive code.
Group Keys by Value; Optionally Transform Returned Values
In Ruby, a Symbol is a core class that provides an identifier for things, and the Symbol is never duplicated during runtime. Setting aside how they're used internally, the most common use case for using a Symbol in your code is to define keys in a Hash. You can use other types of keys, but the properties of a Symbol make them especially useful as Hash keys.
With that out of the way, it looks like you're trying to group similar Hash values, but it's unclear how you expect to use this grouping. There is more than one way to do this, so I'll just pick one as an example.
Given a Hash like this one:
produce =
{
:orange => "citrus",
:grapefruit => "citrus",
:tangerine => "citrus",
:raspberry => "berry",
:strawberry => "berry",
:canteloupe => "melon",
:honeydew => "melon"
}
you can use Hash#group_by (inherited from Enumerable) to quickly sort your Hash by value. For example, using Ruby 3.0.0:
produce.group_by { _2 }
#=>
{"citrus"=>
[[:orange, "citrus"], [:grapefruit, "citrus"], [:tangerine, "citrus"]],
"berry"=>[[:raspberry, "berry"], [:strawberry, "berry"]],
"melon"=>[[:canteloupe, "melon"], [:honeydew, "melon"]]}
This returns a Hash grouped by your unique values, but you may prefer to discard the produce type in the nested Array objects. You can do that with Hash#transform_values like so:
produce.group_by { _2 }.transform_values { _1.map &:first }
#=>
{"citrus"=>[:orange, :grapefruit, :tangerine],
"berry"=>[:raspberry, :strawberry],
"melon"=>[:canteloupe, :honeydew]}
Either way, the main point is that a Hash key is associated with a value that can be of almost any class, and so you can examine the contents of each value to determine whether or not they belong to the grouping you want (which is currently defined by your key).
Your current data structure isn't really optimized for retrieving types of produce (e.g. citrus fruits) easily, but it can certainly be done. However, you may want to reconsider whether you have the right data structure for the way you want to access or manipulate your data. Your mileage will certainly vary.
Your comment about motherboards and blueprints suggest that you are given something like
h = { :mb1=>:bp3, :mb_2=>:bp1, :mb3=>:bp3, :mb4=>:bp2, :mb5=>:bp1 }
and want to produce the hash
{ :bp3=>[:mb1, :mb3], :bp1=>[:mb_2, :mb5], :bp2=>[:mb4] }
One of many ways to do that is the following:
h.each_with_object({}) { |(k,v),g| (g[v] ||= []) << k }
See Enumerable#each_with_object, and to understand why I've written the block varaiables |(k,v),g|, see array decomposition.
This is a condensed translation of the following code (which I've salted with three puts statement to show the calculations being performed):
g = {}
h.each do |key_value_pair|
k, v = key_value_pair
puts "\nkey_value_pair = #{key_value_pair}, k = #{k}, v = #{v}"
puts "g[#{v}] set to [] because g[#{v}] == nil" if g[v].nil?
g[v] = [] if g[v].nil?
g[v] << k
puts "g after g[#{v}] << #{g}"
end
#=> {:mb1=>:bp3, :mb_2=>:bp1, :mb3=>:bp3, :mb4=>:bp2, :mb5=>:bp1}
The following is displayed:
key_value_pair = [:mb1, :bp3], k = mb1, v = bp3
g[bp3] set to [] because g[bp3] == nil
g after g[bp3] << {:bp3=>[:mb1]}
key_value_pair = [:mb_2, :bp1], k = mb_2, v = bp1
g[bp1] set to [] because g[bp1] == nil
g after g[bp1] << {:bp3=>[:mb1], :bp1=>[:mb_2]}
key_value_pair = [:mb3, :bp3], k = mb3, v = bp3
g after g[bp3] << {:bp3=>[:mb1, :mb3], :bp1=>[:mb_2]}
key_value_pair = [:mb4, :bp2], k = mb4, v = bp2
g[bp2] set to [] because g[bp2] == nil
g after g[bp2] << {:bp3=>[:mb1, :mb3], :bp1=>[:mb_2], :bp2=>[:mb4]}
key_value_pair = [:mb5, :bp1], k = mb5, v = bp1
g after g[bp1] << {:bp3=>[:mb1, :mb3], :bp1=>[:mb_2, :mb5], :bp2=>[:mb4]}
#Charles Persson, if I understood your question correctly, your main goal is to refactor this snippet:
hash = {
:orange => "citrus",
:grapefruit => "citrus",
:tangerine => "citrus",
:raspberry => "berry",
:cherry => "drupe",
:strawberry => "berry",
:canteloupe => "melon",
:honeydew => "melon",
:apple => "pome"
}
to something similar to:
hash = {
[:orange, :grapefruit, :tangerine] => "citrus",
[:raspberry, :strawberry] => "berry",
:cherry => "drupe",
[:canteloupe, :honeydew] => "melon",
:apple => "pome"
}
If I am right, then I can suggest implementing a method like this one:
# source - an input hash that can contain arrays as keys.
# dest - a new output hash where all array keys are replaced by singular keys.
def mhash(source)
dest = {}
source.each_pair do |key, value|
if key.instance_of?(Array)
key.each do |sub_key|
dest[sub_key] = value
end
else
dest[key] = value
end
end
dest
end
or its shorter alternative:
def mhash(source)
source.each_pair.with_object({}) do |(key, value), dest|
key.instance_of?(Array) ? key.each { |sub_key| dest[sub_key] = value } : dest[key] = value
end
end
This way, you will be able to write a code like the following:
hash = mhash({
[:orange, :grapefruit, :tangerine] => "citrus",
[:raspberry, :strawberry] => "berry",
:cherry => "drupe",
[:canteloupe, :honeydew] => "melon",
:apple => "pome"
})
p hash
# => {
:orange => "citrus",
:grapefruit => "citrus",
:tangerine => "citrus",
:raspberry => "berry",
:strawberry => "berry",
:cherry => "drupe",
:canteloupe => "melon",
:honeydew => "melon",
:apple => "pome"
}
Otherwise, please provide a better explanation of your question. Thanks.

Access to merged cells using Ruby-Roo

According to example below: Value is stored only in A1, other cells return nil.
How is possible to get the A1'a value from the others merged cells, or simply check range of the A1 cell?
here is my take, if all merged fields are same as prev - then non-merged fields should become array
xlsx = Roo::Excelx.new(__dir__ + "/output.xlsx", { expand_merged_ranges: true })
parsed = xlsx.sheet(0).parse(headers: true).drop(1)
parsed_merged = []
.tap do |parsed_merged|
parsed.each do |x|
if parsed_merged.empty?
parsed_merged << {
"field_non_merged1" => x["field_non_merged1"],
"field_merged1" => [x["field_merged1"]],
"field_merged2" => [x["field_merged2"]],
"field_merged3" => [x["field_merged3"]],
"field_merged4" => [x["field_merged4"]],
"field_non_merged2" => x["field_non_merged2"],
"field_non_merged3" => x["field_non_merged3"],
}
else
field_merged1_is_same_as_prev = x["field_non_merged1"] == parsed_merged.last["field_non_merged1"]
field_merged2_is_same_as_prev = x["field_non_merged2"] == parsed_merged.last["field_non_merged2"]
field_merged3_is_same_as_prev = x["field_non_merged3"] == parsed_merged.last["field_non_merged3"]
merged_rows_are_all_same_as_prev = field_non_merged1_is_same_as_prev && field_merged2_is_same_as_prev && field_merged3_is_same_as_prev
if merged_rows_are_all_same_as_prev
parsed_merged.last["field_merged1"].push x["field_merged1"]
parsed_merged.last["field_merged2"].push x["field_merged2"]
parsed_merged.last["field_merged3"].push x["field_merged3"]
parsed_merged.last["field_merged4"].push x["field_merged4"]
else
parsed_merged << {
"field_non_merged1" => x["field_non_merged1"],
"field_merged1" => [x["field_merged1"]],
"field_merged2" => [x["field_merged2"]],
"field_merged3" => [x["field_merged3"]],
"field_merged4" => [x["field_merged4"]],
"field_non_merged2" => x["field_non_merged2"],
"field_non_merged3" => x["field_non_merged3"],
}
end
end
end
end
.map do |x|
{
"field_non_merged1" => x["field_non_merged1"],
"field_merged1" => x["field_merged1"].compact.uniq,
"field_merged2" => x["field_merged2"].compact.uniq,
"field_merged3" => x["field_merged3"].compact.uniq,
"field_merged4" => x["field_merged4"].compact.uniq,
"field_non_merged2" => x["field_non_merged2"],
"field_non_merged3" => x["field_non_merged3"],
}
end
This is not possible without first assigning the value to all the cells of the range, even in Excel VBA this is the case.
See this sample
require 'axlsx'
p = Axlsx::Package.new
wb = p.workbook
wb.add_worksheet(:name => "Basic Worksheet") do |sheet|
sheet.add_row ["Val", nil]
sheet.add_row [nil, nil]
merged = sheet.merge_cells('A1:B2')
p sheet.rows[0].cells[0].value # "Val"
p sheet.rows[0].cells[1].value # nil
sheet[*merged].each{|cell|cell.value = sheet[*merged].first.value}
p sheet.rows[0].cells[0].value # "Val"
p sheet.rows[0].cells[1].value # "Val"
end
p.serialize('./simple.xlsx')
Please add a sample yourself next time so that we see which gem you used, which code, error etc.

Convert Nested Array into Nested Hash in Ruby

Without knowing the dimension of array, how do I convert an array to a nested hash?
For example:
[["Message", "hello"]]
to:
{{:message => "Hello"}}
Or:
[["Memory", [["Internal Memory", "32 GB"], ["Card Type", "MicroSD"]]]]
to:
{{:memory => {:internal_memroy => "32 GB", :card_type => "MicroSD"}}}
or:
[["Memory", [["Internal Memory", "32 GB"], ["Card Type", "MicroSD"]]], ["Size", [["Width", "12cm"], ["height", "20cm"]]]]
to:
{ {:memory => {:internal_memroy => "32 GB", :card_type => "MicroSD"}, {:size => {:width => "12cm", :height => "20cm" } } }
Considering your format of nested arrays of pairs, that following function transforms it into the hash you'd like
def nested_arrays_of_pairs_to_hash(array)
result = {}
array.each do |elem|
second = if elem.last.is_a?(Array)
nested_arrays_to_hash(elem.last)
else
elem.last
end
result.merge!({elem.first.to_sym => second})
end
result
end
A shorter version
def nested_arrays_to_hash(array)
return array unless array.is_a? Array
array.inject({}) do |result, (key, value)|
result.merge!(key.to_sym => nested_arrays_to_hash(value))
end
end
> [:Message => "hello"]
=> [{:Message=>"hello"}]
Thus:
> [:Message => "hello"][0]
=> {:Message=>"hello"}

Nesting loop within a block in Ruby

I have a helper module to generate an array hash data, which is something like:
[{:date => d, :total_amount => 31, :first_category => 1, :second_category => 2,...},
{:date => d+1, :total_amount => 31, :first_category => 1, :second_category => 2,...}]
So I make the method like:
def records_chart_data(category = nil, start = 3.weeks.ago)
total_by_day = Record.total_grouped_by_day(start)
category_sum_by_day = Record.sum_of_category_by_day(start)
(start.to_date..Time.zone.today).map do |date|
{
:date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0,
Category.find(1).title => category_sum_by_day[0][date].try(:first).try(:total_amount) || 0,
Category.find(2).title => category_sum_by_day[1][date].try(:first).try(:total_amount) || 0,
Category.find(3).title => category_sum_by_day[2][date].try(:first).try(:total_amount) || 0,
}
end
end
Since the Category will always change, I try to use loop in this method like:
def records_chart_data(category = nil, start = 3.weeks.ago)
total_by_day = Record.total_grouped_by_day(start)
category_sum_by_day = Record.sum_of_category_by_day(start)
(start.to_date..Time.zone.today).map do |date|
{
:date => date,
Category.all.each_with_index do |category, index|
category.title => category_sum_by_day[index][date].try(:first).try(:total_amount) || 0,
end
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}
end
end
But ruby alerts me with an error:
/Users/tsu/Code/CashNotes/app/helpers/records_helper.rb:10: syntax error, unexpected tASSOC, expecting keyword_end
category.title => category_sum_by_day[index][d...
Why does it say expecting keyword_end, and how should I fix it?
The method category_sum_by_day it calls looks like:
def self.sum_of_category_by_day(start)
records = where(date: start.beginning_of_day..Time.zone.today)
records = records.group('category_id, date(date)')
records = records.select('category_id, date, sum(amount) as total_amount')
records = records.group_by{ |r| r.category_id }
records.map do |category_id, value|
value.group_by {|r| r.date.to_date}
end
end
Or should I alter this method to generate a more friendly method for the helper above?
Category.all.each_with_index do |category, index|
category.title => category_sum_by_day # ...snip!
end
Unfortunately, this piece of code does not adhere to Ruby's grammar. The problem is the body of the block. x => y is not an expression and the syntax requires bodies of blocks to be expressions.
If you want to generate a hash by one key-value pair at a time try the following combination of Hash::[], Array#flatten and the splat operator (i.e. unary *):
Hash[*5.times.map { |i| [i * 3, - i * i] }.flatten]
As a result I'd rewrite the last expresion of records_chart_data more or less as follows
(start.to_date..Time.zone.today).map do |date|
categories = Hash[*Category.all.each_with_index do |category, index|
[ category.title, category_sum_by_day[...] ]
end .flatten]
{ :date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}.merge categories
end
If you consider it unreadable you can do it in a less sophisticated way, i.e.:
(start.to_date..Time.zone.today).map do |date|
hash = {
:date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}
Category.all.each_with_index do |category, index|
hash[category.title] = category_sum_by_day[...]
end
hash
end
Another idea is to use Array#reduce and adopt a more functional approach.
(start.to_date..Time.zone.today).map do |date|
Category.all.each_with_index.reduce({
:date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}) do |hash, (category, index)|
hash.merge category.title => category_sum_by_day[...]
end
hash
end

Testing hash contents using RSpec

I have a test like so:
it "should not indicate backwards jumps if the checker position is not a king" do
board = Board.new
game_board = board.create_test_board
board.add_checker(game_board, :red, 3, 3)
x_coord = 3
y_coord = 3
jump_locations = {}
jump_locations["upper_left"] = true
jump_locations["upper_right"] = false
jump_locations["lower_left"] = false
jump_locations["lower_right"] = true
adjusted_jump_locations = #bs.adjust_jump_locations_if_not_king(game_board, x_coord, y_coord, jump_locations)
adjusted_jump_locations["upper_left"].should == true
adjusted_jump_locations["upper_right"].should == false
adjusted_jump_locations["lower_left"].should == false
adjusted_jump_locations["lower_right"].should == false
end
which, I know, is verbose. Is there a more concise way to state my expectations? I've looked at the docs but I can't see where to compress my expectations. Thanks.
It works for hashes too:
expect(jump_locations).to include(
"upper_left" => true,
"upper_right" => false,
"lower_left" => false,
"lower_right" => true
)
Source:
include matcher # relishapp.com
Just wanna add to #David's answer. You could nest and use matchers in your include hash. For example:
# Pass
expect({
"num" => 5,
"a" => {
"b" => [3, 4, 5]
}
}).to include({
"num" => a_value_between(3, 10),
"a" => {
"b" => be_an(Array)
}
})
A caveat: a nested include hash must test all keys or the test will fail, e.g.:
# Fail
expect({
"a" => {
"b" => 1,
"c" => 2
}
}).to include({
"a" => {
"b" => 1
}
})
Syntax has changed for RSpec 3, but include matcher is still the one:
expect(jump_locations).to include(
"upper_left" => true,
"upper_right" => false,
"lower_left" => false,
"lower_right" => true
)
See built-in-matchers#include-matcher.
Another easy way to test if the whole content is a Hash is to checkout if the content is the Hash Object itself:
it 'is to be a Hash Object' do
workbook = {name: 'A', address: 'La'}
expect(workbook.is_a?(Hash)).to be_truthy
end
For the question above we can check as follow:
expect(adjusted_jump_locations).to match(hash_including('upper_left' => true))

Resources