Creating Searchable Scopes by Age in Rails When You Only Have Date of Birth
Published: 2023-11-08 01:25:43

When working with user databases in Ruby on Rails, it's common to store a user's date of birth (DOB) rather than their age, since the latter changes over time and would require constant updates. However, there are scenarios where we need to filter users based on their age, such as in search forms. How do we accomplish this when we only have the dob column in our database? Let's explore a practical way to create searchable scopes by age.

## Understanding Scopes in Rails
Scopes in Rails are custom queries that you define inside your model to encapsulate commonly used queries. They allow you to keep your code DRY (Don't Repeat Yourself) and your model logic clean. A scope takes in a condition and returns an `ActiveRecord::Relation`, which can be further chained with more query methods.

## The Age Calculation Challenge
The challenge is to calculate the age based on the stored dob and create scopes that allow searching for users above or below a certain age. We can achieve this with two scopes: `age_gt` (age greater than) and `age_lt` (age less than).

Here's how to define these scopes in your Rails model:

```Ruby
class User < ApplicationRecord
  # Scope to find users older than a given age
  scope :age_gt, ->(age) { where("dob <= ?", age.to_i.years.ago.to_date) }
  
  # Scope to find users younger than a given age
  scope :age_lt, ->(age) { where("dob >= ?", age.to_i.years.ago.to_date) }

  # Make scopes available for search (for Ransack, etc.)
  def self.ransackable_scopes(auth_object = nil)
    %w[age_gt age_lt]
  end
end

```

## How Do These Scopes Work?

- `age_gt`: Takes an integer as age and returns users whose DOB is on or before the date age years ago from today. For instance, `User.age_gt(18)` returns users who are 18 or older.

- `age_lt`: Similarly, it takes an age and returns users whose DOB is on or after the date age years ago. For example, `User.age_lt(30) `returns users who are younger than 30.

## Integrating Scopes with Forms
To use these scopes in a form, you'd typically have a slider or input field that allows the user to set a minimum and maximum age range. Here's an example using form helpers in Rails:

```erb
<%= form_with model: User, url: search_path, method: :get do |f| %>
  <label for="age_min">Age min</label>
  <%= f.range_field :age_gt, in: 18..60, step: 1, value: @age_gt, class: "form-range", id: "age_min", oninput: "updateSlider()" %>
  <span id="age">18</span><br>

  <label for="age_max">Age max</label>
  <%= f.range_field :age_lt, in: 18..60, step: 1, value: @age_lt, class: "form-range", id: "age_max", oninput: "updateSlider2()" %>
  <span id="age2">60</span>
<% end %>

```

## Why Use Scopes?
Using scopes for such queries provides several benefits:

- Reusability: Once defined, scopes can be used throughout your application without rewriting the query.

- Chainability: Scopes return `ActiveRecord::Relation` objects, which means you can chain them with other query methods or scopes.

- Readability: Scopes make your model methods and your search implementations more readable and intent-expressive.

## Conclusion
When you don't have a direct column to search by in your Rails application, like age, you can creatively use scopes to define the desired search logic. By leveraging Ruby's powerful date handling and ActiveRecord's `where` clause, we can easily search by computed values like age, even when we only have a date of birth in our database. This approach maintains the integrity of your database and the flexibility of your search capabilities.

In this blog post, the Rails application is assumed to be using Ransack gem for handling advanced search forms. Ransack utilizes named scopes and allows for easy integration of complex search functionality in Rails applications.