in_time_scope 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -2
- data/.rulesync/commands/translate-readme.md +46 -0
- data/{CLAUDE.md → .rulesync/rules/project.md} +23 -7
- data/README.md +104 -223
- data/Steepfile +25 -0
- data/docs/book.toml +14 -0
- data/docs/de/SUMMARY.md +5 -0
- data/docs/de/index.md +192 -0
- data/docs/de/point-system.md +295 -0
- data/docs/de/user-name-history.md +164 -0
- data/docs/fr/SUMMARY.md +5 -0
- data/docs/fr/index.md +192 -0
- data/docs/fr/point-system.md +295 -0
- data/docs/fr/user-name-history.md +164 -0
- data/docs/ja/SUMMARY.md +5 -0
- data/docs/ja/index.md +192 -0
- data/docs/ja/point-system.md +295 -0
- data/docs/ja/user-name-history.md +164 -0
- data/docs/src/SUMMARY.md +5 -0
- data/docs/src/index.md +194 -0
- data/docs/src/point-system.md +295 -0
- data/docs/src/user-name-history.md +164 -0
- data/docs/zh/SUMMARY.md +5 -0
- data/docs/zh/index.md +192 -0
- data/docs/zh/point-system.md +295 -0
- data/docs/zh/user-name-history.md +164 -0
- data/lib/in_time_scope/class_methods.rb +393 -0
- data/lib/in_time_scope/version.rb +2 -1
- data/lib/in_time_scope.rb +115 -213
- data/rbs_collection.yaml +16 -0
- data/rulesync.jsonc +6 -0
- data/sig/in_time_scope.rbs +90 -1
- metadata +75 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f46c50d90f896b920a86f45ea49329a9629843c4c7550e065f22c4b20df35149
|
|
4
|
+
data.tar.gz: 6c927de51e73ba689314a7caaf5f419a23af0f61f35b80c09f1c5d4cfa63273d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d861b025f3f455367ef485e40df6f98d83e55b1fd3c3b5fdf2cc6f57bbae2dd3dec94586bb1265b76859a83a3bd0c782a523315cf5188e3ccf9f62a8dcb6b2f
|
|
7
|
+
data.tar.gz: f712f1b0e82dfa596c1d805ef5f7ddd715bc39eb1fffee564df6eb98a68582f21d1f9822f7980471d91c70823e86c1e693599adcb2d77eb96c909059e7d90576
|
data/.rubocop.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
AllCops:
|
|
2
|
-
TargetRubyVersion: 3.
|
|
2
|
+
TargetRubyVersion: 3.0
|
|
3
3
|
NewCops: enable
|
|
4
4
|
SuggestExtensions: false
|
|
5
5
|
|
|
@@ -16,7 +16,7 @@ Metrics/AbcSize:
|
|
|
16
16
|
Enabled: false
|
|
17
17
|
|
|
18
18
|
Metrics/MethodLength:
|
|
19
|
-
Max:
|
|
19
|
+
Max: 40
|
|
20
20
|
|
|
21
21
|
Metrics/ModuleLength:
|
|
22
22
|
Enabled: false
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
targets:
|
|
3
|
+
- claudecode
|
|
4
|
+
description: Translate README.md and sync docs for all languages (ja, zh, fr, de)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Translate Documentation
|
|
8
|
+
|
|
9
|
+
Translate and sync documentation for multiple languages.
|
|
10
|
+
|
|
11
|
+
## Instructions
|
|
12
|
+
|
|
13
|
+
### 1. Sync English Documentation
|
|
14
|
+
|
|
15
|
+
1. Read the current `README.md`
|
|
16
|
+
2. Copy content to `docs/src/index.md`:
|
|
17
|
+
- Remove the language links line (`[English](README.md) | [日本語]...`)
|
|
18
|
+
- Keep everything else
|
|
19
|
+
|
|
20
|
+
### 2. Translate to Other Languages
|
|
21
|
+
|
|
22
|
+
For each language directory (`docs/ja/`, `docs/zh/`, `docs/fr/`, `docs/de/`):
|
|
23
|
+
|
|
24
|
+
1. Translate `docs/src/index.md` to `docs/{lang}/index.md`
|
|
25
|
+
2. Translate `docs/src/point-system.md` to `docs/{lang}/point-system.md`
|
|
26
|
+
3. Translate `docs/src/user-name-history.md` to `docs/{lang}/user-name-history.md`
|
|
27
|
+
4. Update `docs/{lang}/SUMMARY.md` with translated titles
|
|
28
|
+
|
|
29
|
+
### 3. Translation Guidelines
|
|
30
|
+
|
|
31
|
+
For each translation:
|
|
32
|
+
- Keep all code blocks unchanged
|
|
33
|
+
- Translate all text content naturally (not literal translation)
|
|
34
|
+
- Keep the same markdown structure
|
|
35
|
+
- Keep URLs and links unchanged
|
|
36
|
+
- Do NOT include language links in docs files
|
|
37
|
+
- Translate headings and navigation text in SUMMARY.md
|
|
38
|
+
|
|
39
|
+
### 4. Language Codes
|
|
40
|
+
|
|
41
|
+
- `ja` - Japanese (日本語)
|
|
42
|
+
- `zh` - Chinese (中文)
|
|
43
|
+
- `fr` - French (Français)
|
|
44
|
+
- `de` - German (Deutsch)
|
|
45
|
+
|
|
46
|
+
$ARGUMENTS
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
targets:
|
|
3
|
+
- claudecode
|
|
4
|
+
root: true
|
|
5
|
+
---
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
7
|
+
# Project Overview
|
|
6
8
|
|
|
7
9
|
InTimeScope is a Ruby gem that adds time-window scopes to ActiveRecord models. It provides a convenient way to query records that fall within specific time periods (between `start_at` and `end_at` timestamps), with support for nullable columns, custom column names, and multiple scopes per model.
|
|
8
10
|
|
|
@@ -39,7 +41,7 @@ bundle exec rake install
|
|
|
39
41
|
|
|
40
42
|
## Code Style
|
|
41
43
|
|
|
42
|
-
- Ruby 3.
|
|
44
|
+
- Ruby 3.0+ required
|
|
43
45
|
- Use double-quoted strings (enforced by RuboCop)
|
|
44
46
|
- All files must have `# frozen_string_literal: true` header
|
|
45
47
|
|
|
@@ -47,8 +49,19 @@ bundle exec rake install
|
|
|
47
49
|
|
|
48
50
|
Entry point is `lib/in_time_scope.rb` which defines the `InTimeScope` module. When included in an ActiveRecord model, it provides the `in_time_scope` class method that generates:
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
+
**Primary scopes (records in time window):**
|
|
53
|
+
- `Model.in_time`, `Model.in_time(timestamp)` - class scope
|
|
54
|
+
- `instance.in_time?`, `instance.in_time?(timestamp)` - instance method
|
|
55
|
+
|
|
56
|
+
**Inverse scopes (records outside time window):**
|
|
57
|
+
- `Model.before_in_time` - records not yet started (`start_at > time`)
|
|
58
|
+
- `Model.after_in_time` - records already ended (`end_at <= time`)
|
|
59
|
+
- `Model.out_of_time` - records outside window (before OR after)
|
|
60
|
+
- Corresponding instance methods: `before_in_time?`, `after_in_time?`, `out_of_time?`
|
|
61
|
+
|
|
62
|
+
**Additional scopes for start-only/end-only patterns:**
|
|
63
|
+
- `Model.latest_in_time(:foreign_key)` - latest record per FK (for `has_one`)
|
|
64
|
+
- `Model.earliest_in_time(:foreign_key)` - earliest record per FK
|
|
52
65
|
|
|
53
66
|
The gem auto-detects column nullability from the database schema to generate optimized SQL queries (simpler queries for NOT NULL columns, NULL-aware queries otherwise).
|
|
54
67
|
|
|
@@ -59,6 +72,9 @@ Key configuration options for `in_time_scope`:
|
|
|
59
72
|
|
|
60
73
|
Setting `column: nil` disables that boundary, enabling start-only (history) or end-only (expiration) patterns.
|
|
61
74
|
|
|
75
|
+
Named scopes generate all methods with the scope name:
|
|
76
|
+
- `in_time_scope :published` → `in_time_published`, `before_in_time_published`, `after_in_time_published`, `out_of_time_published`
|
|
77
|
+
|
|
62
78
|
## Test Structure
|
|
63
79
|
|
|
64
80
|
Tests use RSpec with SQLite3 in-memory database. Test models are defined in `spec/support/create_test_database.rb`:
|
data/README.md
CHANGED
|
@@ -1,310 +1,191 @@
|
|
|
1
1
|
# InTimeScope
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.md) | [日本語](docs/ja/index.md) | [中文](docs/zh/index.md) | [Français](docs/fr/index.md) | [Deutsch](docs/de/index.md)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Install the gem and add to the application's Gemfile by executing:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
bundle add in_time_scope
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
gem install in_time_scope
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
### Basic: Nullable Time Window
|
|
22
|
-
Use the defaults (`start_at` / `end_at`) even when the columns allow `NULL`.
|
|
5
|
+
Are you writing this every time in Rails?
|
|
23
6
|
|
|
24
7
|
```ruby
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
t.datetime :end_at, null: true
|
|
28
|
-
|
|
29
|
-
t.timestamps
|
|
30
|
-
end
|
|
8
|
+
# Before
|
|
9
|
+
Event.where("start_at <= ? AND (end_at IS NULL OR end_at > ?)", Time.current, Time.current)
|
|
31
10
|
|
|
11
|
+
# After
|
|
32
12
|
class Event < ActiveRecord::Base
|
|
33
|
-
include InTimeScope
|
|
34
|
-
|
|
35
|
-
# Uses start_at / end_at by default
|
|
36
13
|
in_time_scope
|
|
37
14
|
end
|
|
38
15
|
|
|
39
16
|
Event.in_time
|
|
40
|
-
# => SELECT "events".* FROM "events" WHERE ("events"."start_at" IS NULL OR "events"."start_at" <= '2026-01-24 19:50:05.738232') AND ("events"."end_at" IS NULL OR "events"."end_at" > '2026-01-24 19:50:05.738232')
|
|
41
|
-
|
|
42
|
-
# Check at a specific time
|
|
43
|
-
Event.in_time(Time.parse("2024-06-01 12:00:00"))
|
|
44
|
-
|
|
45
|
-
# Is the current time within the window?
|
|
46
|
-
event = Event.first
|
|
47
|
-
event.in_time?
|
|
48
|
-
#=> true or false
|
|
49
|
-
|
|
50
|
-
# Check any arbitrary timestamp
|
|
51
|
-
event.in_time?(Time.parse("2024-06-01 12:00:00"))
|
|
52
|
-
#=> true or false
|
|
53
17
|
```
|
|
54
18
|
|
|
55
|
-
|
|
56
|
-
When both timestamps are required (no `NULL`s), the generated query is simpler and faster.
|
|
19
|
+
That's it. One line of DSL, zero raw SQL in your models.
|
|
57
20
|
|
|
58
|
-
|
|
59
|
-
create_table :events do |t|
|
|
60
|
-
t.datetime :start_at, null: false
|
|
61
|
-
t.datetime :end_at, null: false
|
|
21
|
+
**This is a simple, thin gem that just provides scopes. No learning curve required.**
|
|
62
22
|
|
|
63
|
-
|
|
64
|
-
end
|
|
23
|
+
## Why This Gem?
|
|
65
24
|
|
|
66
|
-
|
|
67
|
-
Event.in_time
|
|
68
|
-
# => SELECT "events".* FROM "events" WHERE ("events"."start_at" <= '2026-01-24 19:50:05.738232') AND ("events"."end_at" > '2026-01-24 19:50:05.738232')
|
|
25
|
+
This gem exists to:
|
|
69
26
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
27
|
+
- **Keep time-range logic consistent** across your entire codebase
|
|
28
|
+
- **Avoid copy-paste SQL** that's easy to get wrong
|
|
29
|
+
- **Make time a first-class domain concept** with named scopes like `in_time_published`
|
|
30
|
+
- **Auto-detect nullability** from your schema for optimized queries
|
|
73
31
|
|
|
74
|
-
|
|
75
|
-
include InTimeScope
|
|
76
|
-
|
|
77
|
-
# Explicitly mark columns as NOT NULL (even if the DB allows NULL)
|
|
78
|
-
in_time_scope start_at: { null: false }, end_at: { null: false }
|
|
79
|
-
end
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### Options Reference
|
|
83
|
-
Use these options in `in_time_scope` to customize column behavior.
|
|
32
|
+
## Recommended For
|
|
84
33
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
| `start_at: { column: ... }` | start_at | `Symbol` / `nil` | `:start_at` (or `:"<scope>_start_at"` when `:scope_name` is set) | Use a custom column name; set `nil` to disable `start_at` | `start_at: { column: :available_at }` |
|
|
89
|
-
| `end_at: { column: ... }` | end_at | `Symbol` / `nil` | `:end_at` (or `:"<scope>_end_at"` when `:scope_name` is set) | Use a custom column name; set `nil` to disable `end_at` | `end_at: { column: nil }` |
|
|
90
|
-
| `start_at: { null: ... }` | start_at | `true/false` | auto (schema) | Force NULL-aware vs NOT NULL behavior | `start_at: { null: false }` |
|
|
91
|
-
| `end_at: { null: ... }` | end_at | `true/false` | auto (schema) | Force NULL-aware vs NOT NULL behavior | `end_at: { null: true }` |
|
|
92
|
-
| `prefix: true` | scope_name | `true/false` | `false` | Use prefix style method name like `published_in_time` instead of `in_time_published` | `in_time_scope :published, prefix: true` |
|
|
34
|
+
- New Rails applications with validity periods
|
|
35
|
+
- Models with `start_at` / `end_at` columns
|
|
36
|
+
- Teams that want consistent time logic without scattered `where` clauses
|
|
93
37
|
|
|
94
|
-
|
|
95
|
-
Use this when periods never overlap and you want exactly one "current" row.
|
|
38
|
+
## Installation
|
|
96
39
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
- the latest row is the current one
|
|
40
|
+
```bash
|
|
41
|
+
bundle add in_time_scope
|
|
42
|
+
```
|
|
101
43
|
|
|
102
|
-
|
|
44
|
+
## Quick Start
|
|
103
45
|
|
|
104
46
|
```ruby
|
|
105
47
|
class Event < ActiveRecord::Base
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
# Ignore end_at even if the column exists
|
|
109
|
-
in_time_scope start_at: { null: false }, end_at: { column: nil }
|
|
48
|
+
in_time_scope
|
|
110
49
|
end
|
|
111
50
|
|
|
112
|
-
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
# Use .first with order to get the most recent single record
|
|
116
|
-
Event.in_time.order(start_at: :desc).first
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
With no `end_at`, each row implicitly ends at the next row's `start_at`.
|
|
120
|
-
The scope returns all matching records (WHERE only, no ORDER), so:
|
|
121
|
-
- Add `.order(start_at: :desc).first` for a single latest record
|
|
122
|
-
- Use `latest_in_time` for efficient `has_one` associations
|
|
51
|
+
# Class scope
|
|
52
|
+
Event.in_time # Records active now
|
|
53
|
+
Event.in_time(Time.parse("2024-06-01")) # Records active at specific time
|
|
123
54
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
CREATE INDEX index_events_on_start_at ON events (start_at);
|
|
55
|
+
# Instance method
|
|
56
|
+
event.in_time? # Is this record active now?
|
|
57
|
+
event.in_time?(some_time) # Was it active at that time?
|
|
128
58
|
```
|
|
129
59
|
|
|
130
|
-
|
|
131
|
-
Use this when a record is active immediately and expires at `end_at`.
|
|
60
|
+
## Features
|
|
132
61
|
|
|
133
|
-
|
|
134
|
-
- `start_at` is not used (implicit "always active")
|
|
135
|
-
- `end_at` can be `NULL` for "never expires"
|
|
62
|
+
### Auto-Optimized SQL
|
|
136
63
|
|
|
137
|
-
|
|
64
|
+
The gem reads your schema and generates the right SQL:
|
|
138
65
|
|
|
139
66
|
```ruby
|
|
140
|
-
|
|
141
|
-
|
|
67
|
+
# NULL-allowed columns → NULL-aware query
|
|
68
|
+
WHERE (start_at IS NULL OR start_at <= ?) AND (end_at IS NULL OR end_at > ?)
|
|
142
69
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
Event.in_time(Time.parse("2024-06-01 12:00:00"))
|
|
148
|
-
# => SELECT "events".* FROM "events" WHERE ("events"."end_at" IS NULL OR "events"."end_at" > '2024-06-01 12:00:00.000000')
|
|
70
|
+
# NOT NULL columns → simple query
|
|
71
|
+
WHERE start_at <= ? AND end_at > ?
|
|
149
72
|
```
|
|
150
73
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
```sql
|
|
154
|
-
CREATE INDEX index_events_on_end_at ON events (end_at);
|
|
155
|
-
```
|
|
74
|
+
### Named Scopes
|
|
156
75
|
|
|
157
|
-
|
|
158
|
-
Customize which columns are used and define more than one time window per model.
|
|
76
|
+
Multiple time windows per model:
|
|
159
77
|
|
|
160
78
|
```ruby
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
t.datetime :published_start_at, null: false
|
|
165
|
-
t.datetime :published_end_at, null: false
|
|
166
|
-
|
|
167
|
-
t.timestamps
|
|
79
|
+
class Article < ActiveRecord::Base
|
|
80
|
+
in_time_scope :published # → Article.in_time_published
|
|
81
|
+
in_time_scope :featured # → Article.in_time_featured
|
|
168
82
|
end
|
|
169
|
-
|
|
170
|
-
class Event < ActiveRecord::Base
|
|
171
|
-
include InTimeScope
|
|
172
|
-
|
|
173
|
-
# Use different column names
|
|
174
|
-
in_time_scope start_at: { column: :available_at }, end_at: { column: :expired_at }
|
|
175
|
-
|
|
176
|
-
# Define an additional scope - uses published_start_at / published_end_at by default
|
|
177
|
-
in_time_scope :published
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
Event.in_time
|
|
181
|
-
# => uses available_at / expired_at
|
|
182
|
-
|
|
183
|
-
Event.in_time_published
|
|
184
|
-
# => uses published_start_at / published_end_at
|
|
185
83
|
```
|
|
186
84
|
|
|
187
|
-
###
|
|
188
|
-
Use the `prefix: true` option if you prefer the scope name as a prefix instead of suffix.
|
|
85
|
+
### Custom Columns
|
|
189
86
|
|
|
190
87
|
```ruby
|
|
191
|
-
class
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
# With prefix: true, the method name becomes published_in_time instead of in_time_published
|
|
195
|
-
in_time_scope :published, prefix: true
|
|
88
|
+
class Campaign < ActiveRecord::Base
|
|
89
|
+
in_time_scope start_at: { column: :available_at },
|
|
90
|
+
end_at: { column: :expired_at }
|
|
196
91
|
end
|
|
197
|
-
|
|
198
|
-
Event.published_in_time
|
|
199
|
-
# => uses published_start_at / published_end_at
|
|
200
92
|
```
|
|
201
93
|
|
|
202
|
-
###
|
|
203
|
-
|
|
204
|
-
The start-only pattern provides scopes for `has_one` associations:
|
|
94
|
+
### Start-Only Pattern (Version History)
|
|
205
95
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
`in_time` provides WHERE only. Add `order` externally:
|
|
96
|
+
For records where each row is valid until the next one:
|
|
209
97
|
|
|
210
98
|
```ruby
|
|
211
99
|
class Price < ActiveRecord::Base
|
|
212
|
-
include InTimeScope
|
|
213
|
-
belongs_to :user
|
|
214
|
-
|
|
215
100
|
in_time_scope start_at: { null: false }, end_at: { column: nil }
|
|
216
101
|
end
|
|
217
102
|
|
|
103
|
+
# Bonus: efficient has_one with NOT EXISTS
|
|
218
104
|
class User < ActiveRecord::Base
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
# in_time is WHERE only, add order externally
|
|
222
|
-
has_one :current_price,
|
|
223
|
-
-> { in_time.order(start_at: :desc) },
|
|
224
|
-
class_name: "Price"
|
|
105
|
+
has_one :current_price, -> { latest_in_time(:user_id) }, class_name: "Price"
|
|
225
106
|
end
|
|
107
|
+
|
|
108
|
+
User.includes(:current_price) # No N+1, fetches only latest per user
|
|
226
109
|
```
|
|
227
110
|
|
|
228
|
-
|
|
111
|
+
### End-Only Pattern (Expiration)
|
|
229
112
|
|
|
230
|
-
|
|
113
|
+
For records that are active until they expire:
|
|
231
114
|
|
|
232
115
|
```ruby
|
|
233
|
-
class
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
# Uses NOT EXISTS subquery - only loads the latest record per user
|
|
237
|
-
has_one :current_price,
|
|
238
|
-
-> { latest_in_time(:user_id) },
|
|
239
|
-
class_name: "Price"
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
# Direct access
|
|
243
|
-
user.current_price
|
|
244
|
-
# => Returns the most recent price where start_at <= Time.current
|
|
245
|
-
|
|
246
|
-
# Efficient with includes (only fetches latest record per user from DB)
|
|
247
|
-
User.includes(:current_price).each do |user|
|
|
248
|
-
puts user.current_price&.amount
|
|
116
|
+
class Coupon < ActiveRecord::Base
|
|
117
|
+
in_time_scope start_at: { column: nil }, end_at: { null: false }
|
|
249
118
|
end
|
|
250
119
|
```
|
|
251
120
|
|
|
252
|
-
|
|
121
|
+
### Inverse Scopes
|
|
253
122
|
|
|
254
|
-
|
|
123
|
+
Query records outside the time window:
|
|
255
124
|
|
|
256
125
|
```ruby
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
# Uses NOT EXISTS subquery - only loads the earliest record per user
|
|
261
|
-
has_one :first_price,
|
|
262
|
-
-> { earliest_in_time(:user_id) },
|
|
263
|
-
class_name: "Price"
|
|
264
|
-
end
|
|
126
|
+
# Records not yet started (start_at > time)
|
|
127
|
+
Event.before_in_time
|
|
128
|
+
event.before_in_time?
|
|
265
129
|
|
|
266
|
-
#
|
|
267
|
-
|
|
268
|
-
|
|
130
|
+
# Records already ended (end_at <= time)
|
|
131
|
+
Event.after_in_time
|
|
132
|
+
event.after_in_time?
|
|
269
133
|
|
|
270
|
-
#
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
end
|
|
134
|
+
# Records outside time window (before OR after)
|
|
135
|
+
Event.out_of_time
|
|
136
|
+
event.out_of_time? # Logical inverse of in_time?
|
|
274
137
|
```
|
|
275
138
|
|
|
276
|
-
|
|
139
|
+
Works with named scopes too:
|
|
277
140
|
|
|
278
|
-
|
|
141
|
+
```ruby
|
|
142
|
+
Article.before_in_time_published # Not yet published
|
|
143
|
+
Article.after_in_time_published # Publication ended
|
|
144
|
+
Article.out_of_time_published # Not currently published
|
|
145
|
+
```
|
|
279
146
|
|
|
280
|
-
|
|
147
|
+
## Options Reference
|
|
281
148
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
149
|
+
| Option | Default | Description | Example |
|
|
150
|
+
| --- | --- | --- | --- |
|
|
151
|
+
| `scope_name` (1st arg) | `:in_time` | Named scope like `in_time_published` | `in_time_scope :published` |
|
|
152
|
+
| `start_at: { column: }` | `:start_at` | Custom column name, `nil` to disable | `start_at: { column: :available_at }` |
|
|
153
|
+
| `end_at: { column: }` | `:end_at` | Custom column name, `nil` to disable | `end_at: { column: nil }` |
|
|
154
|
+
| `start_at: { null: }` | auto-detect | Force NULL handling | `start_at: { null: false }` |
|
|
155
|
+
| `end_at: { null: }` | auto-detect | Force NULL handling | `end_at: { null: true }` |
|
|
285
156
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
# => InTimeScope::ColumnNotFoundError: Column 'hoge_start_at' does not exist on table 'events'
|
|
290
|
-
```
|
|
157
|
+
## Acknowledgements
|
|
158
|
+
|
|
159
|
+
Inspired by [onk/shibaraku](https://github.com/onk/shibaraku). This gem extends the concept with:
|
|
291
160
|
|
|
292
|
-
|
|
161
|
+
- Schema-aware NULL handling for optimized queries
|
|
162
|
+
- Multiple named scopes per model
|
|
163
|
+
- Start-only / End-only patterns
|
|
164
|
+
- `latest_in_time` / `earliest_in_time` for efficient `has_one` associations
|
|
165
|
+
- Inverse scopes: `before_in_time`, `after_in_time`, `out_of_time`
|
|
293
166
|
|
|
294
167
|
## Development
|
|
295
168
|
|
|
296
|
-
|
|
169
|
+
```bash
|
|
170
|
+
# Install dependencies
|
|
171
|
+
bin/setup
|
|
297
172
|
|
|
298
|
-
|
|
173
|
+
# Run tests
|
|
174
|
+
bundle exec rspec
|
|
299
175
|
|
|
300
|
-
|
|
176
|
+
# Run linting
|
|
177
|
+
bundle exec rubocop
|
|
301
178
|
|
|
302
|
-
|
|
179
|
+
# Generate CLAUDE.md (for AI coding assistants)
|
|
180
|
+
npx rulesync generate
|
|
181
|
+
```
|
|
303
182
|
|
|
304
|
-
|
|
183
|
+
This project uses [rulesync](https://github.com/dyoshikawa/rulesync) to manage AI assistant rules. Edit `.rulesync/rules/*.md` and run `npx rulesync generate` to update `CLAUDE.md`.
|
|
305
184
|
|
|
306
|
-
|
|
185
|
+
## Contributing
|
|
307
186
|
|
|
308
|
-
|
|
187
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/kyohah/in_time_scope).
|
|
188
|
+
|
|
189
|
+
## License
|
|
309
190
|
|
|
310
|
-
|
|
191
|
+
MIT License
|
data/Steepfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Steepfile for InTimeScope type checking
|
|
4
|
+
|
|
5
|
+
target :lib do
|
|
6
|
+
signature "sig"
|
|
7
|
+
|
|
8
|
+
check "lib"
|
|
9
|
+
|
|
10
|
+
# Use RBS collection for external gem types
|
|
11
|
+
collection_config "rbs_collection.yaml"
|
|
12
|
+
|
|
13
|
+
# Configure libraries
|
|
14
|
+
library "time"
|
|
15
|
+
|
|
16
|
+
# Ignore implementation details that use ActiveRecord internals
|
|
17
|
+
# The public API is properly typed, but internal methods use
|
|
18
|
+
# dynamic ActiveRecord features that are hard to type statically
|
|
19
|
+
configure_code_diagnostics do |hash|
|
|
20
|
+
# Allow untyped method calls for ActiveRecord dynamic methods
|
|
21
|
+
hash[Steep::Diagnostic::Ruby::NoMethod] = :hint
|
|
22
|
+
hash[Steep::Diagnostic::Ruby::UnknownInstanceVariable] = :hint
|
|
23
|
+
hash[Steep::Diagnostic::Ruby::RequiredBlockMissing] = :hint
|
|
24
|
+
end
|
|
25
|
+
end
|
data/docs/book.toml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[book]
|
|
2
|
+
title = "InTimeScope"
|
|
3
|
+
authors = ["kyohah"]
|
|
4
|
+
language = "en"
|
|
5
|
+
src = "src"
|
|
6
|
+
|
|
7
|
+
[build]
|
|
8
|
+
build-dir = "book"
|
|
9
|
+
|
|
10
|
+
[output.html]
|
|
11
|
+
default-theme = "light"
|
|
12
|
+
preferred-dark-theme = "navy"
|
|
13
|
+
git-repository-url = "https://github.com/kyohah/in_time_scope"
|
|
14
|
+
edit-url-template = "https://github.com/kyohah/in_time_scope/edit/main/docs/{path}"
|