├── lib ├── spaced_repetition.rb └── spaced_repetition │ ├── sm2.rb │ ├── sm2_mod.rb │ └── sm2_lg.rb ├── spaced_repetition.gemspec ├── README.md └── test ├── test_sm2_lg.rb └── test_sm2.rb /lib/spaced_repetition.rb: -------------------------------------------------------------------------------- 1 | require 'spaced_repetition/sm2' 2 | require 'spaced_repetition/sm2_mod' 3 | require 'spaced_repetition/sm2_lg' -------------------------------------------------------------------------------- /spaced_repetition.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "spaced_repetition" 5 | s.version = "1.1.1" 6 | s.platform = Gem::Platform::RUBY 7 | s.authors = ["Michał Ostrowski"] 8 | s.email = ["michol@linuxcsb.org"] 9 | s.summary = %q{Ruby gem for calculating spaced repetition intervals} 10 | s.description = %q{Ruby gem for calculating spaced repetition intervals.} 11 | 12 | s.files = `git ls-files`.split("\n") 13 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 14 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 15 | s.require_paths = ["lib"] 16 | 17 | s.add_dependency("bundler", ["~> 1.0"]) 18 | end 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Supermemo2 (SM2) 2 | A spaced repetition algorithm calculator for Ruby. 3 | 4 | ## Notes 5 | This class is used to calculate easiness factor and interval for study. 6 | 7 | Check out the details of how this algorithm is used here: 8 | http://www.supermemo.com/english/ol/sm2.htm 9 | 10 | ## Install 11 | Add to your Gemfile: 12 | 13 | gem 'spaced_repetition', :git => 'git://github.com/espresse/spaced_repetition.git' 14 | 15 | and run 16 | 17 | bundle install 18 | 19 | ## Typical use-case: 20 | 21 | require 'rubygems' 22 | require 'spaced_repetition' 23 | 24 | #user's quality_response is 5; no other arguments are given, because there have been no previous repetitions. 25 | sm2 = SpacedRepetition::Sm2.new(5) 26 | 27 | #user's quality response is 3, his/her prevoius interval was 3 days and easiness factor was 2.1 28 | sm2 = SpacedRepetition::Sm2.new(3,3,2.1) 29 | 30 | Now, you can fetch results: 31 | 32 | #new interval 33 | new_interval = sm2.interval 34 | 35 | #new easiness_factor 36 | new_ef = sm2.easniness_factor 37 | 38 | #new repetition date 39 | new_date = sm2.next_repetition_date 40 | 41 | If you're a bit confused, check out the test cases for some examples of this class in action. 42 | 43 | By default, SM2, uses 6 possible answers (0-5), where 0 is very bad and 5 is perfect. 44 | 45 | ## SM2_mod 46 | If you want to use 4 answers (0 /very bad/ - 3 /perfect/) you can choose SM2Mod. It works in the same manner as SM2 does. 47 | 48 | ## sm2_lg 49 | - TODO: docs? 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /lib/spaced_repetition/sm2.rb: -------------------------------------------------------------------------------- 1 | #http://www.supermemo.com/english/ol/sm2.htm 2 | require 'date' 3 | 4 | module SpacedRepetition 5 | class Sm2 6 | def initialize(quality_response, prev_interval=0, prev_ef=2.5) 7 | @prev_ef = prev_ef 8 | @prev_interval = prev_interval 9 | @quality_response = quality_response 10 | 11 | @calculated_interval = nil 12 | @calculated_ef = nil 13 | @repetition_date = nil 14 | 15 | #if quality_response is below 3 start repetition from the begining, but without changing easiness_factor 16 | if @quality_response < 3 17 | 18 | @calculated_ef = @prev_ef 19 | @calculated_interval = 0 20 | else 21 | calculate_easiness_factor 22 | calculate_interval 23 | end 24 | calculate_date 25 | end 26 | 27 | def interval 28 | @calculated_interval 29 | end 30 | 31 | def easiness_factor 32 | @calculated_ef 33 | end 34 | 35 | def next_repetition_date 36 | @repetition_date 37 | end 38 | 39 | private 40 | 41 | def calculate_interval 42 | if @prev_interval == 0 43 | @calculated_interval = 1 44 | elsif @prev_interval == 1 45 | @calculated_interval = 6 46 | else 47 | @calculated_interval = (@prev_interval*@prev_ef).to_i 48 | end 49 | end 50 | 51 | def calculate_easiness_factor 52 | @calculated_ef = @prev_ef+(0.1-(5-@quality_response)*(0.08+(5-@quality_response)*0.02)) 53 | if @calculated_ef < 1.3 54 | @calculated_ef = 1.3 55 | end 56 | @calculated_ef 57 | end 58 | 59 | def calculate_date 60 | @repetition_date = Date.today + @calculated_interval 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/test_sm2_lg.rb: -------------------------------------------------------------------------------- 1 | require "spaced_repetition" 2 | require "test/unit" 3 | include SpacedRepetition 4 | 5 | class TestSm2Lg < Test::Unit::TestCase 6 | 7 | def test_scenario_user_starts_learning_new_element 8 | sm2 = Sm2Lg.new(3) 9 | assert((7..9).include?(sm2.interval)) 10 | assert_in_delta 2.6, sm2.easiness_factor, 0.001 11 | 12 | sm2 = Sm2Lg.new(2) 13 | assert((3..5).include?(sm2.interval)) 14 | assert_in_delta 2.5, sm2.easiness_factor, 0.001 15 | 16 | sm2 = Sm2Lg.new(1) 17 | assert_equal(1, sm2.interval) 18 | assert_in_delta 2.2, sm2.easiness_factor, 0.001 19 | 20 | sm2 = Sm2Lg.new(0) 21 | assert_equal(0, sm2.interval) 22 | assert_in_delta 1.7, sm2.easiness_factor, 0.001 23 | end 24 | 25 | def test_scenario_user_is_doing_good 26 | sm2 = Sm2Lg.new(3, 9, 2.6, Date.today) 27 | assert_equal(23, sm2.interval) 28 | 29 | sm2 = Sm2Lg.new(3, 9, 2.6, Date.today-60) 30 | assert_equal(83, sm2.interval) 31 | end 32 | 33 | def test_scenario_user_is_doing_ok 34 | sm2 = Sm2Lg.new(2, 5, 2.5, Date.today) 35 | assert_equal(12, sm2.interval) 36 | 37 | sm2 = Sm2Lg.new(2, 5, 2.5, Date.today-60) 38 | assert_equal(42, sm2.interval) 39 | end 40 | 41 | def test_scenario_user_is_doing_so_so 42 | sm2 = Sm2Lg.new(1, 1, 2.2, Date.today) 43 | assert_equal(2, sm2.interval) 44 | 45 | sm2 = Sm2Lg.new(1, 1, 2.2, Date.today-60) 46 | assert_equal(22, sm2.interval) 47 | end 48 | 49 | def test_scenario_user_is_doing_bad 50 | sm2 = Sm2Lg.new(0, 1, 1.7, Date.today) 51 | assert_equal(0, sm2.interval) 52 | assert_in_delta 1.3, sm2.easiness_factor, 0.001 53 | 54 | sm2 = Sm2Lg.new(0, 1, 1.7, Date.today-60) 55 | assert_equal(0, sm2.interval) 56 | assert_in_delta 1.3, sm2.easiness_factor, 0.001 57 | end 58 | 59 | end 60 | 61 | -------------------------------------------------------------------------------- /test/test_sm2.rb: -------------------------------------------------------------------------------- 1 | require "spaced_repetition" 2 | require "test/unit" 3 | 4 | class TestSm2 < Test::Unit::TestCase 5 | 6 | def test_scenario_user_starts_learning_new_element 7 | sm2 = SpacedRepetition::Sm2.new(5) 8 | assert_equal(1, sm2.interval) 9 | assert_in_delta 2.6, sm2.easiness_factor, 0.001 10 | 11 | sm2 = SpacedRepetition::Sm2.new(4) 12 | assert_equal(1, sm2.interval) 13 | assert_in_delta 2.5, sm2.easiness_factor, 0.001 14 | 15 | sm2 = SpacedRepetition::Sm2.new(3) 16 | assert_equal(1, sm2.interval) 17 | assert_in_delta 2.36, sm2.easiness_factor, 0.001 18 | end 19 | 20 | def test_scenario_user_is_doing_good 21 | sm2 = SpacedRepetition::Sm2.new(4, 6, 2.1) 22 | assert_equal(12, sm2.interval) 23 | assert_in_delta 2.1, sm2.easiness_factor, 0.001 24 | 25 | sm2 = SpacedRepetition::Sm2.new(5, 12, 2.1) 26 | assert_equal(25, sm2.interval) 27 | assert_in_delta 2.2, sm2.easiness_factor, 0.001 28 | 29 | sm2 = SpacedRepetition::Sm2.new(5, 25, 2.1) 30 | assert_equal(52, sm2.interval) 31 | assert_in_delta 2.2, sm2.easiness_factor, 0.001 32 | end 33 | 34 | def test_scenario_user_is_doing_good_but_later_is_going_bad 35 | sm2 = SpacedRepetition::Sm2.new(3, 6, 2.1) 36 | assert_equal(12, sm2.interval) 37 | assert_in_delta 1.96, sm2.easiness_factor, 0.001 38 | 39 | sm2 = SpacedRepetition::Sm2.new(0, 12, 1.96) 40 | assert_equal(1, sm2.interval) 41 | assert_in_delta 1.96, sm2.easiness_factor, 0.001 42 | 43 | sm2 = SpacedRepetition::Sm2.new(0, 1, 1.96) 44 | assert_equal(1, sm2.interval) 45 | assert_in_delta 1.96, sm2.easiness_factor, 0.001 46 | end 47 | 48 | def test_scenario_user_passed_char_instead_of_number 49 | assert_raise( ArgumentError ) { SpacedRepetition::Sm2.new("a", 6, 2.1) } 50 | end 51 | 52 | 53 | end 54 | 55 | -------------------------------------------------------------------------------- /lib/spaced_repetition/sm2_mod.rb: -------------------------------------------------------------------------------- 1 | #modified sm2 algorithm 2 | #changes: 3 | # - initial interval depends on given quality_response 4 | # - states (easiness_factor is calulated using that states, sm2 for ok uses 4) 5 | # - 0 - bad (0, -0.8), 1 - so so(2, -0.3), 2 - ok(4, 0), 3 - more than better(5, 0.1) 6 | require 'date' 7 | 8 | module SpacedRepetition 9 | class Sm2Mod 10 | def initialize(quality_response, prev_interval=0, prev_ef=2.5) 11 | @prev_ef = prev_ef 12 | @prev_interval = prev_interval 13 | @quality_response = quality_response 14 | 15 | @calculated_interval = nil 16 | @calculated_ef = nil 17 | @repetition_date = nil 18 | 19 | #if quality_response is below 3 start repetition from the begining, but without changing easiness_factor 20 | if @quality_response < 2 21 | @calculated_interval=0 22 | @calculated_ef = @prev_ef 23 | else 24 | calculate_easiness_factor 25 | calculate_interval 26 | end 27 | 28 | calculate_date 29 | end 30 | 31 | def interval 32 | @calculated_interval 33 | end 34 | 35 | def easiness_factor 36 | @calculated_ef 37 | end 38 | 39 | def next_repetition_date 40 | @repetition_date 41 | end 42 | 43 | private 44 | 45 | def calculate_interval 46 | if @prev_interval == 0 47 | @calculated_interval = 1 48 | if @quality_response == 3 49 | @calculated_interval = 6 50 | end 51 | elsif @prev_interval == 1 52 | @calculated_interval = 6 53 | else 54 | @calculated_interval = (@prev_interval*@prev_ef).to_i 55 | end 56 | end 57 | 58 | def calculate_easiness_factor 59 | @calculated_ef = @prev_ef+(0.1-(3-@quality_response)*((3-@quality_response)*0.1)) 60 | if @calculated_ef < 1.3 61 | @calculated_ef = 1.3 62 | end 63 | @calculated_ef 64 | end 65 | 66 | def calculate_date 67 | @repetition_date = Date.today + @calculated_interval 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/spaced_repetition/sm2_lg.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | module SpacedRepetition 4 | class Sm2Lg 5 | 6 | def initialize(quality_response, prev_interval=0, prev_ef=2.5, practice_date=nil) 7 | @prev_ef=prev_ef 8 | @quality_response = quality_response 9 | @prev_interval = prev_interval 10 | @practice_date = practice_date 11 | 12 | if @quality_response > 3 13 | @quality_response = 3 14 | end 15 | 16 | if @prev_interval == 0 17 | @calculated_interval = first_interval 18 | else 19 | @calculated_interval = calculate_interval 20 | end 21 | 22 | @easiness_factor = calculate_easiness_factor 23 | 24 | @next_date = calculate_next_date 25 | end 26 | 27 | def interval 28 | @calculated_interval 29 | end 30 | 31 | def easiness_factor 32 | @easiness_factor 33 | end 34 | 35 | def date 36 | @next_date 37 | end 38 | 39 | private 40 | 41 | def first_interval 42 | #initial interval (0 - 0), (1 - 1), (2 - 3-5), (3 - 7-9) 43 | next_interval = 0 44 | 45 | if @quality_response == 1 46 | next_interval = 1 47 | elsif @quality_response == 2 48 | next_interval = my_rand(3, 5) 49 | elsif @quality_response == 3 50 | next_interval = my_rand(7, 9) 51 | end 52 | next_interval 53 | end 54 | 55 | def calculate_interval 56 | normal_interval = (@prev_interval*@prev_ef).to_i 57 | premium = ((Date.today-@practice_date)/(4-@quality_response)).to_i 58 | 59 | if @quality_response < 1 60 | premium = 0 61 | end 62 | 63 | if @quality_response == 0 64 | normal_interval = 0 65 | end 66 | 67 | normal_interval + premium 68 | end 69 | 70 | def calculate_easiness_factor 71 | calculated_ef = @prev_ef+(0.1-(3-@quality_response)*((3-@quality_response)*0.1)) 72 | if calculated_ef < 1.3 73 | calculated_ef = 1.3 74 | end 75 | calculated_ef 76 | end 77 | 78 | def calculate_next_date 79 | Date.today + @calculated_interval 80 | end 81 | 82 | 83 | def my_rand(x, y) 84 | rand(y - x) + x 85 | end 86 | 87 | end 88 | end --------------------------------------------------------------------------------