forgetful 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +26 -0
- data/LICENSE +1 -1
- data/README.rdoc +1 -3
- data/Rakefile +22 -34
- data/VERSION +1 -1
- data/bin/forgetful +25 -9
- data/lib/forgetful.rb +2 -1
- data/lib/forgetful/cli/quizzer.rb +46 -0
- data/lib/forgetful/extensions/csv/reminder.rb +17 -0
- data/lib/forgetful/extensions/csv/reminder_file.rb +50 -0
- data/lib/forgetful/questionaire.rb +37 -0
- data/spec/questionaire_spec.rb +49 -0
- data/spec/reminder_csv_spec.rb +18 -60
- data/spec/reminder_file_spec.rb +70 -0
- data/spec/reminder_spec.rb +3 -3
- data/spec/spec_helper.rb +3 -6
- data/spec/supermemo_spec.rb +4 -4
- metadata +47 -19
- data/lib/forgetful/cli.rb +0 -78
- data/lib/forgetful/csv_ext/reminder.rb +0 -51
- data/spec/spec.opts +0 -1
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
diff-lcs (1.1.2)
|
5
|
+
git (1.2.5)
|
6
|
+
jeweler (1.6.0)
|
7
|
+
bundler (~> 1.0.0)
|
8
|
+
git (>= 1.2.5)
|
9
|
+
rake
|
10
|
+
rake (0.8.7)
|
11
|
+
rspec (2.3.0)
|
12
|
+
rspec-core (~> 2.3.0)
|
13
|
+
rspec-expectations (~> 2.3.0)
|
14
|
+
rspec-mocks (~> 2.3.0)
|
15
|
+
rspec-core (2.3.1)
|
16
|
+
rspec-expectations (2.3.0)
|
17
|
+
diff-lcs (~> 1.1.2)
|
18
|
+
rspec-mocks (2.3.0)
|
19
|
+
|
20
|
+
PLATFORMS
|
21
|
+
ruby
|
22
|
+
|
23
|
+
DEPENDENCIES
|
24
|
+
bundler
|
25
|
+
jeweler
|
26
|
+
rspec
|
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
= forgetful
|
2
2
|
|
3
|
-
(tested with ree-1.8.7 and 1.9.2dev)
|
4
|
-
|
5
3
|
A minimal command-line app implementing the SuperMemo 2 algorithm.
|
6
4
|
|
7
5
|
The SuperMemo algorithm is designed to refresh your memory at
|
@@ -53,4 +51,4 @@ Enjoy!
|
|
53
51
|
|
54
52
|
== Copyright
|
55
53
|
|
56
|
-
Copyright (c)
|
54
|
+
Copyright (c) 2011 Jonathan Palardy. See LICENSE for details.
|
data/Rakefile
CHANGED
@@ -1,44 +1,32 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
require '
|
3
|
-
|
2
|
+
require 'bundler'
|
4
3
|
begin
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
gem.email = "jonathan.palardy@gmail.com"
|
11
|
-
gem.homepage = "http://github.com/jpalardy/forgetful"
|
12
|
-
gem.authors = ["Jonathan Palardy"]
|
13
|
-
gem.add_development_dependency "rspec", "= 1.2.9"
|
14
|
-
end
|
15
|
-
Jeweler::GemcutterTasks.new
|
16
|
-
rescue LoadError
|
17
|
-
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
18
9
|
end
|
10
|
+
require 'rake'
|
19
11
|
|
20
|
-
require '
|
21
|
-
|
22
|
-
|
23
|
-
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
gem.name = "forgetful"
|
15
|
+
gem.homepage = "http://github.com/jpalardy/forgetful"
|
16
|
+
gem.license = "MIT"
|
17
|
+
gem.summary = "A minimal command-line implementation of the SuperMemo 2 algorithm."
|
18
|
+
gem.description = "A minimal command-line implementation of the SuperMemo 2 algorithm."
|
19
|
+
gem.email = "jonathan.palardy@gmail.com"
|
20
|
+
gem.authors = ["Jonathan Palardy"]
|
21
|
+
# dependencies defined in Gemfile
|
24
22
|
end
|
23
|
+
Jeweler::RubygemsDotOrgTasks.new
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
spec.
|
25
|
+
require 'rspec/core'
|
26
|
+
require 'rspec/core/rake_task'
|
27
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
28
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
30
29
|
end
|
31
30
|
|
32
|
-
task :spec => :check_dependencies
|
33
|
-
|
34
31
|
task :default => :spec
|
35
32
|
|
36
|
-
require 'rake/rdoctask'
|
37
|
-
Rake::RDocTask.new do |rdoc|
|
38
|
-
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
39
|
-
|
40
|
-
rdoc.rdoc_dir = 'rdoc'
|
41
|
-
rdoc.title = "forgetful #{version}"
|
42
|
-
rdoc.rdoc_files.include('README*')
|
43
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
44
|
-
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.0
|
data/bin/forgetful
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require "forgetful"
|
4
|
-
require "forgetful/cli"
|
5
3
|
require 'optparse'
|
6
4
|
|
7
5
|
#-------------------------------------------------
|
@@ -29,7 +27,6 @@ end
|
|
29
27
|
|
30
28
|
def parse(args)
|
31
29
|
options = { :filenames => [],
|
32
|
-
:verbose => false,
|
33
30
|
:action => :quiz,
|
34
31
|
:delay => 0..0 }
|
35
32
|
|
@@ -53,10 +50,6 @@ def parse(args)
|
|
53
50
|
arg.separator ""
|
54
51
|
arg.separator "Common options:"
|
55
52
|
|
56
|
-
arg.on( '-v', '--verbose', "Show more information") do
|
57
|
-
options[:verbose] = true
|
58
|
-
end
|
59
|
-
|
60
53
|
arg.on( '--version', "Show version") do
|
61
54
|
puts File.read(File.join(File.dirname(__FILE__), "..", "VERSION"))
|
62
55
|
exit
|
@@ -80,12 +73,35 @@ end
|
|
80
73
|
|
81
74
|
options = parse(ARGV)
|
82
75
|
|
76
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
77
|
+
require "forgetful"
|
78
|
+
require "forgetful/extensions/csv/reminder_file"
|
79
|
+
require "forgetful/questionaire"
|
80
|
+
require "forgetful/cli/quizzer"
|
81
|
+
|
83
82
|
begin
|
84
83
|
options[:filenames].each do |filename|
|
85
|
-
ReminderFile.new(filename, options[:
|
84
|
+
csv_file = ReminderFile.new(filename, options[:delay])
|
85
|
+
|
86
|
+
case options[:action]
|
87
|
+
when :touch
|
88
|
+
puts "### TOUCH: #{filename}"
|
89
|
+
# subtle -- reading and writing back a file will:
|
90
|
+
# 1. validate the file
|
91
|
+
# 2. add the date column
|
92
|
+
csv_file.write(csv_file.read)
|
93
|
+
when :quiz
|
94
|
+
puts "### QUIZ: #{filename}"
|
95
|
+
|
96
|
+
questionaire = Questionaire.new(csv_file)
|
97
|
+
quizzer = CLI::Quizzer.new(questionaire)
|
98
|
+
|
99
|
+
quizzer.quiz
|
100
|
+
else
|
101
|
+
raise 'hell' # this should not happen
|
102
|
+
end
|
86
103
|
end
|
87
104
|
rescue Interrupt
|
88
105
|
puts
|
89
106
|
# aborting ... won't save file
|
90
107
|
end
|
91
|
-
|
data/lib/forgetful.rb
CHANGED
@@ -0,0 +1,46 @@
|
|
1
|
+
module CLI
|
2
|
+
class Quizzer
|
3
|
+
attr_reader :questionaire
|
4
|
+
|
5
|
+
def initialize(questionaire)
|
6
|
+
@questionaire = questionaire
|
7
|
+
end
|
8
|
+
|
9
|
+
def quiz
|
10
|
+
results = []
|
11
|
+
questions = questionaire.questions.sort_by { rand }
|
12
|
+
|
13
|
+
begin
|
14
|
+
questions.each_with_index do |question, i|
|
15
|
+
q = ask(question, i+1, questions.size)
|
16
|
+
results << [question[:id], q]
|
17
|
+
end
|
18
|
+
rescue EOFError
|
19
|
+
# tolerate Ctrl-D, skips the rest of the quiz
|
20
|
+
end
|
21
|
+
|
22
|
+
questionaire.grade(results)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def ask(question, i, n)
|
27
|
+
width = "#{n}/#{n}. ".size
|
28
|
+
padding = " " * width
|
29
|
+
|
30
|
+
print "#{i}/#{n}. ".rjust(width) + "Q: #{question[:question]}"
|
31
|
+
readline
|
32
|
+
|
33
|
+
puts padding + "A: #{question[:answer]}"
|
34
|
+
|
35
|
+
while true
|
36
|
+
print padding + "? "
|
37
|
+
answer = readline.chomp
|
38
|
+
|
39
|
+
if answer =~ /\A[0-5]\Z/
|
40
|
+
puts
|
41
|
+
return answer.to_i
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "csv"
|
2
|
+
if CSV.const_defined? :Reader
|
3
|
+
# Ruby 1.8 compatible
|
4
|
+
require 'fastercsv'
|
5
|
+
Object.send(:remove_const, :CSV)
|
6
|
+
CSV = FasterCSV
|
7
|
+
end
|
8
|
+
|
9
|
+
class Reminder
|
10
|
+
def to_csv
|
11
|
+
if history.empty?
|
12
|
+
[question, answer, due_on.to_s]
|
13
|
+
else
|
14
|
+
[question, answer, due_on.to_s, history.join]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'forgetful/extensions/csv/reminder'
|
2
|
+
|
3
|
+
class ReminderFile
|
4
|
+
attr_reader :filename, :delay
|
5
|
+
|
6
|
+
def initialize(filename, delay=0..0)
|
7
|
+
@filename = filename
|
8
|
+
@delay = delay
|
9
|
+
end
|
10
|
+
|
11
|
+
def read
|
12
|
+
File.open(filename) do |file|
|
13
|
+
parse_csv(file)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def write(reminders)
|
18
|
+
data = CSV.generate do |csv|
|
19
|
+
reminders.each do |reminder|
|
20
|
+
csv << reminder.to_csv
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
File.open(filename, "w") do |file|
|
25
|
+
file.write(data)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def parse_csv(io)
|
31
|
+
converters = [lambda { |question| question },
|
32
|
+
lambda { |answer| answer },
|
33
|
+
lambda { |due_on| Date.parse(due_on) },
|
34
|
+
lambda { |history| history.scan(/./).map { |x| x.to_i } }]
|
35
|
+
|
36
|
+
CSV.parse(io, :skip_blanks => true).map do |list|
|
37
|
+
list = list.zip(converters).map { |col, converter| converter[col] }
|
38
|
+
list << Date.today + rand_from_range(delay) if list.length == 2 # missing date
|
39
|
+
Reminder.new(*list)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def rand_from_range(range)
|
44
|
+
if RUBY_VERSION < '1.9'
|
45
|
+
(range.min + (range.max - range.min + 1) * rand).floor
|
46
|
+
else
|
47
|
+
Random.new.rand(range)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Questionaire
|
2
|
+
attr_reader :source
|
3
|
+
|
4
|
+
def initialize(source)
|
5
|
+
@source = source
|
6
|
+
end
|
7
|
+
|
8
|
+
def questions
|
9
|
+
i = 0
|
10
|
+
reminders.map { |reminder| pair = [reminder, i]; i+=1; pair }.
|
11
|
+
select { |reminder, i| reminder.due_on <= Date.today }.
|
12
|
+
map do |reminder, i|
|
13
|
+
{ :id => i,
|
14
|
+
:question => reminder.question,
|
15
|
+
:answer => reminder.answer }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def grade(results)
|
20
|
+
source.write(update(reminders, results))
|
21
|
+
end
|
22
|
+
|
23
|
+
#-------------------------------------------------
|
24
|
+
private
|
25
|
+
# results is a list of pairs: [id, q]
|
26
|
+
def update(reminders, results)
|
27
|
+
reminders = reminders.dup
|
28
|
+
results.each do |id,q|
|
29
|
+
reminders[id] = reminders.fetch(id).next(q)
|
30
|
+
end
|
31
|
+
reminders
|
32
|
+
end
|
33
|
+
|
34
|
+
def reminders
|
35
|
+
source.read
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'forgetful/questionaire'
|
3
|
+
|
4
|
+
describe "questionaire" do
|
5
|
+
describe "source" do
|
6
|
+
before do
|
7
|
+
@source = double("source", :read => [])
|
8
|
+
@questionaire = Questionaire.new(@source)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should be pulled" do
|
12
|
+
@source.should_receive(:read).once
|
13
|
+
@questionaire.questions
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should be pushed" do
|
17
|
+
@source.should_receive(:read).once
|
18
|
+
@source.should_receive(:write).once
|
19
|
+
@questionaire.grade([])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "reminders" do
|
24
|
+
before do
|
25
|
+
@reminder1 = double("reminder1", :due_on => Date.today, :question => "some question", :answer => "some answer")
|
26
|
+
@reminder2 = double("reminder2", :due_on => (Date.today - 1), :question => "another question", :answer => "another answer")
|
27
|
+
@reminder3 = double("reminder3", :due_on => (Date.today + 1), :question => "final question", :answer => "final answer")
|
28
|
+
@source = double("source", :read => [@reminder1, @reminder2, @reminder3])
|
29
|
+
|
30
|
+
@questionaire = Questionaire.new(@source)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should be converted" do
|
34
|
+
@questionaire.questions.should == [{:question=>"some question", :answer=>"some answer", :id=>0}, {:question=>"another question", :answer=>"another answer", :id=>1}]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should be adjusted" do
|
38
|
+
@reminder2_1 = double("reminder2_1")
|
39
|
+
@reminder2.stub(:next) { @reminder2_1 }
|
40
|
+
|
41
|
+
@source.should_receive(:write).once.with([@reminder1, @reminder2_1, @reminder3])
|
42
|
+
@questionaire.grade([[1, 3]])
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should raise on unknown indices" do
|
46
|
+
expect{ @questionaire.grade([[42, 3]]) }.to raise_error(IndexError)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/spec/reminder_csv_spec.rb
CHANGED
@@ -1,67 +1,25 @@
|
|
1
|
-
require
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
oxygen,8
|
10
|
-
END
|
11
|
-
@reminders = Reminder.parse_csv(csv)
|
12
|
-
end
|
13
|
-
|
14
|
-
it "should have the right number of lines" do
|
15
|
-
@reminders.size.should == 3
|
16
|
-
end
|
17
|
-
|
18
|
-
it "should have the right data" do
|
19
|
-
@reminders[0].to_a.should == ['carbon', '6',Date.today, []]
|
20
|
-
@reminders[1].to_a.should == ['nitrogen','7',Date.today, []]
|
21
|
-
@reminders[2].to_a.should == ['oxygen', '8',Date.today, []]
|
22
|
-
end
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'forgetful/extensions/csv/reminder'
|
3
|
+
|
4
|
+
describe "reminder_csv" do
|
5
|
+
before do
|
6
|
+
@question = "some question"
|
7
|
+
@answer = "some answer"
|
8
|
+
@date = Date.today + 1
|
23
9
|
end
|
24
10
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
carbon,6,2008-11-11,5
|
29
|
-
nitrogen,7,2008-11-11,535
|
30
|
-
|
31
|
-
oxygen,8,2008-11-11,5042
|
32
|
-
END
|
33
|
-
@reminders = Reminder.parse_csv(csv)
|
34
|
-
end
|
35
|
-
|
36
|
-
it "should have the right number of lines" do
|
37
|
-
@reminders.size.should == 3
|
38
|
-
end
|
39
|
-
|
40
|
-
it "should have the right data" do
|
41
|
-
@reminders[0].to_a.should == ['carbon', '6',Date.parse('2008-11-11'), [5]]
|
42
|
-
@reminders[1].to_a.should == ['nitrogen','7',Date.parse('2008-11-11'), [5,3,5]]
|
43
|
-
@reminders[2].to_a.should == ['oxygen', '8',Date.parse('2008-11-11'), [5,0,4,2]]
|
44
|
-
end
|
11
|
+
it "should handle 2 fields" do
|
12
|
+
reminder = Reminder.new(@question, @answer)
|
13
|
+
reminder.to_csv.should == [@question, @answer, Date.today.to_s]
|
45
14
|
end
|
46
15
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
@reminders << Reminder.new('nitrogen','7').next(5).next(5).next(5)
|
52
|
-
@reminders << Reminder.new('oxygen','8').next(1).next(1).next(1)
|
53
|
-
@reminders << Reminder.new('fluorine','9').next(1).next(5).next(1).next(5)
|
54
|
-
@reminders << Reminder.new('neon','10',Date.today+5)
|
55
|
-
end
|
16
|
+
it "should handle 3 fields" do
|
17
|
+
reminder = Reminder.new(@question, @answer, @date)
|
18
|
+
reminder.to_csv.should == [@question, @answer, @date.to_s]
|
19
|
+
end
|
56
20
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
nitrogen,7,#{(Date.today+16)},555
|
61
|
-
oxygen,8,#{(Date.today+1)},111
|
62
|
-
fluorine,9,#{(Date.today+1)},1515
|
63
|
-
neon,10,#{(Date.today+5)}
|
64
|
-
END
|
65
|
-
end
|
21
|
+
it "should handle 4 fields" do
|
22
|
+
reminder = Reminder.new(@question, @answer, @date, [1,2,3])
|
23
|
+
reminder.to_csv.should == [@question, @answer, @date.to_s, "123"]
|
66
24
|
end
|
67
25
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'forgetful/extensions/csv/reminder_file'
|
3
|
+
|
4
|
+
describe "reminder file parsing (csv)" do
|
5
|
+
before do
|
6
|
+
@filename = "DUMMY"
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "parsing the bare format" do
|
10
|
+
before do
|
11
|
+
csv = <<END
|
12
|
+
carbon,6
|
13
|
+
nitrogen,7
|
14
|
+
oxygen,8
|
15
|
+
END
|
16
|
+
@reminder_file = ReminderFile.new(@filename)
|
17
|
+
@reminders = @reminder_file.send(:parse_csv, csv)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should have the right number of lines" do
|
21
|
+
@reminders.size.should == 3
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should have the right data" do
|
25
|
+
@reminders[0].to_a.should == ['carbon', '6',Date.today, []]
|
26
|
+
@reminders[1].to_a.should == ['nitrogen','7',Date.today, []]
|
27
|
+
@reminders[2].to_a.should == ['oxygen', '8',Date.today, []]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "parsing the bare format (with delay)" do
|
32
|
+
before do
|
33
|
+
@csv = <<END
|
34
|
+
carbon,6
|
35
|
+
END
|
36
|
+
@reminder_file = ReminderFile.new(@filename, 3..7)
|
37
|
+
@date_range = (Date.today + 3)..(Date.today + 7)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should have dates in the given range" do
|
41
|
+
100.times do
|
42
|
+
reminder = @reminder_file.send(:parse_csv, @csv).first
|
43
|
+
@date_range.should include(reminder.due_on)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "parsing the full format" do
|
49
|
+
before do
|
50
|
+
csv = <<END
|
51
|
+
carbon,6,2008-11-11,5
|
52
|
+
nitrogen,7,2008-11-11,535
|
53
|
+
|
54
|
+
oxygen,8,2008-11-11,5042
|
55
|
+
END
|
56
|
+
@reminder_file = ReminderFile.new(@filename)
|
57
|
+
@reminders = @reminder_file.send(:parse_csv, csv)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should have the right number of lines" do
|
61
|
+
@reminders.size.should == 3
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should have the right data" do
|
65
|
+
@reminders[0].to_a.should == ['carbon', '6',Date.parse('2008-11-11'), [5]]
|
66
|
+
@reminders[1].to_a.should == ['nitrogen','7',Date.parse('2008-11-11'), [5,3,5]]
|
67
|
+
@reminders[2].to_a.should == ['oxygen', '8',Date.parse('2008-11-11'), [5,0,4,2]]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/spec/reminder_spec.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
describe "reminder" do
|
4
4
|
before do
|
@@ -18,7 +18,7 @@ describe "reminder" do
|
|
18
18
|
end
|
19
19
|
|
20
20
|
it "should have an ef of 2.5" do
|
21
|
-
@reminder.ef.should
|
21
|
+
@reminder.ef.should be_within(EPSILON).of(2.5)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -39,7 +39,7 @@ describe "reminder" do
|
|
39
39
|
end
|
40
40
|
|
41
41
|
it "should have an ef of 2.8 (based on history)" do
|
42
|
-
@reminder.ef.should
|
42
|
+
@reminder.ef.should be_within(EPSILON).of(2.8)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
data/spec/spec_helper.rb
CHANGED
data/spec/supermemo_spec.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
describe "supermemo" do
|
4
4
|
describe "next_i" do
|
@@ -62,7 +62,7 @@ describe "supermemo" do
|
|
62
62
|
|
63
63
|
qs.zip(results).each do |q,result|
|
64
64
|
it "-- next_ef(q, 2.5) = #{result}" do
|
65
|
-
SuperMemo::next_ef(q, 2.5).should
|
65
|
+
SuperMemo::next_ef(q, 2.5).should be_within(EPSILON).of(result)
|
66
66
|
end
|
67
67
|
end
|
68
68
|
end
|
@@ -73,7 +73,7 @@ describe "supermemo" do
|
|
73
73
|
|
74
74
|
qs.zip(results).each do |q,result|
|
75
75
|
it "-- next_ef(q, 1.3) = #{result}" do
|
76
|
-
SuperMemo::next_ef(q, 1.3).should
|
76
|
+
SuperMemo::next_ef(q, 1.3).should be_within(EPSILON).of(result)
|
77
77
|
end
|
78
78
|
end
|
79
79
|
end
|
@@ -86,7 +86,7 @@ describe "supermemo" do
|
|
86
86
|
it "-- traverse(#{history.inspect}) = #{result.inspect}" do
|
87
87
|
ef, i, interval = SuperMemo::traverse(history)
|
88
88
|
|
89
|
-
ef.should
|
89
|
+
ef.should be_within(EPSILON).of(result[0])
|
90
90
|
i.should == result[1]
|
91
91
|
interval.should == result[2]
|
92
92
|
end
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
7
|
+
- 5
|
8
8
|
- 0
|
9
|
-
version: 0.
|
9
|
+
version: 0.5.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Jonathan Palardy
|
@@ -14,24 +14,48 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-
|
17
|
+
date: 2011-05-14 00:00:00 -04:00
|
18
18
|
default_executable: forgetful
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: rspec
|
22
|
-
prerelease: false
|
23
22
|
requirement: &id001 !ruby/object:Gem::Requirement
|
24
23
|
none: false
|
25
24
|
requirements:
|
26
|
-
- - "
|
25
|
+
- - ">="
|
27
26
|
- !ruby/object:Gem::Version
|
28
27
|
segments:
|
29
|
-
-
|
30
|
-
|
31
|
-
- 9
|
32
|
-
version: 1.2.9
|
28
|
+
- 0
|
29
|
+
version: "0"
|
33
30
|
type: :development
|
31
|
+
prerelease: false
|
34
32
|
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: bundler
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
segments:
|
41
|
+
- 0
|
42
|
+
version: "0"
|
43
|
+
type: :development
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: *id002
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: jeweler
|
48
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
segments:
|
54
|
+
- 0
|
55
|
+
version: "0"
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *id003
|
35
59
|
description: A minimal command-line implementation of the SuperMemo 2 algorithm.
|
36
60
|
email: jonathan.palardy@gmail.com
|
37
61
|
executables:
|
@@ -42,6 +66,9 @@ extra_rdoc_files:
|
|
42
66
|
- LICENSE
|
43
67
|
- README.rdoc
|
44
68
|
files:
|
69
|
+
- .rspec
|
70
|
+
- Gemfile
|
71
|
+
- Gemfile.lock
|
45
72
|
- LICENSE
|
46
73
|
- README.rdoc
|
47
74
|
- Rakefile
|
@@ -50,19 +77,22 @@ files:
|
|
50
77
|
- examples/katakana_romaji.csv
|
51
78
|
- examples/romaji_katakana.csv
|
52
79
|
- lib/forgetful.rb
|
53
|
-
- lib/forgetful/cli.rb
|
54
|
-
- lib/forgetful/
|
80
|
+
- lib/forgetful/cli/quizzer.rb
|
81
|
+
- lib/forgetful/extensions/csv/reminder.rb
|
82
|
+
- lib/forgetful/extensions/csv/reminder_file.rb
|
83
|
+
- lib/forgetful/questionaire.rb
|
55
84
|
- lib/forgetful/reminder.rb
|
56
85
|
- lib/forgetful/supermemo.rb
|
86
|
+
- spec/questionaire_spec.rb
|
57
87
|
- spec/reminder_csv_spec.rb
|
88
|
+
- spec/reminder_file_spec.rb
|
58
89
|
- spec/reminder_spec.rb
|
59
|
-
- spec/spec.opts
|
60
90
|
- spec/spec_helper.rb
|
61
91
|
- spec/supermemo_spec.rb
|
62
92
|
has_rdoc: true
|
63
93
|
homepage: http://github.com/jpalardy/forgetful
|
64
|
-
licenses:
|
65
|
-
|
94
|
+
licenses:
|
95
|
+
- MIT
|
66
96
|
post_install_message:
|
67
97
|
rdoc_options: []
|
68
98
|
|
@@ -73,6 +103,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
103
|
requirements:
|
74
104
|
- - ">="
|
75
105
|
- !ruby/object:Gem::Version
|
106
|
+
hash: -36298353
|
76
107
|
segments:
|
77
108
|
- 0
|
78
109
|
version: "0"
|
@@ -91,8 +122,5 @@ rubygems_version: 1.3.7
|
|
91
122
|
signing_key:
|
92
123
|
specification_version: 3
|
93
124
|
summary: A minimal command-line implementation of the SuperMemo 2 algorithm.
|
94
|
-
test_files:
|
95
|
-
|
96
|
-
- spec/reminder_spec.rb
|
97
|
-
- spec/spec_helper.rb
|
98
|
-
- spec/supermemo_spec.rb
|
125
|
+
test_files: []
|
126
|
+
|
data/lib/forgetful/cli.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
|
2
|
-
class ReminderFile
|
3
|
-
attr_reader :filename, :verbose, :delay
|
4
|
-
|
5
|
-
def initialize(filename, verbose=false, delay=0..0)
|
6
|
-
@filename = filename
|
7
|
-
@verbose = verbose
|
8
|
-
@delay = delay
|
9
|
-
end
|
10
|
-
|
11
|
-
def reminders
|
12
|
-
@reminders ||= Reminder.read_csv(filename, delay)
|
13
|
-
end
|
14
|
-
|
15
|
-
def write(reminders)
|
16
|
-
raise "Writing back a different number of reminders than read from file (#{reminders.size} != #{@reminders.size})" if reminders.size != @reminders.size
|
17
|
-
Reminder.write_csv(filename, reminders)
|
18
|
-
end
|
19
|
-
|
20
|
-
# subtle -- reading and writing back a file will:
|
21
|
-
# 1. validate the file
|
22
|
-
# 2. add the date column
|
23
|
-
def touch
|
24
|
-
puts "### TOUCH: #{filename}"
|
25
|
-
write(reminders)
|
26
|
-
end
|
27
|
-
|
28
|
-
def quiz
|
29
|
-
puts "### QUIZ: #{filename}"
|
30
|
-
dues, not_dues = reminders.partition { |reminder| reminder.due_on <= Date.today }
|
31
|
-
gradeds, ungradeds = quiz_map(dues.sort_by { rand })
|
32
|
-
|
33
|
-
write(gradeds + ungradeds + not_dues)
|
34
|
-
|
35
|
-
faileds = gradeds.select { |reminder| reminder.review? }
|
36
|
-
until faileds.empty? || ungradeds.any?
|
37
|
-
puts "### REVIEW: #{filename}"
|
38
|
-
gradeds, ungradeds = quiz_map(faileds.sort_by { rand })
|
39
|
-
faileds = gradeds.select { |reminder| reminder.review? }
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
def quiz_map(reminders)
|
45
|
-
gradeds = []
|
46
|
-
ungradeds = reminders.dup
|
47
|
-
|
48
|
-
reminders.each_with_index do |reminder, i|
|
49
|
-
q = ask(reminder, i+1, reminders.size)
|
50
|
-
gradeds << ungradeds.shift.next(q)
|
51
|
-
end
|
52
|
-
rescue EOFError
|
53
|
-
# tolerate Ctrl-D, skips the rest of the quiz
|
54
|
-
ensure
|
55
|
-
return gradeds, ungradeds
|
56
|
-
end
|
57
|
-
|
58
|
-
def ask(reminder, i, n)
|
59
|
-
width = "#{n}/#{n}. ".size
|
60
|
-
padding = " " * width
|
61
|
-
|
62
|
-
print "#{i}/#{n}. ".rjust(width) + "Q: #{reminder.question}"
|
63
|
-
readline
|
64
|
-
|
65
|
-
puts padding + "A: #{reminder.answer}"
|
66
|
-
puts padding + " %.2f -> %s" % [reminder.ef, reminder.history.join] if verbose
|
67
|
-
|
68
|
-
while true
|
69
|
-
print padding + "? "
|
70
|
-
answer = readline.chomp
|
71
|
-
|
72
|
-
if answer =~ /\A[0-5]\Z/
|
73
|
-
puts
|
74
|
-
return answer.to_i
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
@@ -1,51 +0,0 @@
|
|
1
|
-
require "csv"
|
2
|
-
if CSV.const_defined? :Reader
|
3
|
-
# Ruby 1.8 compatible
|
4
|
-
require 'rubygems'
|
5
|
-
require 'fastercsv'
|
6
|
-
Object.send(:remove_const, :CSV)
|
7
|
-
CSV = FasterCSV
|
8
|
-
end
|
9
|
-
|
10
|
-
class Reminder
|
11
|
-
def to_csv
|
12
|
-
if history.empty?
|
13
|
-
[question, answer, due_on.to_s]
|
14
|
-
else
|
15
|
-
[question, answer, due_on.to_s, history.join]
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.read_csv(filename, delay=0..0)
|
20
|
-
File.open(filename) do |file|
|
21
|
-
self.parse_csv(file, delay)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def self.parse_csv(io, delay=0..0)
|
26
|
-
converters = [lambda { |question| question },
|
27
|
-
lambda { |answer| answer },
|
28
|
-
lambda { |due_on| Date.parse(due_on) },
|
29
|
-
lambda { |history| history.scan(/./).map { |x| x.to_i } }]
|
30
|
-
|
31
|
-
CSV.parse(io, :skip_blanks => true).map do |list|
|
32
|
-
list = list.zip(converters).map { |col, converter| converter[col] }
|
33
|
-
list << Date.today + Random.new.rand(delay) if list.length == 2 # missing date
|
34
|
-
new(*list)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def self.write_csv(filename, reminders)
|
39
|
-
File.open(filename, "w") do |file|
|
40
|
-
file.write(generate_csv(reminders))
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def self.generate_csv(reminders)
|
45
|
-
CSV.generate do |csv|
|
46
|
-
reminders.each do |reminder|
|
47
|
-
csv << reminder.to_csv
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
data/spec/spec.opts
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
--color --backtrace
|