in_time_scope 0.1.5 → 0.1.7
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 +5 -1
- data/.rulesync/commands/translate-readme.md +46 -0
- data/{CLAUDE.md → .rulesync/rules/project.md} +23 -7
- data/README.md +104 -221
- 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 +183 -91
- data/lib/in_time_scope/version.rb +1 -1
- data/rulesync.jsonc +6 -0
- data/sig/in_time_scope.rbs +24 -14
- metadata +41 -4
|
@@ -21,8 +21,6 @@ module InTimeScope
|
|
|
21
21
|
# @option end_at [Boolean] :null Whether the column allows NULL values
|
|
22
22
|
# (auto-detected from schema if not specified)
|
|
23
23
|
#
|
|
24
|
-
# @param prefix [Boolean] If true, creates +<scope_name>_in_time+ instead of +in_time_<scope_name>+
|
|
25
|
-
#
|
|
26
24
|
# @raise [ColumnNotFoundError] When a specified column doesn't exist (at class load time)
|
|
27
25
|
# @raise [ConfigurationError] When both columns are nil, or when using start-only/end-only
|
|
28
26
|
# pattern with a nullable column (at scope call time)
|
|
@@ -56,7 +54,7 @@ module InTimeScope
|
|
|
56
54
|
# in_time_scope start_at: { column: nil }, end_at: { null: false }
|
|
57
55
|
# # Also creates: Model.latest_in_time(:foreign_key), Model.earliest_in_time(:foreign_key)
|
|
58
56
|
#
|
|
59
|
-
def in_time_scope(scope_name = :in_time, start_at: {}, end_at: {}
|
|
57
|
+
def in_time_scope(scope_name = :in_time, start_at: {}, end_at: {})
|
|
60
58
|
table_column_hash = columns_hash
|
|
61
59
|
time_column_prefix = scope_name == :in_time ? "" : "#{scope_name}_"
|
|
62
60
|
|
|
@@ -66,10 +64,8 @@ module InTimeScope
|
|
|
66
64
|
start_at_null = fetch_null_option(start_at, start_at_column, table_column_hash)
|
|
67
65
|
end_at_null = fetch_null_option(end_at, end_at_column, table_column_hash)
|
|
68
66
|
|
|
69
|
-
scope_method_name = method_name(scope_name, prefix)
|
|
70
|
-
|
|
71
67
|
define_scope_methods(
|
|
72
|
-
|
|
68
|
+
scope_name == :in_time ? "" : "_#{scope_name}",
|
|
73
69
|
start_at_column: start_at_column,
|
|
74
70
|
start_at_null: start_at_null,
|
|
75
71
|
end_at_column: end_at_column,
|
|
@@ -97,96 +93,97 @@ module InTimeScope
|
|
|
97
93
|
column_info.null
|
|
98
94
|
end
|
|
99
95
|
|
|
100
|
-
# Generates the method name for the scope
|
|
101
|
-
#
|
|
102
|
-
# @param scope_name [Symbol] The scope name
|
|
103
|
-
# @param prefix [Boolean] Whether to use prefix style
|
|
104
|
-
# @return [Symbol] The generated method name
|
|
105
|
-
# @api private
|
|
106
|
-
def method_name(scope_name, prefix)
|
|
107
|
-
return :in_time if scope_name == :in_time
|
|
108
|
-
|
|
109
|
-
prefix ? "#{scope_name}_in_time" : "in_time_#{scope_name}"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
96
|
# Defines the appropriate scope methods based on configuration
|
|
113
97
|
#
|
|
114
|
-
# @param
|
|
98
|
+
# @param suffix [String] The suffix for method names ("" or "_#{scope_name}")
|
|
115
99
|
# @param start_at_column [Symbol, nil] Start column name
|
|
116
100
|
# @param start_at_null [Boolean, nil] Whether start column allows NULL
|
|
117
101
|
# @param end_at_column [Symbol, nil] End column name
|
|
118
102
|
# @param end_at_null [Boolean, nil] Whether end column allows NULL
|
|
119
103
|
# @return [void]
|
|
120
104
|
# @api private
|
|
121
|
-
def define_scope_methods(
|
|
105
|
+
def define_scope_methods(suffix, start_at_column:, start_at_null:, end_at_column:, end_at_null:)
|
|
122
106
|
# Define class-level scope and instance method
|
|
123
107
|
if start_at_column.nil? && end_at_column.nil?
|
|
124
|
-
define_error_scope_and_method(
|
|
108
|
+
define_error_scope_and_method(suffix,
|
|
125
109
|
"At least one of start_at or end_at must be specified")
|
|
126
110
|
elsif end_at_column.nil?
|
|
127
111
|
# Start-only pattern (history tracking) - requires non-nullable column
|
|
128
112
|
if start_at_null
|
|
129
|
-
define_error_scope_and_method(
|
|
113
|
+
define_error_scope_and_method(suffix,
|
|
130
114
|
"Start-only pattern requires non-nullable column. " \
|
|
131
115
|
"Set `start_at: { null: false }` or add an end_at column")
|
|
132
116
|
else
|
|
133
|
-
define_start_only_scope(
|
|
134
|
-
define_instance_method(
|
|
117
|
+
define_start_only_scope(suffix, start_at_column)
|
|
118
|
+
define_instance_method(suffix, start_at_column, start_at_null, end_at_column, end_at_null)
|
|
119
|
+
define_latest_one_scope(suffix, start_at_column)
|
|
120
|
+
define_earliest_one_scope(suffix, start_at_column)
|
|
121
|
+
define_before_scope(suffix, start_at_column, start_at_null)
|
|
122
|
+
define_after_scope(suffix, end_at_column, end_at_null)
|
|
123
|
+
define_out_of_time_scope(suffix)
|
|
135
124
|
end
|
|
136
125
|
elsif start_at_column.nil?
|
|
137
126
|
# End-only pattern (expiration) - requires non-nullable column
|
|
138
127
|
if end_at_null
|
|
139
|
-
define_error_scope_and_method(
|
|
128
|
+
define_error_scope_and_method(suffix,
|
|
140
129
|
"End-only pattern requires non-nullable column. " \
|
|
141
130
|
"Set `end_at: { null: false }` or add a start_at column")
|
|
142
131
|
else
|
|
143
|
-
define_end_only_scope(
|
|
144
|
-
define_instance_method(
|
|
132
|
+
define_end_only_scope(suffix, end_at_column)
|
|
133
|
+
define_instance_method(suffix, start_at_column, start_at_null, end_at_column, end_at_null)
|
|
134
|
+
define_latest_one_scope(suffix, end_at_column)
|
|
135
|
+
define_earliest_one_scope(suffix, end_at_column)
|
|
136
|
+
define_before_scope(suffix, start_at_column, start_at_null)
|
|
137
|
+
define_after_scope(suffix, end_at_column, end_at_null)
|
|
138
|
+
define_out_of_time_scope(suffix)
|
|
145
139
|
end
|
|
146
140
|
else
|
|
147
141
|
# Both start and end
|
|
148
|
-
define_full_scope(
|
|
149
|
-
define_instance_method(
|
|
142
|
+
define_full_scope(suffix, start_at_column, start_at_null, end_at_column, end_at_null)
|
|
143
|
+
define_instance_method(suffix, start_at_column, start_at_null, end_at_column, end_at_null)
|
|
144
|
+
define_before_scope(suffix, start_at_column, start_at_null)
|
|
145
|
+
define_after_scope(suffix, end_at_column, end_at_null)
|
|
146
|
+
define_out_of_time_scope(suffix)
|
|
150
147
|
end
|
|
151
148
|
end
|
|
152
149
|
|
|
153
150
|
# Defines a scope and instance method that raise ConfigurationError
|
|
154
151
|
#
|
|
155
|
-
# @param
|
|
152
|
+
# @param suffix [String] The suffix for method names
|
|
156
153
|
# @param message [String] The error message
|
|
157
154
|
# @return [void]
|
|
158
155
|
# @api private
|
|
159
|
-
def define_error_scope_and_method(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
156
|
+
def define_error_scope_and_method(suffix, message)
|
|
157
|
+
method_names = [
|
|
158
|
+
:"in_time#{suffix}",
|
|
159
|
+
:"before_in_time#{suffix}",
|
|
160
|
+
:"after_in_time#{suffix}",
|
|
161
|
+
:"out_of_time#{suffix}"
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
method_names.each do |method_name|
|
|
165
|
+
scope method_name, ->(_time = Time.current) {
|
|
166
|
+
raise InTimeScope::ConfigurationError, message
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
define_method("#{method_name}?") do |_time = Time.current|
|
|
170
|
+
raise InTimeScope::ConfigurationError, message
|
|
171
|
+
end
|
|
168
172
|
end
|
|
169
173
|
end
|
|
170
174
|
|
|
171
175
|
# Defines a start-only scope (for history tracking pattern)
|
|
172
176
|
#
|
|
173
|
-
# @param
|
|
177
|
+
# @param suffix [String] The suffix for method names
|
|
174
178
|
# @param column [Symbol] The start column name
|
|
175
179
|
# @return [void]
|
|
176
180
|
# @api private
|
|
177
|
-
def define_start_only_scope(
|
|
178
|
-
col = column
|
|
179
|
-
|
|
181
|
+
def define_start_only_scope(suffix, column)
|
|
180
182
|
# Simple scope - WHERE only, no ORDER BY
|
|
181
183
|
# Users can add .order(start_at: :desc) externally if needed
|
|
182
|
-
scope
|
|
183
|
-
where(
|
|
184
|
+
scope :"in_time#{suffix}", ->(time = Time.current) {
|
|
185
|
+
where(column => ..time)
|
|
184
186
|
}
|
|
185
|
-
|
|
186
|
-
# Efficient scope for has_one + includes using NOT EXISTS subquery
|
|
187
|
-
# Usage: has_one :current_price, -> { latest_in_time(:user_id) }, class_name: 'Price'
|
|
188
|
-
define_latest_one_scope(scope_method_name, column)
|
|
189
|
-
define_earliest_one_scope(scope_method_name, column)
|
|
190
187
|
end
|
|
191
188
|
|
|
192
189
|
# Defines the latest_in_time scope using NOT EXISTS subquery
|
|
@@ -194,7 +191,13 @@ module InTimeScope
|
|
|
194
191
|
# This scope efficiently finds the latest record per foreign key,
|
|
195
192
|
# suitable for use with has_one associations and includes.
|
|
196
193
|
#
|
|
197
|
-
# @
|
|
194
|
+
# @note When multiple records share the same timestamp for a given foreign key,
|
|
195
|
+
# all of them will be returned. This is safe for +has_one+ associations
|
|
196
|
+
# (ActiveRecord picks one), but callers using this as a standalone scope
|
|
197
|
+
# should be aware that the result may contain multiple records per foreign key
|
|
198
|
+
# in the case of timestamp ties.
|
|
199
|
+
#
|
|
200
|
+
# @param suffix [String] The suffix for method names
|
|
198
201
|
# @param column [Symbol] The timestamp column name
|
|
199
202
|
# @return [void]
|
|
200
203
|
#
|
|
@@ -202,25 +205,39 @@ module InTimeScope
|
|
|
202
205
|
# has_one :current_price, -> { latest_in_time(:user_id) }, class_name: 'Price'
|
|
203
206
|
#
|
|
204
207
|
# @api private
|
|
205
|
-
def define_latest_one_scope(
|
|
206
|
-
latest_method_name = scope_method_name == :in_time ? :latest_in_time : :"latest_#{scope_method_name}"
|
|
207
|
-
col = column
|
|
208
|
-
|
|
208
|
+
def define_latest_one_scope(suffix, column)
|
|
209
209
|
# NOT EXISTS approach: select records where no later record exists for the same foreign key
|
|
210
|
-
scope
|
|
210
|
+
scope :"latest_in_time#{suffix}", ->(foreign_key, time = Time.current) {
|
|
211
211
|
p2 = arel_table.alias("p2")
|
|
212
212
|
|
|
213
213
|
subquery = Arel::SelectManager.new(arel_table)
|
|
214
214
|
.from(p2)
|
|
215
215
|
.project(Arel.sql("1"))
|
|
216
216
|
.where(p2[foreign_key].eq(arel_table[foreign_key]))
|
|
217
|
-
.where(p2[
|
|
218
|
-
.where(p2[
|
|
217
|
+
.where(p2[column].lteq(time))
|
|
218
|
+
.where(p2[column].gt(arel_table[column]))
|
|
219
219
|
.where(p2[:id].not_eq(arel_table[:id]))
|
|
220
220
|
|
|
221
|
+
# Propagate simple equality conditions from the current scope into the NOT EXISTS
|
|
222
|
+
# subquery. This ensures that chained scopes like `.approved.latest_in_time(:user_id)`
|
|
223
|
+
# only consider approved records when searching for "a newer record exists",
|
|
224
|
+
# preventing a newer rejected record from shadowing the latest approved one.
|
|
225
|
+
where_values_hash.each do |col, val|
|
|
226
|
+
next if col.to_s == column.to_s # time column already handled above
|
|
227
|
+
next if col.to_s == foreign_key.to_s # foreign key already handled above
|
|
228
|
+
|
|
229
|
+
subquery = if val.nil?
|
|
230
|
+
subquery.where(p2[col].eq(nil))
|
|
231
|
+
elsif val.is_a?(Array)
|
|
232
|
+
subquery.where(p2[col].in(val))
|
|
233
|
+
else
|
|
234
|
+
subquery.where(p2[col].eq(val))
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
221
238
|
not_exists = Arel::Nodes::Not.new(Arel::Nodes::Exists.new(subquery.ast))
|
|
222
239
|
|
|
223
|
-
where(
|
|
240
|
+
where(column => ..time).where(not_exists)
|
|
224
241
|
}
|
|
225
242
|
end
|
|
226
243
|
|
|
@@ -229,7 +246,13 @@ module InTimeScope
|
|
|
229
246
|
# This scope efficiently finds the earliest record per foreign key,
|
|
230
247
|
# suitable for use with has_one associations and includes.
|
|
231
248
|
#
|
|
232
|
-
# @
|
|
249
|
+
# @note When multiple records share the same timestamp for a given foreign key,
|
|
250
|
+
# all of them will be returned. This is safe for +has_one+ associations
|
|
251
|
+
# (ActiveRecord picks one), but callers using this as a standalone scope
|
|
252
|
+
# should be aware that the result may contain multiple records per foreign key
|
|
253
|
+
# in the case of timestamp ties.
|
|
254
|
+
#
|
|
255
|
+
# @param suffix [String] The suffix for method names
|
|
233
256
|
# @param column [Symbol] The timestamp column name
|
|
234
257
|
# @return [void]
|
|
235
258
|
#
|
|
@@ -237,70 +260,73 @@ module InTimeScope
|
|
|
237
260
|
# has_one :first_price, -> { earliest_in_time(:user_id) }, class_name: 'Price'
|
|
238
261
|
#
|
|
239
262
|
# @api private
|
|
240
|
-
def define_earliest_one_scope(
|
|
241
|
-
earliest_method_name = scope_method_name == :in_time ? :earliest_in_time : :"earliest_#{scope_method_name}"
|
|
242
|
-
col = column
|
|
243
|
-
|
|
263
|
+
def define_earliest_one_scope(suffix, column)
|
|
244
264
|
# NOT EXISTS approach: select records where no earlier record exists for the same foreign key
|
|
245
|
-
scope
|
|
265
|
+
scope :"earliest_in_time#{suffix}", ->(foreign_key, time = Time.current) {
|
|
246
266
|
p2 = arel_table.alias("p2")
|
|
247
267
|
|
|
248
268
|
subquery = Arel::SelectManager.new(arel_table)
|
|
249
269
|
.from(p2)
|
|
250
270
|
.project(Arel.sql("1"))
|
|
251
271
|
.where(p2[foreign_key].eq(arel_table[foreign_key]))
|
|
252
|
-
.where(p2[
|
|
253
|
-
.where(p2[
|
|
272
|
+
.where(p2[column].lteq(time))
|
|
273
|
+
.where(p2[column].lt(arel_table[column]))
|
|
254
274
|
.where(p2[:id].not_eq(arel_table[:id]))
|
|
255
275
|
|
|
276
|
+
# Propagate simple equality conditions from the current scope into the NOT EXISTS
|
|
277
|
+
# subquery (same reasoning as define_latest_one_scope).
|
|
278
|
+
where_values_hash.each do |col, val|
|
|
279
|
+
next if col.to_s == column.to_s
|
|
280
|
+
next if col.to_s == foreign_key.to_s
|
|
281
|
+
|
|
282
|
+
subquery = if val.nil?
|
|
283
|
+
subquery.where(p2[col].eq(nil))
|
|
284
|
+
elsif val.is_a?(Array)
|
|
285
|
+
subquery.where(p2[col].in(val))
|
|
286
|
+
else
|
|
287
|
+
subquery.where(p2[col].eq(val))
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
256
291
|
not_exists = Arel::Nodes::Not.new(Arel::Nodes::Exists.new(subquery.ast))
|
|
257
292
|
|
|
258
|
-
where(
|
|
293
|
+
where(column => ..time).where(not_exists)
|
|
259
294
|
}
|
|
260
295
|
end
|
|
261
296
|
|
|
262
297
|
# Defines an end-only scope (for expiration pattern)
|
|
263
298
|
#
|
|
264
|
-
# @param
|
|
299
|
+
# @param suffix [String] The suffix for method names
|
|
265
300
|
# @param column [Symbol] The end column name
|
|
266
301
|
# @return [void]
|
|
267
302
|
# @api private
|
|
268
|
-
def define_end_only_scope(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
scope scope_method_name, ->(time = Time.current) {
|
|
272
|
-
where.not(col => ..time)
|
|
303
|
+
def define_end_only_scope(suffix, column)
|
|
304
|
+
scope :"in_time#{suffix}", ->(time = Time.current) {
|
|
305
|
+
where.not(column => ..time)
|
|
273
306
|
}
|
|
274
|
-
|
|
275
|
-
# Efficient scope for has_one + includes using NOT EXISTS subquery
|
|
276
|
-
define_latest_one_scope(scope_method_name, column)
|
|
277
|
-
define_earliest_one_scope(scope_method_name, column)
|
|
278
307
|
end
|
|
279
308
|
|
|
280
309
|
# Defines a full scope with both start and end columns
|
|
281
310
|
#
|
|
282
|
-
# @param
|
|
311
|
+
# @param suffix [String] The suffix for method names
|
|
283
312
|
# @param start_column [Symbol] The start column name
|
|
284
313
|
# @param start_null [Boolean] Whether start column allows NULL
|
|
285
314
|
# @param end_column [Symbol] The end column name
|
|
286
315
|
# @param end_null [Boolean] Whether end column allows NULL
|
|
287
316
|
# @return [void]
|
|
288
317
|
# @api private
|
|
289
|
-
def define_full_scope(
|
|
290
|
-
|
|
291
|
-
e_col = end_column
|
|
292
|
-
|
|
293
|
-
scope scope_method_name, ->(time = Time.current) {
|
|
318
|
+
def define_full_scope(suffix, start_column, start_null, end_column, end_null)
|
|
319
|
+
scope :"in_time#{suffix}", ->(time = Time.current) {
|
|
294
320
|
start_scope = if start_null
|
|
295
|
-
where(
|
|
321
|
+
where(start_column => nil).or(where(start_column => ..time))
|
|
296
322
|
else
|
|
297
|
-
where(
|
|
323
|
+
where(start_column => ..time)
|
|
298
324
|
end
|
|
299
325
|
|
|
300
326
|
end_scope = if end_null
|
|
301
|
-
where(
|
|
327
|
+
where(end_column => nil).or(where.not(end_column => ..time))
|
|
302
328
|
else
|
|
303
|
-
where.not(
|
|
329
|
+
where.not(end_column => ..time)
|
|
304
330
|
end
|
|
305
331
|
|
|
306
332
|
start_scope.merge(end_scope)
|
|
@@ -313,15 +339,15 @@ module InTimeScope
|
|
|
313
339
|
|
|
314
340
|
# Defines the instance method to check if a record is within the time window
|
|
315
341
|
#
|
|
316
|
-
# @param
|
|
342
|
+
# @param suffix [String] The suffix for method names
|
|
317
343
|
# @param start_column [Symbol, nil] The start column name
|
|
318
344
|
# @param start_null [Boolean, nil] Whether start column allows NULL
|
|
319
345
|
# @param end_column [Symbol, nil] The end column name
|
|
320
346
|
# @param end_null [Boolean, nil] Whether end column allows NULL
|
|
321
347
|
# @return [void]
|
|
322
348
|
# @api private
|
|
323
|
-
def define_instance_method(
|
|
324
|
-
define_method("#{
|
|
349
|
+
def define_instance_method(suffix, start_column, start_null, end_column, end_null)
|
|
350
|
+
define_method("in_time#{suffix}?") do |time = Time.current|
|
|
325
351
|
start_ok = if start_column.nil?
|
|
326
352
|
true
|
|
327
353
|
elsif start_null
|
|
@@ -341,5 +367,71 @@ module InTimeScope
|
|
|
341
367
|
start_ok && end_ok
|
|
342
368
|
end
|
|
343
369
|
end
|
|
370
|
+
|
|
371
|
+
# Defines before_in_time scope (records not yet started: start_at > time)
|
|
372
|
+
#
|
|
373
|
+
# @param suffix [String] The suffix for method names
|
|
374
|
+
# @param start_column [Symbol, nil] The start column name
|
|
375
|
+
# @param start_null [Boolean, nil] Whether start column allows NULL
|
|
376
|
+
# @return [void]
|
|
377
|
+
# @api private
|
|
378
|
+
def define_before_scope(suffix, start_column, start_null)
|
|
379
|
+
# No start column means always started (never before)
|
|
380
|
+
# start_at > time means not yet started
|
|
381
|
+
# NULL start_at is treated as "already started" (not before)
|
|
382
|
+
scope :"before_in_time#{suffix}", ->(time = Time.current) {
|
|
383
|
+
start_column.nil? ? none : where.not(start_column => ..time)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
define_method("before_in_time#{suffix}?") do |time = Time.current|
|
|
387
|
+
return false if start_column.nil?
|
|
388
|
+
|
|
389
|
+
val = send(start_column)
|
|
390
|
+
return false if val.nil? && start_null
|
|
391
|
+
|
|
392
|
+
val > time
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Defines after_in_time scope (records already ended: end_at <= time)
|
|
397
|
+
#
|
|
398
|
+
# @param suffix [String] The suffix for method names
|
|
399
|
+
# @param end_column [Symbol, nil] The end column name
|
|
400
|
+
# @param end_null [Boolean, nil] Whether end column allows NULL
|
|
401
|
+
# @return [void]
|
|
402
|
+
# @api private
|
|
403
|
+
def define_after_scope(suffix, end_column, end_null)
|
|
404
|
+
# No end column means never ends (never after)
|
|
405
|
+
# end_at <= time means already ended
|
|
406
|
+
# NULL end_at is treated as "never ends" (not after)
|
|
407
|
+
scope :"after_in_time#{suffix}", ->(time = Time.current) {
|
|
408
|
+
end_column.nil? ? none : where(end_column => ..time)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
define_method("after_in_time#{suffix}?") do |time = Time.current|
|
|
412
|
+
return false if end_column.nil?
|
|
413
|
+
|
|
414
|
+
val = send(end_column)
|
|
415
|
+
return false if val.nil? && end_null
|
|
416
|
+
|
|
417
|
+
val <= time
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Defines out_of_time scope (records outside time window: before OR after)
|
|
422
|
+
#
|
|
423
|
+
# @param suffix [String] The suffix for method names
|
|
424
|
+
# @return [void]
|
|
425
|
+
# @api private
|
|
426
|
+
def define_out_of_time_scope(suffix)
|
|
427
|
+
# out_of_time = before_in_time OR after_in_time
|
|
428
|
+
scope :"out_of_time#{suffix}", ->(time = Time.current) {
|
|
429
|
+
send(:"before_in_time#{suffix}", time).or(send(:"after_in_time#{suffix}", time))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
define_method("out_of_time#{suffix}?") do |time = Time.current|
|
|
433
|
+
send("before_in_time#{suffix}?", time) || send("after_in_time#{suffix}?", time)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
344
436
|
end
|
|
345
437
|
end
|
data/rulesync.jsonc
ADDED
data/sig/in_time_scope.rbs
CHANGED
|
@@ -36,15 +36,13 @@ module InTimeScope
|
|
|
36
36
|
# @param scope_name [Symbol] The name of the scope (default: :in_time)
|
|
37
37
|
# @param start_at [Hash] Configuration for the start column
|
|
38
38
|
# @param end_at [Hash] Configuration for the end column
|
|
39
|
-
# @param prefix [Boolean] If true, creates prefix-style method names
|
|
40
39
|
# @return [void]
|
|
41
40
|
# @raise [ColumnNotFoundError] When a specified column doesn't exist
|
|
42
41
|
# @raise [ConfigurationError] When the configuration is invalid (at scope call time)
|
|
43
42
|
def in_time_scope: (
|
|
44
43
|
?Symbol scope_name,
|
|
45
44
|
?start_at: start_at_config,
|
|
46
|
-
?end_at: end_at_config
|
|
47
|
-
?prefix: bool
|
|
45
|
+
?end_at: end_at_config
|
|
48
46
|
) -> void
|
|
49
47
|
|
|
50
48
|
private
|
|
@@ -52,30 +50,42 @@ module InTimeScope
|
|
|
52
50
|
# Private implementation methods
|
|
53
51
|
# These use ActiveRecord internals and are typed as untyped for flexibility
|
|
54
52
|
def fetch_null_option: (untyped config, untyped column, untyped table_column_hash) -> untyped
|
|
55
|
-
def
|
|
56
|
-
def
|
|
57
|
-
def
|
|
58
|
-
def
|
|
59
|
-
def
|
|
60
|
-
def
|
|
61
|
-
def
|
|
62
|
-
def
|
|
63
|
-
def
|
|
53
|
+
def define_scope_methods: (String suffix, start_at_column: untyped, start_at_null: untyped, end_at_column: untyped, end_at_null: untyped) -> void
|
|
54
|
+
def define_error_scope_and_method: (String suffix, String message) -> void
|
|
55
|
+
def define_start_only_scope: (String suffix, Symbol column) -> void
|
|
56
|
+
def define_latest_one_scope: (String suffix, Symbol column) -> void
|
|
57
|
+
def define_earliest_one_scope: (String suffix, Symbol column) -> void
|
|
58
|
+
def define_end_only_scope: (String suffix, Symbol column) -> void
|
|
59
|
+
def define_full_scope: (String suffix, Symbol start_column, untyped start_null, Symbol end_column, untyped end_null) -> void
|
|
60
|
+
def define_instance_method: (String suffix, Symbol? start_column, untyped start_null, Symbol? end_column, untyped end_null) -> void
|
|
61
|
+
def define_before_scope: (String suffix, Symbol? start_column, untyped start_null) -> void
|
|
62
|
+
def define_after_scope: (String suffix, Symbol? end_column, untyped end_null) -> void
|
|
63
|
+
def define_out_of_time_scope: (String suffix) -> void
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# Generated scope methods (dynamically defined)
|
|
68
68
|
# When you call `in_time_scope` on a model, it creates these methods:
|
|
69
69
|
#
|
|
70
|
-
# Class methods:
|
|
70
|
+
# Class methods (primary - records in time window):
|
|
71
71
|
# Model.in_time(time = Time.current) -> ActiveRecord::Relation
|
|
72
72
|
# Model.in_time_<name>(time = Time.current) -> ActiveRecord::Relation (for named scopes)
|
|
73
73
|
# Model.latest_in_time(foreign_key, time = Time.current) -> ActiveRecord::Relation (start-only/end-only)
|
|
74
74
|
# Model.earliest_in_time(foreign_key, time = Time.current) -> ActiveRecord::Relation (start-only/end-only)
|
|
75
75
|
#
|
|
76
|
-
#
|
|
76
|
+
# Class methods (inverse - records outside time window):
|
|
77
|
+
# Model.before_in_time(time = Time.current) -> ActiveRecord::Relation (not yet started)
|
|
78
|
+
# Model.after_in_time(time = Time.current) -> ActiveRecord::Relation (already ended)
|
|
79
|
+
# Model.out_of_time(time = Time.current) -> ActiveRecord::Relation (before OR after)
|
|
80
|
+
#
|
|
81
|
+
# Instance methods (primary):
|
|
77
82
|
# model.in_time?(time = Time.current) -> bool
|
|
78
83
|
# model.in_time_<name>?(time = Time.current) -> bool (for named scopes)
|
|
84
|
+
#
|
|
85
|
+
# Instance methods (inverse):
|
|
86
|
+
# model.before_in_time?(time = Time.current) -> bool
|
|
87
|
+
# model.after_in_time?(time = Time.current) -> bool
|
|
88
|
+
# model.out_of_time?(time = Time.current) -> bool
|
|
79
89
|
|
|
80
90
|
# Extend ActiveRecord::Base to include InTimeScope
|
|
81
91
|
class ActiveRecord::Base
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: in_time_scope
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kyohah
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '6.1'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '6.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: irb
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -121,6 +121,20 @@ dependencies:
|
|
|
121
121
|
- - ">="
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
123
|
version: '0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: timecop
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
124
138
|
- !ruby/object:Gem::Dependency
|
|
125
139
|
name: yard
|
|
126
140
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -143,17 +157,40 @@ extensions: []
|
|
|
143
157
|
extra_rdoc_files: []
|
|
144
158
|
files:
|
|
145
159
|
- ".rubocop.yml"
|
|
160
|
+
- ".rulesync/commands/translate-readme.md"
|
|
161
|
+
- ".rulesync/rules/project.md"
|
|
146
162
|
- CHANGELOG.md
|
|
147
|
-
- CLAUDE.md
|
|
148
163
|
- CODE_OF_CONDUCT.md
|
|
149
164
|
- LICENSE.txt
|
|
150
165
|
- README.md
|
|
151
166
|
- Rakefile
|
|
152
167
|
- Steepfile
|
|
168
|
+
- docs/book.toml
|
|
169
|
+
- docs/de/SUMMARY.md
|
|
170
|
+
- docs/de/index.md
|
|
171
|
+
- docs/de/point-system.md
|
|
172
|
+
- docs/de/user-name-history.md
|
|
173
|
+
- docs/fr/SUMMARY.md
|
|
174
|
+
- docs/fr/index.md
|
|
175
|
+
- docs/fr/point-system.md
|
|
176
|
+
- docs/fr/user-name-history.md
|
|
177
|
+
- docs/ja/SUMMARY.md
|
|
178
|
+
- docs/ja/index.md
|
|
179
|
+
- docs/ja/point-system.md
|
|
180
|
+
- docs/ja/user-name-history.md
|
|
181
|
+
- docs/src/SUMMARY.md
|
|
182
|
+
- docs/src/index.md
|
|
183
|
+
- docs/src/point-system.md
|
|
184
|
+
- docs/src/user-name-history.md
|
|
185
|
+
- docs/zh/SUMMARY.md
|
|
186
|
+
- docs/zh/index.md
|
|
187
|
+
- docs/zh/point-system.md
|
|
188
|
+
- docs/zh/user-name-history.md
|
|
153
189
|
- lib/in_time_scope.rb
|
|
154
190
|
- lib/in_time_scope/class_methods.rb
|
|
155
191
|
- lib/in_time_scope/version.rb
|
|
156
192
|
- rbs_collection.yaml
|
|
193
|
+
- rulesync.jsonc
|
|
157
194
|
- sig/in_time_scope.rbs
|
|
158
195
|
homepage: https://github.com/kyohah/in_time_scope
|
|
159
196
|
licenses:
|