How do I do a grouping by year? - ruby-on-rails

I have a books model with a date type column named publish_date. On my views I'm iterating through the books and I want to group the books by year such that I have a heading for every year and books that were published on that year to be listed below the year heading.
So by starting with "2010" all books published on 2010 would be listed, then another heading "2009" with all books published in 2009 listed below it and so forth.
<% #all_books.each do |book| %>
<%=link_to book.title + ", (PDF, " + get_file_size(book.size) + ")" %>
<% end %>
By doing a book.publish_date.strftime("%Y") I am able to get the year but I do not know how to group the entries by year. Any help on this would be appreciated.

You can use group_by (see API) like (of the top of my head
<% #all_books.group_by(&:year).each do |year, book| %>
...
<% end %>

def year
self.created_at.strftime('%Y')
end
< % #all_books.group_by(&:year).each do |year, book| %>
Year < %= year %>
# render books here
< % end %>
What say?

You can use group_by for convenience, but your need can be better served by relying on DB for sorting and a each loop. This avoids the cost of client side sorting and hash manipulations for grouping.
Somewhere in your controller
#all_books = Book.all(:order => "publish_date DESC")
In your view
<%year = nil
#all_books.each do |book|
if year.nil? or year > book.publish_date.year
year = book.publish_date.year
%>
<h1> <%=year%><h1>
<%end % >
<%=link_to book.title + ", (PDF, " + get_file_size(book.size) + ")" %>
<%end %>

The quick and dirty approach is to simply group_by the year and iterate over those:
#all_books.group_by { |b| b.created_at.year }.each do |year, books|
# All books for one year, so put heading here
books.each do |book|
# ...
end
end
This requires sorting within the Rails application, so you will need to retrieve all relevant records in order to have the data properly organized. To do the sort on the server you will probably need to introduce a year column and keep it in sync with the created_at time.

Related

Filling in blanks for DATE_TRUNC grouping with rails

The following query to the postgres database
#interventos_2 = Intervento.where(['previsto >= ?', start_date]).group("DATE_TRUNC('week', previsto)").count
generates a hash from which the view can extract the data as follows
<% #interventos_2.each do |w| %>
<%= w[0].strftime('%Y-%W') %> <%= w[1] %><br />
<% end %>
However if there is a blank (count = 0) in the range of weeks we are concerned with, the sequence of years and commercial weeks will look weird and or misleading.
What is an efficient way to declare the range and then fill in the blank weeks with zero?
Update the query is being run via specific sql for performance reasons as the data set is expected to be sufficiently large, frequently changing (cache may not help all that much) and frequently asked.
If I understand you correctly, you want to present a full range of YYYY-WW labels and counts, starting with start_date regardless of whether there's data for a given week. You didn't mention if previsto is a Date, a Time, or a DateTime; I'll assume it's a Time just for maximum inconvenience. =]
I think the main challenge you're struggling with is that you're conflating the dataset with the presentation. I like handling the two separately: first, get the data and put it into a year-week format; then present the range.
For getting the data, I like a more Railsy, less database-specific solution. Your mileage may vary, especially if you've got a large dataset and/or need to make the database do the heavy lifting. Here's a query that gets only the previsto field for each record while also forcing the database to evaluate the date range. Probably the most concise query without having to break out SQL:
#interventos_2 = Intervento.select(:previsto).
where(previsto: (start_date..Time.now)).
map {|iv| iv.previsto.strftime('%Y-%W')}
Note that this also maps the result down to a simple array of YYYY-WW. Speaking of which, let's map out that range of YYYY-WW now:
# make sure the end_date a clean multiple of 7 days from start_date
end_date = Date.today + (7 - (Date.today - start_date.to_date) % 7)
#timespan = (start_date.to_date..end_date).step(7).map {|date| date.strftime('%Y-%W')}
(There are probably much tidier ways to write that)
Given those bits, here's a version of your view code that presents the full range of weeks and the count for that week, even if it's 0:
<% #timespan.each do |yearweek| %>
<%= yearweek %> <%= #interventos_2.count(yearweek) %><br />
<% end %>
In bocca al lupo!
Update: Your update notes that your use case requires the direct SQL query, so here's the same general approach with that in mind:
#interventos_2 = Intervento.where(['previsto >= ?', start_date]).
group("DATE_TRUNC('week', previsto)").count.
map {|timestamp,count| [timestamp.strftime('%Y-%W'), count]}.to_h
# make sure the end_date a clean multiple of 7 days from start_date
end_date = Date.today + (7 - (Date.today - start_date.to_date) % 7)
#timespan = (start_date.to_date..end_date).step(7).map {|date| date.strftime('%Y-%W')}
<% #timespan.each do |yearweek| %>
<%= yearweek %> <%= #interventos_2[yearweek] || 0 %><br />
<% end %>
With the following controller method, the range of weeks and the data required for the view is generated (beginning_of_week for comparing with the has data, in addition to the data itself).
#weeks = []
while start_date < final_date
#weeks[start_date.year] = [] unless #weeks[start_date.year]
#weeks << [start_date.beginning_of_week, start_date.cweek]
start_date += 1.week
end
#weeks.reject!{|a| a.blank?}
Thus the view can generate a layout item for each week and compare it to the hash, and where there is nil, generate zero.
<% #weeks.each do |week| %>
<% if !week.nil? %>
<%= week[0] %>:
<% z = #interventos_2.detect {|f| f[0] == week[0] } %>
<% if !z.nil? %>
<%= z.to_a[1] %>
<% else %>
0
<% end %>
<% end %>

How to get Date only on a DateTime attribute? working on a calendar

I am working with a Calendar and I think that calendar deals only with date without a time.
Note: requested_date is a DateTime Attribute. I want to get all reservations with requested date and covert it to date only:
users_controller.rb
def myprofile
#reservation = Reservation.all
#resv_by_date = #reservation.group_by(&:requested_date) <-- make requested_date to date only
end
myprofile.html.erb
<div id="admin">
<%= calendar do |date| %>
<%= date.day %><br>
<% #resv_by_date.each do |reservation| %>
<%= reservation.requested_date.to_date %><br>
<% end %>
<% end %>
</div>
(this month 'May') requested_date is existing on my database
image output after the solution of the 1st answer
I don't think you want to use a group on your ActiveRecord relation not group_by which is a Ruby method.
Also, the method to get a Date from a DateTime object is datetime.to_date:
#reservations_by_date = Reservation.select(:requested_date).group(:requested_date)
#reservations_by_date.each { |reservation| puts reservation.requested_date.to_date }
Normally I'd advice you to try to group the data in the DB already (using group) for performance reasons instead of ruby code (group_by). But in your specific case it seems that you indeed need to retrieve all reservations in the given time period (e.g. month) and just group them by the date so that you can display all of them in the calendar view. In that case, you indeed have to group in ruby code.
You can do that simply by updating the block in group_by:
#reservations_by_date = Reservation.all.group_by { |res| res.requested_date.to_date }
Note that most probably you'll want to narrow down the select only to the given time period (e.g. the displayed month) first, using where conditions, I skipped that part from this answer.
The grouping above will create a hash of the reservations where the keys will be the dates (not datetimes) and the values will be arrays of reservations in the given date. In the calendar view, you can simply browse once you've accessed them using the date key:
<% calendar do |date| %>
<%= date.day %><br>
<% #reservations_by_date[date].each do |reservation| %>
<%= reservation.name ... or whatever %><br>
<% end %>

Printing Specific Data From Table Using Ruby

This is my first time posting a question on this site; be gentle, please. This isn't homework. I'll try to be as concise as possible.
I have a table with 5 keyword columns, a date column, and a user ID column for identifying what user added that specific row of data. My goal is to print and count the number of matched keywords from a row iff:
1) Another row(s) contains those keywords AND has the same date.
2) The user ID for each row is unique.
For example:
Row1=> keyword1:(K1) keyword2:(K2) keyword3:(K3) keyword4:(K4) keyword5:(K5) date:(D1) user_id:(U1)
Row2=> keyword1:(K6) keyword2:(K7) keyword3:(K1) keyword4:(K2) keyword5:(K8) date:(D1) user_id:(U2)
Row3=> keyword1:(K6) keyword2:(K7) keyword3:(K1) keyword4:(K2) keyword5:(K8) date:(D2) user_id:(U2)
Row4=> keyword1:(K1) keyword2:(K2) keyword3:(K3) keyword4:(K4) keyword5:(K5) date:(D2) user_id:(U3)
Output:
K1 (2 times), K2 (2 times), on D1
K1 (2 times), K2 (2 times), on D2
Row3 should be excluded from the first count because even though the words matched, the user was a duplicate.
Here's how I've started:
<% #prophecies.each do |prophecy| %>
<% date1 = prophecy.datetwo %>
<% #prophecies.each do |prophecy| %>
<% if date1.eql?(prophecy.datetwo) == true %>
<tr>
<td><%= prophecy.keyone %></td>
<td><%= prophecy.keytwo %></td>
<td><%= prophecy.keythree %></td>
<td><%= prophecy.keyfour %></td>
<td><%= prophecy.keyfive %></td>
</tr>
<% end %>
<% end %>
<% end %>
But it's nowhere near what I'd like to accomplish. If anyone could even help me sort out the pseudocode for this I'd be happy.
First of all, it's better to do it in your model, not in your view.
You can define methods in your prophecy class something like that
def self.keywords_at_day(date)
#find prophecies for particular dae
prophecies = self.find_by(date:date)
# create hash with keywords for user
user_keywords = {}
prophecies.each do |prophecy|
user_keywords[prophecy.user] ||= []
user_keywords[prophecy.user] << prophecy.keyword
end
# create general array with all keywords
keywords = []
user_keywords.each do |user, kw|
keywords << kw.uniq
end
# count keywords
keywords_count = {}
keywords.each do |keyword|
keywords_count[keyword] ||= 0
keywords_count[keyword] += 1
end
keywords_count
end
def self.keywoards_lists
dates = Prophecy.all.map{|p| p.date}.uniq
keywords_lists = {}
dates.each do |date|
keywords_lists[date] = keywords_at_day(date)
end
keywords_lists
end
You controller
def index
#keywords_lists = Prophecy.keywords_lists
end
View
<%= #keywords_lists.each do |date, keywords_list| %>
<tr>
<td> <%= date %> </td>
<td>
<%= keywords_list.each do |keyword, count| %>
<%= "#{keyword} encountered #{count} times" %>
<% end %>
</td>
</tr>
<% end %>
If it's really not homework, I suggest you to read about MVC, ruby name convention(date_two instead of datetwo), booleans (you don't need "==true" for eql?).
And besides all it's not good type of question for stackoverflow
Cheers
My suggestion is to create a data structure that match your need. BAsically a hash where key is keyword, and content the list of uid that uniquely use your keyword:
r=[]<<{:keyword1=>"K1",keyword2:"K2",keyword3:"K3",keyword4:"k4",:date=>d1,uid:1}
r<<{:keyword1=>"K6",keyword2:"K7",keyword3:"K1",keyword4:"k2",:date=>d1,uid:2}
r<<{:keyword1=>"K6",keyword2:"K7",keyword3:"K1",keyword4:"k2",:date=>d2,uid:2}
(just for test purposes)
then:
r.inject({}) do |sum,aRow|
# loop for all keywords in the row.
[:keyword1,:keyword2,:keyword3,:keyword4,:keyword4].each do |keyword|
# get each entry or create it
sum[aRow[keyword]]=elem=sum[aRow[keyword]]||{}
count=elem[aRow[:date]]||{:users=>Set.new}
count[:users]<<aRow[:uid]
elem[aRow[:date]]=count
end
sum
end
The result if the following:
=> {"K1"=>
{2014-09-04 15:05:21 +0200=>{:users=>#<Set: {1, 2}>},
2014-01-01 00:00:00 +0100=>{:users=>#<Set: {2}>}},
"K2"=>{2014-09-04 15:05:21 +0200=>{:users=>#<Set: {1}>}},
"K3"=>{2014-09-04 15:05:21 +0200=>{:users=>#<Set: {1}>}},
"k4"=>{2014-09-04 15:05:21 +0200=>{:users=>#<Set: {1}>}},
"K6"=>
{2014-01-01 00:00:00 +0100=>{:users=>#<Set: {2}>},
2014-09-04 15:05:21 +0200=>{:users=>#<Set: {2}>}},
"K7"=>
{2014-01-01 00:00:00 +0100=>{:users=>#<Set: {2}>},
2014-09-04 15:05:21 +0200=>{:users=>#<Set: {2}>}},
"k2"=>
{2014-01-01 00:00:00 +0100=>{:users=>#<Set: {2}>},
2014-09-04 15:05:21 +0200=>{:users=>#<Set: {2}>}}
So you know that keyword "K1" has been used with two different date, and by two users (1,2) for first one, and 2 for second date.
Than, it will be easy to display this array.

Is this an efficient way of producing sales reporting / analytics?

I have an app where I receive & ingest daily sales reports from multiple sources. All are structured differently so I store down into my Postgres db in separate tables.
I'm currently doing something like this to iterate over the last 30 days sales for one report source and it seems to work quite nicely. My concern is how efficient & scaleable this will be when I add additional report sources as the way I currently have it structured means I'd have to add and repeat large amounts of the code for each new source.
<% from = Date.today - 30 %> #30 days ago
<% to = Date.today %> #Today
<% step_date = from %>
<% source_one_chart_data = [] %> #Initialise empty array to later pass to JS Chart library
<% begin %>
<% count = #product.sales_source_one.total.where(:report_date => step_date).count %> #check if there are any sales for this product on the current step date
<% if count != 0 %>
<% sale = #product.sum_total_net_by_day(step_date) %>
<% source_one_chart_data.push(sale.to_s) %> #Push sales total to array if sales exist on that date
<% else %>
<% source_one_chart_data.push("0") %> #Otherwise push a zero into array so all 30 days map to a value
<% end %>
<% step_date += 1.day %> #Increase step_date by 1 on each iteration of the loop
<% end while step_date <= to %> #Stop loop when we reach to date
Can anyone offer any guidance on how efficiently bring in additional sales sources without having to repeat code? Also, it would be good if I could change the step from day to week/month/year and have it calculate sales accordingly; so if the sales source is reported daily and the step is week it would sum all values that occur in the step week.
Why do you have all of that code in your view? You should move most of it to your model / controller.
def process_chart_data
from = 1.month.ago.to_date
to = Date.today
step_date = from
chart_data = []
while step_date <= to
sales_total = sales_source_one.total.where(report_date: step_date).count
if sales_total.zero?
chart_data.push(0)
else
sale = sum_total_net_by_day(step_date)
chart_data.push(sale.to_s)
end
step_date += 1
end
return chart_data
end
The above could probably be refactored further, but now if you place it in your Product model then in your controller you can just do:
#product = Product.find(params[:id]) # or whatever
#chart_data = #product.process_chart_data
Then in your view you can use #chart_data.
Moving that code into a method also allows you to update it quicker, lets say I want the user to control how far back records are retrieved:
def process_chart_data(start_date)
from = start_date
...
end
In the controller:
#product = Product.find(params[:id])
#chart_data = #product.process_chart_data(params[:start_date])

Rails. Sum a specific attribute on a collection

I have a list of invoices...
#invoices_1_week = Invoice.order("due_date DESC").where("status != 'paid' AND due_date >= ? AND due_date < ?", Date.today, 1.week.from_now)
The invoices model has a total attribute. How can I get a sum of the totals in the #invoice_1_week collection?
I know I can do it in the view like this...
<% week_1_total = 0 %>
<% #invoices_1_week.each do |invoice| %>
<% week_1_total = week_1_total + invoice.total %>
<% end %>
<%= week_1_total %>
But I'm wondering if there is a more Railsy way of doing it.
Here's a Rails way, using ActiveRecord's sum method:
#invoices_1_week.sum("total")
Here are the docs.
You might want to consider using the symbol notation
#invoices_1_week.sum(:total)
or use single quotes
#invoices_1_week.sum('total')
In both cases, the attribute name is immutable.

Resources