How do I aggregate a has_many field in Phoenix / Ecto? - phoenix-framework

Say for example I have the relation:
Posts has_many Comments
I'm trying to do something along the lines of:
Post |> Repo.aggregate(:count, :comments)
However, Ecto is complaining that :comments is a virtual field, and therefore it cannot count it. What's a good way of fixing this?

I assume you want the comment count for a set of posts. If you want the comment count for all posts you can leave out the where clause.
post_ids = [1, 2, 3]
Comment
|> where([c], c.post_id in ^post_ids)
|> group_by([c], c.post_id)
|> select([c], {c.post_id, count("*")})
|> Repo.all()
This will group the comments by post, given the post_ids, and count how many there are for each. It will return a list with tuples, e.g. like this
[
{1, 10},
{2, 3},
{3, 5}
]
If a post has no comments it will not be listed in the resultset.

Here was my final solution, where link has_many clicks
def list_links_count do
query =
from l in Link,
join: c in Click, as: :click,
where: c.link_id == l.id,
group_by: l.id,
select: {l, count(l.id)}
query |> Repo.all
end

Related

How to compare two arrays in Ruby

I'm trying to compare two arrays:
compareFrom = []
compareTo = ["John Doe", "Eric Schulz", "Tom Jerry"]
I tried the following:
arrayField1 = []
for r in empData
compareFrom << r.employeeName
end
if compareFrom.include?(compareTo)
#yes got it
end
I cannot figure out why I get false even though compareFrom has the same values as compareTo.
Is there anything that I need to change in the code?
Array#include? only tests membership of one element in an array. Your compare_from.include?(compare_to) tests whether compare_to is an element of compare_from, and would e.g. return true in case compare_from is [1, 2, 3, ["John Doe", "Eric Schulz", "Tom Jerry"], 5].
If you want to see if all elements of compare_to are in compare_from, compare_to.all? { |element| compare_from.include?(element) } is idiomatic and legible but slow; tadman's (compare_from & compare_to).size == compare_to.size is much more performant. A third option, when speaking of subsets, and the one I'd likely prefer, is to use sets:
require 'set'
Set[compare_to].subset?(Set[compare_from])
This code boils down to:
compare_from = emp_data.map(&:employee_name)
Where that's calling the employee_name method on each of the items in the emp_data array and returning a new array with the result. You can easily test overlap on two arrays using & to find the intersection:
compare_to = ["John Doe", "Eric Schulz", "Tom Jerry"]
common = compare_from & compare_to
If that array common has any entries then you have matches.

Using a Ruby map on a range

I'm trying to use .map so I don't need to initialize a products array.
Here's the original code:
products = []
for page in (1..(ShopifyAPI::Product.count.to_f/150.0).ceil)
products += ShopifyAPI::Product.find(:all, :params => {:page => page, :limit => 150})
end
Here's what I've tried:
products = (1..(ShopifyAPI::Product.count.to_f/150.0).ceil).map do |page|
ShopifyAPI::Product.find(:all, :params => {:page => page.to_i, :limit => 150})
end
Which only returns the first product? What am I doing wrong?
The ShopifyAPI::Product returns a list of products based on the sent parameters page, and limit.
I'm not sure why you're finding the second snippet only returns the first product, but in order to make it functionally equivalent to the first, you could use flat_map instead of map here, or alternatively tack on a .flatten at the end (or flatten(1), if you want to be more specific)
Given that the .find call returns an array, you can see the difference in the following examples:
a = []
(0..2).each { |x| a += [x] }
# a == [0,1,2]
(0..2).map { |x| [x] }
# [[0], [1], [2]]
(0..2).flat_map { |x| [x] }
# [0, 1, 2]
That's because array + array combines the two of them.
If your first snippet instead used products.push(<find result>) then you'd see the same nested array result.
See Enumerable#flat_map and Array#flatten

Selecting from a Hash

I'm learning Ruby on Codecademy and I'm having trouble with this problem:
Create a new variable, good_movies, and set it equal to the result of
calling .select on movie_ratings, selecting only movies with a rating
strictly greater than 3.
Here's my code:
movie_ratings = {
memento: 3,
primer: 3.5,
the_matrix: 5,
truman_show: 4,
red_dawn: 1.5,
skyfall: 4,
alex_cross: 2,
uhf: 1,
lion_king: 3.5
}
# Add your code below!
good_movies = movie_ratings.each {|k,v| v > 3}
Here is the result:
{:memento=>3, :primer=>3.5, :the_matrix=>5, :truman_show=>4, :red_dawn=>1.5, :skyfall=>4, :alex_cross=>2, :uhf=>1, :lion_king=>3.5}
And this is the error that I'm getting:
Oops, try again. It looks like good_movies includes memento, but it
shouldn't.
"memento" has a value of 3 and I thought that my "v > 3" condition would filter it out; what am I doing wrong?
Use Hash#select to filter as per conditions, not Hash#each.
movie_ratings.select {|k,v| v > 3}
Basically Hash#each returns the receiver on which you called it, and in your case, it the original hash movie_ratings. Indeed it contains memento key, as I said, Hash#each not for filtering purposes. But Hash#select, will filter memento with its value out from the output Hash, thus your code will not give any objections.
You must have change each for select like this: good_movies = movie_ratings.select {|k,v| v > 3}

Shorthand way to take an array of Ruby models and turn it into a hash with the id as the key?

I have an array of models that I would like to turn into a hash so I can reference them by id. I know I can iterate over the items and put them in a hash, but I know there must be a quick, shorthand way of doing this same thing:
my_models_hash = {}
#my_models.each do |model|
my_models_hash[model.id] = model
end
How can I do this same thing in one, short line?
You're after each_with_object.
my_models_hash = #my_models.each_with_object({}) { |m,h| h[m.id] = m }
One way:
#my_models.map { |m| [m.id, m] }.to_h
Prior to v2.0, this would have to be written:
Hash[#my_models.map { |m| [m.id, m] }]
If you're using Rails (or more specifically ActiveSupport), there's Enumerable#index_by:
#my_models.index_by(&:id)
#=> { 1 => #<Model id: 1, ...>, 2 => #<Model id: 2, ...>, ...}

How to combine one hash with another hash in ruby

I have two hashes...
a = {:a => 5}
b = {:b => 10}
I want...
c = {:a => 5,:b => 10}
How do I create hash c?
It's a pretty straight-forward operation if you're just interleaving:
c = a.merge(b)
If you want to actually add the values together, this would be a bit trickier, but not impossible:
c = a.dup
b.each do |k, v|
c[k] ||= 0
c[k] += v
end
The reason for a.dup is to avoid mangling the values in the a hash, but if you don't care you could skip that part. The ||= is used to ensure it starts with a default of 0 as nil + 1 is not valid.
(TL;DR: hash1.merge(hash2))
As everyone is saying you can use merge method to solve your problem. However there is slightly some problem with using the merge method. Here is why.
person1 = {"name" => "MarkZuckerberg", "company_name" => "Facebook", "job" => "CEO"}
person2 = {"name" => "BillGates", "company_name" => "Microsoft", "position" => "Chairman"}
Take a look at these two fields name and company_name. Here name and company_name both are same in the two hashes(I mean the keys). Next job and position have different keys.
When you try to merge two hashes person1 and person2 If person1 and person2 keys are same? then the person2 key value will override the peron1 key value . Here the second hash will override the first hash fields because both are same. Here name and company name are same. See the result.
people = person1.merge(person2)
Output: {"name"=>"BillGates", "company_name"=>"Microsoft",
"job"=>"CEO", "position"=>"Chairman"}
However if you don't want your second hash to override the first hash. You can do something like this
people = person1.merge(person2) {|key, old, new| old}
Output: {"name"=>"MarkZuckerberg", "company_name"=>"Facebook",
"job"=>"CEO", "position"=>"Chairman"}
It is just a quick note when using merge()
I think you want
c = a.merge(b)
you can check out the docs at http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-merge
Use merge method:
c = a.merge b

Resources