active_recall 2.1.0 → 2.3.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.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- active_recall (2.1.0)
4
+ active_recall (2.3.0)
5
5
  activerecord (>= 7.0, < 9.0)
6
6
  activesupport (>= 7.0, < 9.0)
7
7
 
@@ -119,6 +119,7 @@ GEM
119
119
  net-smtp
120
120
  marcel (1.0.2)
121
121
  mini_mime (1.1.5)
122
+ mini_portile2 (2.8.8)
122
123
  minitest (5.21.2)
123
124
  mutex_m (0.2.0)
124
125
  net-imap (0.4.9.1)
@@ -131,7 +132,14 @@ GEM
131
132
  net-smtp (0.4.0.1)
132
133
  net-protocol
133
134
  nio4r (2.7.0)
134
- nokogiri (1.16.0-arm64-darwin)
135
+ nokogiri (1.18.7)
136
+ mini_portile2 (~> 2.8.2)
137
+ racc (~> 1.4)
138
+ nokogiri (1.18.7-arm64-darwin)
139
+ racc (~> 1.4)
140
+ nokogiri (1.18.7-x86_64-darwin)
141
+ racc (~> 1.4)
142
+ nokogiri (1.18.7-x86_64-linux-gnu)
135
143
  racc (~> 1.4)
136
144
  parallel (1.24.0)
137
145
  parser (3.3.1.0)
@@ -216,7 +224,8 @@ GEM
216
224
  rubocop-ast (>= 1.30.0, < 2.0)
217
225
  ruby-progressbar (1.13.0)
218
226
  ruby2_keywords (0.0.5)
219
- sqlite3 (1.7.3-arm64-darwin)
227
+ sqlite3 (1.7.3)
228
+ mini_portile2 (~> 2.8.0)
220
229
  standard (1.35.1)
221
230
  language_server-protocol (~> 3.17.0.2)
222
231
  lint_roller (~> 1.0)
@@ -242,11 +251,15 @@ GEM
242
251
  zeitwerk (2.6.12)
243
252
 
244
253
  PLATFORMS
245
- arm64-darwin-23
254
+ arm64-darwin
255
+ ruby
256
+ x86_64-darwin
257
+ x86_64-linux
246
258
 
247
259
  DEPENDENCIES
248
260
  active_recall!
249
261
  appraisal
262
+ nokogiri (>= 1.16.2)
250
263
  rails (~> 7.1)
251
264
  rake (>= 12.0)
252
265
  rdoc
@@ -2,6 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
+ gem "nokogiri", ">= 1.16.2"
5
6
  gem "rails", "~> 8.0"
6
7
  gem "sqlite3", ">= 2.1"
7
8
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- active_recall (2.1.0)
4
+ active_recall (2.3.0)
5
5
  activerecord (>= 7.0, < 9.0)
6
6
  activesupport (>= 7.0, < 9.0)
7
7
 
@@ -117,7 +117,7 @@ GEM
117
117
  net-smtp
118
118
  marcel (1.0.4)
119
119
  mini_mime (1.1.5)
120
- mini_portile2 (2.8.5)
120
+ mini_portile2 (2.8.9)
121
121
  minitest (5.25.4)
122
122
  net-imap (0.5.4)
123
123
  date
@@ -129,8 +129,15 @@ GEM
129
129
  net-smtp (0.5.0)
130
130
  net-protocol
131
131
  nio4r (2.7.4)
132
+ nokogiri (1.18.0)
133
+ mini_portile2 (~> 2.8.2)
134
+ racc (~> 1.4)
132
135
  nokogiri (1.18.0-arm64-darwin)
133
136
  racc (~> 1.4)
137
+ nokogiri (1.18.0-x86_64-darwin)
138
+ racc (~> 1.4)
139
+ nokogiri (1.18.0-x86_64-linux-gnu)
140
+ racc (~> 1.4)
134
141
  parallel (1.26.3)
135
142
  parser (3.3.6.0)
136
143
  ast (~> 2.4.1)
@@ -214,6 +221,9 @@ GEM
214
221
  securerandom (0.4.1)
215
222
  sqlite3 (2.5.0)
216
223
  mini_portile2 (~> 2.8.0)
224
+ sqlite3 (2.5.0-arm64-darwin)
225
+ sqlite3 (2.5.0-x86_64-darwin)
226
+ sqlite3 (2.5.0-x86_64-linux-gnu)
217
227
  standard (1.43.0)
218
228
  language_server-protocol (~> 3.17.0.2)
219
229
  lint_roller (~> 1.0)
@@ -242,11 +252,15 @@ GEM
242
252
  zeitwerk (2.7.1)
243
253
 
244
254
  PLATFORMS
245
- arm64-darwin-23
255
+ arm64-darwin
256
+ ruby
257
+ x86_64-darwin
258
+ x86_64-linux
246
259
 
247
260
  DEPENDENCIES
248
261
  active_recall!
249
262
  appraisal
263
+ nokogiri (>= 1.16.2)
250
264
  rails (~> 8.0)
251
265
  rake (>= 12.0)
252
266
  rdoc
@@ -63,11 +63,10 @@ module ActiveRecall
63
63
  SEQUENCE = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765].freeze
64
64
 
65
65
  def fibonacci_number_at(index)
66
- if (0...SEQUENCE.length).cover?(index)
67
- SEQUENCE[index]
68
- else
69
- fibonacci_number_at(index - 1) + fibonacci_number_at(index - 2)
70
- end
66
+ return SEQUENCE[index] if (0...SEQUENCE.length).cover?(index)
67
+
68
+ @fibonacci_cache ||= {}
69
+ @fibonacci_cache[index] ||= fibonacci_number_at(index - 1) + fibonacci_number_at(index - 2)
71
70
  end
72
71
 
73
72
  def next_review
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from rb-fsrs 0.9.0 (https://github.com/open-spaced-repetition/rb-fsrs).
4
+ # MIT License, Copyright (c) clayton. See VENDORED_LICENSES.md for the full notice.
5
+ #
6
+ # Pulled into ActiveRecall::FSRS::Internal because the published rb-fsrs gem
7
+ # pins activesupport to ~> 7.0, which excludes Rails 8 — incompatible with
8
+ # this gem's Rails 8 support. Re-evaluate when an upstream release widens
9
+ # the constraint (master already does).
10
+ #
11
+ # Local divergence from upstream:
12
+ # - schedule_new_state uses 1.minute / 5.minutes / 10.minutes instead of
13
+ # bare integers (which DateTime treats as days). Tracks upstream PR
14
+ # https://github.com/open-spaced-repetition/rb-fsrs/pull/9; remove this
15
+ # patch once a release with that fix is available.
16
+
17
+ require "date"
18
+
19
+ module ActiveRecall
20
+ class FSRS
21
+ module Internal
22
+ class InvalidDateError < StandardError
23
+ def initialize(msg = "Date must be UTC and timezone-aware")
24
+ super
25
+ end
26
+ end
27
+
28
+ class SchedulingInfo
29
+ attr_accessor :card, :review_log
30
+
31
+ def initialize(card, review_log)
32
+ @card = card
33
+ @review_log = review_log
34
+ end
35
+ end
36
+
37
+ class ReviewLog
38
+ attr_accessor :rating, :scheduled_days, :elapsed_days, :review, :state
39
+
40
+ def initialize(rating, scheduled_days, elapsed_days, review, state)
41
+ @rating = rating
42
+ @scheduled_days = scheduled_days
43
+ @elapsed_days = elapsed_days
44
+ @review = review
45
+ @state = state
46
+ end
47
+ end
48
+
49
+ class Rating
50
+ AGAIN = 1
51
+ HARD = 2
52
+ GOOD = 3
53
+ EASY = 4
54
+ end
55
+
56
+ class State
57
+ NEW = 0
58
+ LEARNING = 1
59
+ REVIEW = 2
60
+ RELEARNING = 3
61
+ end
62
+
63
+ class Card
64
+ attr_accessor :due, :stability, :difficulty, :elapsed_days,
65
+ :scheduled_days, :reps, :lapses, :state, :last_review
66
+
67
+ def initialize
68
+ @due = DateTime.new
69
+ @stability = 0.0
70
+ @difficulty = 0.0
71
+ @elapsed_days = 0
72
+ @scheduled_days = 0
73
+ @reps = 0
74
+ @lapses = 0
75
+ @state = State::NEW
76
+ end
77
+
78
+ def get_retrievability(now)
79
+ decay = -0.5
80
+ factor = (0.9**(1 / decay)) - 1
81
+
82
+ return nil unless @state == State::REVIEW
83
+
84
+ elapsed_days = [0, (now - @last_review).to_i].max
85
+ (1 + (factor * elapsed_days / @stability))**decay
86
+ end
87
+
88
+ def deep_clone
89
+ Marshal.load(Marshal.dump(self))
90
+ end
91
+ end
92
+
93
+ class CardScheduler
94
+ attr_accessor :again, :hard, :good, :easy
95
+
96
+ def initialize(card)
97
+ @again = card.clone
98
+ @hard = card.clone
99
+ @good = card.clone
100
+ @easy = card.clone
101
+ end
102
+
103
+ def update_state(state)
104
+ case state
105
+ when State::NEW
106
+ update_new_state
107
+ when State::LEARNING, State::RELEARNING
108
+ update_learning_relearning_state(state)
109
+ when State::REVIEW
110
+ update_review_state
111
+ end
112
+ end
113
+
114
+ def schedule(now, hard_interval, good_interval, easy_interval)
115
+ update_schedule_days(hard_interval, good_interval, easy_interval)
116
+ update_due_dates(now, hard_interval, good_interval, easy_interval)
117
+ end
118
+
119
+ def record_log(card, now)
120
+ {
121
+ Rating::AGAIN => record_again_log(card, now),
122
+ Rating::HARD => record_hard_log(card, now),
123
+ Rating::GOOD => record_good_log(card, now),
124
+ Rating::EASY => record_easy_log(card, now)
125
+ }
126
+ end
127
+
128
+ private
129
+
130
+ def update_due_dates(now, hard_interval, good_interval, easy_interval)
131
+ @again.due = now + 5.minutes
132
+ @hard.due = hard_interval.positive? ? now + hard_interval.days : now + 10.minutes
133
+ @good.due = now + good_interval.days
134
+ @easy.due = now + easy_interval.days
135
+ end
136
+
137
+ def update_schedule_days(hard_interval, good_interval, easy_interval)
138
+ @again.scheduled_days = 0
139
+ @hard.scheduled_days = hard_interval
140
+ @good.scheduled_days = good_interval
141
+ @easy.scheduled_days = easy_interval
142
+ end
143
+
144
+ def update_new_state
145
+ @again.state = State::LEARNING
146
+ @hard.state = State::LEARNING
147
+ @good.state = State::LEARNING
148
+ @easy.state = State::REVIEW
149
+ end
150
+
151
+ def update_learning_relearning_state(state)
152
+ @again.state = state
153
+ @hard.state = state
154
+ @good.state = State::REVIEW
155
+ @easy.state = State::REVIEW
156
+ end
157
+
158
+ def update_review_state
159
+ @again.state = State::RELEARNING
160
+ @hard.state = State::REVIEW
161
+ @good.state = State::REVIEW
162
+ @easy.state = State::REVIEW
163
+ @again.lapses += 1
164
+ end
165
+
166
+ def record_again_log(card, now)
167
+ SchedulingInfo.new(
168
+ @again,
169
+ ReviewLog.new(Rating::AGAIN, @again.scheduled_days, card.elapsed_days, now, card.state)
170
+ )
171
+ end
172
+
173
+ def record_hard_log(card, now)
174
+ SchedulingInfo.new(
175
+ @hard,
176
+ ReviewLog.new(Rating::HARD, @hard.scheduled_days, card.elapsed_days, now, card.state)
177
+ )
178
+ end
179
+
180
+ def record_good_log(card, now)
181
+ SchedulingInfo.new(
182
+ @good,
183
+ ReviewLog.new(Rating::GOOD, @good.scheduled_days, card.elapsed_days, now, card.state)
184
+ )
185
+ end
186
+
187
+ def record_easy_log(card, now)
188
+ SchedulingInfo.new(
189
+ @easy,
190
+ ReviewLog.new(Rating::EASY, @easy.scheduled_days, card.elapsed_days, now, card.state)
191
+ )
192
+ end
193
+ end
194
+
195
+ class Parameters
196
+ attr_accessor :request_retention, :maximum_interval, :w
197
+
198
+ def initialize
199
+ @request_retention = 0.9
200
+ @maximum_interval = 36_500
201
+ @w = [
202
+ 0.4, 0.6, 2.4, 5.8, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14,
203
+ 0.94, 2.18, 0.05, 0.34, 1.26, 0.29, 2.61
204
+ ]
205
+ end
206
+ end
207
+
208
+ class Scheduler
209
+ attr_accessor :p, :decay, :factor
210
+
211
+ def initialize
212
+ @p = Parameters.new
213
+ @decay = -0.5
214
+ @factor = (0.9**(1 / @decay)) - 1
215
+ end
216
+
217
+ def repeat(card, now)
218
+ raise InvalidDateError unless now.utc?
219
+
220
+ card = card.clone
221
+ card.elapsed_days = if card.state == State::NEW
222
+ 0
223
+ else
224
+ (now - card.last_review).to_i
225
+ end
226
+ card.last_review = now
227
+ card.reps += 1
228
+ card_scheduler = CardScheduler.new(card)
229
+ card_scheduler.update_state(card.state)
230
+
231
+ case card.state
232
+ when State::NEW
233
+ schedule_new_state(card_scheduler, now)
234
+ when State::LEARNING, State::RELEARNING
235
+ schedule_learning_relearning_state(card_scheduler, now)
236
+ when State::REVIEW
237
+ schedule_review_state(card_scheduler, card, now)
238
+ end
239
+ card_scheduler.record_log(card, now)
240
+ end
241
+
242
+ def schedule_new_state(s, now)
243
+ init_ds(s)
244
+ s.again.due = now + 1.minute
245
+ s.hard.due = now + 5.minutes
246
+ s.good.due = now + 10.minutes
247
+ easy_interval = next_interval(s.easy.stability)
248
+ s.easy.scheduled_days = easy_interval
249
+ s.easy.due = now + easy_interval.days
250
+ end
251
+
252
+ def schedule_learning_relearning_state(s, now)
253
+ hard_interval = 0
254
+ good_interval = next_interval(s.good.stability)
255
+ easy_interval = [next_interval(s.easy.stability), good_interval + 1].max
256
+ s.schedule(now, hard_interval, good_interval, easy_interval)
257
+ end
258
+
259
+ def schedule_review_state(s, card, now)
260
+ interval = card.elapsed_days
261
+ last_d = card.difficulty
262
+ last_s = card.stability
263
+ retrievability = forgetting_curve(interval, last_s)
264
+ next_ds(s, last_d, last_s, retrievability)
265
+ hard_interval = next_interval(s.hard.stability)
266
+ good_interval = next_interval(s.good.stability)
267
+ hard_interval = [hard_interval, good_interval].min
268
+ good_interval = [good_interval, hard_interval + 1].max
269
+ easy_interval = [next_interval(s.easy.stability), good_interval + 1].max
270
+ s.schedule(now, hard_interval, good_interval, easy_interval)
271
+ end
272
+
273
+ def init_ds(s)
274
+ s.again.difficulty = init_difficulty(Rating::AGAIN)
275
+ s.again.stability = init_stability(Rating::AGAIN)
276
+ s.hard.difficulty = init_difficulty(Rating::HARD)
277
+ s.hard.stability = init_stability(Rating::HARD)
278
+ s.good.difficulty = init_difficulty(Rating::GOOD)
279
+ s.good.stability = init_stability(Rating::GOOD)
280
+ s.easy.difficulty = init_difficulty(Rating::EASY)
281
+ s.easy.stability = init_stability(Rating::EASY)
282
+ end
283
+
284
+ def next_ds(s, last_d, last_s, retrievability)
285
+ s.again.difficulty = next_difficulty(last_d, Rating::AGAIN)
286
+ s.again.stability = next_forget_stability(last_d, last_s, retrievability)
287
+ s.hard.difficulty = next_difficulty(last_d, Rating::HARD)
288
+ s.hard.stability = next_recall_stability(last_d, last_s, retrievability, Rating::HARD)
289
+ s.good.difficulty = next_difficulty(last_d, Rating::GOOD)
290
+ s.good.stability = next_recall_stability(last_d, last_s, retrievability, Rating::GOOD)
291
+ s.easy.difficulty = next_difficulty(last_d, Rating::EASY)
292
+ s.easy.stability = next_recall_stability(last_d, last_s, retrievability, Rating::EASY)
293
+ end
294
+
295
+ def init_stability(r)
296
+ [p.w[r - 1], 0.1].max
297
+ end
298
+
299
+ def init_difficulty(r)
300
+ (p.w[4] - (p.w[5] * (r - 3))).clamp(1, 10)
301
+ end
302
+
303
+ def forgetting_curve(elapsed_days, stability)
304
+ (1 + (factor * elapsed_days / stability))**decay
305
+ end
306
+
307
+ def next_interval(s)
308
+ new_interval = s / factor * ((p.request_retention**(1 / decay)) - 1)
309
+ new_interval.round.clamp(1, p.maximum_interval)
310
+ end
311
+
312
+ def next_difficulty(d, r)
313
+ next_d = d - (p.w[6] * (r - 3))
314
+ mean_reversion(p.w[4], next_d).clamp(1, 10)
315
+ end
316
+
317
+ def mean_reversion(init, current)
318
+ (p.w[7] * init) + ((1 - p.w[7]) * current)
319
+ end
320
+
321
+ def next_recall_stability(d, s, r, rating)
322
+ hard_penalty = (rating == Rating::HARD) ? p.w[15] : 1
323
+ easy_bonus = (rating == Rating::EASY) ? p.w[16] : 1
324
+ s * (1 + (Math.exp(p.w[8]) * (11 - d) * (s**-p.w[9]) *
325
+ (Math.exp((1 - r) * p.w[10]) - 1) * hard_penalty * easy_bonus))
326
+ end
327
+
328
+ def next_forget_stability(d, s, r)
329
+ p.w[11] * (d**-p.w[12]) * (((s + 1)**p.w[13]) - 1) *
330
+ Math.exp((1 - r) * p.w[14])
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_recall/algorithms/fsrs/internal"
4
+
5
+ module ActiveRecall
6
+ class FSRS
7
+ REQUIRED_ATTRIBUTES = [
8
+ :box,
9
+ :stability,
10
+ :difficulty,
11
+ :state,
12
+ :lapses,
13
+ :elapsed_days,
14
+ :scheduled_days,
15
+ :times_right,
16
+ :times_wrong,
17
+ :last_reviewed,
18
+ :grade
19
+ ].freeze
20
+
21
+ GRADE_TO_RATING = {
22
+ 1 => Internal::Rating::AGAIN,
23
+ 2 => Internal::Rating::HARD,
24
+ 3 => Internal::Rating::GOOD,
25
+ 4 => Internal::Rating::EASY
26
+ }.freeze
27
+
28
+ def self.required_attributes
29
+ REQUIRED_ATTRIBUTES
30
+ end
31
+
32
+ def self.type
33
+ :gradable
34
+ end
35
+
36
+ def self.score(**kwargs)
37
+ new(**kwargs).score
38
+ end
39
+
40
+ def initialize(box:, stability:, difficulty:, state:, lapses:,
41
+ elapsed_days:, scheduled_days:, times_right:, times_wrong:,
42
+ last_reviewed:, grade:, current_time: Time.current)
43
+ @box = box || 0
44
+ @stability = stability
45
+ @difficulty = difficulty
46
+ @state = state || Internal::State::NEW
47
+ @lapses = lapses || 0
48
+ @elapsed_days = elapsed_days || 0
49
+ @scheduled_days = scheduled_days || 0
50
+ @times_right = times_right || 0
51
+ @times_wrong = times_wrong || 0
52
+ @last_reviewed = last_reviewed
53
+ @grade = grade
54
+ @current_time = current_time
55
+ end
56
+
57
+ def score
58
+ raise "Grade must be between 1-4!" unless GRADE_TO_RATING.key?(@grade)
59
+
60
+ now = to_utc_datetime(@current_time)
61
+ scheduling = scheduler.repeat(build_card, now)
62
+ result = scheduling[GRADE_TO_RATING[@grade]].card
63
+
64
+ {
65
+ box: result.reps,
66
+ stability: result.stability,
67
+ difficulty: result.difficulty,
68
+ state: result.state,
69
+ lapses: result.lapses,
70
+ elapsed_days: result.elapsed_days,
71
+ scheduled_days: result.scheduled_days,
72
+ last_reviewed: result.last_review,
73
+ next_review: result.due,
74
+ times_right: @times_right + ((@grade >= 2) ? 1 : 0),
75
+ times_wrong: @times_wrong + ((@grade == 1) ? 1 : 0)
76
+ }
77
+ end
78
+
79
+ private
80
+
81
+ def build_card
82
+ card = Internal::Card.new
83
+ card.stability = @stability if @stability
84
+ card.difficulty = @difficulty if @difficulty
85
+ card.state = @state
86
+ card.lapses = @lapses
87
+ card.elapsed_days = @elapsed_days
88
+ card.scheduled_days = @scheduled_days
89
+ card.reps = @box
90
+ card.last_review = to_utc_datetime(@last_reviewed) if @last_reviewed
91
+ card
92
+ end
93
+
94
+ def scheduler
95
+ scheduler = Internal::Scheduler.new
96
+ config = ActiveRecall.configuration
97
+ scheduler.p.request_retention = config.fsrs_request_retention if config.fsrs_request_retention
98
+ scheduler.p.maximum_interval = config.fsrs_maximum_interval if config.fsrs_maximum_interval
99
+ scheduler.p.w = config.fsrs_weights if config.fsrs_weights
100
+ scheduler
101
+ end
102
+
103
+ def to_utc_datetime(value)
104
+ case value
105
+ when DateTime then value.new_offset(0)
106
+ else value.utc.to_datetime
107
+ end
108
+ end
109
+ end
110
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecall
2
4
  class SM2
3
5
  MIN_EASINESS_FACTOR = 1.3
@@ -38,7 +40,7 @@ module ActiveRecall
38
40
  def score
39
41
  raise "Grade must be between 0-5!" unless GRADES.include?(@grade)
40
42
  old_ef = @easiness_factor
41
- update_easiness_factor
43
+ update_easiness_factor if @grade >= 3
42
44
  update_repetition_and_interval(old_ef)
43
45
 
44
46
  {
@@ -2,7 +2,10 @@
2
2
 
3
3
  module ActiveRecall
4
4
  class Configuration
5
- attr_accessor :algorithm_class
5
+ attr_accessor :algorithm_class,
6
+ :fsrs_request_retention,
7
+ :fsrs_maximum_interval,
8
+ :fsrs_weights
6
9
 
7
10
  def initialize
8
11
  @algorithm_class = LeitnerSystem
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecall
4
- VERSION = "2.1.0"
4
+ VERSION = "2.3.0"
5
5
  end
data/lib/active_recall.rb CHANGED
@@ -7,6 +7,7 @@ require "active_recall/algorithms/fibonacci_sequence"
7
7
  require "active_recall/algorithms/leitner_system"
8
8
  require "active_recall/algorithms/soft_leitner_system"
9
9
  require "active_recall/algorithms/sm2"
10
+ require "active_recall/algorithms/fsrs"
10
11
  require "active_recall/configuration"
11
12
  require "active_recall/models/deck"
12
13
  require "active_recall/models/item"
@@ -21,6 +21,7 @@ class ActiveRecallGenerator < Rails::Generators::Base
21
21
  create_migration_file_if_not_exist "create_active_recall_tables"
22
22
  create_migration_file_if_not_exist "add_active_recall_item_answer_counts"
23
23
  create_migration_file_if_not_exist "add_active_recall_item_easiness_factor"
24
+ create_migration_file_if_not_exist "add_active_recall_item_fsrs_fields"
24
25
  create_migration_file_if_not_exist "migrate_okubo_to_active_recall" if options["migrate_data"]
25
26
  end
26
27
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddActiveRecallItemFsrsFields < ActiveRecord::Migration[5.2]
4
+ def self.up
5
+ add_column :active_recall_items, :stability, :float
6
+ add_column :active_recall_items, :difficulty, :float
7
+ add_column :active_recall_items, :state, :integer, default: 0
8
+ add_column :active_recall_items, :lapses, :integer, default: 0
9
+ add_column :active_recall_items, :elapsed_days, :integer, default: 0
10
+ add_column :active_recall_items, :scheduled_days, :integer, default: 0
11
+ end
12
+
13
+ def self.down
14
+ remove_column :active_recall_items, :stability
15
+ remove_column :active_recall_items, :difficulty
16
+ remove_column :active_recall_items, :state
17
+ remove_column :active_recall_items, :lapses
18
+ remove_column :active_recall_items, :elapsed_days
19
+ remove_column :active_recall_items, :scheduled_days
20
+ end
21
+ end
data/standard.yml CHANGED
@@ -1,3 +1,3 @@
1
1
  fix: true
2
2
  parallel: true
3
- ruby_version: 3.0
3
+ ruby_version: 3.2