active_recall 2.3.0 → 2.3.1
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/Gemfile.lock +4 -1
- data/active_recall.gemspec +4 -0
- data/gemfiles/rails_7_0.gemfile.lock +4 -1
- data/gemfiles/rails_7_1.gemfile.lock +4 -1
- data/gemfiles/rails_8_0.gemfile.lock +4 -1
- data/lib/active_recall/algorithms/fsrs.rb +10 -10
- data/lib/active_recall/version.rb +1 -1
- metadata +21 -3
- data/VENDORED_LICENSES.md +0 -54
- data/lib/active_recall/algorithms/fsrs/internal.rb +0 -335
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25a05ad2ad88bc270c9e58cb8a2760b280677fb6d1d3c35ee76df8b021e1c6fd
|
|
4
|
+
data.tar.gz: 1503c4d5cc0863a42d51a084c7651d79fe574f9d98fb60254fd2084b6cf4ec13
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aab226bca430aac827eb11c7c46b1765f6b367181e233a614748f509640589306d86cd3fc4ad32655b431f4b49d5beaa163f056b7dfb09ccd51a17e5d727cc9b
|
|
7
|
+
data.tar.gz: c630d5a2fe83551b8929ef1a9f22b9408c908f3cb90a376b56dd056cdfa90e0c530f0c6aac02cae4d3a6d6891b457db6022258cf549687241783954a61ceb65f
|
data/Gemfile.lock
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
active_recall (2.3.
|
|
4
|
+
active_recall (2.3.1)
|
|
5
5
|
activerecord (>= 7.0, < 9.0)
|
|
6
6
|
activesupport (>= 7.0, < 9.0)
|
|
7
|
+
fsrs (>= 0.9.2, < 1.0)
|
|
7
8
|
|
|
8
9
|
GEM
|
|
9
10
|
remote: https://rubygems.org/
|
|
@@ -98,6 +99,8 @@ GEM
|
|
|
98
99
|
drb (2.2.0)
|
|
99
100
|
ruby2_keywords
|
|
100
101
|
erubi (1.12.0)
|
|
102
|
+
fsrs (0.9.2)
|
|
103
|
+
activesupport (>= 7.0, < 9.0)
|
|
101
104
|
globalid (1.2.1)
|
|
102
105
|
activesupport (>= 6.1)
|
|
103
106
|
i18n (1.14.1)
|
data/active_recall.gemspec
CHANGED
|
@@ -41,5 +41,9 @@ Gem::Specification.new do |spec|
|
|
|
41
41
|
spec.add_development_dependency "standard"
|
|
42
42
|
spec.add_runtime_dependency "activerecord", ">= 7.0", "< 9.0"
|
|
43
43
|
spec.add_runtime_dependency "activesupport", ">= 7.0", "< 9.0"
|
|
44
|
+
# fsrs 0.9.2 is the first release that widens activesupport to allow Rails 8
|
|
45
|
+
# and ships the new-card scheduling fix (minute-vs-day in schedule_new_state).
|
|
46
|
+
# Do not relax this floor without verifying both still hold.
|
|
47
|
+
spec.add_runtime_dependency "fsrs", ">= 0.9.2", "< 1.0"
|
|
44
48
|
spec.required_ruby_version = ">= 3.2"
|
|
45
49
|
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
active_recall (2.3.
|
|
4
|
+
active_recall (2.3.1)
|
|
5
5
|
activerecord (>= 7.0, < 9.0)
|
|
6
6
|
activesupport (>= 7.0, < 9.0)
|
|
7
|
+
fsrs (>= 0.9.2, < 1.0)
|
|
7
8
|
|
|
8
9
|
GEM
|
|
9
10
|
remote: https://rubygems.org/
|
|
@@ -98,6 +99,8 @@ GEM
|
|
|
98
99
|
drb (2.2.0)
|
|
99
100
|
ruby2_keywords
|
|
100
101
|
erubi (1.12.0)
|
|
102
|
+
fsrs (0.9.2)
|
|
103
|
+
activesupport (>= 7.0, < 9.0)
|
|
101
104
|
globalid (1.2.1)
|
|
102
105
|
activesupport (>= 6.1)
|
|
103
106
|
i18n (1.14.1)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
active_recall (2.3.
|
|
4
|
+
active_recall (2.3.1)
|
|
5
5
|
activerecord (>= 7.0, < 9.0)
|
|
6
6
|
activesupport (>= 7.0, < 9.0)
|
|
7
|
+
fsrs (>= 0.9.2, < 1.0)
|
|
7
8
|
|
|
8
9
|
GEM
|
|
9
10
|
remote: https://rubygems.org/
|
|
@@ -98,6 +99,8 @@ GEM
|
|
|
98
99
|
drb (2.2.0)
|
|
99
100
|
ruby2_keywords
|
|
100
101
|
erubi (1.12.0)
|
|
102
|
+
fsrs (0.9.2)
|
|
103
|
+
activesupport (>= 7.0, < 9.0)
|
|
101
104
|
globalid (1.2.1)
|
|
102
105
|
activesupport (>= 6.1)
|
|
103
106
|
i18n (1.14.1)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
active_recall (2.3.
|
|
4
|
+
active_recall (2.3.1)
|
|
5
5
|
activerecord (>= 7.0, < 9.0)
|
|
6
6
|
activesupport (>= 7.0, < 9.0)
|
|
7
|
+
fsrs (>= 0.9.2, < 1.0)
|
|
7
8
|
|
|
8
9
|
GEM
|
|
9
10
|
remote: https://rubygems.org/
|
|
@@ -95,6 +96,8 @@ GEM
|
|
|
95
96
|
diff-lcs (1.5.1)
|
|
96
97
|
drb (2.2.1)
|
|
97
98
|
erubi (1.13.1)
|
|
99
|
+
fsrs (0.9.2)
|
|
100
|
+
activesupport (>= 7.0, < 9.0)
|
|
98
101
|
globalid (1.2.1)
|
|
99
102
|
activesupport (>= 6.1)
|
|
100
103
|
i18n (1.14.6)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "fsrs"
|
|
4
4
|
|
|
5
5
|
module ActiveRecall
|
|
6
6
|
class FSRS
|
|
@@ -19,10 +19,10 @@ module ActiveRecall
|
|
|
19
19
|
].freeze
|
|
20
20
|
|
|
21
21
|
GRADE_TO_RATING = {
|
|
22
|
-
1 =>
|
|
23
|
-
2 =>
|
|
24
|
-
3 =>
|
|
25
|
-
4 =>
|
|
22
|
+
1 => Fsrs::Rating::AGAIN,
|
|
23
|
+
2 => Fsrs::Rating::HARD,
|
|
24
|
+
3 => Fsrs::Rating::GOOD,
|
|
25
|
+
4 => Fsrs::Rating::EASY
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
28
28
|
def self.required_attributes
|
|
@@ -33,8 +33,8 @@ module ActiveRecall
|
|
|
33
33
|
:gradable
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def self.score(**
|
|
37
|
-
new(**
|
|
36
|
+
def self.score(**)
|
|
37
|
+
new(**).score
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def initialize(box:, stability:, difficulty:, state:, lapses:,
|
|
@@ -43,7 +43,7 @@ module ActiveRecall
|
|
|
43
43
|
@box = box || 0
|
|
44
44
|
@stability = stability
|
|
45
45
|
@difficulty = difficulty
|
|
46
|
-
@state = state ||
|
|
46
|
+
@state = state || Fsrs::State::NEW
|
|
47
47
|
@lapses = lapses || 0
|
|
48
48
|
@elapsed_days = elapsed_days || 0
|
|
49
49
|
@scheduled_days = scheduled_days || 0
|
|
@@ -79,7 +79,7 @@ module ActiveRecall
|
|
|
79
79
|
private
|
|
80
80
|
|
|
81
81
|
def build_card
|
|
82
|
-
card =
|
|
82
|
+
card = Fsrs::Card.new
|
|
83
83
|
card.stability = @stability if @stability
|
|
84
84
|
card.difficulty = @difficulty if @difficulty
|
|
85
85
|
card.state = @state
|
|
@@ -92,7 +92,7 @@ module ActiveRecall
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
def scheduler
|
|
95
|
-
scheduler =
|
|
95
|
+
scheduler = Fsrs::Scheduler.new
|
|
96
96
|
config = ActiveRecall.configuration
|
|
97
97
|
scheduler.p.request_retention = config.fsrs_request_retention if config.fsrs_request_retention
|
|
98
98
|
scheduler.p.maximum_interval = config.fsrs_maximum_interval if config.fsrs_maximum_interval
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_recall
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.3.
|
|
4
|
+
version: 2.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Robert Gravina
|
|
@@ -154,6 +154,26 @@ dependencies:
|
|
|
154
154
|
- - "<"
|
|
155
155
|
- !ruby/object:Gem::Version
|
|
156
156
|
version: '9.0'
|
|
157
|
+
- !ruby/object:Gem::Dependency
|
|
158
|
+
name: fsrs
|
|
159
|
+
requirement: !ruby/object:Gem::Requirement
|
|
160
|
+
requirements:
|
|
161
|
+
- - ">="
|
|
162
|
+
- !ruby/object:Gem::Version
|
|
163
|
+
version: 0.9.2
|
|
164
|
+
- - "<"
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '1.0'
|
|
167
|
+
type: :runtime
|
|
168
|
+
prerelease: false
|
|
169
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
170
|
+
requirements:
|
|
171
|
+
- - ">="
|
|
172
|
+
- !ruby/object:Gem::Version
|
|
173
|
+
version: 0.9.2
|
|
174
|
+
- - "<"
|
|
175
|
+
- !ruby/object:Gem::Version
|
|
176
|
+
version: '1.0'
|
|
157
177
|
description: A spaced-repetition system to be used with ActiveRecord models
|
|
158
178
|
email:
|
|
159
179
|
- robert.gravina@gmail.com
|
|
@@ -173,7 +193,6 @@ files:
|
|
|
173
193
|
- LICENSE
|
|
174
194
|
- README.md
|
|
175
195
|
- Rakefile
|
|
176
|
-
- VENDORED_LICENSES.md
|
|
177
196
|
- active_recall.gemspec
|
|
178
197
|
- bin/console
|
|
179
198
|
- bin/flatten
|
|
@@ -189,7 +208,6 @@ files:
|
|
|
189
208
|
- lib/active_recall.rb
|
|
190
209
|
- lib/active_recall/algorithms/fibonacci_sequence.rb
|
|
191
210
|
- lib/active_recall/algorithms/fsrs.rb
|
|
192
|
-
- lib/active_recall/algorithms/fsrs/internal.rb
|
|
193
211
|
- lib/active_recall/algorithms/leitner_system.rb
|
|
194
212
|
- lib/active_recall/algorithms/sm2.rb
|
|
195
213
|
- lib/active_recall/algorithms/soft_leitner_system.rb
|
data/VENDORED_LICENSES.md
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# Vendored Licenses
|
|
2
|
-
|
|
3
|
-
This file records third-party source code that has been vendored into this
|
|
4
|
-
repository, along with the upstream license terms. ActiveRecall's own license
|
|
5
|
-
is in [LICENSE](LICENSE).
|
|
6
|
-
|
|
7
|
-
## rb-fsrs
|
|
8
|
-
|
|
9
|
-
- **Vendored at:** [`lib/active_recall/algorithms/fsrs/internal.rb`](lib/active_recall/algorithms/fsrs/internal.rb)
|
|
10
|
-
- **Upstream:** https://github.com/open-spaced-repetition/rb-fsrs
|
|
11
|
-
- **Version:** 0.9.0 (commit pulled from the published gem)
|
|
12
|
-
- **License:** MIT
|
|
13
|
-
- **Reason for vendoring:** The published `fsrs` 0.9.0 gem pins
|
|
14
|
-
`activesupport ~> 7.0`, which excludes Rails 8. ActiveRecall supports
|
|
15
|
-
Rails 8, so the code was vendored under `ActiveRecall::FSRS::Internal`.
|
|
16
|
-
The constraint has been widened on rb-fsrs `master`; revisit the
|
|
17
|
-
dependency-vs-vendoring decision once a release with the wider
|
|
18
|
-
constraint ships.
|
|
19
|
-
|
|
20
|
-
### Local divergences from upstream
|
|
21
|
-
|
|
22
|
-
- `Scheduler#schedule_new_state` uses `1.minute` / `5.minutes` /
|
|
23
|
-
`10.minutes` in place of upstream's bare integer arithmetic
|
|
24
|
-
(`now + 60`, `now + (5 * 60)`, `now + (10 * 60)`). With `now` as a
|
|
25
|
-
`DateTime`, the upstream form adds days, not seconds, scheduling new
|
|
26
|
-
cards rated Again/Hard/Good 60 / 300 / 600 days out instead of
|
|
27
|
-
1 / 5 / 10 minutes. Tracks upstream PR
|
|
28
|
-
https://github.com/open-spaced-repetition/rb-fsrs/pull/9.
|
|
29
|
-
|
|
30
|
-
### Upstream license text
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
The MIT License (MIT)
|
|
34
|
-
|
|
35
|
-
Copyright (c) 2024 clayton
|
|
36
|
-
|
|
37
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
38
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
39
|
-
in the Software without restriction, including without limitation the rights
|
|
40
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
41
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
42
|
-
furnished to do so, subject to the following conditions:
|
|
43
|
-
|
|
44
|
-
The above copyright notice and this permission notice shall be included in
|
|
45
|
-
all copies or substantial portions of the Software.
|
|
46
|
-
|
|
47
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
48
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
49
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
50
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
51
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
52
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
53
|
-
THE SOFTWARE.
|
|
54
|
-
```
|
|
@@ -1,335 +0,0 @@
|
|
|
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
|