pwz2000
pwz2000

Reputation: 1395

How to list the ages of users from their DOB

I want to create statistics that will show the age group of my registered Users.

Example:

18-24 - 150 registered users
25-34 - 3948 registered users
35-44 - 10028 registered users
45+ - 538 registered users

I have a birthday (dob) attribute in the Users table. I calculate their age from the User model:

def age
  now = Time.now.utc.to_date
  now.year - birthday.year - ((now.month > birthday.month || (now.month == birthday.month && now.day >= birthday.day)) ? 0 : 1)
end

The best I can do on my own is:

User.group(:birthday).count

Which outputs the total users for each date of birth (not the age).

I want to instead output groups for ages in the view. Can someone assist me? OR the alternative can be listing total users for each age. Whichever would be the best solution.

Upvotes: 2

Views: 1424

Answers (2)

0x4a6f4672
0x4a6f4672

Reputation: 28245

You could do something like this in MySQL to get the sum of all 8-10 year old people:

SELECT COUNT(*) as FROM users WHERE Year(dob) BETWEEN 2004 AND 2006;

or in Rails

User.where('Year(dob) IN (?)',(2004..2006)).count

Using this code, you can do just the following to get the number of people in an age group:

groups = [[18,24],[25,35],[34,44],[45,99]]
current_year = DateTime.now.year
groups.each do |age_group|
  range = Range.new(*age_group.map{|y| current_year - y}.reverse)
  print age_group.join(" - ")+": "
  puts User.where('Year(dob) IN (?)',range).count
end

Upvotes: 2

Marek Takac
Marek Takac

Reputation: 3048

  1. create instance method to compute age of a user app/models/user.rb:

    def age
      now = Time.now.utc.to_date
      now.year - birthdate.year - ((now.month > birthdate.month || (now.month == birthdate.month && now.day >= birthdate.day)) ? 0 : 1)
    end
    
  2. Create object representing an age group (I'll go with tableless model, but it can be pure ruby object or even struct) app/models/age_group.rb:

    class AgeGroup
      include ActiveModel::Model # not really necessary, but will add some AM functionality which could be nice later
      attr_accessor :from, :to, :count
    end
    
  3. Create service object for computing age groups (Choosing service object is just my personal preference. You can create a helper or whatever you think will fit your needs the best). app/services/age_groups_service.rb:

    class AgeGroupService
      # @params 
      #   ranges - an array of age group ranges. E.g.: [[0, 18], [19, 24], [25, 34], ...]
      #   users - an array of users from which the age groups will be computed. Defaults to all users  
      def initialize(ranges = [], users = User.all.to_a)
        @ranges = ranges
        @users = users
        @age_groups = []
      end
    
      # Count users in each age group range
      # @return
      #   an array of age groups. E.g.: [{ from: 0, to: 18, count: 12 }, ...]
      def call
        @ranges.each do |range|      
          users = @users.select { |user| user.age >= range[0] && user.age <= range[1] }      
          @age_groups << AgeGroup.new(from: range[0], to: range[1], count: users.length)                
       end
       @age_groups
      end
    end
    
  4. Assign value returned by call to the AgeGroupsService to an instance variable in your controller:

    age_group_service = AgeGroupsService.new([[18, 24], [25, 34], [35, 44], [45, 100]])
    @age_groups = age_group_service.call
    
  5. Print out the results in a view:

    <ul>
      <% @age_groups.each do |age_group| %>
        <li><%= age_group.from %> - <%= age_group.to %>: <b><%= age_group.count %></b></li>
      <% end %>
    </ul>
    

Some further ideas / notes:

  • AgeGroup model and AgeGroupsService aren't necessary. You can use PORO and helpers instead or you can even put all the code into User model and relevant controller. Anyway I would strongly recommend against this as it will go messy and it just feels wrong. Age groups seem like something deserving it's own place.
  • This is very simple solution. It will work fine if you don't have a lot of users. This implementation would be problematic in a production app with thousands of users. The problem here is the computational complexity of the algorithm assigning users into age groups. The algorithm iterates through every user for each age group range. You could solve this problem in different ways: 1. improve the algorithm by reducing its complexity or number of users it has to iterate over. My first try would be to make sure the iteration over the ranges will stop as soon as it's clear there aren't any more users for the current range and iterations over all following ranges won't iterate over users already assigned to a range. 2. use SQL for the computation if you aren't afraid of using raw SQL and don't mind loosing the DB agnosticity. You can create a combined query or even a DB view which will return the count of all users with birthday between two dates. 3. Make it work asynchronously. Move the computation into backround (so it won't block the controller) and then load it using async call in Javascript.

Upvotes: 2

Related Questions