Grouping by one column and summing multiple other columns in Rails - ruby-on-rails

I'm found a very ugly way to do what I need (currently just in the Rails view, I'll move it later), which is basically to find all the articles of a particular user, group them into their individual publications, and then sum the share counts of all of the articles the user has added for that publication.
...But it's not pretty. If there's a better way to do this, can someone advise?
=#user.articles.group(:publication).map do |p|
=p.publication
=#user.articles.where("publication = ?", p.publication).sum(:twitter_count)
=#user.articles.where("publication = ?", p.publication).sum(:facebook_count)
=#user.articles.where("publication = ?", p.publication).sum(:linkedin_count)
This gives the output (e.g.) NYT 12 18 14 BBC 45 46 47 CNN 75 54 78, which is pretty much what I need.
However, at present, it's also outputting some extra stuff on the end - "[5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] sum(:twitter_count)" - I think this is to do with the .map but I'm not sure why.

Change = by - in first line.
- #user.articles.group(:publication).map do |p|

You are right. .map is creating new array.
Substitute = with - on the first line. (credits goes to juanpastas, I did not remember HAML)
Docs for #map:
Creates a new array containing the values returned by the block.
a = [ "a", "b", "c", "d" ]
a.map { |x| x + "!" } #=> ["a!", "b!", "c!", "d!"]
a #=> ["a", "b", "c", "d"]

Related

i am trying to add a method each_run to the Ruby Array class which takes a code block expecting two arguments

Add a method each_run to the Ruby Array class which takes a code block expecting two arguments. It calls the code block once for each contiguous run of equal items in the array, sending the length of the run and the item repeated.the output will appear like this:
irb(main):001:0> load("eachrun.rb")
=> true
irb(main):002:0> [5,9,3,4,4,4,7,8,8].each_run { |n,x|
irb(main):003:1* print x, " appearing ", n, " times\n" }
5 appearing 1 times
9 appearing 1 times
3 appearing 1 times
4 appearing 3 times
7 appearing 1 times
8 appearing 2 times
=> nil
I would use group_by and yield the keys and size of the associated groups.
def each_run
group_by(&:itself).map {|k,v| yield v.length, k }
end
I wouldn't monkey-patch it directly into Array, though. First, it would make more sense as part of Enumerable, and second, monkey-patching can get messy. You could make it a refinement, though. Sadly, you can only refine classes, not modules, so it's back to Array instead of Enumerable:
module EachRun
refine Array do
def each_run
group_by(&:itself).map {|k,v| yield v.length, k }
end
end
end
Then just put using EachRun at the top of any code that you want to be able to use your new method.
I read the intention of the problem "It calls the code block once for each contiguous run of equal items in the array" as meaning the example list is a touch misleading since it contains no subsequent runs of a digit so that you can ignore the structure of the input list and just count how many times any given item appears. If the example had been:
[5,9,3,4,4,4,7,8,8,5,5]
Then printing "5 appears 3 times" doesn't suggest itself as correct.
I haven't written any Ruby in quite a while so this may not be optimal but it's what I came up with:
require 'test/unit/assertions'
include Test::Unit::Assertions
module Enumerable
def runs
if empty?
[]
else
runs = [[first,1]]
self[1..-1].each do |item|
if item == runs.last.first
runs = runs[0..-2] + [[item,runs.last.last+1]]
else
runs = runs + [[item,1]]
end
end
runs
end
end
def each_run(&block)
runs.each(&block)
end
end
assert_equal( [], [].runs )
assert_equal( [[5,1],[9,1],[3,1],[4,3],[7,1],[8,2],[5,2]], [5,9,3,4,4,4,7,8,8,5,5].runs )
[5,9,3,4,4,4,7,8,8,5,5].each_run do |m,n|
puts "#{m} appears #{n} times"
end
Outputting
> 5 appears 1 times
> 9 appears 1 times
> 3 appears 1 times
> 4 appears 3 times
> 7 appears 1 times
> 8 appears 2 times
> 5 appears 2 times
When I thought about it in Clojure what I came up with was this:
(defn runs [l]
(reduce (fn [memo [x n :as e]]
(let [[last-x last-n] (last memo)]
(if (= x last-x)
(conj (vec (drop-last memo)) [x (inc last-n)])
(conj memo e))))
[] (map (fn [e] [e 1]) l)))
(runs [5 5 9 8 1 1 4 3 3 3 3 5 5 5 2 2])
=> [[5 2] [9 1] [8 1] [1 2] [4 1] [3 4] [5 3] [2 2]]
This may give you a few ideas about how to proceed:
irb(main):001:0> a = [5,9,3,4,4,4,7,8,8]
=> [5, 9, 3, 4, 4, 4, 7, 8, 8]
irb(main):002:0> a.uniq.each { |e| printf("%d appears %d times\n", e, a.count(e)) }
5 appears 1 times
9 appears 1 times
3 appears 1 times
4 appears 3 times
7 appears 1 times
8 appears 2 times
I went with the suggest of using uniq and each but that's only if you need to return the uniq values as an array over all. There's lots of ways you can iterate it's up to you but the key here is monkey patching and yielding your values in order to use a block.
class Array
def each_run
uniq.each do |item|
yield(item, self.count(item))
end
end
end
[12] pry(main)> [5,9,3,4,4,4,7,8,8].each_run do |num, count|
[12] pry(main)* printf("%d appears %d times\n", num, count)
[12] pry(main)* end
5 appears 1 times
9 appears 1 times
3 appears 1 times
4 appears 3 times
7 appears 1 times
8 appears 2 times
=> [5, 9, 3, 4, 7, 8]
sources:
Monkey Patch Array Stack Overflow

How to get next values in hash

I have hash for example
{ 1 => 5, 3 => 6, 5 => 5, 8 => 10, 11 => 11}
and I have one key - 5, and I need get hash with next three key-value. in this case result will be:
{ 8 => 10, 11 => 11, 1 => 5}
How i can do this?
That is not a usual use case for a hash table. The whole point is to be able to look up specific keys efficiently, not iterate over keys in sequence.
If you want to do that, you'll need to choose another data structure, either instead of a hash or alongside the hash (if you still wish for efficient lookup).
If you know that they're integer keys, you could test for the existence of subsequent ones until you find three but that's pretty inefficient, especially if the current one is the second highest, for example. You would be better maintaining a different data structure.
as others have said you can't get the 'next' pair of values. If you're looking specifically for numerically ordered pairs where the keys are all numbers, you could do something like this:
h = { 1 => 5, 3 => 6, 5 => 5, 8 => 10, 11 => 11}
sorted_keys = h.keys.sort
sorted_keys.each do |key|
p "#{key} = #{h[key]}"
end
which returns:
"1 = 5"
"3 = 6"
"5 = 5"
"8 = 10"
"11 = 11"
You can't.
Ruby hashes are unordered. There's no reliable "next" key/value.

Rails 3: How to get all Posts which ids are not in a given list?

To get all posts with publisher_id equals to 10, 16, or 17, I do:
Post.where(:publisher_id => [10, 16, 17])
How would I get all posts with publisher_id not equals to 10, 16, or 17 (i.e. all possible ids besides those three) ?
Just perform a :
Post.where(["publisher_id NOT IN (?)", [10, 16, 17]])
in rails 4 we can do like below
Post.where.not(:publisher_id => [10, 16, 17])
it will generate SQL like below
SELECT "posts".* FROM "posts" WHERE ("posts"."publisher_id" NOT IN (10, 16, 17))
Untested, but should be like (using metawhere gem):
Post.where( :id.not_eq => [10,16,17] )
Using "pure" ActiveRecord syntax sprinkled with Arel using Rails 3 you can do something like this:
Post.where( Post.arel_table[:publisher_id].not_in([10, 16, 17]) )
Every single answer on this page is wrong because none of these answers take care of ALL array cases, Especially arrays that have only one element.
Here is an example that will FAIL using any of the 'so called' solutions on this page:
#ids = [1]
Post.where("publisher_id NOT IN (?)", #ids)
#ERROR
Post.where("publisher_id NOT IN (?)", [4])
#ERROR
#...etc
#ALSO
#ids = []
Post.where("publisher_id NOT IN (?)", #ids)
#ERROR
Post.where("publisher_id NOT IN (?)", [])
#ERROR
#...etc
#The problem here is that when the array only has one item, only that element is
#returned, NOT an array, like we had specified
#Part of the sql that is generated looks like:
#...WHERE (publisher_id NOT IN 166)
#It should be:
#...WHERE (publisher_id NOT IN (166))
The only answer on this page that is actually on the right track and takes care of this very important case is #Tudor Constantin's. But the problem is he didn't actually show a 'way' of using his methodology to solve the real abstract example question the OP posted (not just using the hard-coded numbers).
here is my solution to dynamically find the ids not in an Activerecord association given an array of ids to exclude, that will work with an array of n elements (...including n=1 and n=0)
#ids = [166]
#attribute = "publisher_id"
#predicate = "NOT IN"
#ids = "(" + #ids.join(",") + ")"
if #ids == "()"
#Empty array, just set #ids, #attribute, and #predicate to nil
#ids = #attribute = #predicate = nil
end
#Finally, make the query
Post.where( [#attribute, #predicate, #ids].join(" ") )
#Part of the sql that is generated looks like:
#...WHERE (publisher_id NOT IN (166))
#CORRECT!
#If we had set #ids = [] (empty array)
#Then the if statement sets everything to nil, and then
#rails removes the blank " " space in the where clause automatically and does
#the query as if all records should be returned, which
#logically makes sense!
If this helped you in anyway, please up vote! If you are confused or don't understand one of my comments, please let me know.
Neat solution I've used:
ids = #however you get the IDS
Post.where(["id not in (?)", [0,*ids])
The presence of the 0 means it always has one element in (assuming nothing has an ID of 0)
ID becoming a splat means it'll always be an array.
Post.where(" id NOT IN ( 10, 16, 17) ")

#users.each do |user| --- Is there a way to do this for multiple objects

I'm currently doing:
#users.each do |user|
Given I have #users, #users1, #users2
Is there a way to do:
[#users, #users1, #users2].each do |user|
Having the each loop go through all the objects?
Thanks
You can concatenate the arrays and iterate the result:
(#users + #users1 + #users2).each do |user|
...
end
It may be tempting to use flatten, but you should be careful with it. A simple no-arguments flatten call doesn't behave the way you expect if any elements of any array are arrays themselves:
users, users1, users2 = [1,2], [ [3,4], [5,6] ], [ 7,8]
puts [users,users1,users2].flatten.inspect
# Will print [1, 2, 3, 4, 5, 6, 7, 8]
# Two small arrays are smashed!!!
However, as jleedev suggests in one of the comments, flatten function in Ruby may accept an integer argument that defines the maximum level the arrays will be smashed to, so:
puts [users,users1,users2].flatten(1).inspect
# Will print [1, 2, [3, 4], [5, 6], 7, 8]
# Just as planned.
[#users,#users1,#users2].flatten.each do |user|
Various techniques:
# Concatenate
(#users+#users1+#users2).each{ ... }
# Splat (Ruby 1.9 only)
[*#users,*#users1,*#users2].each{ ... }
# Interleave them
#users.zip( #users1, #users2 ).each{ |u,u1,u2| ... }

Clean way to find ActiveRecord objects by id in the order specified

I want to obtain an array of ActiveRecord objects given an array of ids.
I assumed that
Object.find([5,2,3])
Would return an array with object 5, object 2, then object 3 in that order, but instead I get an array ordered as object 2, object 3 and then object 5.
The ActiveRecord Base find method API mentions that you shouldn't expect it in the order provided (other documentation doesn't give this warning).
One potential solution was given in Find by array of ids in the same order?, but the order option doesn't seem to be valid for SQLite.
I can write some ruby code to sort the objects myself (either somewhat simple and poorly scaling or better scaling and more complex), but is there A Better Way?
It's not that MySQL and other DBs sort things on their own, it's that they don't sort them. When you call Model.find([5, 2, 3]), the SQL generated is something like:
SELECT * FROM models WHERE models.id IN (5, 2, 3)
This doesn't specify an order, just the set of records you want returned. It turns out that generally MySQL will return the database rows in 'id' order, but there's no guarantee of this.
The only way to get the database to return records in a guaranteed order is to add an order clause. If your records will always be returned in a particular order, then you can add a sort column to the db and do Model.find([5, 2, 3], :order => 'sort_column'). If this isn't the case, you'll have to do the sorting in code:
ids = [5, 2, 3]
records = Model.find(ids)
sorted_records = ids.collect {|id| records.detect {|x| x.id == id}}
Based on my previous comment to Jeroen van Dijk you can do this more efficiently and in two lines using each_with_object
result_hash = Model.find(ids).each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}
For reference here is the benchmark i used
ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do
100000.times do
result_hash = results.each_with_object({}) {|result,result_hash| result_hash[result.id] = result }
ids.map {|id| result_hash[id]}
end
end.real
#=> 4.45757484436035 seconds
Now the other one
ids = [5,3,1,4,11,13,10]
results = Model.find(ids)
Benchmark.measure do
100000.times do
ids.collect {|id| results.detect {|result| result.id == id}}
end
end.real
# => 6.10875988006592
Update
You can do this in most using order and case statements, here is a class method you could use.
def self.order_by_ids(ids)
order_by = ["case"]
ids.each_with_index.map do |id, index|
order_by << "WHEN id='#{id}' THEN #{index}"
end
order_by << "end"
order(order_by.join(" "))
end
# User.where(:id => [3,2,1]).order_by_ids([3,2,1]).map(&:id)
# #=> [3,2,1]
Apparently mySQL and other DB management system sort things on their own. I think that you can bypass that doing :
ids = [5,2,3]
#things = Object.find( ids, :order => "field(id,#{ids.join(',')})" )
A portable solution would be to use an SQL CASE statement in your ORDER BY. You can use pretty much any expression in an ORDER BY and a CASE can be used as an inlined lookup table. For example, the SQL you're after would look like this:
select ...
order by
case id
when 5 then 0
when 2 then 1
when 3 then 2
end
That's pretty easy to generate with a bit of Ruby:
ids = [5, 2, 3]
order = 'case id ' + (0 .. ids.length).map { |i| "when #{ids[i]} then #{i}" }.join(' ') + ' end'
The above assumes that you're working with numbers or some other safe values in ids; if that's not the case then you'd want to use connection.quote or one of the ActiveRecord SQL sanitizer methods to properly quote your ids.
Then use the order string as your ordering condition:
Object.find(ids, :order => order)
or in the modern world:
Object.where(:id => ids).order(order)
This is a bit verbose but it should work the same with any SQL database and it isn't that difficult to hide the ugliness.
As I answered here, I just released a gem (order_as_specified) that allows you to do native SQL ordering like this:
Object.where(id: [5, 2, 3]).order_as_specified(id: [5, 2, 3])
Just tested and it works in SQLite.
Justin Weiss wrote a blog article about this problem just two days ago.
It seems to be a good approach to tell the database about the preferred order and load all records sorted in that order directly from the database. Example from his blog article:
# in config/initializers/find_by_ordered_ids.rb
module FindByOrderedIdsActiveRecordExtension
extend ActiveSupport::Concern
module ClassMethods
def find_ordered(ids)
order_clause = "CASE id "
ids.each_with_index do |id, index|
order_clause << "WHEN #{id} THEN #{index} "
end
order_clause << "ELSE #{ids.length} END"
where(id: ids).order(order_clause)
end
end
end
ActiveRecord::Base.include(FindByOrderedIdsActiveRecordExtension)
That allows you to write:
Object.find_ordered([2, 1, 3]) # => [2, 1, 3]
Here's a performant (hash-lookup, not O(n) array search as in detect!) one-liner, as a method:
def find_ordered(model, ids)
model.find(ids).map{|o| [o.id, o]}.to_h.values_at(*ids)
end
# We get:
ids = [3, 3, 2, 1, 3]
Model.find(ids).map(:id) == [1, 2, 3]
find_ordered(Model, ids).map(:id) == ids
Another (probably more efficient) way to do it in Ruby:
ids = [5, 2, 3]
records_by_id = Model.find(ids).inject({}) do |result, record|
result[record.id] = record
result
end
sorted_records = ids.map {|id| records_by_id[id] }
Here's the simplest thing I could come up with:
ids = [200, 107, 247, 189]
results = ModelObject.find(ids).group_by(&:id)
sorted_results = ids.map {|id| results[id].first }
#things = [5,2,3].map{|id| Object.find(id)}
This is probably the easiest way, assuming you don't have too many objects to find, since it requires a trip to the database for each id.

Resources