Ruby -- group items by day with shifted beginning of the day - ruby-on-rails

I have a dictionary as such:
{"status": "ok", "data": [{"temp": 22, "datetime": "20160815-0330"}]}
20160815-0330 means 2016-08-15 03:30. I want to group by day and calculate min and max temp, but the day should start at 09:00 and end at 08:59. How could I do that?
Here is my code:
#results = #data['data'].group_by { |d| d['datetime'].split('-')[0] }.map do |date, values|
[date, {
min_temp: values.min_by { |value| value['temp'] || 0 }['temp'],
max_temp: values.max_by { |value| value['temp'] || 0 }['temp']
}]
end
#=> {"20160815"=>{:min_temp =>12, :max_temp =>34}}
I works, but the starting point is 00:00, not 09:00.

I would suggest to use a timezone trick:
def adjust_datetime(str)
DateTime.parse(str.sub('-', '') + '+0900').utc.strftime('%Y%m%d')
end
data.group_by { |d| adjust_datetime(d['datetime']) }
Here is an explanation:
str = '20160815-0330'
str = str.sub('-', '') #=> '201608150330'
str = str + '+0900' #=> '201608150330+0900'
dt = DateTime.parse(str) #=> Mon, 15 Aug 2016 03:30:00 +0900
dt = dt.utc #=> 2016-08-14 18:30:00 UTC
dt.strftime('%Y%d%m') #=> '20160814'

Related

Best way to count days in period spitted by month

Need to example how to calculate the count of days in a period splited by month.
For example:
Wed, 25 Nov 2020 : Tue, 15 Dec 2020 => [6 (nov), 15(dec)]
Thank you!
This would be a job for tally_by, but that is not added to Ruby (yet?).
tally works too:
require 'date'
range = Date.parse("Wed, 25 Nov 2020") .. Date.parse("Tue, 15 Dec 2020")
p month_counts = range.map{|d| Date::ABBR_MONTHNAMES[d.month] }.tally
# => {"Nov"=>6, "Dec"=>15}
date1 = Date.new(2020, 11, 25)
date2 = Date.new(2020, 12, 15)
(date1..date2).group_by { |date| [date.year, date.month] }
.map { |(year, month), dates| ["#{year}/#{month}", dates.length] }
=> [["2020/11", 6], ["2020/12", 15]]
What about the interval is so long that you have same months but of different years? I've added years because of this case.
This works in pure ruby too, you just need require 'date'
Code
require 'date'
def count_days_by_month(str)
Range.new(*str.split(/ +: +/).
map { |s| Date.strptime(s, '%a, %d %b %Y') }).
slice_when { |d1,d2| d1.month != d2.month }.
with_object({}) do |a,h|
day1 = a.first
h[[day1.year, Date::ABBR_MONTHNAMES[day1.month]]] = a.size
end
end
See Range::new, Date::strptime and Enumerable#slice_when.
Examples
count_days_by_month "Wed, 25 Nov 2020 : Tue, 15 Dec 2020"
#=> {[2020, "Nov"]=>6, [2020, "Dec"]=>15}
count_days_by_month "Wed, 25 Nov 2020 : Tue, 15 Dec 2021"
#=> {[2020, "Nov"]=>6, [2020, "Dec"]=>31, [2021, "Jan"]=>31,
# ...
# [2021, "Nov"]=>30, [2021, "Dec"]=>15}
Explanation
For the first example the steps are as follows.
str = "Wed, 25 Nov 2020 : Tue, 15 Dec 2020"
b = str.split(/ +: +/)
#=> ["Wed, 25 Nov 2020", "Tue, 15 Dec 2020"]
c = b.map { |s| Date.strptime(s, '%a, %d %b %Y') }
#=> [#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-12-15 ((2459199j,0s,0n),+0s,2299161j)>]
d = Range.new(*c)
#=> #<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>..
# #<Date: 2020-12-15 ((2459199j,0s,0n),+0s,2299161j)>
e = d.slice_when { |d1,d2| d1.month != d2.month }
#=> #<Enumerator: #<Enumerator::Generator:0x00007fb1058abb10>:each>
We can see the elements generated by this enumerator by converting it to an array.
e.to_a
#=> [[#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-11-26 ((2459180j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-11-30 ((2459184j,0s,0n),+0s,2299161j)>],
# [#<Date: 2020-12-01 ((2459185j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-12-02 ((2459186j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-12-15 ((2459199j,0s,0n),+0s,2299161j)>]]
Continuing,
f = e.with_object({})
#=> #<Enumerator: #<Enumerator: #<Enumerator::Generator:0x00007fb1058abb10>
# :each>:with_object({})>
f.each do |a,h|
day1 = a.first
h[[day1.year, Date::ABBR_MONTHNAMES[day1.month]]] = a.size
end
#=> {[2020, "Nov"]=>6, [2020, "Dec"]=>15}
The first element generated by f and passed to the block, and the block variables are assign values by the rules of array decomposition:
a,h = f.next
#=> [[#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-11-26 ((2459180j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-11-30 ((2459184j,0s,0n),+0s,2299161j)>],
# {}]
a #=> [#<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>,
# #<Date: 2020-11-26 ((2459180j,0s,0n),+0s,2299161j)>,
# ...
# #<Date: 2020-11-30 ((2459184j,0s,0n),+0s,2299161j)>],
h #=> {}
Key-value pairs will be added to h over the course of the calculations. See Enumerator#next. The block calculation is now performed.
day1 = a.first
#=> #<Date: 2020-11-25 ((2459179j,0s,0n),+0s,2299161j)>
g = day1.year
#=> 2020
i = day1.month
#=> 11
j = Date::ABBR_MONTHNAMES[day1.month]
#=> "Nov"
k = a.size
#=> 6
h[[g,j]] = k
#=> 6
resulting in:
h #=> {[2020, "Nov"]=>6}
The remaining steps are similar.
I would start by breaking the whole period into periods for each month.
Since Ruby has ranges, I'd write a helper method that takes a date range and yields month-ranges:
def each_month(range)
return enum_for(__method__, range) unless block_given?
date = range.begin.beginning_of_month
loop do
from = date.clamp(range)
to = (date.end_of_month).clamp(range)
yield from..to
date = date.next_month
break unless range.cover?(date)
end
end
clamp ensures that the range's bounds are taken into account when calculating each month's range. For Ruby version prior to 2.7 you have to pass the bounds separately:
from = date.clamp(range.begin, range.end)
to = (date.end_of_month).clamp(range.begin, range.end)
Example usage:
from = '25 Nov 2020'.to_date
to = '13 Jan 2021'.to_date
each_month(from..to).to_a
#=> [
# Wed, 25 Nov 2020..Mon, 30 Nov 2020
# Tue, 01 Dec 2020..Thu, 31 Dec 2020
# Fri, 01 Jan 2021..Wed, 13 Jan 2021
# ]
Now all we need is a way to count the days in each month-range: (e.g. via jd)
def days(range)
range.end.jd - range.begin.jd + 1
end
and some formatting:
each_month(from..to).map { |r| format('%d (%s)', days(r), r.begin.strftime('%b')) }
#=> ["6 (Nov)", "31 (Dec)", "13 (Jan)"]

ruby date format from date A to date B

I just wonder how the date period can be written in Ruby?
date_a = Time.at() # <= new
date_b = Time.at() # <= old
I'd love to have something like:
September 1 - 30, 2016
Also it needs to be considered the year.
(Ex. if it's in January,
It should be December 25 2016 - January 25 2017)
You could do something like this
def format_dates(*dates)
date1, date2 = dates.sort
return "#{date1.strftime("%B %d %Y")} if date1 == date2
if date1.year == date2.year
if date1.month == date2.month
"#{date1.strftime("%B")} #{date1.day} - #{date2.day}, #{date1.year}"
else
"#{date1.strftime("%B %d")} - #{date2.strftime("%B %d")}, #{date1.year}"
end
else
"#{date1.strftime("%B %d %Y")} - #{date2.strftime("%B %d %Y")}"
end
end
p format_dates(Date.parse('25/12/2016'), Date.parse('25/01/2017'))
# => "December 25 2016 - January 25 2017"
p format_dates(Date.parse('25/12/2016'), Date.parse('25/01/2016'))
# => "January 25 - December 25, 2016"

Efficiency of Ruby code: hash of month + frequency into formatted sorted array

I've hacked up some code that fulfills its purpose but it feels very clunky/inefficient. From a table of many entries, each has a month + year string associated with it: "September 2016" etc. From these I create a chronologically ordered array of months and their frequencies to be used in a dropdown selection form: ['Novemeber 2016 (5)', 'September 2016 (5)'].
#months = []
banana = Post.pluck(:month)
#array of all months posted in, eg ['September 2016', 'July 2017', etc
strawberry = banana.each_with_object(Hash.new(0)){|key,hash| hash[key] += 1}
#hash of unique month + frequency
strawberry.each { |k, v| strawberry[k] = "(#{v.to_s})" }
#value into string with brackets
pineapple = (strawberry.sort_by { |k,_| Date.strptime(k,"%b %Y") }).reverse
#sorts into array of months ordered by most recent
pineapple.each { |month, frequency| #months.push("#{month}" + " " + "#{frequency}") }
#array of formatted months + frequency, eg ['July 2017 (5)', 'September 2016 (5)']
I was hoping some of the Ruby gurus here could advise me in some ways to improve this code. Any ideas or suggestions would be greatly appreciated!
Thanks!
['September 2016', 'July 2017', 'September 2016', 'July 2017']
.group_by { |e| e } # .group_by(&:itself) since Ruby2.3
.sort_by { |k, _| Date.parse(k) }
.reverse
.map { |k, v| "#{k} (#{v.count})" }
#⇒ [
# [0] "July 2017 (2)",
# [1] "September 2016 (2)"
# ]

How to determine the nth weekday for a given month?

This snippet of code determines the date for the nth weekday of a given month.
Example: for the 2nd Tuesday of December 2013:
>> nth_weekday(2013,11,2,2)
=> Tue Nov 12 00:00:00 UTC 2013
Last Sunday of December 2013:
>> nth_weekday(2013,12,'last',0)
=> Sun Dec 29 00:00:00 UTC 2013
I was not able to find working code for this question so I'm sharing my own.
If you are using Rails, you can do this.
def nth_weekday(year, month, n, wday)
first_day = DateTime.new(year, month, 1)
arr = (first_day..(first_day.end_of_month)).to_a.select {|d| d.wday == wday }
n == 'last' ? arr.last : arr[n - 1]
end
> n = nth_weekday(2013,11,2,2)
# => Tue, 12 Nov 2013 00:00:00 +0000
You can use:
require 'date'
class Date
#~ class DateError < ArgumentError; end
#Get the third monday in march 2008: new_by_mday( 2008, 3, 1, 3)
#
#Based on http://forum.ruby-portal.de/viewtopic.php?f=1&t=6157
def self.new_by_mday(year, month, weekday, nr)
raise( ArgumentError, "No number for weekday/nr") unless weekday.respond_to?(:between?) and nr.respond_to?(:between?)
raise( ArgumentError, "Number not in Range 1..5: #{nr}") unless nr.between?(1,5)
raise( ArgumentError, "Weekday not between 0 (Sunday)and 6 (Saturday): #{nr}") unless weekday.between?(0,6)
day = (weekday-Date.new(year, month, 1).wday)%7 + (nr-1)*7 + 1
if nr == 5
lastday = (Date.new(year, (month)%12+1, 1)-1).day # each december has the same no. of days
raise "There are not 5 weekdays with number #{weekday} in month #{month}" if day > lastday
end
Date.new(year, month, day)
end
end
p Date.new_by_mday(2013,11,2,2)
This is also available in a gem:
gem "date_tools", "~> 0.1.0"
require 'date_tools/date_creator'
p Date.new_by_mday(2013,11,2,2)
# nth can be 1..4 or 'last'
def nth_weekday(year,month,nth,week_day)
first_date = Time.gm(year,month,1)
last_date = month < 12 ? Time.gm(year,month+1)-1.day : Time.gm(year+1,1)-1.day
date = nil
if nth.class == Fixnum and nth > 0 and nth < 5
date = first_date
nth_counter = 0
while date <= last_date
nth_counter += 1 if date.wday == week_day
nth_counter == nth ? break : date += 1.day
end
elsif nth == 'last'
date = last_date
while date >= first_date
date.wday == week_day ? break : date -= 1.day
end
else
raise 'Error: nth_weekday called with out of range parameters'
end
return date
end

How do I convert a string into a Time object?

I searched for my problem and got a lot of solutions, but unfortunately none satisfy my need.
My problem is, I have two or more strings, and I want to convert those strings into times, and add them:
time1 = "10min 43s"
time2 = "32min 30s"
The output will be: 43min 13s
My attempted solution is:
time1 = "10min 43s"
d1=DateTime.strptime(time1, '%M ')
# Sat, 02 Nov 2013 00:10:00 +0000
time2 = "32min 30s"
d2=DateTime.strptime(time2, '%M ')
# Sat, 02 Nov 2013 00:32:00 +0000
Then I can't progress.
There are many ways to do this. Here's another:
time1 = "10min 43s"
time2 = "32min 30s"
def get_mins_and_secs(time_str)
time_str.scan(/\d+/).map(&:to_i)
#=> [10, 43] for time_str = time1, [32, 30] for time_str = time2
end
min, sec = get_mins_and_secs(time1)
min2, sec2 = get_mins_and_secs(time2)
min += min2
sec += sec2
if sec > 59
min += 1
sec -= 60
end
puts "#{min}min #{sec}sec"
Let's consider what's happening here. Firstly, you need to extract the minutes and seconds from the time strings. I made a method to do that:
def get_mins_and_secs(time_str)
time_str.scan(/\d+/).map(&:to_i)
#=> [10, 43] for time_str = time1, [32, 30] for time_str = time2
end
For time_str = "10min 43s", we apply the String#scan method to extract the two numbers as strings:
"10min 43s".scan(/\d+/) # => ["10", "43"]
Array#map is then used to convert these two strings to integers
["10", "43"].map {|e| e.to_i} # => [10, 43]
This can be written more succinctly as
["10", "43"].map(&:to_i} # => [10, 43]
By chaining map to to scan we obtain
"10min 43s".scan(/\d+/).map(&:to_i} # => [10, 43]
The array [10, 43] is returned and received (deconstructed) by the variables min and sec:
min, sec = get_mins_and_secs(time_str)
The rest is straightforward.
Here's a simple solution assuming that the format stays the same:
time1 = "10min 43s"
time2 = "32min 30s"
strings = [time1, time2]
total_time = strings.inject(0) do |sum, entry|
minutes, seconds = entry.split(' ')
minutes = minutes.gsub("min", "").to_i.send(:minutes)
seconds = seconds.gsub("s", "").to_i.send(:seconds)
sum + minutes + seconds
end
puts "#{total_time/60}min #{total_time%60}s"
Something like the following should do the trick:
# split the string on all the integers in the string
def to_seconds(time_string)
min, sec = time_string.gsub(/\d+/).map(&:to_i)
min.minutes + sec.seconds
end
# Divide the seconds with 60 to get minutes and format the output.
def to_time_str(seconds)
minutes = seconds / 60
seconds = seconds % 60
format("%02dmin %02dsec", minutes, seconds)
end
time_in_seconds1 = to_seconds("10min 43s")
time_in_seconds2 = to_seconds("32min 30s")
to_time_str(time_in_seconds1 + time_in_seconds2)
My solution that takes any number of time strings and return the sum in the same format:
def add_times(*times)
digits = /\d+/
total_time = times.inject(0){|sum, entry|
m, s = entry.scan(digits).map(&:to_i)
sum + m*60 + s
}.divmod(60)
times.first.gsub(digits){total_time.shift}
end
p add_times("10min 43s", "32min 55s", "1min 2s") #=> "44min, 40s"
p add_times("10:43", "32:55") #=> "38:43"

Resources