active_recall 1.8.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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