active_recall 1.8.5 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e96531570c81f3e7457acc497a7db792708b6b765794e2294b00cce0bd0cfe2
4
- data.tar.gz: 06c2f07e0438fc376beefe980dbdc579f06bb7d8bab77e692faed876b5a69629
3
+ metadata.gz: 5241d31af65cc21d9fd3ab8a68696587e5af6fdfe62ecf6b4ab719eadda9ee80
4
+ data.tar.gz: 7572b3444399f07faa04c093ca4b0f0b95f0d272804aee5e884852824f5718c7
5
5
  SHA512:
6
- metadata.gz: c8c4385a5646b277f27b6a90505e6f55847eba3d5e96bb32e0694f9148a018f71846d5cb9d552078a5c215965e5d72f6661f96c64885e9199ff557e77b47b059
7
- data.tar.gz: 9089edaa9ef21c0d1a8750d60b223ac4cda49c075268bee4bea25493b7951c3bd31747a129a93721f0e239efb41bb9d7db3a6f96d157d9b2f95930494f812c91
6
+ metadata.gz: 8d44c8efe2acb539484adcb6c4d421ddebf2cdfb35b05c9b49e02c72a65ea1047c8b7dd145261fbbd9e8678221dc5300cdc3551600e263ac7b52947a78d94660
7
+ data.tar.gz: bb043b33bd7242b2020a65d4e9e10953dcfbd136e42c5ac0964110c01a26400ad3b0a655d23ca2d74e634d7d41c8791d504f2cca78007c9db0cf49b35dd790c2
@@ -19,16 +19,16 @@ jobs:
19
19
  - macos
20
20
  - ubuntu
21
21
  ruby:
22
- - 2.7
23
22
  - 3.0
24
23
  - 3.1
25
24
  - 3.2
25
+ - 3.3
26
26
  allow_failures:
27
27
  - false
28
28
  include:
29
29
  - os: ubuntu
30
30
  ruby: ruby-head
31
- allow_failures: false
31
+ allow_failures: true
32
32
  env:
33
33
  BUNDLE_GEMFILE: "${{ matrix.gemfile }}"
34
34
  ALLOW_FAILURES: "${{ matrix.allow_failures }}"
data/.gitignore CHANGED
@@ -14,3 +14,4 @@ spec/reports
14
14
  test/tmp
15
15
  test/version_tmp
16
16
  tmp
17
+ .idea/
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.2.2
1
+ ruby 3.3.0
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- active_recall (1.8.5)
5
- activerecord (>= 5.2.3, <= 7.1)
6
- activesupport (>= 5.2.3, <= 7.1)
4
+ active_recall (2.0.0)
5
+ activerecord (>= 6.0, <= 7.2)
6
+ activesupport (>= 6.0, <= 7.2)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -2,7 +2,7 @@
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
4
  It it based on, and is intended to be backwards compatible with, the [okubo](https://github.com/rgravina/okubo) gem.
5
- The primary differentiating features are that it lets the user specify the scheduling algorithm and is fully compatible with Rails 6+ and Ruby 3+.
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
8
8
 
@@ -35,6 +35,7 @@ ActiveRecall.configure do |config|
35
35
  config.algorithm_class = ActiveRecall::FibonacciSequence
36
36
  end
37
37
  ```
38
+ Algorithms include `FibonacciSequence`, `LeitnerSystem`, `SoftLeitnerSystem`, and `SM2` (see [here](https://en.wikipedia.org/wiki/SuperMemo#Description_of_SM-2_algorithm)).
38
39
  For Rails applications, try doing this from within an [initializer file](https://guides.rubyonrails.org/configuring.html#using-initializer-files).
39
40
 
40
41
  Assume you have an application allowing your users to study words in a foreign language. Using the `has_deck` method you can set up a deck of flashcards that the user will study:
@@ -58,7 +59,7 @@ You can add words and record attempts to guess the word as right or wrong. Vario
58
59
  user.words << word
59
60
  user.words.untested #=> [word]
60
61
 
61
- # Guessing a word correctly
62
+ # Guessing a word correctly (when using a binary algorithm)
62
63
  user.right_answer_for!(word)
63
64
  user.words.known #=> [word]
64
65
 
@@ -92,6 +93,16 @@ user.right_answer_for!(word)
92
93
  user.words.expired #=> [word]
93
94
  ```
94
95
 
96
+ When using a gradable algorithm (rather than binary) such as the SM2 algorithm, you will need to supply your own grade along with the item:
97
+ ```ruby
98
+ grade = 3
99
+ user.score!(grade, word)
100
+
101
+ # Using the binary-only methods will raise an error
102
+ user.right_answer_for!(word)
103
+ => ActiveRecall::IncompatibleAlgorithmError
104
+ ```
105
+
95
106
  Reviewing
96
107
  ---------
97
108
 
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.add_development_dependency "rdoc"
37
37
  spec.add_development_dependency "rspec", ">= 3.0"
38
38
  spec.add_development_dependency "sqlite3"
39
- spec.add_runtime_dependency "activerecord", ">= 5.2.3", "<= 7.1"
40
- spec.add_runtime_dependency "activesupport", ">= 5.2.3", "<= 7.1"
41
- spec.required_ruby_version = ">= 2.6"
39
+ spec.add_runtime_dependency "activerecord", ">= 6.0", "<= 7.2"
40
+ spec.add_runtime_dependency "activesupport", ">= 6.0", "<= 7.2"
41
+ spec.required_ruby_version = ">= 3.0"
42
42
  end
@@ -2,6 +2,10 @@
2
2
 
3
3
  module ActiveRecall
4
4
  class FibonacciSequence
5
+ def self.required_attributes
6
+ REQUIRED_ATTRIBUTES
7
+ end
8
+
5
9
  def self.right(box:, times_right:, times_wrong:, current_time: Time.current)
6
10
  new(
7
11
  box: box,
@@ -11,6 +15,10 @@ module ActiveRecall
11
15
  ).right
12
16
  end
13
17
 
18
+ def self.type
19
+ :binary
20
+ end
21
+
14
22
  def self.wrong(box:, times_right:, times_wrong:, current_time: Time.current)
15
23
  new(
16
24
  box: box,
@@ -51,6 +59,7 @@ module ActiveRecall
51
59
 
52
60
  attr_reader :box, :current_time, :times_right, :times_wrong
53
61
 
62
+ REQUIRED_ATTRIBUTES = [:box, :times_right, :times_wrong].freeze
54
63
  SEQUENCE = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765].freeze
55
64
 
56
65
  def fibonacci_number_at(index)
@@ -2,7 +2,9 @@
2
2
 
3
3
  module ActiveRecall
4
4
  class LeitnerSystem
5
- DELAYS = [3, 7, 14, 30, 60, 120, 240].freeze
5
+ def self.required_attributes
6
+ REQUIRED_ATTRIBUTES
7
+ end
6
8
 
7
9
  def self.right(box:, times_right:, times_wrong:, current_time: Time.current)
8
10
  new(
@@ -13,6 +15,10 @@ module ActiveRecall
13
15
  ).right
14
16
  end
15
17
 
18
+ def self.type
19
+ :binary
20
+ end
21
+
16
22
  def self.wrong(box:, times_right:, times_wrong:, current_time: Time.current)
17
23
  new(
18
24
  box: box,
@@ -53,6 +59,9 @@ module ActiveRecall
53
59
 
54
60
  attr_reader :box, :current_time, :times_right, :times_wrong
55
61
 
62
+ DELAYS = [3, 7, 14, 30, 60, 120, 240].freeze
63
+ REQUIRED_ATTRIBUTES = [:box, :times_right, :times_wrong].freeze
64
+
56
65
  def next_review
57
66
  (current_time + DELAYS[[DELAYS.count, box + 1].min - 1].days)
58
67
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecall
4
+ class SM2
5
+ MIN_EASINESS_FACTOR = 1.3
6
+
7
+ def self.required_attributes
8
+ REQUIRED_ATTRIBUTES
9
+ end
10
+
11
+ def self.score(box:, easiness_factor:, times_right:, times_wrong:, grade:, current_time: Time.current)
12
+ new(
13
+ box: box,
14
+ easiness_factor: easiness_factor,
15
+ times_right: times_right,
16
+ times_wrong: times_wrong,
17
+ grade: grade,
18
+ current_time: current_time
19
+ ).score
20
+ end
21
+
22
+ def self.type
23
+ :gradable
24
+ end
25
+
26
+ def initialize(box:, easiness_factor:, times_right:, times_wrong:, grade:, current_time: Time.current)
27
+ @box = box
28
+ @easiness_factor = easiness_factor || 2.5
29
+ @times_right = times_right
30
+ @times_wrong = times_wrong
31
+ @grade = grade
32
+ @current_time = current_time
33
+ @interval = [1, box].max
34
+ end
35
+
36
+ def score
37
+ raise "Grade must be between 0-5!" unless GRADES.include?(@grade)
38
+ update_easiness_factor
39
+ update_repetition_and_interval
40
+
41
+ {
42
+ box: @box,
43
+ easiness_factor: @easiness_factor,
44
+ times_right: @times_right,
45
+ times_wrong: @times_wrong,
46
+ last_reviewed: @current_time,
47
+ next_review: next_review
48
+ }
49
+ end
50
+
51
+ private
52
+
53
+ GRADES = [
54
+ 5, # Perfect response. The learner recalls the information without hesitation.
55
+ 4, # Correct response after a hesitation. The learner recalls the information but with some difficulty.
56
+ 3, # Correct response recalled with serious difficulty. The learner struggles but eventually recalls the information.
57
+ 2, # Incorrect response, but the learner was very close to the correct answer. This might involve recalling some of the information correctly but not all of it.
58
+ 1, # Incorrect response, but the learner feels they should have remembered it. This is typically used when the learner has a sense of familiarity with the material but fails to recall it correctly.
59
+ 0 # Complete blackout. The learner does not recall the information at all.
60
+ ].freeze
61
+ REQUIRED_ATTRIBUTES = [
62
+ :box,
63
+ :easiness_factor,
64
+ :grade,
65
+ :times_right,
66
+ :times_wrong
67
+ ].freeze
68
+
69
+ def update_easiness_factor
70
+ @easiness_factor += (0.1 - (5 - @grade) * (0.08 + (5 - @grade) * 0.02))
71
+ @easiness_factor = [@easiness_factor, MIN_EASINESS_FACTOR].max
72
+ end
73
+
74
+ def update_repetition_and_interval
75
+ if @grade >= 3
76
+ @box += 1
77
+ @times_right += 1
78
+ @interval = case @box
79
+ when 1
80
+ 1
81
+ when 2
82
+ 6
83
+ else
84
+ (@interval || 1) * @easiness_factor
85
+ end
86
+ else
87
+ @box = 0
88
+ @times_wrong += 1
89
+ @interval = 1
90
+ end
91
+ end
92
+
93
+ def next_review
94
+ @current_time + @interval.days
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecall
4
+ class SoftLeitnerSystem
5
+ def self.required_attributes
6
+ REQUIRED_ATTRIBUTES
7
+ end
8
+
9
+ def self.right(box:, times_right:, times_wrong:, current_time: Time.current)
10
+ new(
11
+ box: box,
12
+ current_time: current_time,
13
+ times_right: times_right,
14
+ times_wrong: times_wrong
15
+ ).right
16
+ end
17
+
18
+ def self.type
19
+ :binary
20
+ end
21
+
22
+ def self.wrong(box:, times_right:, times_wrong:, current_time: Time.current)
23
+ new(
24
+ box: box,
25
+ current_time: current_time,
26
+ times_right: times_right,
27
+ times_wrong: times_wrong
28
+ ).wrong
29
+ end
30
+
31
+ def initialize(box:, times_right:, times_wrong:, current_time: Time.current)
32
+ @box = box
33
+ @current_time = current_time
34
+ @times_right = times_right
35
+ @times_wrong = times_wrong
36
+ end
37
+
38
+ def right
39
+ self.box = [box + 1, DELAYS.count].min
40
+
41
+ {
42
+ box: box,
43
+ times_right: times_right + 1,
44
+ times_wrong: times_wrong,
45
+ last_reviewed: current_time,
46
+ next_review: next_review
47
+ }
48
+ end
49
+
50
+ def wrong
51
+ self.box = [box - 1, 0].max
52
+
53
+ {
54
+ box: box,
55
+ times_right: times_right,
56
+ times_wrong: times_wrong + 1,
57
+ last_reviewed: current_time,
58
+ next_review: next_review
59
+ }
60
+ end
61
+
62
+ private
63
+
64
+ DELAYS = [3, 7, 14, 30, 60, 120, 240].freeze
65
+ REQUIRED_ATTRIBUTES = [:box, :times_right, :times_wrong].freeze
66
+
67
+ attr_accessor :box
68
+ attr_reader :current_time, :times_right, :times_wrong
69
+
70
+ def next_review
71
+ (current_time + DELAYS[[DELAYS.count, box].min - 1].days)
72
+ end
73
+ end
74
+ end
@@ -9,5 +9,9 @@ module ActiveRecall
9
9
  def wrong_answer_for!(item)
10
10
  deck.items.find_by(source_id: item.id).wrong!
11
11
  end
12
+
13
+ def score!(grade, item)
14
+ deck.items.find_by(source_id: item.id).score!(grade)
15
+ end
12
16
  end
13
17
  end
@@ -17,16 +17,34 @@ module ActiveRecall
17
17
  where(["box > ? and next_review > ?", 0, current_time])
18
18
  end
19
19
 
20
+ def score!(grade)
21
+ if algorithm_class.type == :gradable
22
+ update!(
23
+ algorithm_class.score(**scoring_attributes.merge(grade: grade))
24
+ ).score
25
+ else
26
+ raise IncompatibleAlgorithmError, "#{algorithm_class.name} is a not an gradable algorithm, so is not compatible with the #score! method"
27
+ end
28
+ end
29
+
20
30
  def source
21
31
  source_type.constantize.find(source_id)
22
32
  end
23
33
 
24
34
  def right!
25
- update!(algorithm_class.right(**scoring_attributes))
35
+ if algorithm_class.type == :binary
36
+ update!(algorithm_class.right(**scoring_attributes))
37
+ else
38
+ raise IncompatibleAlgorithmError, "#{algorithm_class.name} is not a binary algorithm, so is not compatible with the #right! method"
39
+ end
26
40
  end
27
41
 
28
42
  def wrong!
29
- update!(algorithm_class.wrong(**scoring_attributes))
43
+ if algorithm_class.type == :binary
44
+ update!(algorithm_class.wrong(**scoring_attributes))
45
+ else
46
+ raise IncompatibleAlgorithmError, "#{algorithm_class.name} is not a binary algorithm, so is not compatible with the #wrong! method"
47
+ end
30
48
  end
31
49
 
32
50
  private
@@ -36,7 +54,9 @@ module ActiveRecall
36
54
  end
37
55
 
38
56
  def scoring_attributes
39
- attributes.symbolize_keys.slice(:box, :times_right, :times_wrong)
57
+ attributes
58
+ .symbolize_keys
59
+ .slice(*algorithm_class.required_attributes)
40
60
  end
41
61
  end
42
62
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecall
4
- VERSION = "1.8.5"
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/active_recall.rb CHANGED
@@ -5,6 +5,8 @@ require "active_recall/deck_methods"
5
5
  require "active_recall/item_methods"
6
6
  require "active_recall/algorithms/fibonacci_sequence"
7
7
  require "active_recall/algorithms/leitner_system"
8
+ require "active_recall/algorithms/soft_leitner_system"
9
+ require "active_recall/algorithms/sm2"
8
10
  require "active_recall/configuration"
9
11
  require "active_recall/models/deck"
10
12
  require "active_recall/models/item"
@@ -28,4 +30,6 @@ module ActiveRecall
28
30
  def self.reset
29
31
  @configuration = Configuration.new
30
32
  end
33
+
34
+ class IncompatibleAlgorithmError < StandardError; end
31
35
  end
@@ -20,6 +20,7 @@ class ActiveRecallGenerator < Rails::Generators::Base
20
20
  def create_migration_files
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
+ create_migration_file_if_not_exist "add_active_recall_item_easiness_factor"
23
24
  create_migration_file_if_not_exist "migrate_okubo_to_active_recall" if options["migrate_data"]
24
25
  end
25
26
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddActiveRecallItemEasinessFactor < ActiveRecord::Migration[5.2]
4
+ def self.up
5
+ add_column :active_recall_items, :easiness_factor, :float, default: 2.5
6
+ end
7
+
8
+ def self.down
9
+ remove_column :active_recall_items, :easiness_factor
10
+ end
11
+ 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: 1.8.5
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Gravina
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-08-06 00:00:00.000000000 Z
12
+ date: 2024-01-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -73,40 +73,40 @@ dependencies:
73
73
  requirements:
74
74
  - - ">="
75
75
  - !ruby/object:Gem::Version
76
- version: 5.2.3
76
+ version: '6.0'
77
77
  - - "<="
78
78
  - !ruby/object:Gem::Version
79
- version: '7.1'
79
+ version: '7.2'
80
80
  type: :runtime
81
81
  prerelease: false
82
82
  version_requirements: !ruby/object:Gem::Requirement
83
83
  requirements:
84
84
  - - ">="
85
85
  - !ruby/object:Gem::Version
86
- version: 5.2.3
86
+ version: '6.0'
87
87
  - - "<="
88
88
  - !ruby/object:Gem::Version
89
- version: '7.1'
89
+ version: '7.2'
90
90
  - !ruby/object:Gem::Dependency
91
91
  name: activesupport
92
92
  requirement: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: 5.2.3
96
+ version: '6.0'
97
97
  - - "<="
98
98
  - !ruby/object:Gem::Version
99
- version: '7.1'
99
+ version: '7.2'
100
100
  type: :runtime
101
101
  prerelease: false
102
102
  version_requirements: !ruby/object:Gem::Requirement
103
103
  requirements:
104
104
  - - ">="
105
105
  - !ruby/object:Gem::Version
106
- version: 5.2.3
106
+ version: '6.0'
107
107
  - - "<="
108
108
  - !ruby/object:Gem::Version
109
- version: '7.1'
109
+ version: '7.2'
110
110
  description: A spaced-repetition system to be used with ActiveRecord models
111
111
  email:
112
112
  - robert.gravina@gmail.com
@@ -130,6 +130,8 @@ files:
130
130
  - lib/active_recall.rb
131
131
  - lib/active_recall/algorithms/fibonacci_sequence.rb
132
132
  - lib/active_recall/algorithms/leitner_system.rb
133
+ - lib/active_recall/algorithms/sm2.rb
134
+ - lib/active_recall/algorithms/soft_leitner_system.rb
133
135
  - lib/active_recall/base.rb
134
136
  - lib/active_recall/configuration.rb
135
137
  - lib/active_recall/deck_methods.rb
@@ -139,6 +141,7 @@ files:
139
141
  - lib/active_recall/version.rb
140
142
  - lib/generators/active_recall/active_recall_generator.rb
141
143
  - lib/generators/active_recall/templates/add_active_recall_item_answer_counts.rb
144
+ - lib/generators/active_recall/templates/add_active_recall_item_easiness_factor.rb
142
145
  - lib/generators/active_recall/templates/create_active_recall_tables.rb
143
146
  - lib/generators/active_recall/templates/migrate_okubo_to_active_recall.rb
144
147
  homepage: https://github.com/jaysonvirissimo/active_recall
@@ -154,14 +157,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
154
157
  requirements:
155
158
  - - ">="
156
159
  - !ruby/object:Gem::Version
157
- version: '2.6'
160
+ version: '3.0'
158
161
  required_rubygems_version: !ruby/object:Gem::Requirement
159
162
  requirements:
160
163
  - - ">="
161
164
  - !ruby/object:Gem::Version
162
165
  version: '0'
163
166
  requirements: []
164
- rubygems_version: 3.4.10
167
+ rubygems_version: 3.5.3
165
168
  signing_key:
166
169
  specification_version: 4
167
170
  summary: A spaced-repetition system