0

I want to calculate how long the user has been a member of my app. I should display it as:

User member for 2y, 9m

To do so I've created method inside of User model

  def member_for
    #calculate number of months
    month = (Time.current.year * 12 + Time.current.month) - (created_at.year * 12 + created_at.month)
    #an array [years, months]
    result = month.divmod(12)

    if result[0].zero? && result[1].nonzero?
      "User member for #{result[1]}m"
    elsif result[1].zero? && result[0].nonzero?
      "User member for #{result[0]}y"
    elsif result[0].zero? && result[1].zero?
      'User member for 1m'
    else
      "User member for #{result[0]}y, #{result[1]}m"
    end
  end

But honestly this code smells, isn't there some built-in method in Rails6 to do this better and make the code look a little cleaner?

mr_muscle
  • 2,536
  • 18
  • 61
  • 1
    You might want to check out the [distance_of_time_in_words](https://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html#method-i-distance_of_time_in_words) helper method: – spickermann Apr 23 '21 at 15:44

2 Answers2

2

You can use ActiveSupport::Duration for this. All you need to do is pass the time difference to the ActiveSupport::Duration.build method. For example:

time_diff = Time.now - 1000.days.ago
ActiveSupport::Duration.build(time_diff.to_i)         # 2 years, 8 months, 3 weeks, 5 days, 28 minutes, and 47 seconds 
ActiveSupport::Duration.build(time_diff.to_i).parts   # 2 years, 8 months, 3 weeks, 5 days, 28 minutes, and 47 seconds 
Sachin Singh
  • 993
  • 8
  • 16
  • Won't 'Time.now - created_at' give the time difference? – Sachin Singh Apr 23 '21 at 16:50
  • yes it will. You still need write a formatting method though to get something more succint. – max Apr 23 '21 at 17:14
  • I am not supposed to write the exact code for the problem. I guess the suggestion I gave should have been helpful and the author can move forward with that and add logic around that if he finds it useful. – Sachin Singh Apr 23 '21 at 17:36
  • This solution leads to the same ifology I wrote. The only difference is that I will have to operate on a hash instead of an array. – mr_muscle Apr 26 '21 at 15:06
  • Yes, but this is a built-in method that needs small processing for your use case. You don't have to write an internal method that does almost the same as what this method does. – Sachin Singh Apr 26 '21 at 16:09
0

If you want to avoid the caching/timezone headaches involved with distance_of_time_in_words* which will essentially invalidate your cached responses/fragments continously then just output a HTML5 time element and use a JS library like moment.js to calculate the elapsed time and display it to the user:

module TimeHelper
  def time_tag(date_or_time, **options)
    tag.time(datetime: date_or_time.iso8601, **options)
  end
end
$ yarn install moment
// app/javascripts/packs/application.js
require('moment');
<% @users.each do |user| %>
  <p><%= time_tag(user.created_at, class: 'member-since') %></p>
<% end %>

document.querySelectorAll('time.member-since').forEach((el) => {
  const m = moment(el.dateTime); 
  const str = `User has been a member for ${m.toNow(true)}`;
  const textnode = document.createTextNode(str);
  el.appendChild(textnode);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js" integrity="sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ==" crossorigin="anonymous"></script>

<p><time datetime="2001-05-15T19:00" class="member-since" /></p>
<p><time datetime="2005-05-15T09:00" class="member-since" /></p>
<p><time datetime="2021-04-15T09:00" class="member-since" /></p>

See Difference between two dates in years, months, days in JavaScript for alternatives to moment.js.

max
  • 96,212
  • 14
  • 104
  • 165
  • * This really applies no matter how you output the time on the server side. – max Apr 23 '21 at 16:47
  • So the answer to how do to solve this basic coding issue in Ruby is go find another language. I mean you're right, it just hurts. – Tom Sep 16 '22 at 13:36