how to convert integer(months) into human friendly string? - ruby-on-rails

I need to translate a number of months, integer based, into a human friendly string containing information about years. its not easy to explain so I will just provide examples. inputs and outputs I want are:
input: 19
output: "1 year, 7 months"
input: 24
output: "2 years"
input: 26
output: "2 years, 2 months"
do you know any out of the box solutions? if not, how would you implement it yourself?

input = 26
year, month = input.divmod(12)
if month.eql? 0 and year > 1
puts "#{year} years"
elsif month.eql? 0 and year.eql? 1
puts "#{year} year"
elsif year > 1
puts "#{year} years, #{month} month"
else
puts "#{year} year, #{month} month"
end
Output
2 years, 2 month

Since this question is tagged with Ruby on Rails, ActiveSupport extensions are available, so this works too:
number_of_months = 19 # for example
ActiveSupport::Duration.build(number_of_months.months).inspect.gsub(' and',', ')
Edit
I just noticed that there was a bug in ActiveSupport::Duration version 6.0.2.2 that was fixed sometime prior to version 6.1.0.alpha that caused rounding errors for certain values of number_of_months.

Just for fun:
num.divmod(12).then { |y, m| [y, m].zip ['years', 'months'] }
.reject{ |e| e.first.zero? }
.each{ |e| e.last.delete_suffix!('s') if e.first == 1 }
.join(' ')
.tap{ |res| res.replace('0 months') if res.empty? }
Samples:
[11, 12, 13, 23, 24, 25, 26].each { |n| p [n, n.divmod(12).then.......] } # pseudo
[11, "11 months"]
[12, "1 year"]
[13, "1 year 1 month"]
[23, "1 year 11 months"]
[24, "2 years"]
[25, "2 years 1 month"]
[26, "2 years 2 months"]

def pluralize(num, string)
[num, (num == 1 ? string : "#{string}s")] * ' '
end
def humanize_months(months)
months = input % 12
years = input / 12
text = []
text << pluralize(years, 'year') if years > 0
text << pluralize(months, 'month') if months > 0
text * ', '
end

Related

What's a good way to create a string array in Ruby based on integer variables?

The integer variables are:
toonie = 2, loonie = 1, quarter = 1, dime = 0, nickel = 1, penny = 3
I want the final output to be
"2 toonies, 1 loonie, 1 quarter, 1 nickel, 3 pennies"
Is there a way to interpolate this all from Ruby code inside [] array brackets and then add .join(", ")?
Or will I have to declare an empty array first, and then write some Ruby code to add to the array if the integer variable is greater than 0?
I would do something like this:
coins = { toonie: 2, loonie: 1, quarter: 1, dime: 0, nickel: 1, penny: 3 }
coins.map { |k, v| pluralize(v, k) if v > 0 }.compact.join(', ')
#=> "2 toonie, 1 loonie, 1 quarter, 1 nickel, 3 penny"
Note that pluralize is a ActionView::Helpers::TextHelper method. Therefore it is only available in views and helpers.
When you want to use your example outside of views, you might want to use pluralize from ActiveSupport instead - what makes the solution slightly longer:
coins.map { |k, v| "#{v} #{v == 1 ? k : k.pluralize}" if v > 0 }.compact.join(', ')
#=> "2 toonie, 1 loonie, 1 quarter, 1 nickel, 3 penny"
Can be done in rails:
hash = {
"toonie" => 2,
"loonie" => 1,
"quarter" => 1,
"dime" => 0,
"nickel" => 1,
"penny" => 3
}
hash.to_a.map { |ele| "#{ele.last} #{ele.last> 1 ? ele.first.pluralize : ele.first}" }.join(", ")
Basically what you do is convert the hash to an array, which will look like this:
[["toonie", 2], ["loonie", 1], ["quarter", 1], ["dime", 0], ["nickel", 1], ["penny", 3]]
Then you map each element to the function provided, which takes the inner array, takes the numeric value in the last entry, places it in a string and then adds the plural or singular value based on the numeric value you just checked. And finally merge it all together
=> "2 toonies, 1 loonie, 1 quarter, 1 nickel, 3 pennies"
I'm not sure what exactly you're looking for, but I would start with a hash like:
coins = {"toonie" => 2, "loonie" => 1, "quarter" => 1, "dime" => 0, "nickel" => 1, "penny" => 3}
then you can use this to print the counts
def coin_counts(coins)
(coins.keys.select { |coin| coins[coin] > 0}.map {|coin| coins[coin].to_s + " " + coin}).join(", ")
end
If you would like appropriate pluralizing, you can do the following:
include ActionView::Helpers::TextHelper
def coin_counts(coins)
(coins.keys.select { |coin| coins[coin] > 0}.map {|coin| pluralize(coins[coin], coin)}).join(", ")
end
This is just for fun and should not be used in production but you can achieve it like
def run
toonie = 2
loonie = 1
quarter = 1
dime = 0
nickel = 1
penny = 3
Kernel::local_variables.each_with_object([]) { |var, array|
next if eval(var.to_s).to_i.zero?
array << "#{eval(var.to_s)} #{var}"
}.join(', ')
end
run # returns "2 toonie, 1 loonie, 1 quarter, 1 nickel, 3 penny"
The above does not implement the pluralization requirement because it really depends if you will have irregular plural nouns or whatever.
I would go with a hash solution as described in the other answers

Ruby: Calculate time difference between 2 times

I want to calculate the difference between 2 times.
start_time: 22:00 (Rails interprets this as 2015-12-31 22:00:00 +0100)
second_time: 02:00 (Rails interprets this as 2015-12-31 02:00:00 +0100). The second time is 4 hours later, so in the next day. Is there a way to calculate this difference?
I can not simply do this: second_time - first_time, because this gives me a difference of 22 hours instead of 4 hours.
Edit:
Some background information:
A job is starting at 22:00 and ending the next day at 02:00. Because i fill in the form of this job only times, this times for the above 2 values are 2015-12-31 22:00:00 +0100 and 2015-12-31 02:00:00 +0100. I don't want the user to fill in the time including the date. The real difference between the times should be 4 hours.
So what i actually want is calculate the difference between 22:00 and 02:00 (in the next day).
I don't understand why you think it should return 4 hours or why it does return 22 hours. 20 hours would be correct for your example:
require 'time'
a = Time.parse('2015-12-31 22:00:00 +0100')
b = Time.parse('2015-12-31 02:00:00 +0100')
a - b
#=> 72000.0 # difference in seconds
(a - b) / 3600
#=> 20.0 # difference in hours
Update: It seems like you are dealing only with the time portion and not with the actual date. And I assume the maximum difference you will have to deal with is 24 hours:
def time_difference(time_a, time_b)
difference = time_b - time_a
if difference > 0
difference
else
24 * 3600 + difference
end
end
a = Time.parse('2015-12-31 22:00:00 +0100')
b = Time.parse('2015-12-31 02:00:00 +0100')
time_difference(a, b) / 3600
# => 4 # hours
a = Time.parse('2015-12-31 02:00:00 +0100')
b = Time.parse('2015-12-31 22:00:00 +0100')
time_difference(a, b) / 3600
# => 20 # hours
Old question but I did a nice method to deal with it:
def time(start,ending)
if start != ending
medidas=["year","month","day","hour","minute","second"]
array=[1970,1,1,0,0,0]
text = ""
Time.at(ending-start).utc.to_a.take(6).reverse.each_with_index do |k,i|
text = "#{text} #{I18n.translate medidas[i].to_sym, count: k-array[i]}"
end
text = text.strip.squish
pos = text.rindex(" ",(text.rindex(" ")-1))
unless pos.nil?
text = text.insert(pos," and")
end
text = text.strip.squish #This shouldn't be needed but just in case
else
"0 seconds"
end
end
Then in config/locales/en.yml I added:
en:
año:
zero: ''
one: '1 year'
other: '%{count} years'
mes:
zero: ''
one: '1 month'
other: '%{count} months'
dia:
zero: ''
one: '1 day'
other: '%{count} days'
hora:
zero: ''
one: '1 hour'
other: '%{count} hours'
minuto:
zero: ''
one: '1 minute'
other: '%{count} minutes'
segundo:
zero: ''
one: '1 second'
other: '%{count} seconds'
So for example when you call:
start = Time.now
ending = start + (60*60)
time(start,ending)
=> "1 hour"
ending = start + (60*60*28)
time(start,ending)
=> "1 day and 4 hours"
ending = start + (53*60*5874)
time(start,ending)
=> "7 months 4 days 4 hours and 42 minutes"
Hope it's useful
I'd write it thusly (before adding data checks), in an attempt to make it self-documenting:
require 'time'
DT_FMT = '%Y-%m-%d %H:%M:%S %z'
SECONDS_PER_DAY = 24*60*60
def hours_elapsed(start_str, finish_str)
start = DateTime.strptime(start_str, DT_FMT).to_time
finish = DateTime.strptime(finish_str, DT_FMT).to_time
finish = same_time_tomorrow(finish) if finish < start
(finish-start)/3600
end
def same_time_tomorrow(time)
time + SECONDS_PER_DAY
end
hours_elapsed '2015-12-31 22:00:00 +0100',
'2015-12-31 02:00:00 +0100'
#=> 4.0
hours_elapsed '2015-12-31 02:00:00 +0100',
'2015-12-31 22:00:00 +0100'
#=> 20.0
It may be better for the arguments of hours_elapsed to be strings containing hours and minutes only, in which case we might rename the method as well. time_elapsed("18:00", "2:30") is an example of how this method might be invoked.
MINUTES_PER_DAY = 24*60
def time_elapsed(start_str, finish_str)
start_mins = time_str_to_minutes(start_str)
finish_mins = time_str_to_minutes(finish_str)
finish_mins += MINUTES_PER_DAY if
finish_mins < start_mins
(finish_mins-start_mins).divmod(60)
end
def time_str_to_minutes(str)
hrs, mins = str.split(':').map(&:to_i)
60 * hrs + mins
end
time_elapsed("8:00", "17:30")
#=> [9, 30]
time_elapsed("18:00", "2:30")
#=> [8, 30]

Extracting numbers from a string

Consider i have a string like this:
"1 hour 7 mins"
I need to extract number of hour (1) and min (7). the problem is either hour or mins can be nill so in this case the string would be 1 hour ot just 7 mins
I am mostly interested in regular expression. I have already seen this and run this code
result = duration.gsub(/[^\d]/, '')
result[0]!= nil ? hour=result[0] : hour=0
result[1]!=nil ? mins=result[1] : mins=0
the problem is, when i have only 5 mins it gives me 5 and i do not know it is mins or hour
So how can i do it?
What do you think about something like this:
hours = duration.match(/[\d]* hour/).to_s.gsub(/[^\d]/, '')
minutes = duration.match(/[\d]* mins/).to_s.gsub(/[^\d]/, '')
You could do that :
a = duration[/(\d*)(\s*hour)?s?\s*(\d*)(\s*min)?s?/][0]
if a.include?("hour")
hour = a[0]
min = a[2]
else
min = a[0]
end
Improved, this is what I wanted :
capture = duration.match(/^((\d*) ?hour)?s? ?((\d*) ?min)?s?/)
hour = capture[2]
min = capture[4]
You can try the regex here :
http://rubular.com/r/ACwfzUIHBo
I couldn't resist a bit of code golf:
You can do:
hours,_,mins = (duration.match /^([\d]* h)?([^\d]*)?([\d]* m)?/)[1..3].map(&:to_i)
Explanation:
matches number then 'h', then anything not a number, then number then 'm'. Then gets the match data and does .to_i (which in ruby if it starts with a number uses this number). It then assigns 1st and third match to hours and minutes respectively:
Output:
2.2.1 :001 > duration = "5 hours 26 min"
=> "5 hours 26 min"
2.2.1 :002 > hours,_,mins = (duration.match /^([\d]* h)?([^\d]*)?([\d]* m)?/)[1..3].map(&:to_i)
=> [5, 0, 26]
2.2.1 :003 > hours
=> 5
2.2.1 :004 > mins
=> 26
2.2.1 :005 > duration = "5 hours"
=> "5 hours"
2.2.1 :006 > hours,_,mins = (duration.match /^([\d]* h)?([^\d]*)?([\d]* m)?/)[1..3].map(&:to_i)
=> [5, 0, 0]
2.2.1 :007 > duration = "54 mins"
=> "54 mins"
2.2.1 :008 > hours,_,mins = (duration.match /^([\d]* h)?([^\d]*)?([\d]* m)?/)[1..3].map(&:to_i)
=> [0, 0, 54]
2.2.1 :009 >

Ruby: given a date find the next 2nd or 4th Tuesday

I can't seem to find an elegant way to do this...
Given a date how can I find the next Tuesday that is either the 2nd or the 4th Tuesday of the calendar month?
For example:
Given 2012-10-19 then return 2012-10-23
or
Given 2012-10-31 then return 2012-11-13
October November
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1 2 3 4 5 6 1 2 3
7 8 9 10 11 12 13 4 5 6 7 8 9 10
14 15 16 17 18 19 20 11 12 13 14 15 16 17
21 22 23 24 25 26 27 18 19 20 21 22 23 24
28 29 30 31 25 26 27 28 29 30
Scroll to the bottom if you just want to see what the end result can look like..
Using code snippets from some date processing work I've done recently in ruby 1.9.3.
Some upgrades to DateTime:
require 'date'
class DateTime
ALL_DAYS = [ 'sunday', 'monday', 'tuesday',
'wednesday', 'thursday', 'friday', 'saturday' ]
def next_week
self + (7 - self.wday)
end
def next_wday (n)
n > self.wday ? self + (n - self.wday) : self.next_week.next_day(n)
end
def nth_wday (n, i)
current = self.next_wday(n)
while (i > 0)
current = current.next_wday(n)
i = i - 1
end
current
end
def first_of_month
self - self.mday + 1
end
def last_of_month
self.first_of_month.next_month - 1
end
end
method_missing Tricks:
I have also supplemented the class with some method missing tricks to map calls from next_tuesday to next_wday(2) andnth_tuesday(2)tonth_wday(2, 2)`, which makes the next snippet easier on the eyes.
class DateTime
# ...
def method_missing (sym, *args, &block)
day = sym.to_s.gsub(/^(next|nth)_(?<day>[a-zA-Z]+)$/i, '\k<day>')
dindex = ALL_DAYS.include?(day) ? ALL_DAYS.index(day.downcase) : nil
if (sym =~ /^next_[a-zA-Z]+$/i) && dindex
self.send(:next_wday, dindex)
elsif (sym =~ /^nth_[a-zA-Z]+$/i) && dindex
self.send(:nth_wday, dindex, args[0])
else
super(sym, *args, &block)
end
end
def respond_to? (sym)
day = sym.to_s.gsub(/^(next|nth)_(?<day>[a-zA-Z]+)$/i, '\k<day>')
(((sym =~ /^next_[a-zA-Z]+$/i) || (sym =~ /^nth_[a-zA-Z]+$/i)) && ALL_DAYS.include?(day)) || super(sym)
end
end
Example:
Given a date:
today = DateTime.now
second_tuesday = (today.first_of_month - 1).nth_tuesday(2)
fourth_tuesday = (today.first_of_month - 1).nth_tuesday(4)
if today == second_tuesday
puts "Today is the second tuesday of this month!"
elsif today == fourth_tuesday
puts "Today is the fourth tuesday of this month!"
else
puts "Today is not interesting."
end
You could also edit method_missing to handle calls such as :second_tuesday_of_this_month, :fourth_tuesday_of_this_month, etc. I'll post the code here if I decide to write it at a later date.
Take a look at Chronic or Tickle, both are gems for parsing complex times and dates. Tickle in particular will parse recurring times (I think it uses Chronic as well).
Check out this gem, you might be able to figure out your answer
https://github.com/mojombo/chronic/
Since you already use Rails, you don't need the includes, but this works in pure Ruby as well for reference.
require 'rubygems'
require 'active_support/core_ext'
d = DateTime.parse('2012-10-19')
result = nil
valid_weeks = [d.beginning_of_month.cweek + 1, d.beginning_of_month.cweek + 3]
if valid_weeks.include?(d.next_week(:tuesday).cweek)
result = d.next_week(:tuesday)
else
result = d.next_week.next_week(:tuesday)
end
puts result
I think you should probably use a library if you're needing to branch out into more interesting logic, but if what you've described is all you need, the code below should help
SECONDS_PER_DAY = 60 * 60 * 24
def find_tuesday_datenight(now)
tuesdays = [*-31..62].map { |i| now + (SECONDS_PER_DAY * i) }
.select { |d| d.tuesday? }
.group_by { |d| d.month }
[tuesdays[now.month][1], tuesdays[now.month][-2], tuesdays[(now.month + 1) % 12][1]]
.find {|d| d.yday > now.yday }
end
Loop through the last month and next month, grab the tuesdays, group by month, take the 2nd and the 2nd last tuesday of the current month (If you actually do want the 4th tuesday, just change -2 to 3) and the 2nd tuesday of the next month and then choose the first one after the provided date.
Here's some tests, 4 tuesdays in month, 5 tuesdays in month, random, and your examples:
[[2013, 5, 1], [2013, 12, 1], [2012, 10, 1], [2012, 10, 19], [2012, 10, 31]].each do |t|
puts "#{t} => #{find_tuesday_datenight(Time.new *t)}"
end
which produces
[2013, 5, 1] => 2013-05-14 00:00:00 +0800
[2013, 12, 1] => 2013-12-10 00:00:00 +0800
[2012, 10, 1] => 2012-10-09 00:00:00 +0800
[2012, 10, 19] => 2012-10-23 00:00:00 +0800
[2012, 10, 31] => 2012-11-13 00:00:00 +0800
I'm sure it could be simplified, and I'd love to hear some suggestions :) (way too late &tired to even bother figuring out what the actual range should be for valid dates i.e. smaller than -31..62)
so here is the code that will resolve a weekday for a given week in a month (what you asked for with little sugar). You should not have problems if you are running inside rails framework. Otherwise make sure you have active_support gem installed. Method name is stupid so feel free to change it :)
usage: get_next_day_of_week(some_date, :friday, 1)
require 'rubygems'
require 'active_support/core_ext'
def get_next_day_of_week(date, day_name, count)
next_date = date + (-date.days_to_week_start(day_name.to_sym) % 7)
while (next_date.mday / 7) != count - 1 do
next_date = next_date + 7
end
next_date
end
I use the following to calculate Microsoft's patch Tuesday date. It was adapted from some C# code.
require 'date'
#find nth iteration of given day (day specified in 'weekday' variable)
findnthday = 2
#Ruby wday number (days are numbered 0-7 beginning with Sunday)
weekday = 2
today = Time.now
todayM = today.month
todayY = today.year
StrtMonth = DateTime.new(todayY,todayM ,1)
while StrtMonth.wday != weekday do
StrtMonth = StrtMonth + 1;
end
PatchTuesday = StrtMonth + (7 * (findnthday - 1))

How to generate a human readable time range using ruby on rails

I'm trying to find the best way to generate the following output
<name> job took 30 seconds
<name> job took 1 minute and 20 seconds
<name> job took 30 minutes and 1 second
<name> job took 3 hours and 2 minutes
I started this code
def time_range_details
time = (self.created_at..self.updated_at).count
sync_time = case time
when 0..60 then "#{time} secs"
else "#{time/60} minunte(s) and #{time-min*60} seconds"
end
end
Is there a more efficient way of doing this. It seems like a lot of redundant code for something super simple.
Another use for this is:
<title> was posted 20 seconds ago
<title> was posted 2 hours ago
The code for this is similar, but instead i use Time.now:
def time_since_posted
time = (self.created_at..Time.now).count
...
...
end
If you need something more "precise" than distance_of_time_in_words, you can write something along these lines:
def humanize(secs)
[[60, :seconds], [60, :minutes], [24, :hours], [Float::INFINITY, :days]].map{ |count, name|
if secs > 0
secs, n = secs.divmod(count)
"#{n.to_i} #{name}" unless n.to_i==0
end
}.compact.reverse.join(' ')
end
p humanize 1234
#=>"20 minutes 34 seconds"
p humanize 12345
#=>"3 hours 25 minutes 45 seconds"
p humanize 123456
#=>"1 days 10 hours 17 minutes 36 seconds"
p humanize(Time.now - Time.local(2010,11,5))
#=>"4 days 18 hours 24 minutes 7 seconds"
Oh, one remark on your code:
(self.created_at..self.updated_at).count
is really bad way to get the difference. Use simply:
self.updated_at - self.created_at
There are two methods in DateHelper that might give you what you want:
time_ago_in_words
time_ago_in_words( 1234.seconds.from_now ) #=> "21 minutes"
time_ago_in_words( 12345.seconds.ago ) #=> "about 3 hours"
distance_of_time_in_words
distance_of_time_in_words( Time.now, 1234.seconds.from_now ) #=> "21 minutes"
distance_of_time_in_words( Time.now, 12345.seconds.ago ) #=> "about 3 hours"
chronic_duration parses numeric time to readable and vice versa
If you want to show significant durations in the seconds to days range, an alternative would be (as it doesn't have to perform the best):
def human_duration(secs, significant_only = true)
n = secs.round
parts = [60, 60, 24, 0].map{|d| next n if d.zero?; n, r = n.divmod d; r}.
reverse.zip(%w(d h m s)).drop_while{|n, u| n.zero? }
if significant_only
parts = parts[0..1] # no rounding, sorry
parts << '0' if parts.empty?
end
parts.flatten.join
end
start = Time.now
# perform job
puts "Elapsed time: #{human_duration(Time.now - start)}"
human_duration(0.3) == '0'
human_duration(0.5) == '1s'
human_duration(60) == '1m0s'
human_duration(4200) == '1h10m'
human_duration(3600*24) == '1d0h'
human_duration(3600*24 + 3*60*60) == '1d3h'
human_duration(3600*24 + 3*60*60 + 59*60) == '1d3h' # simple code, doesn't round
human_duration(3600*24 + 3*60*60 + 59*60, false) == '1d3h59m0s'
Alternatively you may be only interested in stripping the seconds part when it doesn't matter (also demonstrating another approach):
def human_duration(duration_in_seconds)
n = duration_in_seconds.round
parts = []
[60, 60, 24].each{|d| n, r = n.divmod d; parts << r; break if n.zero?}
parts << n unless n.zero?
pairs = parts.reverse.zip(%w(d h m s)[-parts.size..-1])
pairs.pop if pairs.size > 2 # do not report seconds when irrelevant
pairs.flatten.join
end
Hope that helps.
There is problem with distance_of_time_in_words if u ll pass there 1 hour 30 min it ll return about 2 hours
Simply add in helper:
PERIODS = {
'day' => 86400,
'hour' => 3600,
'minute' => 60
}
def formatted_time(total)
return 'now' if total.zero?
PERIODS.map do |name, span|
next if span > total
amount, total = total.divmod(span)
pluralize(amount, name)
end.compact.to_sentence
end
Basically just pass your data in seconds.
Rails has a DateHelper for views. If that is not exactly what you want, you may have to write your own.
#Mladen Jablanović has an answer with good sample code. However, if you don't mind continuing to customize a sample humanize method, this might be a good starting point.
def humanized_array_secs(sec)
[[60, 'minutes '], [60, 'hours '], [24, 'days ']].inject([[sec, 'seconds']]) do |ary, (count, next_name)|
div, prev_name = ary.pop
quot, remain = div.divmod(count)
ary.push([remain, prev_name])
ary.push([quot, next_name])
ary
end.reverse
end
This gives you an array of values and unit names that you can manipulate.
If the first element is non-zero, it is the number of days. You may want to write code to handle multiple days, like showing weeks, months, and years. Otherwise, trim off the leading 0 values, and take the next two.
def humanized_secs(sec)
return 'now' if 1 > sec
humanized_array = humanized_array_secs(sec.to_i)
days = humanized_array[-1][0]
case
when 366 <= days
"#{days / 365} years"
when 31 <= days
"#{days / 31} months"
when 7 <= days
"#{days / 7} weeks"
else
while humanized_array.any? && (0 == humanized_array[-1][0])
humanized_array.pop
end
humanized_array.reverse[0..1].flatten.join
end
end
The code even finds use for a ruby while statement.

Resources