Rails Model Syntax Confusion - ruby-on-rails

I came across this code in Rails app using mongodb:
"""
Folder format:
{
name: <folder name>,
stocks: [
{
name: <stock name>,
id: <stock id>,
qty: <stock quantity>
}
]
}
"""
def format_with_folders(stocks)
fmap = stock_folder_map
res = stocks.group_by {|s| fmap[s["id"]] }.collect {|fname, ss|
{
"name" => fname,
"stocks" => ss
}
}
new(folders: res)
end
def stock_folder_map
res = {}
folders.each { |ff|
ff.stocks.each { |s|
res[s["id"]] = ff["name"]
}
}
return res
end
end
The doubts are:
1) What does the code inside triple quote signify? Is is a commented code?
2)What would be the right format to use this code inside a ruby script?

First of all, the triple quoted string is often used as a comment, and that is the case here.
To get this to work outside of the class, you would need create a folders method that returns an array of folders in the correct structure. You could do something like this:
Folder = Struct.new(:name, :stocks)
def folders
[
Folder.new(
"Folder 1",
[
{ "name" => "stock name", "id" => "stock id", "qty" => 3 },
{ "name" => "stock name", "id" => "stock id", "qty" => 5 }
]
),
Folder.new(
"Folder 2",
[
{ "name" => "stock name", "id" => "stock id", "qty" => 2 },
{ "name" => "stock name", "id" => "stock id", "qty" => 1 }
]
)
]
end
def format_with_folders(stocks)
# ...
end
def stock_folder_map
# ...
end
The folders method returns an array of Folder objects, which both have a name and stocks attribute. Stocks are an array of hashes.

In Ruby, if you have multiple string literals next to each other, they get concatenated at parse time:
'foo' "bar"
# => 'foobar'
This is a feature inspired by C.
So, what you have there is three string literals next to each other. The first string literal is the empty string:
""
Then comes another string literal:
"
Folder format:
{
name: <folder name>,
stocks: [
{
name: <stock name>,
id: <stock id>,
qty: <stock quantity>
}
]
}
"
And lastly, there is a third string literal which is again empty:
""
At parse time, this will be concatenated into a single string literal:
"
Folder format:
{
name: <folder name>,
stocks: [
{
name: <stock name>,
id: <stock id>,
qty: <stock quantity>
}
]
}
"
And since this string object isn't referenced by anything, isn't assigned to any variable, isn't returned from any method or block, it will just get immediately garbage collected.
In other words: the entire thing is a no-op, it's dead code. A sufficiently smart Ruby compiler (such as JRuby or Rubinius) will probably completely eliminate it, compile it into nothing.

Related

Create a deep nested hash using loops in Ruby

I want to create a nested hash using four values type, name, year, value. ie, key of the first hash will be type, value will be another hash with key name, then value of that one will be another hash with key year and value as value.
The array of objects I'm iterating looks like this:
elements = [
{
year: '2018',
items: [
{
name: 'name1',
value: 'value1',
type: 'type1',
},
{
name: 'name2',
value: 'value2',
type: 'type2',
},
]
},
{
year: '2019',
items: [
{
name: 'name3',
value: 'value3',
type: 'type2',
},
{
name: 'name4',
value: 'value4',
type: 'type1',
},
]
}
]
And I'm getting all values together using two loops like this:
elements.each do |element|
year = element.year
element.items.each |item|
name = item.name
value = item.value
type = item.type
# TODO: create nested hash
end
end
Expected output is like this:
{
"type1" => {
"name1" => {
"2018" => "value1"
},
"name4" => {
"2019" => "value4"
}
},
"type2" => {
"name2" => {
"2018" => "value2"
},
"name3" => {
"2019" => "value3"
}
}
}
I tried out some methods but it doesn't seems to work out as expected. How can I do this?
elements.each_with_object({}) { |g,h| g[:items].each { |f|
h.update(f[:type]=>{ f[:name]=>{ g[:year]=>f[:value] } }) { |_,o,n| o.merge(n) } } }
#=> {"type1"=>{"name1"=>{"2018"=>"value1"}, "name4"=>{"2019"=>"value4"}},
# "type2"=>{"name2"=>{"2018"=>"value2"}, "name3"=>{"2019"=>"value3"}}}
This uses the form of Hash#update (aka merge!) that employs a block (here { |_,o,n| o.merge(n) } to determine the values of keys that are present in both hashes being merged. See the doc for definitions of the three block variables (here _, o and n). Note that in performing o.merge(n) o and n will have no common keys, so a block is not needed for that operation.
Assuming you want to preserve the references (unlike in your desired output,) here you go:
elements = [
{
year: '2018',
items: [
{name: 'name1', value: 'value1', type: 'type1'},
{name: 'name2', value: 'value2', type: 'type2'}
]
},
{
year: '2019',
items: [
{name: 'name3', value: 'value3', type: 'type2'},
{name: 'name4', value: 'value4', type: 'type1'}
]
}
]
Just iterate over everything and reduce into the hash. On the structures of known shape is’s a trivial task:
elements.each_with_object(
Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) } # for deep bury
) do |h, acc|
h[:items].each do |item|
acc[item[:type]][item[:name]][h[:year]] = item[:value]
end
end
#⇒ {"type1"=>{"name1"=>{"2018"=>"value1"},
# "name4"=>{"2019"=>"value4"}},
# "type2"=>{"name2"=>{"2018"=>"value2"},
# "name3"=>{"2019"=>"value3"}}}

split multi-dimensional array in ruby

I am trying to parse json data in ruby my desired output is:
var events = { '01-01-2018' :
[ {content: 'Psalm 2', allDay: true},
{content: 'by ToddWagner', allDay: true}
],
'01-02-2018' :
[ {content: 'Psalm 2', allDay: true},
{content: 'by ToddWagner', allDay: true}
]
}
what I get is
var events = [
{"2017-11-03":
[ {"content":"Romans 14:5-12","allDay":true},
{"content":"by Micah Leiss","allDay":true}
]
},
{"2017-11-06":
[{"content":"Romans 14:13","allDay":true},
{"content":"by Sarah Thomas","allDay":true}
]
}
]
I tried something like
data = []
raw_data['entries'].each do |entry|
data << {entry_date => [
{
"content" => entry.title,
"allDay" => true,
},
{
"content" => entry.writer,
"allDay" => true,
},
]
}
end
data.to_json
but I didn't get desired results, I have also tried data.pop data.shift.
Ruby implementation would look like:
data = raw_data['entries'].map do |entry|
[entry.date, [entry.title, entry.writer].map do |content|
{content: content, allDay: true}
end]
end.to_h
First of all, are adding fields to your array data, as I can see from your desired output, you need a hash.
You have to create the hash, not the array:
data = {}
and then in your loop
raw_data['entries'].each do |entry|
add it like that
data[entry_date] = [
{
"content" => entry.title,
"allDay" => true,
},
{
"content" => entry.writer,
"allDay" => true,
},
]
(I am not where do you declare entry_date in your example so it might be entry.date)
I can't tell from your example if entry date is unique or not(and I think it's not) make sure you add to hash, because you might overwrite it.
You can do something like this if entry date isn't unique
data[entry.date] ||= []
data[entry.date] << {hash_you_need}

Ruby on Rails: Concatenate results of Mongoid criterias and paging

I'm pretty sure that I'm doing something wrong. Consider the following code:
criteria1 = Model.where(...)
criteria2 = Model.where(...)
results = (criteria1.to_a + criteria2.to_a)[offset..(offset + count_per_page - 1)]
This code concatenates results of two different criterias and get a certain number of results with a given offset (paging).
The problem in this code is implicit. The to_a method call actually loads all results of a criteria to the memory as an array.
Now consider a really huge collection... The to_a call slows all things down dramatically.
What I wish to do is something like this:
criteria1 = Model.where(...)
criteria2 = Model.where(...)
# A criteria, which returns results of the first criteria concatenated with results of the second criteria
criteria = criteria1 + criteria2
results = criteria.offset(offset).limit(count_per_page)
The important thing is that results of the second criteria goes after results of the first criteria.
Any clues how is it possible to achieve with Mongoid?
Thanks!
UPDATE
Gergo Erdosi suggested to use merge method. I've tried to use this and it is not what I'm looking for. The problem here is the following:
criteria1 = Model.where(:name => "John", :age => "23")
criteria2 = Model.where(:name => "Bob", :gender => "male")
criteria = criteria1.merge(criteria2)
p criteria.selector
# prints: { "name" => "Bob", :age => 23, :gender => "male" }
So here are two problems:
merge doesn't produce OR, it overrides common keys of the first query with the second;
Even if we use Model.or({ :name => "John" }, { :name => "Bob" }) or Model.in(:name => ["John", "Bob"]) results won't have the right order. I wish results of the first criteria go first and then results of the second criteria go after.
It is possible that I don't understand something and Gergo's answer is right. Do you have any other ideas? Thanks.
UPDATE 2
Thank you Gergo for helping me out here. Let's try a simple example in Mongo shell:
// Fill out test db with some simple documents.
for (var i = 0; i < 10; ++i) { db.users.insert({ name: i % 2 ? "John" : "Bob", age: Math.round(Math.random() * 100) }); }
// These queries give me the same order of documents.
db.users.find({ name: { $in: ["Bob", "John"] } });
db.users.find({ $or: [{ name: "Bob" }, { name: "John" }] });
// Like this:
{ "_id" : ObjectId("53732076b110ab9be7619a8e"), "name" : "Bob", "age" : 69 }
{ "_id" : ObjectId("53732076b110ab9be7619a8f"), "name" : "John", "age" : 63 }
{ "_id" : ObjectId("53732076b110ab9be7619a90"), "name" : "Bob", "age" : 25 }
{ "_id" : ObjectId("53732076b110ab9be7619a91"), "name" : "John", "age" : 72 }
// ...
// But I wish to get concatenated results of these queries:
db.users.find({ name: "Bob" });
db.users.find({ name: "John" });
// Like this (results of the first criteria go first):
{ "_id" : ObjectId("53732076b110ab9be7619a8e"), "name" : "Bob", "age" : 69 }
{ "_id" : ObjectId("53732076b110ab9be7619a90"), "name" : "Bob", "age" : 25 }
// ...
{ "_id" : ObjectId("53732076b110ab9be7619a8f"), "name" : "John", "age" : 63 }
{ "_id" : ObjectId("53732076b110ab9be7619a91"), "name" : "John", "age" : 72 }
// ...
Notice, that I cannot use a simple sorting here, because the data in the real application is more complex. In the real application criterias look like these:
// query variable is a string
exact_match_results = Model.where(:name => query)
inexact_match_results = Model.where(:name => /#{query}/i)
So we cannot just sort alphabetically here.
Use the merge method:
criteria = criteria1.merge(criteria2)
results = criteria.offset(offset).limit(count_per_page)
You can see the details in the method description.
Edit: As pointed out, merge doesn't produce an OR query.
irb(main):010:0> Model.where(name: 'John').merge(Model.where(name: 'Bob'))
=> #<Mongoid::Criteria
selector: {"name"=>"Bob"}
options: {}
class: Model
embedded: false>
Which is not the expected behavior in this case. The reason is that merge uses Hash.merge which behaves this way. The relevant code from Criteria.merge:
selector.merge!(criteria.selector)
This can be illustrated as:
irb(main):011:0> {name: 'John'}.merge({name: 'Bob'})
=> {:name=>"Bob"}
Because of this, it's not easy to give a general advice on how to merge two criteria in a way that the result is an OR query. But with a little change in the criteria, it's possible. For example:
criteria1 = Model.any_of(name: 'John').where(age: '23')
criteria2 = Model.any_of(name: 'Bob').where(gender: 'male')
The result of the merge is an OR query which contains both names:
irb(main):014:0> criteria1.merge(criteria2)
=> #<Mongoid::Criteria
selector: {"$or"=>[{"name"=>"John"}, {"name"=>"Bob"}], "age"=>"23", "gender"=>"male"}
options: {}
class: Model
embedded: false>

Render RABL template with two objects

I recently came across a condition were i wanted to send two objects in the RABL as a response.
[
{
id: "1",
name: "XYZ"
},
{
id: "1",
name: "XYZ"
},
{
total: "2"
}
]
All I could manage was this , which is not correct.
[
{
id: "1",
name: "XYZ",
total: "2"
},
{
id: "1",
name: "XYZ",
total: "2"
}
]
I Found a solution which was to use a partial to iterate on the object and just add a new
node(:name) {partial("users/names", :object => #users)}
node(:total){ #total}
This is a hack, which i don't want because it wraps all the names in a node .
Is there any other way to do it ?
In your rabl file try this:
child #users, object_root: false do
attributes :id, :name
end
node(:total) { #users.size }

Rails to_json - exact order of existing columns

I have a task to form JSON data for jqGrid. It requires a special format:
{
total: 50,
page:"1",
records: "1500",
rows: [
{ 20, "{2ae39c44-ca9d-4565-9e05-bbd875c1579c}", "Description 1"},
{ 23, "{e1aaf69d-1040-4afa-8995-fd15c3a591b3}", "Description 2"},
{ 25, "{e3df29c7-ef34-46ba-bf66-7838aca7c137}", "Description 3"},
{ 29, "{768ec164-28e5-4614-a259-63257b79e8e0}", "Description 4"}
]
}
So the basic rules for "rows" are: do not generate root object name, list fields without their names, list fields in exact order to bind to corresponding columns.
Can I force to_json method to modify output as I need?
Currently the to_json produces:
myobjs : [
myobj : { id: 20, uuid: "{2ae39c44-ca9d-4565-9e05-bbd875c1579c}", name: "Description 1"},
myobj : { id: 20, uuid: "{e1aaf69d-1040-4afa-8995-fd15c3a591b3}", name: "Description 2"},
myobj : { id: 20, uuid: "{e3df29c7-ef34-46ba-bf66-7838aca7c137}", name: "Description 3"},
myobj : { id: 20, uuid: "{768ec164-28e5-4614-a259-63257b79e8e0}", name: "Description 4"}
]
You can't do it with a model-level to_json call, you'll need to build an intermediary data representation as #Paul said. Something like:
class MyObj
def to_json
[id, uuid, name]
end
end
And then in the controller:
class MyController < ApplicationController
def grid_data
objs = MyObj.all
json_data = {
:total => objs.count,
:page => 1,
:records => 1500,
:rows => objs.collect {|o| o.to_json}
}
... send json as usual ...
end
end
Note that I set your model up to generate an array, not a hash as you specified, as I think you copied that wrong - your JSON example above is not valid. { 20, 'foo', 'bar' } is not valid JSON as "{...}" represents a hash, which must be keyed, and is not ordered.

Resources