1

I am attempting to round UNIX timestamps in Ruby to the nearest whole month. I have the following UNIX timestamps which I'd like to convert as shown--basically if the day of the month is the 15th and onward, it should round up to the next month (e.g. February 23rd rounds up to March 1st; February 9th rounds down to February 1st).

Here are the timestamps I have and the result I need help achieving:

1455846925 (Feburary 19th, 2016) => 1456790400 (March 1st, 2016)

1447476352 (November 14th, 2015) => 1446336000 (November 1st, 2015)

1242487963 (May 16th, 2009) => 1243814400 (June 1st, 2009).

I am okay solely relying on the logic of 1-14 (round down) / 15+ (round up). I realize this won't always take into account the days in a month and I can accept that for this if needed (although a solution that always takes into account the days in a given month is a bonus).

Ruby's DateTime module may be able to do it in combination with modulo of the number of seconds in a month but I'm not quite sure how to put it all together. If I can convert the UNIX timestamp directly without first translating it to a Ruby Date, that is perfectly fine too.

Thank you in advance for your assistance.

Community
  • 1
  • 1
Kurt W
  • 321
  • 2
  • 15

4 Answers4

3

This rounds to the nearest second.

require 'time'

def round_to_month(secs)
  t1 = Time.at secs
  t2 = (t1.to_datetime >> 1).to_time
  s1 = Time.new(t1.year, month=t1.month)
  s2 = Time.new(t2.year, month=t2.month)
  (t1-s1) < (s2-t1) ? s1 : s2
end

round_to_month(1455846925) # round 2016-02-18 17:55:25 -0800 
  #=> 2016-03-01 00:00:00 -0800
round_to_month(1447476352) # round 2015-11-13 20:45:52 -0800
  #=> 2015-11-01 00:00:00 -0700 
round_to_month(1242487963) # round 2009-05-16 08:32:43 -0700 
  #=> 2009-05-01 00:00:00 -0700 

Consider

secs = 1455846925

The calculations are as follows:

 t1 = Time.at secs
   #=> 2016-02-18 17:55:25 -0800 
 dt = t1.to_datetime
   #=> #<DateTime: 2016-02-18T17:55:25-08:00 ((2457438j,6925s,0n),-28800s,2299161j)> 
 dt_next = dt >> 1
   #=> #<DateTime: 2016-03-18T17:55:25-08:00 ((2457467j,6925s,0n),-28800s,2299161j)> 
 t2 = dt_next.to_time 
   #=> 2016-03-18 18:55:25 -0700 
 s1 = Time.new(t1.year, month=t1.month)
   #=> Time.new(2016, month=2) 
   #=> 2016-02-01 00:00:00 -0800 
 s2 = Time.new(t2.year, month=t2.month)
   # Time.new(2016, month=3)
   #=> 2016-03-01 00:00:00 -0800 
 (t1-s1) < (s2-t1) ? s1 : s2
   #=> 1533325.0 < 972275.0 ? 2016-02-18 17:55:25 -0800 : 2016-03-01 00:00:00 -0800
   #=> 2016-03-01 00:00:00 -0800
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • why are seconds relevant in terms of rounding to the nearest month? (re: `secs` in your method) – Andrew Hendrie Feb 19 '16 at 03:11
  • 1
    Isn't the question about rounding a Unix timestamp to the (beginning of the) nearest month? Unix timestamps are seconds since the Epoch. Kurt said an approximation would be OK, but why not do it properly? – Cary Swoveland Feb 19 '16 at 03:21
  • Hi Cary, nice to see you again and thanks for the quick answer. Shouldn't round_to_month(1447476352) round down to November 1st? It's rounding up to December 1st according to your code snippet. Looking forward to selecting your answer over Jordan's this time ;-) – Kurt W Feb 19 '16 at 03:29
  • Accepted as the best answer. Thanks Cary! This is the most concise in my opinion and a) doesn't require ActiveSupport/Rails and b) is the most precise. Although I asked for an approximate rounding, an exact result (down to the nearest second) is far preferred and will likely help other SO users. Thanks! – Kurt W Feb 19 '16 at 05:07
  • Kurt. you are right. I initially wrote `(s2-t1) < (s2-t1) ? s1 : s2` (cut and pasting `s2-t1`), but then forgot to change the first `s2-t1` to `t1-s1`. I noticed it when I was working through a detailed calculation, which I've now posted. I'm glad it was helpful. – Cary Swoveland Feb 19 '16 at 05:14
1

It would be easy to convert it to Time object and then convert it back to timestamp

If you're using Rails, this method should do, what you want:

def nearest_month(t)
  time = Time.at(t).utc
  time = time.next_month if time.day >= 15

  time.beginning_of_month.to_i
end
Babar Al-Amin
  • 3,939
  • 1
  • 16
  • 20
  • Very concise and a great answer if Rails/Active Support is available. Thanks for your answer. – Kurt W Feb 19 '16 at 05:10
1

I don't know if this is as accurate as @CarySwoveland's solution, but I like it:

require 'time'

FIFTEEN_DAYS = 15 * 24 * 60 * 60

def round_to_month(secs)
  t1 = Time.at(secs + FIFTEEN_DAYS)
  Time.new(t1.year, t1.month)
end

p round_to_month(1455846925) # round 2016-02-18 17:55:25 -0800 
# => 2016-03-01 00:00:00 -0800

p round_to_month(1447476352) # round 2015-11-13 20:45:52 -0800
# => 2015-11-01 00:00:00 -0700

p round_to_month(1242487963) # round 2009-05-16 08:32:43 -0700 
# => 2009-05-01 00:00:00 -0700

If you want it to return a UNIX timestamp instead just tack .to_i onto the last line in the method.

Jordan Running
  • 102,619
  • 17
  • 182
  • 182
  • Jeez, you were at 49.8K just a few days ago... how much time do you spend on SO vs at your job? :) – Kurt W Feb 19 '16 at 05:04
  • This answer makes good sense if precision isn't the most important factor. This is more likely what I would have come up with if I would have had more cycles to work on development during my day job. Unfortunately, cycles are pretty limited right now. I wish I had time to sit down and think through the logic--the coding challenge could be solved in my ways clearly. Thanks again Jordan. – Kurt W Feb 19 '16 at 05:12
  • For what it's worth, at a certain point your SO score takes on a life of its own. At a glance, in the last week I've gotten 120 points on answers I posted months or years ago. – Jordan Running Feb 19 '16 at 05:40
  • 1
    If @Kurt is right, that you went from 49.8K to 45.2K in just a few days, you must have posted some real stinkers. – Cary Swoveland Feb 20 '16 at 10:37
  • Jordan and/or @Cary Swoveland, if you have a moment, could you have a glance at http://stackoverflow.com/questions/35713260/ruby-parse-a-multi-line-tab-delimited-string-into-an-array-of-arrays ? I posted it last night but it has only been viewed a few times, almost as if it never cycled into the questions list. Thanks either way! – Kurt W Mar 01 '16 at 18:02
0

Something like this will work if you use ActiveSupport in Rails:

require 'date'    

def round_to_nearest_month(timestamp)

  # Convert the unix timestamp into a Ruby DateTime object
  datetime = timestamp.to_datetime

  # Get the day of the month from the datetime object
  day_of_month = datetime.mday

  if day_of_month < 15
    datetime.at_beginning_of_month
  else 
    datetime.at_beginning_of_month.next_month
  end    

  return datetime

end
Andrew Hendrie
  • 6,205
  • 4
  • 40
  • 71
  • Does beginning_of_month and next_month work just as shown or does it also require ActiveSupport in a Ruby-only setting? Thanks! – Kurt W Feb 19 '16 at 05:14