Dynamic menu with Mongoid in Sinatra - ruby

I'm building Sinatra/Mongoid app and I want to create a dynamic menu of array values ​​in documents from MongoDB. I guess the following algorithm:
Supose in Mongo stored some documents
{ "name": "doc1", "array": ["foo", "bar", "baz", "quux"] }
{ "name": "doc2", "array": ["foo", "baz"] }
{ "name": "doc3", "array": ["bar", "baz", "quux"] }
{ "name": "doc4", "array": ["quux"] }
{ "name": "doc5", "array": ["foo", "quux"] }
Now I guess it should
ask all docs for "array" field values,
then sort values in order of number of mentions,
delete duplicate values
and give me this new array to build %ul in my view, like so:
%ul
%li foo
%li bar
%li baz
%li quux
And I've got no idea how to implement this.
Thank you in advance for any help.

Suppose you already have an arrays variable with the "array" values of your documents:
arrays = [
["foo", "bar", "baz", "quux"],
["foo", "baz"],
["bar", "baz", "quux"],
["quux"],
["foo", "quux"]
]
In this case, I'd use a Hash to store the array value as key, and a counter a value. This allows to
eliminate double entries, and
create a handle for sorting.
The interesting bit is the inject method, which iterates over an enumerable and injects an accumolator (memo):
list = arrays.flatten.inject({}) do |memo, e|
memo[e] = (memo[e] || 0) + 1
memo
end
#=> [["foo", 3], ["bar", 2], ["baz", 3], ["quux", 4]]
This creates this list which then can be sorted:
list = list.sort{|a,b| a[1] <=> b[1] }
#=> [["bar", 2], ["foo", 3], ["baz", 3], ["quux", 4]]
and filtered:
list = list.map(&:first)
#=> ["bar", "foo", "baz", "quux"]
You then can iterate over that array in your view.
N.B. I usually try to compress the inject call a little more, like so
list = arrays.flatten.inject({}) do |memo, e|
(memo[e] ||= 0) += 1
memo
end
or
list = arrays.flatten.inject(Hash.new {|h,k| h[k]=0 }) do |memo, e|
memo[e] += 1
memo
end
… but for some reason, my Ruby currently thinks that's an syntax error. Maybe it's just a lack of coffee ;-)

Related

Merge hash of arrays into array of hashes

So, I have a hash with arrays, like this one:
{"name": ["John","Jane","Chris","Mary"], "surname": ["Doe","Doe","Smith","Martins"]}
I want to merge them into an array of hashes, combining the corresponding elements.
The results should be like that:
[{"name"=>"John", "surname"=>"Doe"}, {"name"=>"Jane", "surname"=>"Doe"}, {"name"=>"Chris", "surname"=>"Smith"}, {"name"=>"Mary", "surname"=>"Martins"}]
Any idea how to do that efficiently?
Please, note that the real-world use scenario could contain a variable number of hash keys.
Try this
h[:name].zip(h[:surname]).map do |name, surname|
{ 'name' => name, 'surname' => surname }
end
I suggest writing the code to permit arbitrary numbers of attributes. It's no more difficult than assuming there are two (:name and :surname), yet it provides greater flexibility, accommodating, for example, future changes to the number or naming of attributes:
def squish(h)
keys = h.keys.map(&:to_s)
h.values.transpose.map { |a| keys.zip(a).to_h }
end
h = { name: ["John", "Jane", "Chris"],
surname: ["Doe", "Doe", "Smith"],
age: [22, 34, 96]
}
squish(h)
#=> [{"name"=>"John", "surname"=>"Doe", "age"=>22},
# {"name"=>"Jane", "surname"=>"Doe", "age"=>34},
# {"name"=>"Chris", "surname"=>"Smith", "age"=>96}]
The steps for the example above are as follows:
b = h.keys
#=> [:name, :surname, :age]
keys = b.map(&:to_s)
#=> ["name", "surname", "age"]
c = h.values
#=> [["John", "Jane", "Chris"], ["Doe", "Doe", "Smith"], [22, 34, 96]]
d = c.transpose
#=> [["John", "Doe", 22], ["Jane", "Doe", 34], ["Chris", "Smith", 96]]
d.map { |a| keys.zip(a).to_h }
#=> [{"name"=>"John", "surname"=>"Doe", "age"=>22},
# {"name"=>"Jane", "surname"=>"Doe", "age"=>34},
# {"name"=>"Chris", "surname"=>"Smith", "age"=>96}]
In the last step the first value of b is passed to map's block and the block variable is assigned its value.
a = d.first
#=> ["John", "Doe", 22]
e = keys.zip(a)
#=> [["name", "John"], ["surname", "Doe"], ["age", 22]]
e.to_h
#=> {"name"=>"John", "surname"=>"Doe", "age"=>22}
The remaining calculations are similar.
If your dataset is really big, you can consider using Enumerator::Lazy.
This way Ruby will not create intermediate arrays during calculations.
This is how #Ursus answer can be improved:
h[:name]
.lazy
.zip(h[:surname])
.map { |name, surname| { 'name' => name, 'surname' => surname } }
.to_a
Other option for the case where:
[..] the real-world use scenario could contain a variable number of hash keys
h = {
'name': ['John','Jane','Chris','Mary'],
'surname': ['Doe','Doe','Smith','Martins'],
'whathever': [1, 2, 3, 4, 5]
}
You could use Object#then with a splat operator in a one liner:
h.values.then { |a, *b| a.zip *b }.map { |e| (h.keys.zip e).to_h }
#=> [{:name=>"John", :surname=>"Doe", :whathever=>1}, {:name=>"Jane", :surname=>"Doe", :whathever=>2}, {:name=>"Chris", :surname=>"Smith", :whathever=>3}, {:name=>"Mary", :surname=>"Martins", :whathever=>4}]
The first part, works this way:
h.values.then { |a, *b| a.zip *b }
#=> [["John", "Doe", 1], ["Jane", "Doe", 2], ["Chris", "Smith", 3], ["Mary", "Martins", 4]]
The last part just maps the elements zipping each with the original keys then calling Array#to_h to convert to hash.
Here I removed the call .to_h to show the intermediate result:
h.values.then { |a, *b| a.zip *b }.map { |e| h.keys.zip e }
#=> [[[:name, "John"], [:surname, "Doe"], [:whathever, 1]], [[:name, "Jane"], [:surname, "Doe"], [:whathever, 2]], [[:name, "Chris"], [:surname, "Smith"], [:whathever, 3]], [[:name, "Mary"], [:surname, "Martins"], [:whathever, 4]]]
[h[:name], h[:surname]].transpose.map do |name, surname|
{ 'name' => name, 'surname' => surname }
end

Looking to convert information from a file into a hash Ruby

Hello I have been doing some research for sometime on this particular project I have been working on and I am at a loss. What I am looking to do is use information from a file and convert that to a hash using some of those components for my key. Within the file I have:1,Foo,20,Smith,40,John,55
An example of what I am looking for I am looking for an output like so {1 =>[Foo,20], 2 =>[Smith,40] 3 => [John,55]}
Here is what I got.
h = {}
people_file = File.open("people.txt") # I am only looking to read here.
until people_file.eof?
i = products_file.gets.chomp.split(",")
end
people_file.close
FName = 'test'
str = "1,Foo,20,Smith, 40,John,55"
File.write(FName, str)
#=> 26
base, *arr = File.read(FName).
split(/\s*,\s*/)
enum = (base.to_i).step
arr.each_slice(2).
with_object({}) {|pair,h| h[enum.next]=pair}
#=> {1=>["Foo", "20"], 2=>["Smith", "40"],
# 3=>["John", "55"]}
The steps are as follows.
s = File.read(FName)
#=> "1,Foo,20,Smith, 40,John,55"
base, *arr = s.split(/\s*,\s*/)
#=> ["1", "Foo", "20", "Smith", "40", "John", "55"]
base
#=> "1"
arr
#=> ["Foo", "20", "Smith", "40", "John", "55"]
a = base.to_i
#=> 1
I assume the keys are to be sequential integers beginning with a #=> 1.
enum = a.step
#=> (1.step)
enum.next
#=> 1
enum.next
#=> 2
enum.next
#=> 3
Continuing,
enum = a.step
b = arr.each_slice(2)
#=> #<Enumerator: ["Foo", "20", "Smith", "40", "John", "55"]:each_slice(2)>
Note I needed to redefine enum (or execute enum.rewind) to reinitialize it. We can see the elements that will be generated by this enumerator by converting it to an array.
b.to_a
#=> [["Foo", "20"], ["Smith", "40"], ["John", "55"]]
Continuing,
c = b.with_object({})
#=> #<Enumerator: #<Enumerator: ["Foo", "20", "Smith", "40", "John", "55"]
# :each_slice(2)>:with_object({})>
c.to_a
#=> [[["Foo", "20"], {}], [["Smith", "40"], {}], [["John", "55"], {}]]
The now-empty hashes will be constructed as calculations progress.
c.each {|pair,h| h[enum.next]=pair}
#=> {1=>["Foo", "20"], 2=>["Smith", "40"], 3=>["John", "55"]}
To see how the last step is performed, each initially directs the enumerator c to generate the first value, which it passes to the block. The block variables are assigned to that value, and the block calculation is performed.
enum = a.step
b = arr.each_slice(2)
c = b.with_object({})
pair, h = c.next
#=> [["Foo", "20"], {}]
pair
#=> ["Foo", "20"]
h #=> {}
h[enum.next]=pair
#=> ["Foo", "20"]
Now,
h#=> {1=>["Foo", "20"]}
The calculations are similar for the remaining two elements generated by the enumerator c.
See IO::write, IO::read, Numeric#step, Enumerable#each_slice, Enumerator#with_object, Enumerator#next and Enumerator#rewind. write and read respond to File because File is a subclass of IO (File.superclass #=> IO). split's argument, the regular expression, /\s*,\s*/, causes the string to be split on commas together with any spaces that surround the commas. Converting [["Foo", "20"], {}] to pair and h is a product of Array Decompostion.

Take any hash keys and flatten into mixed array

I have a mixed array like: ["foo", "bar", {test: "stuff"}, "foobar", {just: "examples}]
I want to take any hashes, remove the key, and push the value into the array. Taking my example above, I want to return:
["foo", "bar", "stuff", "foobar", "examples"]
Simplest way to do that?
a = ["foo", "bar", {test: "stuff"}, "foobar", {just: "examples"}]
a.flat_map { |x| x.is_a?(Hash) ? x.values : x }
# => ["foo", "bar", "stuff", "foobar", "examples"]
Another way, without if (which, as #Matt pointed out, only works with the hashes have a single key, as in the example):
a = ["foo", "bar", {test: "stuff"}, "foobar", {just: "examples"}]
a.map { |e| [*e].flatten.last }
#=> ["foo", "bar", "stuff", "foobar", "examples"]

Passing a block variable to grep?

When I do:
["bar","quax"].grep(/bar/)
The output is:
=> ["bar"]
If I do:
["bar","quax"].grep(/fish/)
The output is:
=> []
I decided to take it further by attempting to pass a block to grep, but it did not work.
["foo", "bar", "baz", "quax"].each do |some_word|
["fish","jones"].grep(/some_word/)
end
The output is:
=> ["foo", "bar", "baz", "quax"]
I am curious why my extension doesn't work, as it seems fairly straightforward. Or, is it simply illegal to do?
each is iterating over your array but you aren't modifying it. So it's just looping a few times, calling grep which is effectively doing nothing (technically it's returning something, you're just not doing anything with it), then returning the original array.
You need to be doing something with the return value of grep - but it's not clear to me what that is supposed to be (I don't understand why you are using each at all?)
Also, some_word in this case is taken literally as "some_word". You should interpolate your regex like .grep(/#{some_word}/)
["foo", "bar", "baz", "quax"].each do |some_word|
["fish","jones"].grep(/some_word/)
end
isn't correct. some_word is the parameter to the block, but /some_word/ is a regular expression that matches the string 'some_word', whether that is the entire string or just a sub-string:
%w[some_word not_some_word_anymore].grep(/some_word/)
# => ["some_word", "not_some_word_anymore"]
If you want to use the variable/parameter some_word inside the regular expression, you have to substitute it in somehow. A simple way to do it is:
/#{ some_word }/
or:
Regexp.new( some_word )
For instance:
foo = 'some_word'
/#{ foo }/ # => /some_word/
Regexp.new(foo) # => /some_word/
The reason:
["foo", "bar", "baz", "quax"].each do |some_word|
end
returns the same array, is that is how each behaves. We don't care usually. If you want to transform that array, then use map:
["foo", "bar", "baz", "quax"].map { |some_word| some_word.size }
# => [3, 3, 3, 4]
If you're trying to reduce the array use something like grep or select or reject:
["foo", "bar", "baz", "quax"].reject { |some_word| some_word['oo'] }
# => ["bar", "baz", "quax"]
["foo", "bar", "baz", "quax"].select { |some_word| some_word['oo'] }
# => ["foo"]

upcase array that has both strings and numbers

I am comparing large arrays to find missing elements. But one array will all be capitalized and the other is not. So I want to format the one array but am having issues. This is an example of the array I am trying to format.
array = [ 023, "Bac001", "abc123"]
Then I try to capitalize everything
array.map!(&:upcase)
but get undefined method 'upcase' for 023
is there a way around this?
I'd use Object#respond_to?:
def upcase_elements(ary)
ary.map { |e| e.respond_to?(:upcase) ? e.upcase : e }
end
upcase_elements([23, "BAC001", "ABC123"])
#=> [23, "BAC001", "ABC123"]
Checking if the receiver responds to a method is more flexible than checking its type:
upcase_elements([:foo, "bar"])
#=> [:FOO, "BAR"]
array.map! { |s| s.kind_of?(String) ? s.upcase : s }
This will not attempt to upcase any non-string element of the array. So it will work on arrays like:
array = [23, 1.27, "Bac001", "abc123", {:foo => 3}]
Yielding:
[23, 1.27, "BAC001", "ABC123", {:foo => 3}]

Resources