Skip to content

kyohah/in_time_scope

Repository files navigation

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

About

No description, website, or topics provided.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

 
 
 

Contributors