Convert Ruby Hash to ranks with no repeats - ruby-on-rails

[{:listing_id=>1, :vote_size=>1, :created_at=>Wed, 13 Nov 2019 02:19:45 UTC +00:00},
{:listing_id=>2, :vote_size=>0, :created_at=>Thu, 14 Nov 2019 02:19:45 UTC +00:00},
{:listing_id=>3, :vote_size=>0, :created_at=>Fri, 15 Nov 2019 02:19:45 UTC +00:00}]
I have the following hash of listing IDs, the number of votes and the created_at date for said listing in Ruby (higher vote_size is better), and I'd like to rank them. In other words, I want to get the rank from some sort of function and then update the rank on the listing via listing.update_attribute(:rank, function-call) or something of that sort. Rank 1 is the best. If there are listings with the same amount of votes only one of them should get the rank and the other listing should get a rank below. (Let's say the tiebreaker is the listing created_at date, whoever created the listing first gets the higher rank.)
This is what I have so far and well I'm stuck lol and could really use some help.
namespace :server do
desc "update the listings rank"
task update_listing_rank: :environment do
listings = Listing.all
all_listings_with_votes = total_votes_for_listing listings
all_listings_with_votes.map{ |e|
puts all_listings_with_votes.index(e) + 1
}
end
def total_votes_for_listing listings
listings.map do |listing|
{listing_id: listing.id, vote_size: listing.votes.size, created_at: listing.created_at}
end
end
end

Here's a big hint... Take your array and
arr.sort{|x,y| x[:vote_size] <=> y[:vote_size]}
This will give you a sorted array of vote sizes.

You can try this, that means we sort by combination vote_size and created_at, vote_size descending and created_at ascending in case there are two equal values for vote_size
an_array.sort_by {|a| [-a[:vote_size] , a[:created_at]] }

Related

Ruby on Rails query for a given set of dates

I am trying to write a query for my Rails API that will return the total number of hours of job applications that are accepted per month for a given set of dates.
Table: Job, JobApplication
Job has many JobApplication
JobApplication has a field status "SUCCESS" if matched
Start Date: Jul 16 2015
Is it possible to have it return {Jul 16: 100, Aug 16: 200, .... } ?
I am quite confused. Any help would be appreciated.
This query would give the number of successful Job Applications within a date range.
JobApplication.where(status: "SUCCESS").("created_at < ? ", Jul 16).("created_at < ? ", Aug 15)
JobApplication.job.duration would give the number of hours for that job.
I am not sure how to put them together and then loop through the dates.
My expect results is something like this:
{Jul 16: 100, Aug 16: 200, .... }
Hey you can try this way:
JobApplication.joins(:job).where(status: "SUCCESS").where(job_applications => {:created_at => [state_date..end_date]}).group(:created_at).sum("jobs.duration")
It will Return you result like
{Date => total_duration, :date => total_duration}
If You want you can convert date to specific format like DATE_FORMAT(created_at, "%d-%m-%y") so just put group(DATE_FORMAT(created_at, "%m-%y")). It will remove your time part from timestamp in mysql and returns data daywise
Something like
data = JobApplication.where(status: 'SUCCESS').group(:start_date).select('start_date, count(*), sum(duration) as sum')
should do the trick, if i understood your question right. This will return JobApplication instances with sum set
Perhaps it is easier to run a pure SQL query:
SELECT start_date, count(*), sum(duration) FROM job_applications WHERE status LIKE 'SUCCESS' GROUP BY start_date

ruby how to group by given date

I want to find signup count daily, for the date range say this month. so
starts_at = DateTime.now.beginning_of_month
ends_at = DateTime.now.end_of_month
dates = ((starts_at.to_date)..(ends_at.to_date)).to_a
dates.each_with_index do |date,i|
User.where("created_at >= ? and created_at <= ?", date, date.tomorrow)
end
So nearly 30 queries running, how to avoid running 30 query and do it in single query?
I need something like
group_by(:created_at)
But in group by if there is no data present for particular date it's showing nothing, but I need date and count as 0
I followed this:
How do I group by day instead of date?
def group_by_criteria
created_at.to_date.to_s(:db)
end
User.all.group_by(&:group_by_criteria).map {|k,v| [k, v.length]}.sort
Output
[["2016-02-05", 5], ["2016-02-06", 12], ["2016-02-08", 6]]
There is no data for 2016-02-05 so it should be included with count 0
I can't test it at the moment, but it should be possible to filter your date range and group it with a little help of your dbms like this:
User.select('DATE(created_at)').where("created_at >= ? and created_at <= ?", DateTime.now.beginning_of_month, DateTime.now.end_of_month).group('DATE(created_at)').count
Would this do?
starts_at = DateTime.now.beginning_of_month
ends_at = DateTime.now.end_of_month
User.where(created_at: starts_at..ends_at).group("date(created_at)").count
# => {Tue, 09 Feb 2016=>151, Mon, 08 Feb 2016=>130}
Note that you won't get any results for dates when there has been zero creations, so you might want to do something like this:
Hash[*(starts_at..ends_at).to_a.flat_map{|d| [d, 0]}].merge(
User.where(created_at: starts_at..ends_at).group("date(created_at)").count
)
Not pretty, but what happens there is you first create a hash with all dates in the range having zero values and merging the results from database into that hash.

In Active Record (Rails), how to select a sum of a column from a has_many relation with every parent record?

Considering this model:
User:
id: int
Valuable:
id: int
user_id: int
value: int
User has many Valuables.
Now here's the thing. I want to use ActiveRecord to select multiple users with any query, after which I want to be able to see the sum of all their valuables, without having to do N+1 queries.
So, I want to be able to do this:
# #var ids [Array] A bunch of User IDs.
User.where(id: ids).each do |u|
puts "User ##{u.id} has total value of #{u.total_value}
end
and it should do 1 (or max 2) queries and not instantiate all Valuables. I tried playing around with select('*, SUM(valuables.value) as total_value).joins(valuables), but with no luck. I'm using PostgreSQL
If at all possible, I would like this to happen automatically (e.g. using default_scope) and I still want to be able to use includes.
UPDATE: Sorry I haven't been clear about this. (Actually, I did write it). I do not want all valuables to be instantiated. I would like to have PostgreSQL do the calculation for me.
UPDATE: What I mean is, I want to depend on PostgreSQL's SUM method to get the total sum in the resultset. I thought my effort in using SELECT and GROUP BY made that clear. I don't want any data or record objects from the Valuables table be part of the result, because it consumes too much memory and calculating the fields using Ruby simply uses too much CPU and takes too long.
In raw SQL you want something like this:
SELECT users.*, SUM(valuable.value) AS values_sum
FROM users
LEFT OUTER JOIN valuables ON users.id = valuables.user_id
GROUP BY users.id
So to translate this in a ActiveRecord query would look like this:
User.select('users.*, SUM(valuables.value) AS total_value')
.joins('LEFT OUTER JOIN valuables ON users.id = valuables.user_id')
Note that you are not actually selecting any columns from valuables.
[10] pry(main)> #users = User.select('users.*, SUM(valuables.value) AS total_value').joins('LEFT OUTER JOIN valuables ON users.id = valuables.user_id')
User Load (1.4ms) SELECT users.*, SUM(valuables.value) as total_value FROM "users" LEFT OUTER JOIN valuables ON users.id = valuables.user_id
=> [#<User:0x007f96f7dff6e8
id: 8,
created_at: Fri, 05 Feb 2016 20:36:34 UTC +00:00,
updated_at: Fri, 05 Feb 2016 20:36:34 UTC +00:00>]
[11] pry(main)> #users.map(&:total_value)
=> [6]
[12] pry(main)>
However the "default_scope" and "I still want to be able to use includes" requirements might be a little tall unless you want to manually load the associations.
Method 1:
hash = User.joins(:valuables).group('users.id').sum('valuables.value')
hash.each { |uid, tval| puts "User ID #{uid} has total value #{tval}." }
Note that the hash will only contain entries for users that have valuables. To get all users (even those without valuables), you can use includes instead of joins.
Method 2:
value_hash = Valuable.group(:user_id).sum(:value)
all_user_ids = User.pluck(:id)
all_user_ids.each do |uid|
tval = value_hash[uid] || 0
puts "User ID #{uid} has total value #{tval}."
end
I would go with Method 1 and includes.
Edit: After better understanding the question:
user_columns = User.column_names.join(', ')
users = User.
includes(:valuables).
group('users.id').
select("#{user_columns}, sum(valuables.value) AS total_value")
users.each { |u| puts "User ID #{user.id} has #{u.total_value} total value." }

Finding sum of purchases

Each User has many Purchases. I want to find the sum for each of the previous 28 days.
t1 = Time.now - (28.days)
t2 = Time.now
#dailysum = Array.new(29)
(0..29).each do |i|
#dailysum[i] = #user.purchases.where(:created_at => (Time.now-(i.day))..(Time.now-((i-1).days))).sum(:amount)
end
This works, but I'm certain there is a much better way of going about this. Any suggestions?
You need to pass a :group option to sum. The value of the :group option is different based on your db. I'll provide a pg and mysql version.
# purchases.rb
def self.recent(num)
where('created_at > ?', num.days.ago
end
# PG version
#user.purchases.recent.sum(:amount, group: "DATE_PART('year', purchases.created_at) || '-' || DATE_PART('month', purchases.created_at) || '-' || DATE_PART('day', purchases.created_at)")
# mysql version
#user.purchases.recent.sum(:amount, group: "DATE(purchases.created_at)")
this will result in a hash where the keys is the date and the values are the sum of the purchases.
This will result in 27 less queries compared to querying for each day for the last 28 days
/!\ SEE MY OTHER (BETTER) ANSWER
This answer will calculate the whole sum of purchases of the last 28 days
Read my other answer for the calculation of the sum of each last 28 days.
I keep this one online for those who can be interested by it.
#user.purchases
.where('created_at > ?', 28.days.ago) # retrieve all purchases within the last 28 days
.sum(:amount) # get the sum of the amount column
If you want to use a scope, as proposed by #andy :
# in purchase.rb
scope :recent, ->(d) { where('created_at > ?', d.days.ago) }
# then in you controller
#user.purchases
.recent(28) # retrieve all purchases within the last 28 days
.sum(:amount) # get the sum of the amount column
Now that I've better understood the question, here is a second try.
It's hard to do this out of context.
user.purchases
.where('created_at > ?', 28.days.ago)
.group_by { |p| p.created_at.to_date }
.collect { |d, ps| [d, ps.sum { |p| p.amount }] }
That should return an array of arrays containing each the date and the amount sum for this date :
[
["Mon, 28 Jan 2013", 137],
["Tue, 29 Jan 2013", 49]
["Wed, 30 Jan 2013", 237]
]

find_by_sql in Rails, accessing the resulting array

I'm trying to run a query in a very quick and dirty way in Rails, without putting the rest of the model in place. I know this is bad practice but I just need a quick result in a tight timeframe until I've got the whole solution in place.
I've got items that have a shipping price, based on weight. The weight is stored in the item, the price is stored in the table shipping_zone_prices, and all I currently do is look for the price relating to the first row where the weight is heavier than the item for sale:
class Item < ActiveRecord::Base
def shipping_price
item_id = self.id
shipping_price = ShippingZonePrice.find_by_sql(
"SELECT z.price as price
FROM shipping_zone_prices z, items i
WHERE i.id = '#{item_id}'
AND z.weight_g > d.weight
ORDER BY z.weight_g asc limit 1")
end
end
This sort of works. The SQL does the job, but when plugged into the app as follows:
<%= #item.shipping_price %> Shipping
I get the following displayed:
[#<ShippingZonePrice price: 12>] Shipping
In this example, '12' is the price that is being pulled from the db, and is correct. #item.shipping_price.class returns 'Array'. Trying to access the array using [0] (or any other integer) returns a blank.
Is there another way to access this, or am I missing something fundamental?
Since you are defining an instance method, I think it should return the price if it exists or nil
Try something like this:
def shipping_price
ShippingZonePrice.find_by_sql(
"SELECT z.price as price
FROM shipping_zone_prices z, items i
WHERE i.id = '#{self.id}'
AND z.weight_g > d.weight
ORDER BY z.weight_g asc limit 1").first.try(:price)
end
Then this should work for you:
#item.shipping_price
The first.try(:price) part is needed because find_by_sql may return an empty array. If you tried to do something like first.price on an empty array, you would get an exception along the lines of NoMethodError: undefined method 'price' for nil:NilClass.
This is because find_by_sql returns a model, not data. If you want to do a direct fetch of the data in question, use something like this:
ShippingZonePrice.connection.select_value(query)
There are a number of direct-access utility methods available through connection that can fetch single values, a singular array, rows of arrays, or rows of hashes. Look at the documentation for ActiveRecord::ConnectionAdapters::DatabaseStatements.
As when writing an SQL directly, you should be very careful to not create SQL injection bugs. This is why it is usually best to encapsulate this method somewhere safe. Example:
class ShippingZonePrice < ActiveRecord::Base
def self.price_for_item(item)
self.connection.select_value(
self.sanitize_sql(
%Q[
SELECT z.price as price
FROM shipping_zone_prices z, items i
WHERE i.id=?
AND z.weight_g > d.weight
ORDER BY z.weight_g asc limit 1
],
item.id
)
)
end
end
#item.shipping_price.first.price
or
#item.shipping_price[0].price
Thanks Atastor for pointing that out!
When you use AS price in find_by_sql, price becomes a property of the result.
If not for you saying that you tried and failed accessing [0] i'ld say you want to put
#item.shipping_price.first.price # I guess BSeven just forgot the .first. in his solution
into the view...strange
So, I had a hacky solution for this, but it works great.
Create a table that has the same output as your function and reference it, then just call a function that does a find_by_sql to populate the model.
Create a dummy table:
CREATE TABLE report.compliance_year (
id BIGSERIAL,
year TIMESTAMP,
compliance NUMERIC(20,2),
fund_id INT);
Then, create a model that uses the empty table:
class Visualization::ComplianceByYear < ActiveRecord::Base
self.table_name = 'report.compliance_year'
def compliance_by_year(fund_id)
Visualization::ComplianceByYear.find_by_sql(["
SELECT year, compliance, fund_id
FROM report.usp_compliance_year(ARRAY[?])", fund_id])
end
end
In your controller, you can populate it:
def visualizations
#compliancebyyear = Visualization::ComplianceByYear.new()
#compliancefunds = #compliancebyyear.compliance_by_year(current_group.id)
binding.pry
end
Then, you can see it populate with what you need:
[1] pry(#<Thing::ThingCustomController>)> #compliancefunds
[
[0] #<Visualization::ComplianceByYear:0x00000008f78458> {
:year => Mon, 31 Dec 2012 19:00:00 EST -05:00,
:compliance => 0.93,
:fund_id => 1
},
[1] #<Visualization::ComplianceByYear:0x0000000a616a70> {
:year => Tue, 31 Dec 2013 19:00:00 EST -05:00,
:compliance => 0.93,
:fund_id => 4129
},
[2] #<Visualization::ComplianceByYear:0x0000000a6162c8> {
:year => Wed, 31 Dec 2014 19:00:00 EST -05:00,
:compliance => 0.93,
:fund_id => 4129
}
]

Resources