redis-objects-periodical 0.6.0 → 0.8.0

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/skills/dependabot-batch-maintenance/SKILL.md +141 -0
  3. data/.github/dependabot.yml +32 -3
  4. data/.github/scripts/dependabot-auto-merge.cjs +182 -0
  5. data/.github/scripts/dependabot-auto-merge.test.cjs +278 -0
  6. data/.github/scripts/dependabot-policy.cjs +132 -0
  7. data/.github/scripts/dependabot-policy.test.cjs +235 -0
  8. data/.github/scripts/timezone-inventory.cjs +48 -0
  9. data/.github/scripts/timezone-inventory.test.cjs +86 -0
  10. data/.github/workflows/checks.yml +124 -0
  11. data/.github/workflows/ci.yml +191 -0
  12. data/.github/workflows/dependabot-auto-merge.yml +61 -0
  13. data/.github/workflows/release.yml +59 -0
  14. data/.github/workflows/rubocop_challenge.yml +71 -0
  15. data/.rspec +1 -0
  16. data/.rubocop.yml +1 -1
  17. data/.rubocop_todo.yml +3 -2
  18. data/AGENTS.md +133 -0
  19. data/CHANGELOG.md +87 -0
  20. data/CONTRIBUTING.md +203 -0
  21. data/Dockerfile +3 -2
  22. data/Gemfile +6 -6
  23. data/Gemfile.lock +71 -66
  24. data/README.md +58 -180
  25. data/docker-compose.yml +1 -3
  26. data/docs/maintaining.md +237 -0
  27. data/docs/usage.md +216 -0
  28. data/docs/verification/2026-10-27-timezone.md +25 -0
  29. data/gemfiles/active_support/Gemfile +5 -0
  30. data/gemfiles/active_support/Gemfile.lock +137 -0
  31. data/lib/redis/base_value_object.rb +27 -0
  32. data/lib/redis/objects/periodical/version.rb +1 -1
  33. data/lib/redis/objects/periodical_counters.rb +1 -1
  34. data/lib/redis/objects/periodical_hashes.rb +1 -1
  35. data/lib/redis/objects/periodical_sets.rb +1 -1
  36. data/lib/redis/objects/periodical_values.rb +44 -0
  37. data/lib/redis/periodical_counter.rb +1 -1
  38. data/lib/redis/periodical_hash_key.rb +1 -1
  39. data/lib/redis/periodical_set.rb +1 -1
  40. data/lib/redis/periodical_value.rb +15 -0
  41. data/lib/redis/recurring_at_intervals/weekly.rb +3 -1
  42. data/lib/redis/recurring_at_intervals.rb +3 -3
  43. data/lib/redis-objects-periodical.rb +3 -0
  44. data/redis-objects-periodical.gemspec +2 -2
  45. metadata +33 -12
  46. data/.circleci/config.yml +0 -212
data/docs/usage.md ADDED
@@ -0,0 +1,216 @@
1
+ # Usage guide
2
+
3
+ [README](../README.md) introduces installation and a minimal counter example.
4
+ Configure the Redis connection through Redis::Objects before using these fields.
5
+
6
+ ## Declarations and object lifetime
7
+
8
+ The examples below use these four fields. Each type's example illustrates a
9
+ separate sequence of writes to initially empty period keys.
10
+
11
+ ```rb
12
+ class Homepage
13
+ include Redis::Objects
14
+
15
+ daily_counter :pv, expireat: -> { Time.now + 2_678_400 } # about a month
16
+ daily_hash_key :browsing_history, expireat: -> { Time.now + 2_678_400 } # about a month
17
+ daily_set :dau, expireat: -> { Time.now + 2_678_400 } # about a month
18
+ daily_value :cache, expireat: -> { Time.now + 2_678_400 } # about a month
19
+
20
+ def id
21
+ 1
22
+ end
23
+ end
24
+
25
+ homepage = Homepage.new
26
+ ```
27
+
28
+ Each field accessor constructs a periodical object using the current time. Its
29
+ Redis key and cached current time are fixed at construction. Call the accessor
30
+ again after a period boundary to select the new key:
31
+
32
+ ```ruby
33
+ counter = homepage.pv
34
+ # After midnight, counter still addresses the day on which it was constructed.
35
+ # homepage.pv creates a new object and selects the current day's key.
36
+ ```
37
+
38
+ The `global: true` option also supports class-level fields, as in
39
+ `daily_counter :pv, global: true`. Standard Redis::Objects operations remain
40
+ available on each periodical object.
41
+
42
+ ## Periods and key formats
43
+
44
+ Combine any prefix below with `counter`, `value`, `set`, or `hash_key`.
45
+ The normal instance key has the prefix `model_name:id:field_name:`.
46
+
47
+ | Period prefix | Key suffix | Argument for historical access |
48
+ | --- | --- | --- |
49
+ | `annual_` | `yyyy` | `Date` or `Time` |
50
+ | `monthly_` | `yyyy-mm` | `Date` or `Time` |
51
+ | `weekly_` | `yyyyWww` | `Date` or `Time` |
52
+ | `daily_` | `yyyy-mm-dd` | `Date` or `Time` |
53
+ | `hourly_` | `yyyy-mm-ddThh` | `Time` |
54
+ | `minutely_` | `yyyy-mm-ddThh:mi` | `Time` |
55
+
56
+ Weekly keys first normalize the requested date to Monday, then format that
57
+ Monday using `%Y` and `%W`. They are not ISO week-year keys. For example,
58
+ 2021-12-31 and 2022-01-01 both use `2021W52`. The year in the key can therefore
59
+ belong to December of the previous year. A configurable strategy remains an
60
+ unresolved decision in [Issue #265](https://github.com/ryz310/redis-objects-periodical/issues/265);
61
+ this guide describes the existing implementation.
62
+
63
+ Annual periods are not recommended when Redis is the only copy of long-term
64
+ history. Choose retention and expiration deliberately for every period.
65
+
66
+ ## Shared historical operations
67
+
68
+ | Method | Behavior |
69
+ | --- | --- |
70
+ | `field[date_or_time]` or `slice` | Read one period's value |
71
+ | `field[start, length]` | Read/combine `length` successive periods |
72
+ | `field[start..finish]` | Pass an inclusive Range to the range reader |
73
+ | `field.range(start, finish)` | Read/combine successive cursors while cursor is at most `finish` |
74
+ | `field.delete_at(date_or_time)` | Delete one period's key |
75
+ | `field.at(date_or_time)` | Return a standard Redis::Objects object for that fixed key |
76
+
77
+ For the length form, zero returns an empty array (counter/value/set) or hash
78
+ (hash_key), and a negative length returns `nil`. Examples use inclusive ranges;
79
+ do not infer unsupported argument forms from the array-like method names.
80
+
81
+ Range iteration starts at the supplied start date/time and advances one interval
82
+ at a time; it does not first align the cursor to the calendar period boundary.
83
+ The end comparison is inclusive for that cursor, so a partial final interval may
84
+ not be visited. For example, a weekly range from Thursday 2021-04-01 to Monday
85
+ 2021-04-05 reads only the week of March 29: its next cursor is April 8. Use
86
+ period-aligned endpoints when requesting complete calendar periods.
87
+
88
+ Counter and value ranges return arrays in period order. Set ranges return a
89
+ union; member order is not guaranteed. Hash ranges combine fields across periods:
90
+ unsigned integer strings are summed as integers, unsigned decimal strings are
91
+ summed as floats, and other repeated values are joined with commas. The numeric
92
+ aggregate remains a string. Missing periods contribute `0`, `nil`, `[]`, or `{}`
93
+ for counter, value, set, and hash respectively.
94
+
95
+ ## Expiration
96
+
97
+ Use `expireat` as shown in the declarations to bound retention; its callable
98
+ returns the expiration time. The specs also exercise the upstream `expiration`
99
+ option with a duration in seconds. These options are provided through
100
+ Redis::Objects. Changing periods creates different keys; it does not itself
101
+ delete older period keys.
102
+
103
+ ## Periodical Counters
104
+
105
+ ```rb
106
+ # 2021-04-01
107
+ homepage.pv.increment(3)
108
+
109
+ # 2021-04-02 (next day)
110
+ homepage.pv.increment(2)
111
+
112
+ # 2021-04-03 (next day)
113
+ homepage.pv.increment(5)
114
+
115
+ homepage.pv[Date.new(2021, 4, 1)] # => 3
116
+ homepage.pv[Date.new(2021, 4, 1), 3] # => [3, 2, 5]
117
+ homepage.pv[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => [3, 2]
118
+
119
+ homepage.pv.delete_at(Date.new(2021, 4, 1))
120
+ homepage.pv.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => [0, 2, 5]
121
+ homepage.pv.at(Date.new(2021, 4, 2)) # => #<Redis::Counter key="homepage:1:pv:2021-04-02">
122
+ homepage.pv.at(Date.new(2021, 4, 2)).value # 2
123
+ ```
124
+
125
+ ## Periodical Values
126
+
127
+ ```rb
128
+ # 2021-04-01
129
+ homepage.cache.value = 'a'
130
+
131
+ # 2021-04-02 (next day)
132
+ homepage.cache.value = 'b'
133
+
134
+ # 2021-04-03 (next day)
135
+ homepage.cache.value = 'c'
136
+
137
+ homepage.cache[Date.new(2021, 4, 1)] # => 'a'
138
+ homepage.cache[Date.new(2021, 4, 1), 3] # => ['a', 'b', 'c']
139
+ homepage.cache[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => ['a', 'b']
140
+
141
+ homepage.cache.delete_at(Date.new(2021, 4, 1))
142
+ homepage.cache.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => [nil, 'b', 'c']
143
+ homepage.cache.at(Date.new(2021, 4, 2)) # => #<Redis::Value key="homepage:1:cache:2021-04-02">
144
+ homepage.cache.at(Date.new(2021, 4, 2)).value # 'b'
145
+ ```
146
+
147
+ ## Periodical Hashes
148
+
149
+ ```rb
150
+ # 2021-04-01
151
+ homepage.browsing_history.incr('item1')
152
+ homepage.browsing_history.incr('item2')
153
+ homepage.browsing_history.incr('item2')
154
+ puts homepage.browsing_history.all # { 'item1' => '1', 'item2' => '2' }
155
+
156
+ # 2021-04-02 (next day)
157
+ puts homepage.browsing_history.all # {}
158
+
159
+ homepage.browsing_history.bulk_set('item1' => 3, 'item3' => 5)
160
+ puts homepage.browsing_history.all # { 'item1' => '3', 'item3' => '5' }
161
+
162
+ # 2021-04-03 (next day)
163
+ homepage.browsing_history.incr('item2')
164
+ homepage.browsing_history.incr('item4')
165
+ puts homepage.browsing_history.all # { 'item2' => '1', 'item4' => '1' }
166
+
167
+ homepage.browsing_history[Date.new(2021, 4, 1)] # => { 'item1' => '1', 'item2' => '2' }
168
+ homepage.browsing_history[Date.new(2021, 4, 1), 3] # => { 'item1' => '4', 'item2' => '3', 'item3' => '5', 'item4' => '1' }
169
+ homepage.browsing_history[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => { 'item1' => '4', 'item2' => '2', 'item3' => '5' }
170
+
171
+ homepage.browsing_history.delete_at(Date.new(2021, 4, 1))
172
+ homepage.browsing_history.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => { 'item1' => '3', 'item2' => '1', 'item3' => '5', 'item4' => '1' }
173
+ homepage.browsing_history.at(Date.new(2021, 4, 2)) # => #<Redis::HashKey key="homepage:1:browsing_history:2021-04-02">
174
+ homepage.browsing_history.at(Date.new(2021, 4, 2)).all # { 'item1' => '3', 'item3' => '5' }
175
+ ```
176
+
177
+ ## Periodical Sets
178
+
179
+ ```rb
180
+ # 2021-04-01
181
+ homepage.dau << 'user1'
182
+ homepage.dau << 'user2'
183
+ homepage.dau << 'user1' # dup ignored
184
+ puts homepage.dau.members # ['user1', 'user2']
185
+ puts homepage.dau.length # 2
186
+ puts homepage.dau.count # alias of #length
187
+
188
+ # 2021-04-02 (next day)
189
+ puts homepage.dau.members # []
190
+
191
+ homepage.dau.merge('user2', 'user3')
192
+ puts homepage.dau.members # ['user2', 'user3']
193
+
194
+ # 2021-04-03 (next day)
195
+ homepage.dau.merge('user4')
196
+
197
+ homepage.dau[Date.new(2021, 4, 1)] # => ['user1', 'user2']
198
+ homepage.dau[Date.new(2021, 4, 1), 3] # => ['user1', 'user2', 'user3', 'user4']
199
+ homepage.dau[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => ['user1', 'user2', 'user3']
200
+
201
+ homepage.dau.delete_at(Date.new(2021, 4, 1))
202
+ homepage.dau.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => ['user2', 'user3', 'user4']
203
+ homepage.dau.at(Date.new(2021, 4, 2)) # => #<Redis::Set key="homepage:1:dau:2021-04-02">
204
+ homepage.dau.at(Date.new(2021, 4, 2)).members # ['user2', 'user3']
205
+ ```
206
+
207
+ ## Timezone
208
+
209
+ Without ActiveSupport, new periodical objects use `Time.now` in the Ruby
210
+ process timezone. If `Time.current` is available, they use it instead. With
211
+ ActiveSupport, `Time.zone` selects the application zone; when unset, its process
212
+ clock fallback applies. ActiveSupport is optional and is not a runtime dependency.
213
+
214
+ The [contributor guide](../CONTRIBUTING.md#optional-activesupport-integration)
215
+ describes the specific Ruby/ActiveSupport pair and boundary cases verified in CI;
216
+ it does not establish compatibility with every Rails release or timezone.
@@ -0,0 +1,25 @@
1
+ # Timezone verification recorded on 2026-10-27
2
+
3
+ This historical record was moved from README.md during documentation cleanup.
4
+ Its source is [revision e4c8af4](https://github.com/ryz310/redis-objects-periodical/blob/e4c8af42a8b311dbab2d38ed0e6bb2c9613b677a/README.md#L402).
5
+ The cleanup did not rerun the recorded experiment. Current procedures and the
6
+ execution inventory guard are described in
7
+ [Contributing](../../CONTRIBUTING.md#optional-activesupport-integration).
8
+
9
+ On 2026-10-27, the JUnit artifact from the
10
+ [successful CI run for commit 8ab4de1](https://github.com/ryz310/redis-objects-periodical/actions/runs/40409599457)
11
+ confirmed both zone groups actually ran under Ruby 3.3 / ActiveSupport 8.1.4.
12
+ A separate diagnostic changed `Time.use_zone(zone)` to `Time.use_zone(nil)` in
13
+ an isolated temporary copy and ran it against disposable Redis:
14
+
15
+ | Verification | Tokyo: executed / failed | Unset: executed / failed |
16
+ | --- | --- | --- |
17
+ | Actual CI JUnit report | 48 / 0 | 48 / 0 |
18
+ | Temporary copy with the zone setting disabled | 48 / 36 | 48 / 0 |
19
+
20
+ Both reports contained zero errors and zero skipped examples. The diagnostic
21
+ failed on incorrect keys, for example expected annual suffix `2021` versus actual
22
+ `2020`. Some periods share a key at one of the two boundary instants, so disabling
23
+ the zone does not make every Tokyo example fail. This experiment confirms that
24
+ the assertions detect ineffective timezone selection; it is a recorded one-off
25
+ diagnostic, not an additional CI step.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Optional Ruby 3.3 target; see CONTRIBUTING.md's ActiveSupport version selection policy.
4
+ eval_gemfile '../../Gemfile'
5
+ gem 'activesupport', '~> 8.1.4'
@@ -0,0 +1,137 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ redis-objects-periodical (0.8.0)
5
+ redis-objects (~> 1.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (8.1.4)
11
+ base64
12
+ bigdecimal
13
+ concurrent-ruby (~> 1.0, >= 1.3.1)
14
+ connection_pool (>= 2.2.5)
15
+ drb
16
+ i18n (>= 1.6, < 2)
17
+ json
18
+ logger (>= 1.4.2)
19
+ minitest (>= 5.1)
20
+ securerandom (>= 0.3)
21
+ tzinfo (~> 2.0, >= 2.0.5)
22
+ uri (>= 0.13.1)
23
+ ast (2.4.3)
24
+ base64 (0.3.0)
25
+ bigdecimal (4.1.3)
26
+ byebug (13.0.0)
27
+ reline (>= 0.6.0)
28
+ coderay (1.1.3)
29
+ concurrent-ruby (1.3.8)
30
+ connection_pool (2.3.0)
31
+ diff-lcs (1.6.2)
32
+ drb (2.2.3)
33
+ i18n (1.14.8)
34
+ concurrent-ruby (~> 1.0)
35
+ io-console (0.9.4)
36
+ json (3.1.0)
37
+ language_server-protocol (3.17.0.6)
38
+ lint_roller (1.1.0)
39
+ logger (1.7.0)
40
+ method_source (1.0.0)
41
+ minitest (5.26.1)
42
+ parallel (1.28.0)
43
+ parser (3.3.13.0)
44
+ ast (~> 2.4.1)
45
+ racc
46
+ prism (1.9.0)
47
+ pry (0.16.1)
48
+ coderay (~> 1.1)
49
+ method_source (~> 1.0)
50
+ reline (>= 0.6.0)
51
+ pry-byebug (3.12.0)
52
+ byebug (~> 13.0)
53
+ pry (>= 0.13, < 0.17)
54
+ racc (1.8.1)
55
+ rainbow (3.1.1)
56
+ rake (13.4.2)
57
+ redis (5.0.6)
58
+ redis-client (>= 0.9.0)
59
+ redis-client (0.12.1)
60
+ connection_pool
61
+ redis-objects (1.7.0)
62
+ redis
63
+ regexp_parser (2.13.1)
64
+ reline (0.7.0)
65
+ io-console (~> 0.5)
66
+ rspec (3.13.2)
67
+ rspec-core (~> 3.13.0)
68
+ rspec-expectations (~> 3.13.0)
69
+ rspec-mocks (~> 3.13.0)
70
+ rspec-core (3.13.6)
71
+ rspec-support (~> 3.13.0)
72
+ rspec-expectations (3.13.5)
73
+ diff-lcs (>= 1.2.0, < 2.0)
74
+ rspec-support (~> 3.13.0)
75
+ rspec-mocks (3.13.8)
76
+ diff-lcs (>= 1.2.0, < 2.0)
77
+ rspec-support (~> 3.13.0)
78
+ rspec-support (3.13.7)
79
+ rspec_junit_formatter (0.8.0)
80
+ rspec-core (>= 3.13, < 4)
81
+ rubocop (1.92.1)
82
+ json (>= 2.3)
83
+ language_server-protocol (~> 3.17.0.2)
84
+ lint_roller (~> 1.1.0)
85
+ parallel (>= 1.10)
86
+ parser (>= 3.3.0.2)
87
+ rainbow (>= 2.2.2, < 4.0)
88
+ regexp_parser (>= 2.11.3, < 3.0)
89
+ rubocop-ast (>= 1.49.0, < 2.0)
90
+ ruby-progressbar (~> 1.7)
91
+ unicode-display_width (>= 2.4.0, < 4.0)
92
+ rubocop-ast (1.50.0)
93
+ parser (>= 3.3.7.2)
94
+ prism (~> 1.7)
95
+ rubocop-performance (1.27.0)
96
+ lint_roller (~> 1.1)
97
+ rubocop (>= 1.89.0, < 2.0)
98
+ rubocop-ast (>= 1.47.1, < 2.0)
99
+ rubocop-rspec (3.10.2)
100
+ lint_roller (~> 1.1)
101
+ regexp_parser (>= 2.0)
102
+ rubocop (~> 1.86, >= 1.86.2)
103
+ ruby-progressbar (1.13.0)
104
+ securerandom (0.3.2)
105
+ simplecov (1.3.2)
106
+ timecop (0.9.11)
107
+ tzinfo (2.0.6)
108
+ concurrent-ruby (~> 1.0)
109
+ unicode-display_width (3.3.0)
110
+ unicode-emoji (~> 4.3)
111
+ unicode-emoji (4.3.0)
112
+ uri (1.1.2)
113
+ yard (0.9.45)
114
+
115
+ PLATFORMS
116
+ aarch64-linux
117
+ x86_64-darwin-19
118
+ x86_64-darwin-20
119
+ x86_64-linux
120
+
121
+ DEPENDENCIES
122
+ activesupport (~> 8.1.4)
123
+ bundler (~> 4.0)
124
+ pry-byebug (~> 3.12)
125
+ rake (~> 13.4)
126
+ redis-objects-periodical!
127
+ rspec (~> 3.13)
128
+ rspec_junit_formatter
129
+ rubocop (~> 1.92)
130
+ rubocop-performance
131
+ rubocop-rspec
132
+ simplecov (= 1.3.2)
133
+ timecop
134
+ yard
135
+
136
+ BUNDLED WITH
137
+ 4.0.23
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module BaseValueObject
5
+ private
6
+
7
+ def get_redis_object(key)
8
+ Redis::Value.new(key)
9
+ end
10
+
11
+ def get_value_from_redis(key)
12
+ unmarshal(redis.get(key))
13
+ end
14
+
15
+ def get_values_from_redis(keys)
16
+ redis.mget(*keys).map { |v| unmarshal(v) }
17
+ end
18
+
19
+ def delete_from_redis(key)
20
+ redis.del(key)
21
+ end
22
+
23
+ def empty_value
24
+ []
25
+ end
26
+ end
27
+ end
@@ -3,7 +3,7 @@
3
3
  class Redis
4
4
  module Objects
5
5
  module Periodical
6
- VERSION = '0.6.0'
6
+ VERSION = '0.8.0'
7
7
  end
8
8
  end
9
9
  end
@@ -36,5 +36,5 @@ Redis::PERIODICALS.each do |periodical| # rubocop:disable Metrics/BlockLength
36
36
  end
37
37
  end
38
38
  RUBY
39
- Redis::Objects.const_set("#{periodical.capitalize}Counters", new_module)
39
+ Redis::Objects.const_set(:"#{periodical.capitalize}Counters", new_module)
40
40
  end
@@ -46,5 +46,5 @@ Redis::PERIODICALS.each do |periodical| # rubocop:disable Metrics/BlockLength
46
46
  end
47
47
  end
48
48
  RUBY
49
- Redis::Objects.const_set("#{periodical.capitalize}Hashes", new_module)
49
+ Redis::Objects.const_set(:"#{periodical.capitalize}Hashes", new_module)
50
50
  end
@@ -46,5 +46,5 @@ Redis::PERIODICALS.each do |periodical| # rubocop:disable Metrics/BlockLength
46
46
  end
47
47
  end
48
48
  RUBY
49
- Redis::Objects.const_set("#{periodical.capitalize}Sets", new_module)
49
+ Redis::Objects.const_set(:"#{periodical.capitalize}Sets", new_module)
50
50
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis/periodical_value'
4
+
5
+ Redis::PERIODICALS.each do |periodical| # rubocop:disable Metrics/BlockLength
6
+ new_module = Module.new
7
+ new_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
8
+ def self.included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ def #{periodical}_value(name, options = {})
14
+ redis_objects[name.to_sym] = options.merge(:type => :value)
15
+
16
+ mod = Module.new do
17
+ define_method(name) do
18
+ Redis::#{periodical.capitalize}Value.new(
19
+ redis_field_key(name), redis_field_redis(name), redis_options(name)
20
+ )
21
+ end
22
+ define_method(:"#\{name}=") do |value|
23
+ public_send(name).value = value
24
+ end
25
+ end
26
+
27
+ if options[:global]
28
+ extend mod
29
+
30
+ # dispatch to class methods
31
+ define_method(name) do
32
+ self.class.public_send(name)
33
+ end
34
+ define_method(:"#\{name}=") do |value|
35
+ self.class.public_send(:"#\{name}=", value)
36
+ end
37
+ else
38
+ include mod
39
+ end
40
+ end
41
+ end
42
+ RUBY
43
+ Redis::Objects.const_set(:"#{periodical.capitalize}Values", new_module)
44
+ end
@@ -11,5 +11,5 @@ Redis::PERIODICALS.each do |periodical|
11
11
  include Redis::BaseCounterObject
12
12
  include const_get("Redis::RecurringAtIntervals::#{periodical.capitalize}")
13
13
  end
14
- Redis.const_set("#{periodical.capitalize}Counter", new_class)
14
+ Redis.const_set(:"#{periodical.capitalize}Counter", new_class)
15
15
  end
@@ -11,5 +11,5 @@ Redis::PERIODICALS.each do |periodical|
11
11
  include Redis::BaseHashKeyObject
12
12
  include const_get("Redis::RecurringAtIntervals::#{periodical.capitalize}")
13
13
  end
14
- Redis.const_set("#{periodical.capitalize}HashKey", new_class)
14
+ Redis.const_set(:"#{periodical.capitalize}HashKey", new_class)
15
15
  end
@@ -11,5 +11,5 @@ Redis::PERIODICALS.each do |periodical|
11
11
  include Redis::BaseSetObject
12
12
  include const_get("Redis::RecurringAtIntervals::#{periodical.capitalize}")
13
13
  end
14
- Redis.const_set("#{periodical.capitalize}Set", new_class)
14
+ Redis.const_set(:"#{periodical.capitalize}Set", new_class)
15
15
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "#{File.dirname(__FILE__)}/recurring_at_intervals"
4
+ require "#{File.dirname(__FILE__)}/base_value_object"
5
+
6
+ Redis::PERIODICALS.each do |periodical|
7
+ require "#{File.dirname(__FILE__)}/recurring_at_intervals/#{periodical}"
8
+
9
+ new_class = Class.new(Redis::Value) do
10
+ include Redis::RecurringAtIntervals
11
+ include Redis::BaseValueObject
12
+ include const_get("Redis::RecurringAtIntervals::#{periodical.capitalize}")
13
+ end
14
+ Redis.const_set(:"#{periodical.capitalize}Value", new_class)
15
+ end
@@ -6,7 +6,9 @@ class Redis
6
6
  private
7
7
 
8
8
  def redis_periodical_field_key(date_or_time)
9
- date_key = date_or_time.strftime('%YW%W')
9
+ wday = date_or_time.wday
10
+ beginning_of_week_date = date_or_time.to_date - (wday.zero? ? 6 : wday - 1)
11
+ date_key = beginning_of_week_date.strftime('%YW%W')
10
12
  [original_key, date_key].flatten.join(':')
11
13
  end
12
14
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  class Redis
4
4
  module RecurringAtIntervals
5
- def initialize(key, *args)
5
+ def initialize(key, *)
6
6
  @original_key = key
7
- super(redis_periodical_field_key(current_time), *args)
7
+ super(redis_periodical_field_key(current_time), *)
8
8
  end
9
9
 
10
10
  attr_reader :original_key
@@ -16,7 +16,7 @@ class Redis
16
16
  case length <=> 0
17
17
  when 1 then range(date_or_time, next_key(date_or_time, length - 1))
18
18
  when 0 then empty_value
19
- when -1 then nil # Ruby does this (a bit weird)
19
+ when -1 then nil # Ruby does this (a bit weird)
20
20
  end
21
21
  else
22
22
  get_value_from_redis(redis_periodical_field_key(date_or_time))
@@ -9,6 +9,7 @@ class Redis
9
9
  autoload :"#{periodical.capitalize}Counter", 'redis/periodical_counter'
10
10
  autoload :"#{periodical.capitalize}HashKey", 'redis/periodical_hash_key'
11
11
  autoload :"#{periodical.capitalize}Set", 'redis/periodical_set'
12
+ autoload :"#{periodical.capitalize}Value", 'redis/periodical_value'
12
13
  end
13
14
 
14
15
  module Objects
@@ -16,6 +17,7 @@ class Redis
16
17
  autoload :"#{periodical.capitalize}Counters", 'redis/objects/periodical_counters'
17
18
  autoload :"#{periodical.capitalize}Hashes", 'redis/objects/periodical_hashes'
18
19
  autoload :"#{periodical.capitalize}Sets", 'redis/objects/periodical_sets'
20
+ autoload :"#{periodical.capitalize}Values", 'redis/objects/periodical_values'
19
21
  end
20
22
 
21
23
  class << self
@@ -29,6 +31,7 @@ class Redis
29
31
  klass.send :include, const_get("Redis::Objects::#{periodical.capitalize}Counters")
30
32
  klass.send :include, const_get("Redis::Objects::#{periodical.capitalize}Hashes")
31
33
  klass.send :include, const_get("Redis::Objects::#{periodical.capitalize}Sets")
34
+ klass.send :include, const_get("Redis::Objects::#{periodical.capitalize}Values")
32
35
  end
33
36
  end
34
37
  end
@@ -27,9 +27,9 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ['lib']
29
29
 
30
- spec.required_ruby_version = '>= 2.7.0'
30
+ spec.required_ruby_version = '>= 3.3.0'
31
31
 
32
- spec.add_dependency 'redis-objects'
32
+ spec.add_dependency 'redis-objects', '~> 1.0'
33
33
 
34
34
  # For more information and examples about making a new gem, checkout our
35
35
  # guide at: https://bundler.io/guides/creating_gem.html