0.0
A long-lived project that still receives updates
InTimeScope provides time-window scopes for ActiveRecord models.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 0
~> 13.0
>= 0
~> 1.21
>= 0
>= 0
>= 0

Runtime

 Project Readme

InTimeScope

English | 日本語 | 中文 | Français | Deutsch

Are you writing this every time in Rails?

# Before
Event.where("start_at <= ? AND (end_at IS NULL OR end_at > ?)", Time.current, Time.current)

# After
class Event < ActiveRecord::Base
  in_time_scope
end

Event.in_time

That's it. One line of DSL, zero raw SQL in your models.

This is a simple, thin gem that just provides scopes. No learning curve required.

Why This Gem?

This gem exists to:

  • Keep time-range logic consistent across your entire codebase
  • Avoid copy-paste SQL that's easy to get wrong
  • Make time a first-class domain concept with named scopes like in_time_published
  • Auto-detect nullability from your schema for optimized queries

Recommended For

  • New Rails applications with validity periods
  • Models with start_at / end_at columns
  • Teams that want consistent time logic without scattered where clauses

Installation

bundle add in_time_scope

Quick Start

class Event < ActiveRecord::Base
  in_time_scope
end

# Class scope
Event.in_time                          # Records active now
Event.in_time(Time.parse("2024-06-01")) # Records active at specific time

# Instance method
event.in_time?                          # Is this record active now?
event.in_time?(some_time)               # Was it active at that time?

Features

Auto-Optimized SQL

The gem reads your schema and generates the right SQL:

# NULL-allowed columns → NULL-aware query
WHERE (start_at IS NULL OR start_at <= ?) AND (end_at IS NULL OR end_at > ?)

# NOT NULL columns → simple query
WHERE start_at <= ? AND end_at > ?

Named Scopes

Multiple time windows per model:

class Article < ActiveRecord::Base
  in_time_scope :published   # → Article.in_time_published
  in_time_scope :featured    # → Article.in_time_featured
end

Custom Columns

class Campaign < ActiveRecord::Base
  in_time_scope start_at: { column: :available_at },
                end_at: { column: :expired_at }
end

Start-Only Pattern (Version History)

For records where each row is valid until the next one:

id user_id amount start_at
1 1 100 2024-10-01
2 1 120 2024-10-10
3 1 150 2024-10-15
# Time.current = 2024-10-20 → row id=3 (latest) is selected
Price.in_time

# 2024-10-12 → row id=2 (latest before 10/12) is selected
Price.in_time(Time.parse("2024-10-12"))
class Price < ActiveRecord::Base
  in_time_scope start_at: { null: false }, end_at: { column: nil }
end

# Bonus: efficient has_one with NOT EXISTS
class User < ActiveRecord::Base
  has_one :current_price, -> { latest_in_time(:user_id) }, class_name: "Price"
end

User.includes(:current_price)  # No N+1, fetches only latest per user

latest_in_time — latest record per FK before the given time:

flowchart LR
    A["id=1\n10/01"] --> B["id=2\n10/10"] --> C["id=3\n10/15"]
    T(["⏱ time=10/20"]) -.->|latest_in_time| C
Loading
──────●──────────●──────────●──────────▶ time
    10/01      10/10      10/15    10/20(now)
    id=1       id=2       id=3
                             ↑
                       latest_in_time

earliest_in_time — earliest record per FK before the given time:

flowchart LR
    A["id=1\n10/01"] --> B["id=2\n10/10"] --> C["id=3\n10/15"]
    T(["⏱ time=10/20"]) -.->|earliest_in_time| A
Loading
──────●──────────●──────────●──────────▶ time
    10/01      10/10      10/15    10/20(now)
    id=1       id=2       id=3
      ↑
earliest_in_time

End-Only Pattern (Expiration)

For records that are active until they expire:

id code expired_at
1 ABC 2024-11-30
2 XYZ 2024-10-05
3 DEF 2024-12-31
# Time.current = 2024-10-20 → id=1 (ABC) and id=3 (DEF) selected (not yet expired)
Coupon.in_time

# 2024-10-06 → id=1 (ABC) and id=3 (DEF) selected (XYZ expired on 10/05)
Coupon.in_time(Time.parse("2024-10-06"))
class Coupon < ActiveRecord::Base
  in_time_scope start_at: { column: nil }, end_at: { null: false }
end

Inverse Scopes

flowchart LR
    A["before_in_time"] -->|"● start_at"| B["in_time"] -->|"○ end_at"| C["after_in_time"]
    D["out_of_time"] -->|"● start_at"| B
    B -->|"○ end_at"| E["out_of_time"]
Loading
before_in_time │       in_time       │ after_in_time
───────────────●─────────────────────○──────────────▶ time
             start_at             end_at

   out_of_time │       in_time       │  out_of_time
───────────────●─────────────────────○──────────────▶ time

Query records outside the time window:

# Records not yet started (start_at > time)
Event.before_in_time
event.before_in_time?

# Records already ended (end_at <= time)
Event.after_in_time
event.after_in_time?

# Records outside time window (before OR after)
Event.out_of_time
event.out_of_time?  # Logical inverse of in_time?

Works with named scopes too:

Article.before_in_time_published  # Not yet published
Article.after_in_time_published   # Publication ended
Article.out_of_time_published     # Not currently published

Options Reference

Option Default Description Example
scope_name (1st arg) :in_time Named scope like in_time_published in_time_scope :published
start_at: { column: } :start_at Custom column name, nil to disable start_at: { column: :available_at }
end_at: { column: } :end_at Custom column name, nil to disable end_at: { column: nil }
start_at: { null: } auto-detect Force NULL handling start_at: { null: false }
end_at: { null: } auto-detect Force NULL handling end_at: { null: true }

Acknowledgements

Inspired by onk/shibaraku. This gem extends the concept with:

  • Schema-aware NULL handling for optimized queries
  • Multiple named scopes per model
  • Start-only / End-only patterns
  • latest_in_time / earliest_in_time for efficient has_one associations
  • Inverse scopes: before_in_time, after_in_time, out_of_time

Development

# Install dependencies
bin/setup

# Run tests
bundle exec rspec

# Run linting
bundle exec rubocop

# Generate CLAUDE.md (for AI coding assistants)
npx rulesync generate

This project uses rulesync to manage AI assistant rules. Edit .rulesync/rules/*.md and run npx rulesync generate to update CLAUDE.md.

Contributing

Bug reports and pull requests are welcome on GitHub.

License

MIT License