DateTime parse undefined method `year' - ruby-on-rails

I'm trying to parse a string to DateTime and i'm experiencing an error when I try tp drop the year:
undefined method `year' for "Monday, Aug 25, 10:30":String
Controller
dates = []
temps = []
dt = []
#data['data'].flatten.each do |data|
dates << data.keys
temps << data.values
end
dates.flatten.each do |date|
dt << DateTime.parse(date).strftime("%A, %b %d, %H:%M")
end
json
{"status": "ok", "data": [{"2014-08-25 10:30:00": 12.6}]}

Your example isn't clear because you're not showing your use of the year method. But, note that you've left out %y from your strftime string. I would recommend playing around in rails console to get the methods you're looking for. For example:
[1] pry(main)> DateTime.parse("2014-08-25 10:30:00")
Mon, 25 Aug 2014 10:30:00 +0000
[2] pry(main)> DateTime.parse("2014-08-25 10:30:00").year
2014
[3] pry(main)> DateTime.parse("2014-08-25 10:30:00").strftime("%A, %b %d %y, %H:%M")
"Monday, Aug 25 14, 10:30"
[4] pry(main)> DateTime.parse("2014-08-25 10:30:00").strftime("%A, %b %d %y, %H:%M").year
NoMethodError: undefined method `year' for "Monday, Aug 25 14, 10:30":String
from (pry):4:in `<main>'

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 -- group items by day with shifted beginning of the day

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'

How to identify a set of dates from a string in rails

I have the following strings
"sep 04 apr 06"
"29th may 1982"
"may 2006 may 2008"
"since oct 11"
Output
"September 2004 and April 2006"
"29 May 1982"
"May 2006 and May 2008"
"October 2011"
Is there a way to obtain the dates from these string. I used the gem 'dates_from_string', but it is unable to correctly obtain date from first scenario.
When you say "unfortunately I can't predict in which format the date is going to be in.", you imply that you actually need "natural language parsing". Which is something core Date or DateTime objects cannot and should not do.
So, either you will need to parse the strings so you can present them to the more strict parser in an understandable format. Like DateTime.parse('sep 04'). For your examples, it could be as simple as:
datestring = 'sep 04 apr 06'
matches = datestring.match(/[a-z]{3}\s\d{2,4}/)
if matches.many?
matches.map{|m| Date.parse(m) }.join(' and ')
else
Date.parse(datestring)
end
However, when you want true natural language parsing, have a look at Chronic. Which has all sorts of fancy parsers like Chronic.parse('summer').
Edit: on closer inspection, it seems Chronic too can only identify one string, so your example 'sep 04 apr 06' still needs some pre-processing.
The approach I took is as follows:
Divide the string into an array of words.
If the array contains fewer than two words, return an array containing all the date strings found; else go to step 3.
If the array contains at least three words and the first three words represent a date, save it, delete the first three words in the array and repeat step 2; else go to step 4.
If the first two words represent a date, save it, delete the first two words in the array and repeat step 2; else go to step 5.
Delete the first word in the array and go to step 2.
I search for dates using the class method Date::strptime. strptime employs a format string. For example, '%d %b %Y' searches for the day of the month, followed by a space, followed by a (case-insensitive) three-character month abbreviation ('Jan', 'Feb',...,'Dec'), followed by a four-digit year. (I initially consider using Date::parse, but that does not discriminate dates adequately.)
Code
I first generate all the strptime format strings of interest for month, day and year:
MON = %w{ %b %B } # '%b' for 'Jan', '%B' for 'January'
YR = %w{ %y %Y } # '%y' for '11', '%Y' for 2011
DAY = %w{ %d } # '4', '04' or '28'
PERM3 = MON.product(YR, DAY).
flat_map { |arr| arr.permutation(3).to_a }.
map { |arr| arr.join(' ') }
#=> ["%b %y %d", "%b %d %y", "%y %b %d", "%y %d %b", "%d %b %y", "%d %y %b",
# "%b %Y %d", "%b %d %Y", "%Y %b %d", "%Y %d %b", "%d %b %Y", "%d %Y %b",
# "%B %y %d", "%B %d %y", "%y %B %d", "%y %d %B", "%d %B %y", "%d %y %B",
# "%B %Y %d", "%B %d %Y", "%Y %B %d", "%Y %d %B", "%d %B %Y", "%d %Y %B"]
I then do the same for permutations of day and month and month and year:
PERM2 = MON.product(YR).
concat(MON.product(DAY)).
flat_map { |arr| arr.permutation(2).to_a }.
map { |arr| arr.join(' ') }
#=> ["%b %y", "%y %b", "%b %Y", "%Y %b", "%B %y", "%y %B",
# "%B %Y", "%Y %B", "%b %d", "%d %b", "%B %d", "%d %B"]
I then proceed as follows:
require 'date'
def pull_dates(str)
arr = str.split
dates = []
while arr.size > 1
if arr.size > 2
a = depunc(arr[0,3])
if date?(a, PERM3)
dates << a.join(' ')
arr.shift(3)
next
end
end
a = depunc(arr[0,2])
if date?(a, PERM2)
dates << a.join(' ')
arr.shift(2)
next
end
arr.shift
end
dates
end
depunc removes any punctuations at the beginning and end of the string arr.join(' ').
def depunc(arr)
arr.join(' ').gsub(/^\W|\W$/,'').split
end
date? determines if the three- or two-element string arr represents a date. I first obtain a "cleaned" string from arr, and then search through the applicable strptime format strings (the argument perm), looking for one that shows the cleaned string can be converted to a date.
def date?(arr, perm)
clean = to_str_and_clean(arr)
perm.find do |s|
begin
d = Date.strptime(clean, s)
return true
rescue
false
end
end
false
end
to_str_and_clean returns a cleaned string that has punctuation removed and strings such as 'st', 'nd', 'rd', and 'th' following the numerical representation of the day.
def to_str_and_clean(arr)
str = arr.map { |s| s[0][/\d/] ? s.to_i.to_s : s }.join(' ').tr('.?!,:;', '')
end
Example
Let's try it.
str =
"Bubba sighted a flying saucer on sep 04 2013 and again in apr 06. \
Greta was born on 29th may 1982. Hey, may 2006 may 2008 are two years apart.\
We have been at loose ends since oct 11 of this year."
pull_dates(str)
#=> ["sep 04 2013", "apr 06", "29th may 1982", "may 2006 may", "oct 11"]
Well, as you see, it's not perfect. Some tweaking is required, but this might get you started.
You can use the DateTime class like so:
DateTime.parse('sep 04 apr 06')
which outputs a DateTime object:
#<DateTime: 2006-04-04T00:00:00+00:00 ((2453830j,0s,0n),+0s,2299161j)>
You can use DateTime.strptime method

How to covert the date string range into datatime data type

I need to convert the following raw string (date range) into ruby datetime datetype.
How to finish it on Rails ?
raw string
"2014 April/July 24-1"
convert to ruby datetime variable
start_date = 2014-04-24
end_date = 2014-07-01
raw string
"2015 April 06-20"
convert to ruby datetime variable
start_date = 2015-04-06
end_date = 2015-04-20
This may help
# In order to generate
# year = 2014
# months = "April/July"
# days = "24-1"
/(?<year>\d{4})\s*(?<months>\w+\/\w+)\s*(?<days>\d{1,2}\-\d{1,2})/ =~ "2014 April/July 24-1"
date1 = "#{year} #{months.split('/')[0]} #{days.split('-')[0]}"
date2 = "#{year} #{months.split('/')[1]} #{days.split('-')[1]}"
start_date = Date.strptime(date1, "%Y %b %d") #=> Thu, 24 Apr 2014
end_date = Date.strptime(date2, "%Y %b %d") #=> Tue, 01 Jul 2014

How to convert the string 2012 , December, 15 to a Datatime object

I will get the String "2012" then "December" at first time
In the following get the string 1,15, 30
I should convert them into Datetime object
2012-12-01
2012-12-15
2012-12-30
How to do it ?
Date.strptime('2012 December', '%Y %B')
#=> Sat, 01 Dec 2012
date = Date.strptime('2012 December 10', '%Y %B %d')
#=> Mon, 10 Dec 2012
date.strftime("%Y-%m-%d")
#=> "2012-12-10"
You have this string:
[1] pry(main)> str = "2012 December 1,15, 30"
=> "2012 December 1,15, 30"
And you want to get three dates from it (1st, 15th and 30th of December. First at all, get the year, month and days from this string:
[2] pry(main)> m = /(?'year'\d*)\s(?'month'[a-zA-Z]*)(?'days'[\d, ]*)/.match str
=> #<MatchData
"2012 December 1,15, 30"
year:"2012"
month:"December"
days:" 1,15, 30">
Then split the days to and array and iterate on it to convert the days with year and month to Date object (you can use Date.strptime as shivam showed above or Time.parse):
[10] pry(main)> require 'time'
=> true
[11] pry(main)> m[:days].split(',').collect {|day| Time.parse "#{m[:year]} #{m[:month]} #{day}"}
=> [2012-12-01 00:00:00 +0100,
2012-12-15 00:00:00 +0100,
2012-12-30 00:00:00 +0100]

Resources