forgetful 0.4.0 → 0.5.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.
- 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
|