Ruby on Rails time helper - ruby-on-rails

I'm working on my first Rails Application. I am a little stuck with the time. I'm working on a recipe application. I need to add two fields.
Preparation Time
Cook Time
Out of the two, i would like to add the two fields to come up with the Total Time needed to prepare the meal.
I approached it the wrong way which doesn't have logic :(. Basically i have two fields and i used f.select to select predefined times. But the problem i have with that approach is that when adding the two, it ignores the Gregorian format e.g 40 minutes + 50 mins will become 90 Min instead of 1hour 30.
I would appreciate any help from the community.

A quick example:
prep_time = 40.minutes
cook_time = 50.minutes
total_time = prep_time + cook_time
formatted_total_time = Time.at(total_time).gmtime.strftime('%I:%M')
# outputs 01:30 which is HOURS:MINUTES format
If you wanted 90 minutes instead:
formatted_total_time = total_time / 60
# outputs 90
Update:
Put this in the helper file associated with whatever view you are using this in (i.e. app/helpers/recipes_helper.rb)
module RecipesHelper
def convert_to_gregorian_time(prep_time, cook_time)
# returns as 90 mins instead of 1hr30mins
return (prep_time + cook_time) / 60
end
end
Then you'd just call it in your view (i.e. app/views/recipes/show.html.haml like:
# Note: this is HAML code... but ERB should be similar
%p.cooking_time
= convert_to_gregorian_time(#recipe.prep_time, #recipe.cook_time)
If you are storing the times in the database as integers (which you SHOULD be doing), then you can do this:
%p.cooking_time
= convert_to_gregorian_time(#recipe.prep_time.minutes, #recipe.cook_time.minutes)
where #recipe.prep_time is an integer with a value of 40 and #recipe.cook_time is an integer with a value of 50
and your database schema would look something like:
# == Schema Information
#
# Table name: recipes
#
# id :integer not null, primary key
# prep_time :integer
# cook_time :integer
# # other fields in the model...

Related

Rails 5 and PostgreSQL 'Interval' data type

Does Rails really not properly support PostgreSQL's interval data type?
I had to use this Stack Overflow answer from 2013 to create an interval column, and now it looks like I'll need to use this piece of code from 2013 to get ActiveRecord to treat the interval as something other than a string.
Is that how it is? Am I better off just using an integer data type to represent the number of minutes instead?
From Rails 5.1, you can use postgres 'Interval' Data Type, so you can do things like this in a migration:
add_column :your_table, :new_column, :interval, default: "2 weeks"
Although ActiveRecord only treat interval as string, but if you set the IntervalStyle to iso_8601 in your postgresql database, it will display the interval in iso8601 style: 2 weeks => P14D
execute "ALTER DATABASE your_database SET IntervalStyle = 'iso_8601'"
You can then directly parse the column to a ActiveSupport::Duration
In your model.rb
def new_column
ActiveSupport::Duration.parse self[:new_column]
end
More infomation of ISO8601 intervals can be find at https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
I had a similar issue and went with defining reader method for the particular column on the ActiveRecord model. Like this:
class DivingResults < ActiveRecord::Base
# This overrides the same method for db column, generated by rails
def underwater_duration
interval_from_db = super
time_parts = interval_from_db.split(':')
if time_parts.size > 1 # Handle formats like 17:04:41.478432
units = %i(hours minutes seconds)
in_seconds = time_parts
.map.with_index { |t,i| t.to_i.public_send(units[i]) }
.reduce(&:+) # Turn each part to seconds and then sum
ActiveSupport::Duration.build in_seconds
else # Handle formats in seconds
ActiveSupport::Duration.build(interval_from_db.to_i)
end
end
end
This allows to use ActiveSupport::Duration instance elsewhere. Hopefully Rails will start handling the PostgreSQL interval data type automatically in near future.
A more complete and integrated solution is available in Rails 6.1
The current answers suggest overriding readers and writers in the models. I took the alter database suggestion and built a gem for ISO8601 intervals, ar_interval.
It provides a simple ActiveRecord::Type that deals with the serialization and casting of ISO8601 strings for you!
The tests include examples for how to use it.
If there is interest, the additional formats Sam Soffes demonstrates could be included in the tests
Similar to Madis' solution, this one handles fractions of a second and ISO8601 durations:
def duration
return nil unless (value = super)
# Handle ISO8601 duration
return ActiveSupport::Duration.parse(value) if value.start_with?('P')
time_parts = value.split(':')
if time_parts.size > 1
# Handle formats like 17:04:41.478432
units = %i[hours minutes seconds]
in_seconds = time_parts.map.with_index { |t, i| t.to_f.public_send(units[i]) }.reduce(&:+)
ActiveSupport::Duration.build in_seconds
else
# Handle formats in seconds
ActiveSupport::Duration.build(value)
end
end
def duration=(value)
unless value.is_a?(String)
value = ActiveSupport::Duration.build(value).iso8601
end
self[:duration] = value
end
This assumes you setup your database like Leo mentions in his answer. No idea why sometimes they come back from Postgres in the PT42S format and sometimes in the 00:00:42.000 format :/

Ruby, difference between time an array was created and the current time

I am trying to get the difference between the hours in the time an array was created and the current time, i cant seem to get my head around it, I did this, which seems perfectly logical, but it doesnt seem to ever work.
mergelisting = MergeList.Pluck(:created_at, :partner, :gambler, :amount)
(mergelisting).each do |i|
t = Time.now
nowtime = t.strftime("%I")
nowtime.to_i
mergetime = i[1].strftime("%I")
mergetime.to_i
if nowtime - mergetime == 6
do some stuff...
end
end
My real question is the difference in hours between 24 and 6 is 6 hours, but 24-6 would not give 6. What can i do here
I'm assuming MergeList.Pluck returns an Array, and the first item in this array will be a Rails created_at column from the database. Then:
mergelisting = MergeList.Pluck(:created_at, :partner, :gambler, :amount)
mergetime = mergelisting.first # Time from DB (created_at)
nowtime = Time.now # Current time
timediff_s = nowtime - mergetime # Difference in seconds
timediff_h = timediff_s / (60 * 60).to_f # Difference in hours as a floating point number
#
# Not exactly sure if this is what you wanted, but you now
# have the time difference as a floating point number expressed
# in hours. So you can do whatever you want with it
#
if (timediff_h.to_i == 6)
...the difference is anywhere between 6.0 and 7.0 hours...
...do whatever you need to do...
end

Group by decade with counts

I have a table of Album's that has a date column named release_date.
I want to get a list of all the decades along with the number of albums released in that decade.
So, the output might be something like:
2010 - 11
2000 - 4
1990 - 19
1940 - 2
Ruby 2.3.1 w/ Rails 5 on Postgres 9.6, FWIW.
This is essentially a followup question to a previous one I had: Group by month+year with counts
Which may help with the solution...I'm just not sure how to do the grouping by decade.
Using Ruby for processing db data is inefficient in all senses.
I would suggest doing it on the database level:
Album.group("(DATE_PART('year', release_date)::int / 10) * 10").count
What happens here, is basically you take a year part of the release_date, cast it to integer, take it's decade and count albums for this group.
Say, we have a release_date of "2016-11-13T08:30:03+02:00":
2016 / 10 * 10
#=> 2010
Yes, this is pretty similar to your earlier question. In this case, instead of creating month/year combinations and using the combinations as your grouping criteria, you need a method that returns the decade base year from the album year.
Since you have a pattern developing, think about writing the code so it can be reused.
def album_decades
Album.all.map { |album| album.release_date.year / 10 * 10 }
end
def count_each(array)
array.each_with_object(Hash.new(0)) { |element, counts| counts[element] += 1 }
end
Now you can call count_each(album_decades) for the result you want. See if you can write a method album_months_and_years that will produce the result you want from your earlier question by calling count_each(album_months_and_years).
There are more than one possible solution to your problem, but I would try:
Add a new column to the Album table, called decade. You can use a migration for this porpoise.
Create a callback (its like a trigger, but in the programmer side) that set the decade value before saving the Album in the DB.
Finally you can use this useful query to group the Albums by decade. In your case would be Album.group(:decade).count wich would give you a hash with the numbers of Albums by decade.
...
Profit ?
Jokes aside, the callback should be something like:
class Album < ActiveRecord::Base
# some code ...
before_save :set_decade # this is the 'callback'
# ...
private
def set_decade
self.decade = self.release_date.year / 10
end
Then, if you use the step 3, it would return something like:
# => { '195' => 7, '200' => 12 }
I did not test the answer, so try it out and tell me how it went.

Rails Scoping a payroll period

I've built a Rails app that logs employee time (clockin/clockout) and calculates total hours, allows exports to CSV/PDF, search timecards based on dates, etc.
What I'm really wanting to do is to implement payroll periods via a scope of some sort of a method.
The payroll period begins on a Sunday and ends on a Saturday 14 days later. What would be the best way to write a scope like this? Also is it possible to split the weeks into two for the payroll period?
I wrote these scopes but they are flawed:
scope :payroll_week_1, -> {
start = Time.zone.now.beginning_of_week - 1.day
ending = Time.zone.now.end_of_week - 1.day
where(clock_in: start..ending)
}
scope :payroll_week_2, -> {
start = Time.zone.now.end_of_week.beginning_of_day
ending = Time.zone.now.end_of_week.end_of_day + 6.days
where(clock_in: start..ending)
}
These works if you are currently in a payroll period, but once you pass the end of the week, the scopes no longer work because I'm basing my timing off of Time.zone.now
Is there any way to actually do this? Even if I have to set some sort of static scope or value which says April 10 - 23 is payroll period 1, etc etc. I'm really not sure how to approach this problem and what might work. So far what I've written works in the current pay period but as time advances the scope drifts.
Any help would be greatly appreciated.
I think what you want is to create a scope, which can receive a start_day as a parameter:
scope :payroll_week_starting, -> (start_day) {
where(clock_in: start_day..(start_day + 1.week))
}
Then, in the future you will be able to call your scope with the first day of your pay period:
ModelName.payroll_week_starting(Date.parse('31/12/2015'))
UPDATE:
As per your comment, it seems that you're looking for a bit more information from an architectural perspective. It's pretty tough to help you without understanding your database architecture, so I'm just going to go from a high level.
Let's assume you have an Employee model and a Shift model with the clock_in and clock_out fields. You may also want a model called PayPeriod with the fields start_date and end_date.
Each Employee has_many :shifts and each PayPeriod has_many :shifts
You might add a couple of class methods on PayPeriod, so you can find and/or create the PayPeriod for any given datetime.
def self.for(time)
find_by("start_date < ? AND end_date > ?", time, time)
end
def self.create_for(time)
# yday is the number of days into the current year
period_start_yday = 14 * (time.yday / 14)
start_date = Date.new(time.year) + period_start_yday.days
next_year = Date.new(time.year) + 1.year
create(
start_date: start_date,
end_date: [start_date + 14.days, last_day_of_year].min,
)
end
def self.find_or_create_for(time)
for(time) || create_for(time)
end
The create_for logic is pretty complicated, but an example will help you understand:
Say I clocked in today May 17th, 2016, the yday for today is 138, if you use integer division (default for ruby) to divide by 14 (the length of your pay periods), you'll get 9. By multiplying that by 14 again, you'll get 126, which is the most recent yday divisible by 14. If you add that number of days to the beginning of this year, you'll get the begining of the PayPeriod. The end of the PayPeriod is 14 days after the start_date, but not rolling over to the next year.
What I would then do, is add a before_save callback to Shift to find or create the corresponding PayPeriod
before_save :associate_pay_period
def associate_pay_period
self.pay_period_id = PayPeriod.find_or_create_for(clock_in)
end
Then every PayPeriod will have a bunch of Shifts, and every Shift will belong to a PayPeriod.
If, for example, you wanted to get all of the shifts for a specific employee during a specific PayPeriod (to perhaps sum the hours worked for that PayPeriod) add a scope to Shift:
scope :for, -> (user) { where(user: user) }
And call pay_period.shifts.for(user)
UPDATE #2:
One other (much simpler) thought I had (if you don't want to create an actual PayPeriod model), would be to just add a method to the model that has clock_in (I'm going to refer to it as Shift):
def pay_period
clock_in.to_date.mjd / 14
end
Which will basically just boil down any clock_in time to an integer that represents a 14 day period. Then you can call
Shift.all.group_by { |shift| shift.pay_period }
If you need each pay_period to be contained within a single calendar year, you can do:
def pay_period
[clock_in.year, clock_in.to_date.yday / 14]
end

How to create/save this database special calculated field - RoR

I have a database field called sample_code. This field is composed on the following way: sample_id/year/lab_id
The lab_id is fixed, always the same. The year changes accordingly to the current year... The sample_id its incremental (resetting every year and not a database field).
What I want to do is every-time I create a new sample it generates this sample_code for me and saves it along with the rest of the sample fields...
My doubts are:
1 - How can I keep incrementing sample_id if it's not on the database?
2 - How can I reset the sample ID code each year?
3 - Where's the best place to put/call this code? Sample's controller?
Thanks for all the help you can give
If you're not using your database outside of your app, it should be fine to just store the sample_id as its own column. You can then put a method on your model that returns something like:
def sample_code
"#{sample_id}/#{Time.now.year}/<lab_id>"
end
Then you can just increment sample_id each time.
EDIT
Since you need to reset the id to 1 each year and the model is called Sample, you should avoid confusion by calling it something like annual_id instead of sample_id. sample_id would likely be confused with sample.id, which is a completely different thing.
With that change, you should just store the info in three columns on the model to make it easy: annual_id, year, and lab_id. Then for each record you can set:
annual_id = Sample.where(year: Time.now.year).pluck(:annual_id).max.to_i + 1
year = Time.now.year
lab_id = <however you are defining this>
This will use the current year for year and then reset the annual_id to 1 when there are no records because the year has changed (new year will give nil.to_i + 1 => 1).
Then you can just return the format you want for any given data point:
def sample_code
"#{annual_id}/#{year}/#{lab_id}"
end

Resources