active_recall 2.2.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +10 -2
- data/README.md +115 -27
- data/VENDORED_LICENSES.md +54 -0
- data/gemfiles/rails_7_0.gemfile.lock +12 -4
- data/gemfiles/rails_7_1.gemfile.lock +12 -4
- data/gemfiles/rails_8_0.gemfile.lock +17 -4
- data/lib/active_recall/algorithms/fsrs/internal.rb +335 -0
- data/lib/active_recall/algorithms/fsrs.rb +110 -0
- data/lib/active_recall/configuration.rb +4 -1
- data/lib/active_recall/version.rb +1 -1
- data/lib/active_recall.rb +1 -0
- data/lib/generators/active_recall/active_recall_generator.rb +1 -0
- data/lib/generators/active_recall/templates/add_active_recall_item_fsrs_fields.rb +21 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 63a48f6351a76013bbf2922ca75b507b6dccb1ab23867896217b53bc6a77e26f
|
|
4
|
+
data.tar.gz: 9d8e3189bb8f5b1f15b284168e5973e87070bf223c0427b5a574cacd38c8e4b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: efe496383029f35391f1b54533f525451c5ee8a02e440316b990b812923b2061fc94eeff2ed1d691b17a1838b2d266b51ec9dde7d3305d511b484edb53b606b7
|
|
7
|
+
data.tar.gz: ff9f67e94b77d93175af60926ad889655862989c18a9b2fb07b8e75503526b8281f32cd3a982ab1d70933363454e40461a10dd2be72784bc5097243173ff5b1e
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
active_recall (2.
|
|
4
|
+
active_recall (2.3.0)
|
|
5
5
|
activerecord (>= 7.0, < 9.0)
|
|
6
6
|
activesupport (>= 7.0, < 9.0)
|
|
7
7
|
|
|
@@ -119,7 +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.
|
|
122
|
+
mini_portile2 (2.8.9)
|
|
123
123
|
minitest (5.18.1)
|
|
124
124
|
mutex_m (0.2.0)
|
|
125
125
|
net-imap (0.4.9.1)
|
|
@@ -135,6 +135,10 @@ GEM
|
|
|
135
135
|
nokogiri (1.18.7)
|
|
136
136
|
mini_portile2 (~> 2.8.2)
|
|
137
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)
|
|
138
142
|
nokogiri (1.18.7-x86_64-linux-gnu)
|
|
139
143
|
racc (~> 1.4)
|
|
140
144
|
parallel (1.24.0)
|
|
@@ -219,6 +223,8 @@ GEM
|
|
|
219
223
|
ruby2_keywords (0.0.5)
|
|
220
224
|
sqlite3 (2.5.0)
|
|
221
225
|
mini_portile2 (~> 2.8.0)
|
|
226
|
+
sqlite3 (2.5.0-arm64-darwin)
|
|
227
|
+
sqlite3 (2.5.0-x86_64-darwin)
|
|
222
228
|
sqlite3 (2.5.0-x86_64-linux-gnu)
|
|
223
229
|
standard (1.35.1)
|
|
224
230
|
language_server-protocol (~> 3.17.0.2)
|
|
@@ -244,7 +250,9 @@ GEM
|
|
|
244
250
|
zeitwerk (2.6.12)
|
|
245
251
|
|
|
246
252
|
PLATFORMS
|
|
253
|
+
arm64-darwin
|
|
247
254
|
ruby
|
|
255
|
+
x86_64-darwin
|
|
248
256
|
x86_64-linux
|
|
249
257
|
|
|
250
258
|
DEPENDENCIES
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# ActiveRecall
|
|
2
2
|
|
|
3
3
|
**ActiveRecall** is a spaced-repetition system that allows you to treat arbitrary [ActiveRecord](https://github.com/rails/rails/tree/master/activerecord) models as if they were flashcards to be learned and reviewed.
|
|
4
|
-
It
|
|
4
|
+
It is based on, and is intended to be backwards compatible with, the [okubo](https://github.com/rgravina/okubo) gem.
|
|
5
5
|
The primary differentiating features are that it lets the user specify the scheduling algorithm and is fully compatible with (and requires) Rails 6+ and Ruby 3+.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
@@ -28,17 +28,13 @@ Or install it yourself as:
|
|
|
28
28
|
|
|
29
29
|
$ gem install active_recall
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
You can configure the desired SRS algorithm during runtime:
|
|
33
|
-
```ruby
|
|
34
|
-
ActiveRecall.configure do |config|
|
|
35
|
-
config.algorithm_class = ActiveRecall::FibonacciSequence
|
|
36
|
-
end
|
|
37
|
-
```
|
|
38
|
-
Algorithms include `FibonacciSequence`, `LeitnerSystem`, `SoftLeitnerSystem`, and `SM2` (see [here](https://en.wikipedia.org/wiki/SuperMemo#Description_of_SM-2_algorithm)).
|
|
39
|
-
For Rails applications, try doing this from within an [initializer file](https://guides.rubyonrails.org/configuring.html#using-initializer-files).
|
|
31
|
+
The generator creates all the migrations any algorithm needs (including the `easiness_factor` column for SM2 and the FSRS-specific columns), so you don't have to revisit migrations when you switch algorithms later.
|
|
40
32
|
|
|
41
|
-
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
The fastest way to get going — no algorithm choice, no grade scale, just right/wrong feedback. This uses the default `LeitnerSystem`.
|
|
36
|
+
|
|
37
|
+
Suppose you have an application allowing your users to study words in a foreign language. Use `has_deck` to set up a deck of flashcards:
|
|
42
38
|
|
|
43
39
|
```ruby
|
|
44
40
|
class Word < ActiveRecord::Base
|
|
@@ -48,18 +44,76 @@ class User < ActiveRecord::Base
|
|
|
48
44
|
has_deck :words
|
|
49
45
|
end
|
|
50
46
|
|
|
51
|
-
user = User.create!(:
|
|
52
|
-
word = Word.create!(:
|
|
47
|
+
user = User.create!(name: "Robert")
|
|
48
|
+
word = Word.create!(kanji: "日本語", kana: "にほんご", translation: "Japanese language")
|
|
49
|
+
|
|
50
|
+
user.words << word
|
|
51
|
+
user.words.untested #=> [word]
|
|
52
|
+
|
|
53
|
+
user.right_answer_for!(word)
|
|
54
|
+
user.words.known #=> [word]
|
|
55
|
+
|
|
56
|
+
user.wrong_answer_for!(word)
|
|
57
|
+
user.words.failed #=> [word]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That's it. Want graded feedback (Again/Hard/Good/Easy) or modern scheduling? See [Choosing an Algorithm](#choosing-an-algorithm) below.
|
|
61
|
+
|
|
62
|
+
## Choosing an Algorithm
|
|
63
|
+
|
|
64
|
+
> **Not sure which to pick?** Stick with the default `LeitnerSystem` — it works out of the box and only needs right/wrong feedback. Reach for `FSRS` when you want modern, evidence-based scheduling and are willing to collect 1–4 ratings ("Again / Hard / Good / Easy") from users.
|
|
65
|
+
|
|
66
|
+
The full menu, in increasing order of sophistication:
|
|
67
|
+
|
|
68
|
+
| Algorithm | Type | How you grade | Reach for it when |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| **`LeitnerSystem`** *(default — start here)* | binary | `right_answer_for!` / `wrong_answer_for!` | You want the simplest thing that works |
|
|
71
|
+
| `SoftLeitnerSystem` | binary | `right_answer_for!` / `wrong_answer_for!` | Leitner is too punishing on occasional lapses |
|
|
72
|
+
| `FibonacciSequence` | binary | `right_answer_for!` / `wrong_answer_for!` | You want faster-growing intervals than Leitner |
|
|
73
|
+
| `SM2` | gradable | `score!(0..5, item)` | You want the classic SuperMemo behavior users know from Anki |
|
|
74
|
+
| **`FSRS`** *(modern recommendation)* | gradable | `score!(1..4, item)` | You're building something serious and want best-in-class retention |
|
|
75
|
+
|
|
76
|
+
**Binary** algorithms expect right-or-wrong feedback (`user.right_answer_for!(item)` / `user.wrong_answer_for!(item)`). **Gradable** algorithms expect a numeric grade per review (`user.score!(grade, item)`). Mixing them — e.g. calling `right_answer_for!` while configured to use SM2 — raises `ActiveRecall::IncompatibleAlgorithmError`.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
Skip this section if you're sticking with the default `LeitnerSystem` — there's nothing to configure.
|
|
81
|
+
|
|
82
|
+
To switch algorithms, set `algorithm_class` from a Rails [initializer file](https://guides.rubyonrails.org/configuring.html#using-initializer-files):
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# config/initializers/active_recall.rb
|
|
86
|
+
ActiveRecall.configure do |config|
|
|
87
|
+
config.algorithm_class = ActiveRecall::FSRS # or SM2, SoftLeitnerSystem, FibonacciSequence
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### FSRS-specific configuration
|
|
92
|
+
|
|
93
|
+
FSRS exposes three optional knobs. All have sensible defaults; tune only if you have a reason to:
|
|
94
|
+
|
|
95
|
+
- `fsrs_request_retention` — target retention probability (default `0.9`). Lower → longer intervals, more forgetting tolerated.
|
|
96
|
+
- `fsrs_maximum_interval` — caps the scheduled interval, in days.
|
|
97
|
+
- `fsrs_weights` — array of FSRS weights for advanced tuning.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
ActiveRecall.configure do |config|
|
|
101
|
+
config.algorithm_class = ActiveRecall::FSRS
|
|
102
|
+
config.fsrs_request_retention = 0.85
|
|
103
|
+
config.fsrs_maximum_interval = 365
|
|
104
|
+
end
|
|
53
105
|
```
|
|
54
106
|
|
|
55
|
-
|
|
107
|
+
## Usage with binary algorithms
|
|
108
|
+
|
|
109
|
+
Applies to `LeitnerSystem`, `SoftLeitnerSystem`, and `FibonacciSequence`.
|
|
56
110
|
|
|
57
111
|
```ruby
|
|
58
112
|
# Initially adding a word
|
|
59
113
|
user.words << word
|
|
60
114
|
user.words.untested #=> [word]
|
|
61
115
|
|
|
62
|
-
# Guessing a word correctly
|
|
116
|
+
# Guessing a word correctly
|
|
63
117
|
user.right_answer_for!(word)
|
|
64
118
|
user.words.known #=> [word]
|
|
65
119
|
|
|
@@ -75,11 +129,11 @@ As time passes, words need to be reviewed to keep them fresh in memory:
|
|
|
75
129
|
|
|
76
130
|
```ruby
|
|
77
131
|
# Three days later...
|
|
78
|
-
user.words.known
|
|
132
|
+
user.words.known #=> []
|
|
79
133
|
user.words.expired #=> [word]
|
|
80
134
|
```
|
|
81
135
|
|
|
82
|
-
Guessing a word correctly several times in a row
|
|
136
|
+
Guessing a word correctly several times in a row makes the word take longer to expire, demonstrating mastery:
|
|
83
137
|
|
|
84
138
|
```ruby
|
|
85
139
|
user.right_answer_for!(word)
|
|
@@ -93,21 +147,55 @@ user.right_answer_for!(word)
|
|
|
93
147
|
user.words.expired #=> [word]
|
|
94
148
|
```
|
|
95
149
|
|
|
96
|
-
|
|
150
|
+
## Usage with SM2
|
|
151
|
+
|
|
152
|
+
[SM2](https://en.wikipedia.org/wiki/SuperMemo#Description_of_SM-2_algorithm) uses a 0–5 grade scale:
|
|
153
|
+
|
|
154
|
+
| Grade | Meaning |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `5` | Perfect response |
|
|
157
|
+
| `4` | Correct response after a hesitation |
|
|
158
|
+
| `3` | Correct response recalled with serious difficulty |
|
|
159
|
+
| `2` | Incorrect response, but close |
|
|
160
|
+
| `1` | Incorrect response with familiarity |
|
|
161
|
+
| `0` | Complete blackout |
|
|
162
|
+
|
|
163
|
+
Grades **≥ 3** count as a success: the box advances and `times_right` increments. Grades **< 3** reset the box to `0` and increment `times_wrong`. Each item's `easiness_factor` starts at `2.5` and is clamped to a minimum of `1.3`.
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
user.words << word
|
|
167
|
+
|
|
168
|
+
user.score!(5, word) # perfect recall — box advances, EF rises
|
|
169
|
+
user.score!(2, word) # incorrect — box resets to 0
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Calling `user.right_answer_for!(word)` while SM2 is configured raises `ActiveRecall::IncompatibleAlgorithmError` — use `score!` instead.
|
|
173
|
+
|
|
174
|
+
## Usage with FSRS
|
|
175
|
+
|
|
176
|
+
[FSRS](https://github.com/open-spaced-repetition/fsrs4anki) uses a 1–4 grade scale matching the familiar Anki buttons:
|
|
177
|
+
|
|
178
|
+
| Grade | Meaning |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `1` | Again (lapse) |
|
|
181
|
+
| `2` | Hard |
|
|
182
|
+
| `3` | Good |
|
|
183
|
+
| `4` | Easy |
|
|
184
|
+
|
|
185
|
+
FSRS tracks `stability`, `difficulty`, `state`, and `lapses` per item. Those columns are added automatically by `rails generate active_recall` — no extra setup needed.
|
|
186
|
+
|
|
97
187
|
```ruby
|
|
98
|
-
|
|
99
|
-
user.score!(grade, word)
|
|
188
|
+
user.words << word
|
|
100
189
|
|
|
101
|
-
#
|
|
102
|
-
user.
|
|
103
|
-
=> ActiveRecall::IncompatibleAlgorithmError
|
|
190
|
+
user.score!(3, word) # "Good" — typical successful recall
|
|
191
|
+
user.score!(1, word) # "Again" — counts as a lapse
|
|
104
192
|
```
|
|
105
193
|
|
|
106
|
-
|
|
107
|
-
|
|
194
|
+
Calling `user.right_answer_for!(word)` while FSRS is configured raises `ActiveRecall::IncompatibleAlgorithmError` — use `score!` instead.
|
|
195
|
+
|
|
196
|
+
## Reviewing
|
|
108
197
|
|
|
109
|
-
In addition to
|
|
110
|
-
Words are randomly chosen from all untested words, failed, and finally expired in order of precedence.
|
|
198
|
+
In addition to the `expired` scope, ActiveRecall provides a suggested reviewing sequence for all unknown words in the deck. Words are randomly chosen from `untested`, `failed`, and `expired` items, in that order of precedence. This works the same for every algorithm.
|
|
111
199
|
|
|
112
200
|
```ruby
|
|
113
201
|
user.words.review #=> [word]
|
|
@@ -0,0 +1,54 @@
|
|
|
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,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
active_recall (2.
|
|
4
|
+
active_recall (2.3.0)
|
|
5
5
|
activerecord (>= 7.0, < 9.0)
|
|
6
6
|
activesupport (>= 7.0, < 9.0)
|
|
7
7
|
|
|
@@ -132,8 +132,15 @@ GEM
|
|
|
132
132
|
net-smtp (0.4.0.1)
|
|
133
133
|
net-protocol
|
|
134
134
|
nio4r (2.7.0)
|
|
135
|
+
nokogiri (1.18.7)
|
|
136
|
+
mini_portile2 (~> 2.8.2)
|
|
137
|
+
racc (~> 1.4)
|
|
135
138
|
nokogiri (1.18.7-arm64-darwin)
|
|
136
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)
|
|
143
|
+
racc (~> 1.4)
|
|
137
144
|
parallel (1.24.0)
|
|
138
145
|
parser (3.3.1.0)
|
|
139
146
|
ast (~> 2.4.1)
|
|
@@ -244,9 +251,10 @@ GEM
|
|
|
244
251
|
zeitwerk (2.6.12)
|
|
245
252
|
|
|
246
253
|
PLATFORMS
|
|
247
|
-
arm64-darwin
|
|
248
|
-
|
|
249
|
-
|
|
254
|
+
arm64-darwin
|
|
255
|
+
ruby
|
|
256
|
+
x86_64-darwin
|
|
257
|
+
x86_64-linux
|
|
250
258
|
|
|
251
259
|
DEPENDENCIES
|
|
252
260
|
active_recall!
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
active_recall (2.
|
|
4
|
+
active_recall (2.3.0)
|
|
5
5
|
activerecord (>= 7.0, < 9.0)
|
|
6
6
|
activesupport (>= 7.0, < 9.0)
|
|
7
7
|
|
|
@@ -132,8 +132,15 @@ GEM
|
|
|
132
132
|
net-smtp (0.4.0.1)
|
|
133
133
|
net-protocol
|
|
134
134
|
nio4r (2.7.0)
|
|
135
|
+
nokogiri (1.18.7)
|
|
136
|
+
mini_portile2 (~> 2.8.2)
|
|
137
|
+
racc (~> 1.4)
|
|
135
138
|
nokogiri (1.18.7-arm64-darwin)
|
|
136
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)
|
|
143
|
+
racc (~> 1.4)
|
|
137
144
|
parallel (1.24.0)
|
|
138
145
|
parser (3.3.1.0)
|
|
139
146
|
ast (~> 2.4.1)
|
|
@@ -244,9 +251,10 @@ GEM
|
|
|
244
251
|
zeitwerk (2.6.12)
|
|
245
252
|
|
|
246
253
|
PLATFORMS
|
|
247
|
-
arm64-darwin
|
|
248
|
-
|
|
249
|
-
|
|
254
|
+
arm64-darwin
|
|
255
|
+
ruby
|
|
256
|
+
x86_64-darwin
|
|
257
|
+
x86_64-linux
|
|
250
258
|
|
|
251
259
|
DEPENDENCIES
|
|
252
260
|
active_recall!
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
active_recall (2.
|
|
4
|
+
active_recall (2.3.0)
|
|
5
5
|
activerecord (>= 7.0, < 9.0)
|
|
6
6
|
activesupport (>= 7.0, < 9.0)
|
|
7
7
|
|
|
@@ -117,6 +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.9)
|
|
120
121
|
minitest (5.25.4)
|
|
121
122
|
net-imap (0.5.4)
|
|
122
123
|
date
|
|
@@ -128,8 +129,15 @@ GEM
|
|
|
128
129
|
net-smtp (0.5.0)
|
|
129
130
|
net-protocol
|
|
130
131
|
nio4r (2.7.4)
|
|
132
|
+
nokogiri (1.18.0)
|
|
133
|
+
mini_portile2 (~> 2.8.2)
|
|
134
|
+
racc (~> 1.4)
|
|
131
135
|
nokogiri (1.18.0-arm64-darwin)
|
|
132
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)
|
|
133
141
|
parallel (1.26.3)
|
|
134
142
|
parser (3.3.6.0)
|
|
135
143
|
ast (~> 2.4.1)
|
|
@@ -211,7 +219,11 @@ GEM
|
|
|
211
219
|
rubocop-ast (>= 1.31.1, < 2.0)
|
|
212
220
|
ruby-progressbar (1.13.0)
|
|
213
221
|
securerandom (0.4.1)
|
|
222
|
+
sqlite3 (2.5.0)
|
|
223
|
+
mini_portile2 (~> 2.8.0)
|
|
214
224
|
sqlite3 (2.5.0-arm64-darwin)
|
|
225
|
+
sqlite3 (2.5.0-x86_64-darwin)
|
|
226
|
+
sqlite3 (2.5.0-x86_64-linux-gnu)
|
|
215
227
|
standard (1.43.0)
|
|
216
228
|
language_server-protocol (~> 3.17.0.2)
|
|
217
229
|
lint_roller (~> 1.0)
|
|
@@ -240,9 +252,10 @@ GEM
|
|
|
240
252
|
zeitwerk (2.7.1)
|
|
241
253
|
|
|
242
254
|
PLATFORMS
|
|
243
|
-
arm64-darwin
|
|
244
|
-
|
|
245
|
-
|
|
255
|
+
arm64-darwin
|
|
256
|
+
ruby
|
|
257
|
+
x86_64-darwin
|
|
258
|
+
x86_64-linux
|
|
246
259
|
|
|
247
260
|
DEPENDENCIES
|
|
248
261
|
active_recall!
|
|
@@ -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
|
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
|
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.
|
|
4
|
+
version: 2.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Robert Gravina
|
|
@@ -173,6 +173,7 @@ files:
|
|
|
173
173
|
- LICENSE
|
|
174
174
|
- README.md
|
|
175
175
|
- Rakefile
|
|
176
|
+
- VENDORED_LICENSES.md
|
|
176
177
|
- active_recall.gemspec
|
|
177
178
|
- bin/console
|
|
178
179
|
- bin/flatten
|
|
@@ -187,6 +188,8 @@ files:
|
|
|
187
188
|
- gemfiles/rails_8_0.gemfile.lock
|
|
188
189
|
- lib/active_recall.rb
|
|
189
190
|
- lib/active_recall/algorithms/fibonacci_sequence.rb
|
|
191
|
+
- lib/active_recall/algorithms/fsrs.rb
|
|
192
|
+
- lib/active_recall/algorithms/fsrs/internal.rb
|
|
190
193
|
- lib/active_recall/algorithms/leitner_system.rb
|
|
191
194
|
- lib/active_recall/algorithms/sm2.rb
|
|
192
195
|
- lib/active_recall/algorithms/soft_leitner_system.rb
|
|
@@ -200,6 +203,7 @@ files:
|
|
|
200
203
|
- lib/generators/active_recall/active_recall_generator.rb
|
|
201
204
|
- lib/generators/active_recall/templates/add_active_recall_item_answer_counts.rb
|
|
202
205
|
- lib/generators/active_recall/templates/add_active_recall_item_easiness_factor.rb
|
|
206
|
+
- lib/generators/active_recall/templates/add_active_recall_item_fsrs_fields.rb
|
|
203
207
|
- lib/generators/active_recall/templates/create_active_recall_tables.rb
|
|
204
208
|
- lib/generators/active_recall/templates/migrate_okubo_to_active_recall.rb
|
|
205
209
|
- standard.yml
|