7

I'm using Chronic to get the last Sunday of the month of any given year. It will gladly give me the n‌th Sunday, but not the last.

This works, but is not what I need:

Chronic.parse('4th sunday in march', :now => Time.local(2015,1,1))

This is what I need, but doesn't work:

Chronic.parse('last sunday in march', :now => Time.local(2015,1,1))

Is there any way around this apparent limitation?

UPDATE: I'm upvoting the two answers below because they're both good, but I've already implemented this in "pure Ruby" (in 2 lines of code, besides the require 'date' line), but I'm trying to demonstrate to management that Ruby is the right language to use to replace a Java codebase that is going away (and which had dozens of lines of code to compute this), and I told one manager that I probably could do it in one line of Ruby, and it would be readable and easy to maintain.

iconoclast
  • 21,213
  • 15
  • 102
  • 138
  • A pretty cheesy idea, but you could start at the first day of April and then just keep walking backwards in a loop of "yesterday" and break out when you get the first Sunday... – aardvarkk Jul 31 '13 at 14:49
  • @aardvarkk just implemented that idea while you wrote your comment :) – tessi Jul 31 '13 at 14:59
  • 4
    @aardvarkk: For that matter, you can write `Chronic.parse('1 week before 1st sunday in april')`. – ruakh Jul 31 '13 at 15:05
  • @ruakh: I would have suggested you make that an answer, because I would have accepted it, but it returns `nil`. – iconoclast Jul 31 '13 at 15:26
  • 1
    @iconoclast: Oops, sorry. The documentation gives the example `7 hours before tomorrow at noon`, so I assumed the `[interval] before [...]` notation would work more generally. My bad. – ruakh Jul 31 '13 at 15:31
  • @ruakh: I think you've uncovered yet another limitation of Chronic... I think we need to submit patches to fix these! :) – iconoclast Jul 31 '13 at 15:32
  • 2
    About your update: You can (or may) write this with `Chronic` in a single line, because you just call a function. However, using that argument you are basically saying that you can call a ruby function with a single line of code. `Chronic` probably need more than one line to implement this ;) – tessi Jul 31 '13 at 15:35
  • @tessi Yes I realize it's not an apples-to-apples comparison, but I'm looking for a way to dramatize to management the fact that Ruby is *clean*, *readable*, and *concise*. A single line of Ruby to replace a mountain of Java can do that. It's a single line of *our code* (they're paranoid about the supposed cost of Rubyists), so as long as it uses a well-maintained library, I think it's still a reasonable point to make. – iconoclast Jul 31 '13 at 17:29
  • @iconoclast I am counting three one-line solutions now (2 of them without a gem). I hope this (and the feedback you get here from other devs) can convince your management :) – tessi Jul 31 '13 at 20:03

5 Answers5

10

I am not sure about Chronic (I haven't heared about it before), but we can implement this in pure ruby :)

##
# returns a Date object being the last sunday of the given month/year
# month: integer between 1 and 12
def last_sunday(month,year)
  # get the last day of the month
  date = Date.new year, month, -1
  #subtract number of days we are ahead of sunday
  date -= date.wday
end

The last_sunday method can be used like this:

last_sunday 07, 2013
#=> #<Date: 2013-07-28 ((2456502j,0s,0n),+0s,2299161j)>
tessi
  • 13,313
  • 3
  • 38
  • 50
4

Reading the update in your question, I tried to come up with another answer using only a single line of ruby code (without using gems). How about this one?

##
# returns a Date object being the last sunday of the given month/year
# month: integer between 1 and 12
def last_sunday(month,year)
  # get the last day of the month, go back until we have a sunday
  Date.new(year, month, -1).downto(0).find(&:sunday?)
end

last_sunday 07, 2013
#=> #<Date: 2013-07-28 ((2456502j,0s,0n),+0s,2299161j)>
tessi
  • 13,313
  • 3
  • 38
  • 50
  • quite brilliant, but I'm not going to be able to convince them that a non-Rubyist will be able to maintain this, and (e.g.) change it to "first monday in January" if the need arises. – iconoclast Jul 31 '13 at 20:14
  • 1
    Let non-Rubyists maintain (more than a couple of lines of) ruby code is like [parsing HTML using regexes. It's like "asking Paris Hilton to write an operating system"](http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#answer-1733489) – tessi Jul 31 '13 at 20:26
  • Brilliant comparison. But you see what I'm up against. :) – iconoclast Jul 31 '13 at 20:32
3

What about

Chronic.parse('last sunday', now: Chronic.parse('last day of march'))
Ismael Abreu
  • 16,443
  • 6
  • 61
  • 75
  • Nice idea to use Chronic to get `:now`, but this doesn't work. First of all Chronic parses `last day of march` as `2013-03-01 12:00:00 -0500`, and secondly, I still need to specify an arbitrary year. The second problem is solved by passing a `:now` value to the embedded Chronic expression. – iconoclast Jul 31 '13 at 20:26
  • 1
    You could also set the year with Chronic. And I just sent a pull request to be able to pass a string for the :now option https://github.com/mojombo/chronic/pull/199 – Ismael Abreu Aug 01 '13 at 00:24
  • so one could do `Chronic.parse('last sunday', now: '31 march, 2015')` – Ismael Abreu Aug 01 '13 at 00:27
  • Yes, that will be a big improvement when they accept your pull request. It's ironic that a library whose whole point is to parse strings to interpret dates won't do that on one of its own parameters. – iconoclast Aug 01 '13 at 13:33
  • 1
    Well, maybe it's because no one else needed it. I guess you know upfront the date for the now. I guess this is a special case, but I think it looks great just passing a string. – Ismael Abreu Aug 01 '13 at 13:46
3

This works, and is as readable as I can get:

Chronic.parse('last sunday', now: Date.new(year,3,31))

Thanks to Ismael Abreu for the idea to just parse 'last sunday' and control the rest via the :now option.

UPDATE: Please also upvote Ismael's answer.

iconoclast
  • 21,213
  • 15
  • 102
  • 138
  • I know this is an old post, but it fails if the last day of month is a sunday. `Chronic.parse('last sunday', now: Date.new(2017,12,31))` – appleII717 Jan 04 '17 at 16:35
  • From my previous comment, if you know the end of the month, just add 1 day: `Chronic.parse('last sunday', now: Date.new(2017,12,31)+1)` – appleII717 Jan 04 '17 at 17:09
1

It's a bit ugly, but you could simply try the 5th or 4th in that order:

d = [5,4].each do |i| 
  try = Chronic.parse("#{i}th sunday in march", :now => Time.local(2015,1,1))
  break try unless try.nil?
end
 => Sun Mar 29 12:30:00 +0100 2015

d = [5,4].each do |i| 
  try = Chronic.parse("#{i}th sunday in april", :now => Time.local(2015,1,1))
  break try unless try.nil?
end
 => Sun Apr 26 12:00:00 +0100 2015
Neil Slater
  • 26,512
  • 6
  • 76
  • 94