srs 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012, Daniel P. Wright
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,292 @@
1
+ srs
2
+ ===
3
+
4
+ A Spaced Repetition System is a study tool which works by spacing out exercises
5
+ so as to learn in the most efficient manner possible. Further information can
6
+ be found at the following Wikipedia pages:
7
+
8
+ * [Spacing Effect][1]
9
+ * [Forgetting Curve][2]
10
+ * [Spaced Repetition][3]
11
+
12
+ `srs` is a command-line based implementation of the spaced repetition system. It
13
+ is designed to be highly extensible and to promote the sharing of data for study
14
+ by others.
15
+
16
+ Installation
17
+ ------------
18
+
19
+ `srs` is distributed as a Gem. Make sure you have Ruby and RubyGems installed,
20
+ and then type:
21
+
22
+ $ gem install srs
23
+
24
+ Usage
25
+ -----
26
+
27
+ This first release of `srs` is an _alpha_ release -- it is functionally
28
+ complete, but the user interface is in its very early stages, documentation is
29
+ lacking, and there may be bugs. With that in mind, read on...
30
+
31
+ ### Initialising a workspace
32
+
33
+ The first thing you will want to do once you've installed `srs` is to initialise
34
+ a _workspace_. This is where all the data required for one set of material you
35
+ want to study reside. It is generally a good idea to group related items
36
+ together -- for example, I have a workspace for Japanese vocabulary, another for
37
+ kanji, and another for poetry and quotations which I'd like to remember.
38
+
39
+ How you think the things you want to learn should be distributed is a
40
+ personal choice, and you should consider for yourself what will work best for
41
+ you. For example, some people might prefer to put the Japanese vocabulary and
42
+ the kanji together in one workspace -- that is fine. Merging or splitting
43
+ workspaces at a later stage is relatively easy, so do experiment!
44
+
45
+ To initialise a workspace, create a directory and run the following command
46
+ inside it:
47
+
48
+ $ srs init
49
+
50
+ ### Adding an exercise
51
+
52
+ In `srs`, a single item of practice or revision is called an _exercise_. These
53
+ can be anything -- a flashcard-style question-and-answer, or a more interactive
54
+ form or practice. What a particular exercise entails depends entirely on what
55
+ it is you want to practice, and for that reason `srs` introduces the concept of
56
+ _models_.
57
+
58
+ A model is a Ruby class which defines how an exercise is performed. `srs` comes
59
+ packaged with the most basic kind of model, a flashcard, which is distributed
60
+ under the name `SimpleFlashcard`. You can create your own models, but for now
61
+ we'll make use of the `SimpleFlashcard` model to get something up and running
62
+ quickly.
63
+
64
+ A `SimpleFlashcard` exercise comes in two parts:
65
+
66
+ * The _data_, which usually contains the actual thing you want to test
67
+ * The _exercise specification_, which determines how to use that data.
68
+
69
+ This separation allows you to use the same data for multiple exercises. In this
70
+ example, we're going to create a "Production" and a "Recognition" card for the
71
+ Japanese word, 勉強, which means "study".
72
+
73
+ The first thing we need to do is create the DataFile. `SimpleFlashcard`
74
+ currently expects its data to consist of a series of key-value pairs, separated
75
+ by a colon. Currently multi-line fields are not supported, though this will
76
+ change in a future version. Run the following from inside the workspace
77
+ directory you created (The ^D at the end signifies pressing Control-D to send
78
+ the end-of-file marker to `srs`):
79
+
80
+ $ srs insert-into data
81
+ Word: 勉強
82
+ Pronunciation (Hiragana): べんきょう
83
+ Pronunciation (Romaji): Benkyou
84
+ Meaning: Study
85
+ ^Dc13d1e790ef5e8ced8c96a37a6d014f08ddcb3af
86
+
87
+ You should see the output after pressing ^D as above,
88
+ `c13d1e790ef5e8ced8c96a37a6d014f08ddcb3af`. The string itself may be different,
89
+ but it will be a long string of hexadecimal digits. The `insert-into` command
90
+ reads data in from STDIN and outputs an ID which can be used by other `srs`
91
+ commands to access that data.
92
+
93
+ We now have data containing four fields related to the word. We can combine
94
+ these fields in a variety of ways to generate a number of exercises. Here we'll
95
+ generate two; one to produce the English meaning when shown the word and the
96
+ pronunciation; the other to produce the Japanese word when shown the English.
97
+ Input the following, substituting the value passed into the _Data_ field with
98
+ whatever was output from the previous command:
99
+
100
+ $ srs insert-into exercises
101
+ Data: c13d1e790ef5e8ced8c96a37a6d014f08ddcb3af
102
+ Model: SimpleFlashcard
103
+
104
+ [Word]
105
+ [Pronunciation (Hiragana)]
106
+ ---
107
+ [Meaning]
108
+ ^D884bd92624411f5bb42ff9abdf84c3e09ba00cab
109
+
110
+ Note the blank line between the set of key-value pairs and the text below.
111
+ `SimpleFlashcard` expects a series of headers, followed by a blank line,
112
+ followed by some metadata. The metadata is in two parts: the question, which is
113
+ everything before the "---" string, and the answer, which is everything that
114
+ comes after it. Any words within square brackets are substituted with the value
115
+ of their corresponding field in the data.
116
+
117
+ As with the previous command, this command outputs an ID once it has completed.
118
+ Remember this; you will need it later. Let's add the second exercise:
119
+
120
+ $ srs insert-into exercises
121
+ Data: c13d1e790ef5e8ced8c96a37a6d014f08ddcb3af
122
+ Model: SimpleFlashcard
123
+
124
+ [Meaning]
125
+ ---
126
+ [Word]
127
+ ^Dd930b3fce3d2f988758c7088ea77d9075b8c82bf
128
+
129
+ As you can see, this is just the same exercise, with the question and answer
130
+ reversed. Also, we are ignoring pronunciation for this one.
131
+
132
+ You will notice, neither of these exercises make use of the "Pronunciation (Romaji)"
133
+ field. The truth is, I don't much like Romaji. But it is entirely reasonable
134
+ to add fields you won't use as part of the exercises to the data; you may choose
135
+ to create exercises which make use of that data later, or you may just want to
136
+ look it up (for example, you could include the link to a URL where you
137
+ discovered the information).
138
+
139
+ ### Scheduling an exercise
140
+
141
+ The next thing we must do is schedule the exercises we've just created. If we
142
+ don't do this, they will never enter the `srs` scheduling system, and so they
143
+ will simply sit there unasked!
144
+
145
+ There have been a number of spaced repetition algorithms developed over the
146
+ years, perhaps the most famous of which are the [Pimsleur Graduated Recall][4]
147
+ and [SuperMemo 2][5] algorithms. As with models, `srs` allows you to define
148
+ your own custom spacing algorithm by creating a _scheduler_. The base
149
+ distribution comes with probably the most popular spacing algorithm
150
+ pre-installed, SuperMemo 2. We'll use that one.
151
+
152
+ Type the following, substituting the two ids with the ones returned when you
153
+ inserted the two exercises:
154
+
155
+ $ srs schedule -s SuperMemo2 884bd92624411f5bb42ff9abdf84c3e09ba00cab
156
+ schedule/pending/20120708003132.386
157
+ $ srs schedule -s SuperMemo2 d930b3fce3d2f988758c7088ea77d9075b8c82bf
158
+ schedule/pending/20120708003149.754
159
+
160
+ ### Doing some reps -- new exercises
161
+
162
+ Now that you've scheduled some exercises, you're ready to do some reps. Let's
163
+ ask `srs` what the next new exercise is which is available for learning:
164
+
165
+ $ srs next-new
166
+ 20120708003132.386
167
+
168
+ The ID of the first exercise you scheduled above should be output. In order to
169
+ actually test ourselves, we'll need the ID of the exercise we want to run. We
170
+ can get this from the `Exercise` field stored in the schedule (as always,
171
+ remembering to substitute the example ID below with your own):
172
+
173
+ $ srs get-field exercise 20120708003132.386
174
+ 884bd92624411f5bb42ff9abdf84c3e09ba00cab
175
+
176
+ An exercise ID will be output, which we can feed straight into `do-exercise`:
177
+
178
+ $ srs do-exercise a884bd92624411f5bb42ff9abdf84c3e09ba00cab
179
+ 勉強
180
+ べんきょう
181
+ >
182
+
183
+ At this point you are given a prompt. Let's enter the correct answer, "Study",
184
+ and see what happens:
185
+
186
+ > Study
187
+ Correct.
188
+ You scored: 1.0
189
+
190
+ Scores in `srs` are normalised from 0-1, so 1.0 is a full score. Well done! We
191
+ still need to enter this into the scheduler so that it knows when next to repeat
192
+ the exercise. Enter the following to reschedule the exercise. The ID is the
193
+ _schedule_ ID, not the one for the exercise:
194
+
195
+ $ srs reschedule 20120708003132.386 1.0
196
+ Exercise rescheduled for 2012-07-09 00:00:00 +0900
197
+
198
+ Excellent! We'll see this exercise again tomorrow.
199
+
200
+ It's actually possible to wrap up most of the above in a single line. The
201
+ following assumes you use a `bash` shell, though other shells may be similar:
202
+
203
+ $ SCHEDULE=$(srs next-new); EXERCISE=$(srs get-field exercise $SCHEDULE); srs do-exercise $EXERCISE
204
+
205
+ This time we'll try answering the question incorrectly:
206
+
207
+ Study
208
+ > 遊ぶ
209
+ 勉強
210
+ Was your answer: [h] Correct, [j] Close, [k] Wrong, or [l] Very Wrong?
211
+ > l
212
+ You scored: 0.0
213
+
214
+ When you enter a wrong answer, the `SimpleFlashcard` doesn't attempt to judge
215
+ for itself whether or not you were close to the write answer. Instead, it shows
216
+ you the correct answer and lets you specify how close you thought you were. In
217
+ this case, we were miles off, so we selected 'l', to fail the exercise
218
+ completely. Now to reschedule the exercise:
219
+
220
+ $ srs reschedule $SCHEDULE 0.0
221
+ Exercise rescheduled for 2012-07-09 00:00:00 +0900
222
+ Exercise failed; marked for repetition
223
+
224
+ Since we failed the exercise, the scheduler has marked it for repetition. This
225
+ means that once we've finished all our scheduled reps for the day, we will be
226
+ presented with this exercise (and any other failed exercise), to try again until
227
+ we have managed to pass them. Note that only the first attempt affects the
228
+ interval; subsequent repetitions are simply practice.
229
+
230
+ ### Practice makes perfect! Repeating exercises
231
+
232
+ For the most part, you're going to be practicing exercises you've already done
233
+ once. The flow for this is very similar to the above, except that instead of
234
+ `next-new` we use the `next-due` command.
235
+
236
+ Before we can use this command, however, we need to update the srs queue:
237
+
238
+ $ srs queue
239
+
240
+ This command tells `srs` to look through the schedules and determine which
241
+ exercises are due for practice. We can now use `next-due` similarly to the
242
+ way we practised new exercises in the previous section:
243
+
244
+ $ SCHEDULE=$(srs next-due); EXERCISE=$(srs get-field exercise $SCHEDULE); srs do-exercise $EXERCISE
245
+ Study
246
+ > 勉強
247
+ Correct.
248
+ You scored: 1.0
249
+
250
+ $ srs reschedule $SCHEDULE 1.0
251
+ Exercise rescheduled for 2012-07-09 00:00:00 +0900
252
+
253
+ In this case, since the exercise hard already been scheduled and was simply a
254
+ repetition of a failed exercise, the date matched that which was output
255
+ previously.
256
+
257
+ Finally, we can confirm that there are no more exercises left to practice:
258
+
259
+ $ srs queue
260
+ $ srs next-due
261
+
262
+ Contributing
263
+ ------------
264
+
265
+ `srs` is in very early stages and as such there is a _lot_ of work still to do
266
+ on it. Contributions are welcome!
267
+
268
+ To contribute, fork the project on github and send me a pull request, or email
269
+ me a patch. Please bear the following in mind when making contributions:
270
+
271
+ * Try and keep individual commits small and self-contained. If I feel like
272
+ there is too much going on in a single commit, I may ask you to split it up
273
+ into multiple commits.
274
+ * Please write clear, descriptive commit messages. These should be formatted
275
+ with a title of `<=` 50 characters, and body text wrapped at 72 characters.
276
+ I am quite particular about this.
277
+ * I come from a pretty heavy C++ background. Ruby style corrections and
278
+ improvements are very much appreciated! Please be nice about it.
279
+
280
+ Copyright
281
+ ---------
282
+
283
+ Copyright (c) 2012 Daniel P. Wright.
284
+
285
+ This software is released under the Simplified BSD Licence. See LICENCE.md for
286
+ further details.
287
+
288
+ [1]: http://en.wikipedia.org/wiki/Spacing_effect
289
+ [2]: http://en.wikipedia.org/wiki/Forgetting_curve
290
+ [3]: http://en.wikipedia.org/wiki/Spaced_repetition
291
+ [4]: http://en.wikipedia.org/wiki/Graduated_interval_recall
292
+ [5]: http://www.supermemo.com/english/ol/sm2.htm
data/bin/srs ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'srs/cli'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'srs/cli'
8
+ end
9
+
10
+ exit SRS::CLI.run!(*ARGV)
11
+
@@ -0,0 +1,2 @@
1
+ require 'srs/workspace'
2
+ require 'srs/version'
@@ -0,0 +1,43 @@
1
+ require 'srs/cli/init'
2
+ require 'srs/cli/help'
3
+ require 'srs/cli/insert-into'
4
+ require 'srs/cli/schedule'
5
+ require 'srs/cli/do-exercise'
6
+ require 'srs/cli/reschedule'
7
+ require 'srs/cli/queue'
8
+ require 'srs/cli/next-due'
9
+ require 'srs/cli/next-new'
10
+ require 'srs/cli/get-field'
11
+ require 'srs/cli/cat'
12
+
13
+ module SRS
14
+ class CLI
15
+ class << self
16
+ COMMANDS = { "init" => :Init,
17
+ "insert-into" => :InsertInto,
18
+ "schedule" => :Schedule,
19
+ "do-exercise" => :DoExercise,
20
+ "reschedule" => :Reschedule,
21
+ "queue" => :Queue,
22
+ "next-due" => :NextDue,
23
+ "next-new" => :NextNew,
24
+ "get-field" => :GetField,
25
+ "cat" => :Cat,
26
+ "help" => :Help }.freeze
27
+
28
+ def cmd_to_symbol(command)
29
+ return COMMANDS[command]
30
+ end
31
+
32
+ def run!(*arguments)
33
+ command = cmd_to_symbol(arguments.shift)
34
+ if command
35
+ return SRS::CLI.const_get(command).new.run!(arguments)
36
+ else
37
+ return SRS::CLI::Help.new.run!([])
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
@@ -0,0 +1,39 @@
1
+ module SRS
2
+ class CLI
3
+ class Cat
4
+ VALID_SECTIONS = ["data", "exercises"].freeze
5
+ def run!(arguments)
6
+ if not SRS::Workspace.initialised? then
7
+ puts "Current directory is not an SRS Workspace"
8
+ return 3
9
+ end
10
+
11
+ sha1 = arguments.shift
12
+ sha1_start = sha1[0..1]
13
+ sha1_rest = sha1[2..-1]
14
+
15
+ VALID_SECTIONS.each do |section|
16
+ datafile = "#{section}/#{sha1_start}/#{sha1_rest}"
17
+ if File.exists?(datafile) then
18
+ contents = File.open(datafile, "r"){ |file| file.read }
19
+ puts contents
20
+
21
+ return 0
22
+ end
23
+ end
24
+
25
+ puts "No content with that ID exists"
26
+ return 4
27
+ end
28
+
29
+ def help()
30
+ puts <<-EOF
31
+ srs cat <id>
32
+
33
+ Outputs the content matching <id>
34
+ EOF
35
+ end
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,73 @@
1
+ module SRS
2
+ class CLI
3
+ class DoExercise
4
+ def run!(arguments)
5
+ if not SRS::Workspace.initialised? then
6
+ puts "Current directory is not an SRS Workspace"
7
+ return 3
8
+ end
9
+
10
+ sha1 = arguments.shift
11
+ sha1_start = sha1[0..1]
12
+ sha1_rest = sha1[2..-1]
13
+
14
+ datafile = "exercises/#{sha1_start}/#{sha1_rest}"
15
+
16
+ if not File.exists?(datafile) then
17
+ puts "No content with that ID exists"
18
+ return 4
19
+ end
20
+
21
+ headers = {}
22
+ metadata = ""
23
+ File.open(datafile, "r") do |file|
24
+ while( line = file.gets() ) do
25
+ if line.strip.empty? then
26
+ break
27
+ end
28
+
29
+ key, *val = line.split(':').map{|e| e.strip}
30
+ headers[key] = val.join(':')
31
+ end
32
+ metadata = file.read
33
+ end
34
+
35
+ runModel(sha1, headers, metadata)
36
+ end
37
+
38
+ def runModel(sha1, headers, metadata)
39
+ if not headers.has_key?("Model") then
40
+ puts "Exercise #{sha1} has no model!\n"
41
+ return nil
42
+ end
43
+
44
+ modelclass = headers.delete("Model")
45
+
46
+ begin
47
+ require "./models/#{modelclass}"
48
+ rescue LoadError
49
+ begin
50
+ require "srs/models/#{modelclass}"
51
+ rescue LoadError
52
+ puts "Couldn't find model #{modelclass}."
53
+ return nil
54
+ end
55
+ end
56
+
57
+ model = SRS::Models.const_get(modelclass.to_sym).new
58
+ score = model.run(headers, metadata)
59
+
60
+ return score
61
+ end
62
+
63
+ def help()
64
+ puts <<-EOF
65
+ srs do-exercise <id>
66
+
67
+ Runs the exercise defined in <id>
68
+ EOF
69
+ end
70
+ end
71
+ end
72
+ end
73
+
@@ -0,0 +1,56 @@
1
+ module SRS
2
+ class CLI
3
+ class GetField
4
+ def run!(arguments)
5
+ if not SRS::Workspace.initialised? then
6
+ puts "Current directory is not an SRS Workspace"
7
+ return 3
8
+ end
9
+
10
+ field = arguments.shift
11
+ id = arguments.shift
12
+
13
+ is_schedule = (id =~ /\d{14}\.\d{3}/)
14
+
15
+ filename = ""
16
+ if is_schedule then
17
+ filename = "schedule/#{id}"
18
+ filename = "schedule/pending/#{id}" if not File.exists?(filename)
19
+ else
20
+ filename = "exercises/#{id}"
21
+ end
22
+
23
+ if not File.exists?(filename) then
24
+ puts "No content with that ID exists"
25
+ return 4
26
+ end
27
+
28
+ File.open(filename, "r") do |file|
29
+ while( line = file.gets() ) do
30
+ if line.strip.empty? then
31
+ break
32
+ end
33
+
34
+ key, *val = line.split(':').map{|e| e.strip}
35
+ if key.casecmp(field) == 0 then
36
+ puts val.join(':')
37
+ return 0
38
+ end
39
+ end
40
+ end
41
+
42
+ puts "Content #{id} does not contain field \"#{field}\"."
43
+ return 4
44
+ end
45
+
46
+ def help()
47
+ puts <<-EOF
48
+ srs get-field <field-name> <content-id>
49
+
50
+ Returns the value of the field <field-name> from content <content-id>
51
+ EOF
52
+ end
53
+ end
54
+ end
55
+ end
56
+
@@ -0,0 +1,44 @@
1
+ require 'srs/cli'
2
+
3
+ module SRS
4
+ class CLI
5
+ class Help
6
+ def run!(arguments)
7
+ if arguments.empty?
8
+ summary
9
+ else
10
+ command = SRS::CLI::cmd_to_symbol(arguments.first)
11
+ if command
12
+ SRS::CLI.const_get(command).new.help()
13
+ else
14
+ summary
15
+ end
16
+ end
17
+ return 0
18
+ end
19
+
20
+ def help()
21
+ end
22
+
23
+ def summary()
24
+ puts "Usage: srs <command> [args]"
25
+ puts
26
+ puts "Available commands are:"
27
+ puts " init Initialise an SRS workspace"
28
+ puts " insert-into Insert data into the workspace"
29
+ puts " schedule Schedule an exercise"
30
+ puts " do-exercise Perform a rep on an exercise"
31
+ puts " reschedule Update an exercise schedule based on score"
32
+ puts " queue Queue due exercises"
33
+ puts " next-due Retrieve the next due exercise from the queue"
34
+ puts " next-new Retrieve the next available untested exercise"
35
+ puts " get-field Retrieve a field by name from a schedule or exercise"
36
+ puts " cat Output data contained within the workspace"
37
+ puts
38
+ puts "See 'srs help <command>' for more information on a specific command."
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+
@@ -0,0 +1,58 @@
1
+ require 'optparse'
2
+ require 'srs/workspace'
3
+
4
+ module SRS
5
+ class CLI
6
+ class Init
7
+ def initialize
8
+ @options = {}
9
+ @opts = OptionParser.new do |o|
10
+ o.banner = <<-EOF.gsub /^\s+/, ""
11
+ srs init [options] [dirname]
12
+
13
+ Initialises a workspace in directory [dirname]. A `.srs/` folder will be
14
+ created containing the default configuration files. A skeleton directory
15
+ structure may also be created (this is undecided as yet).
16
+
17
+ If no [dirname] is passed, uses the current directory.
18
+ EOF
19
+
20
+ o.on('-f', '--force', 'Initialise workspace even if the directory is not empty') do
21
+ @options[:force] = true
22
+ end
23
+ end
24
+ end
25
+
26
+ def run!(arguments)
27
+ begin
28
+ @opts.parse!(arguments)
29
+ @options[:dir_name] = arguments.shift
30
+ rescue OptionParser::InvalidOption => e
31
+ @options[:invalid_argument] = e.message
32
+ end
33
+
34
+ if @options[:dir_name] == nil then
35
+ @options[:dir_name] = "./"
36
+ end
37
+
38
+ begin
39
+ SRS::Workspace.create(@options[:dir_name], @options[:force])
40
+ rescue SRS::Workspace::AlreadyInitialisedError => e
41
+ puts "SRS is already initialised in #{@options[:dir_name]}."
42
+ return 2
43
+ rescue SRS::Workspace::FolderNotEmptyError => e
44
+ puts "The current folder is not empty!"
45
+ puts "Run 'srs init --force' to initialise in this folder anyway."
46
+ return 1
47
+ end
48
+
49
+ 0
50
+ end
51
+
52
+ def help()
53
+ puts @opts
54
+ end
55
+ end
56
+ end
57
+ end
58
+
@@ -0,0 +1,48 @@
1
+ require 'fileutils'
2
+ require 'digest/sha1'
3
+
4
+ module SRS
5
+ class CLI
6
+ class InsertInto
7
+ VALID_SECTIONS = ["data", "exercises"].freeze
8
+ def run!(arguments)
9
+ if not SRS::Workspace.initialised? then
10
+ puts "Current directory is not an SRS Workspace"
11
+ return 3
12
+ end
13
+
14
+ section = arguments.shift()
15
+ if section == nil or !VALID_SECTIONS.include?(section) then
16
+ help()
17
+ return 4
18
+ end
19
+
20
+ data = STDIN.read()
21
+ sha1 = Digest::SHA1.hexdigest data
22
+ sha1_start = sha1[0..1]
23
+ sha1_rest = sha1[2..-1]
24
+ datafile = "#{section}/#{sha1_start}/#{sha1_rest}"
25
+
26
+ if not File.exists?(datafile) then
27
+ FileUtils::mkdir_p("#{section}/#{sha1_start}")
28
+ File.open(datafile, 'w') {|f| f.write(data)}
29
+ end
30
+
31
+ puts sha1
32
+
33
+ return 0
34
+ end
35
+
36
+ def help()
37
+ puts <<-EOF
38
+ srs insert-into <section>
39
+
40
+ Reads the contents from stdin and inserts it into the appropriate section in the
41
+ workspace. <section> can be one of "data", "exercise", or "schedule". Returns
42
+ the id used to access that exercise.
43
+ EOF
44
+ end
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,44 @@
1
+ module SRS
2
+ class CLI
3
+ class NextDue
4
+ def run!(arguments)
5
+ if not SRS::Workspace.initialised? then
6
+ puts "Current directory is not an SRS Workspace"
7
+ return 3
8
+ end
9
+
10
+ ws = SRS::Workspace.new
11
+
12
+ schedule = nil
13
+ if File.exists? "#{ws.dotsrs}/QUEUED" then
14
+ File.open("#{ws.dotsrs}/QUEUED", "r") do |queued_file|
15
+ schedule = queued_file.gets
16
+ end
17
+ end
18
+
19
+ if schedule == nil then
20
+ if File.exists? "#{ws.dotsrs}/REPEAT" then
21
+ File.open("#{ws.dotsrs}/REPEAT", "r") do |repeat_file|
22
+ schedule = repeat_file.gets
23
+ end
24
+ end
25
+ end
26
+
27
+ if not schedule == nil then
28
+ puts File.basename schedule
29
+ end
30
+
31
+ return 0
32
+ end
33
+
34
+ def help()
35
+ puts <<-EOF
36
+ srs next-due
37
+
38
+ Prints out the id of the next due schedule. Prints nothing if nothing is due.
39
+ EOF
40
+ end
41
+ end
42
+ end
43
+ end
44
+
@@ -0,0 +1,29 @@
1
+ module SRS
2
+ class CLI
3
+ class NextNew
4
+ def run!(arguments)
5
+ if not SRS::Workspace.initialised? then
6
+ puts "Current directory is not an SRS Workspace"
7
+ return 3
8
+ end
9
+
10
+ new_schedules = Dir["schedule/pending/*"].sort
11
+ if not new_schedules.empty?
12
+ puts File.basename new_schedules.first
13
+ end
14
+
15
+ return 0
16
+ end
17
+
18
+ def help()
19
+ puts <<-EOF
20
+ srs next-new
21
+
22
+ Prints out the id of the next untested schedule. Prints nothing if there are no
23
+ pending schedules.
24
+ EOF
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,68 @@
1
+ require 'date'
2
+
3
+ module SRS
4
+ class CLI
5
+ class Queue
6
+ def run!(arguments)
7
+ if not SRS::Workspace.initialised? then
8
+ puts "Current directory is not an SRS Workspace"
9
+ return 3
10
+ end
11
+
12
+ queued = {}
13
+ repeat = []
14
+
15
+ Dir["schedule/*"].each do |filename|
16
+ next if File.directory?(filename)
17
+
18
+ schedule = {}
19
+ File.open(filename, "r") do |file|
20
+ while( line = file.gets() ) do
21
+ if line.strip.empty? then
22
+ break
23
+ end
24
+
25
+ key, *val = line.split(':').map{|e| e.strip}
26
+ schedule[key] = val.join(':')
27
+ end
28
+
29
+ if( schedule["Repeat"] == "true" ) then
30
+ repeat << filename
31
+ else
32
+ due = DateTime.parse(schedule["Due"])
33
+ if( due < DateTime.now ) then
34
+ queued[filename] = due
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ ws = SRS::Workspace.new
41
+
42
+ File.open("#{ws.dotsrs}/QUEUED", "w") do |queued_file|
43
+ queued.sort_by{ |key, value| value }
44
+ queued.each do |filename, due|
45
+ queued_file.puts filename
46
+ end
47
+ end
48
+
49
+ File.open("#{ws.dotsrs}/REPEAT", "w") do |repeat_file|
50
+ repeat.each do |filename|
51
+ repeat_file.puts filename
52
+ end
53
+ end
54
+
55
+ return 0
56
+ end
57
+
58
+ def help()
59
+ puts <<-EOF
60
+ srs queue
61
+
62
+ Queues exercises for review.
63
+ EOF
64
+ end
65
+ end
66
+ end
67
+ end
68
+
@@ -0,0 +1,94 @@
1
+ require 'fileutils'
2
+
3
+ module SRS
4
+ class CLI
5
+ class Reschedule
6
+ def run!(arguments)
7
+ if not SRS::Workspace.initialised? then
8
+ puts "Current directory is not an SRS Workspace"
9
+ return 3
10
+ end
11
+
12
+ schedule_id = arguments.shift
13
+ score = arguments.shift.to_f
14
+
15
+ is_new = false;
16
+ schedulefile = "schedule/#{schedule_id}"
17
+
18
+ if not File.exists?(schedulefile) then
19
+ schedulefile = "schedule/pending/#{schedule_id}"
20
+ is_new = true
21
+ if not File.exists?(schedulefile) then
22
+ puts "No content with that ID exists"
23
+ return 4
24
+ end
25
+ end
26
+
27
+ headers = {}
28
+ File.open(schedulefile, "r") do |file|
29
+ while( line = file.gets() ) do
30
+ if line.strip.empty? then
31
+ break
32
+ end
33
+
34
+ key, *val = line.split(':').map{|e| e.strip}
35
+ headers[key] = val.join(':')
36
+ end
37
+ end
38
+
39
+ if not headers.has_key?("Scheduler") then
40
+ puts "Schedule #{schedule_id} has no scheduler!\n"
41
+ return 6
42
+ end
43
+
44
+ exercise = headers.delete("Exercise")
45
+ schedulername = headers.delete("Scheduler")
46
+
47
+ scheduler = getScheduler(schedulername)
48
+ headersOut = is_new ? scheduler.first_rep(score) : scheduler.rep(score, headers)
49
+
50
+ FileUtils.rm_rf( schedulefile )
51
+
52
+ fileOut = "schedule/#{schedule_id}"
53
+ File.open(fileOut, "w") do |file|
54
+ file.puts "Exercise: #{exercise}"
55
+ file.puts "Scheduler: #{schedulername}"
56
+ headersOut.each do |key, value|
57
+ file.puts "#{key}: #{value.to_s}"
58
+ end
59
+ end
60
+
61
+ puts "Exercise rescheduled for #{headersOut["Due"]}"
62
+ puts "Exercise failed; marked for repetition" if headersOut["Repeat"]
63
+
64
+ return 0
65
+ end
66
+
67
+ def getScheduler(schedulername)
68
+ begin
69
+ require "./schedulers/#{schedulername}"
70
+ rescue LoadError
71
+ begin
72
+ require "srs/schedulers/#{schedulername}"
73
+ rescue LoadError
74
+ puts "Couldn't find scheduler #{schedulername}."
75
+ return nil
76
+ end
77
+ end
78
+
79
+ SRS::Schedulers.const_get(schedulername.to_sym).new
80
+ end
81
+
82
+ def help()
83
+ puts <<-EOF
84
+ srs reschedule <id> <score>
85
+
86
+ Rescedules the exercise being set by schedule id according to the score
87
+ supplied. Makes use of the scheduler defined for that schedule. The score
88
+ passed in will typically be that returned from do-exercise.
89
+ EOF
90
+ end
91
+ end
92
+ end
93
+ end
94
+
@@ -0,0 +1,69 @@
1
+ require 'fileutils'
2
+
3
+ module SRS
4
+ class CLI
5
+ class Schedule
6
+ def initialize
7
+ @options = {}
8
+ @opts = OptionParser.new do |o|
9
+ o.banner = <<-EOF.gsub /^\s+/, ""
10
+ srs schedule [options] <exercise>
11
+
12
+ Schedules an exercise.
13
+ EOF
14
+
15
+ o.on('-s', '--scheduler SCHEDULER_NAME', 'Specifies which scheduler to use') do |s|
16
+ @options[:scheduler] = s
17
+ end
18
+ end
19
+ end
20
+
21
+ def run!(arguments)
22
+ if not SRS::Workspace.initialised? then
23
+ puts "Current directory is not an SRS Workspace"
24
+ return 3
25
+ end
26
+
27
+ begin
28
+ @opts.parse!(arguments)
29
+ @options[:exercise] = arguments.shift()
30
+ rescue OptionParser::InvalidOption => e
31
+ @options[:invalid_argument] = e.message
32
+ end
33
+
34
+ if @options[:exercise] == nil then
35
+ help()
36
+ return 4
37
+ end
38
+
39
+ if @options[:scheduler] == nil then
40
+ puts "No scheduler specified."
41
+ return 5
42
+ end
43
+
44
+ t = Time.now
45
+ filename = "schedule/pending/#{t.strftime("%Y%m%d%H%M%S.%L")}"
46
+
47
+ if File.exists?(filename) then
48
+ puts "Cannot schedule two items within a millisecond. Try again."
49
+ return 6
50
+ end
51
+
52
+ FileUtils::mkdir_p("schedule/pending")
53
+ File.open(filename, 'w') do |f|
54
+ f.puts "Exercise: #{@options[:exercise]}"
55
+ f.puts "Scheduler: #{@options[:scheduler]}"
56
+ end
57
+
58
+ puts filename
59
+
60
+ return 0
61
+ end
62
+
63
+ def help()
64
+ puts @opts
65
+ end
66
+ end
67
+ end
68
+ end
69
+
@@ -0,0 +1,85 @@
1
+ require "stringio"
2
+
3
+ module SRS
4
+ module Models
5
+ class SimpleFlashcard
6
+ def initialize()
7
+ end
8
+
9
+ def run(headers, metadata)
10
+ data = headers.delete("Data")
11
+
12
+ sha1_start = data[0..1]
13
+ sha1_rest = data[2..-1]
14
+
15
+ datafile = "data/#{sha1_start}/#{sha1_rest}"
16
+
17
+ if not File.exists?(datafile) then
18
+ puts "No content with that ID exists"
19
+ return 4
20
+ end
21
+
22
+ self.load(datafile)
23
+
24
+ score = 0.0
25
+ StringIO.open(metadata) do |metadata|
26
+ while( line = metadata.gets() ) do
27
+ break if line.strip == "---"
28
+ line.gsub!(/\[([^\]]+)\]/) { "#{@fields[$1]}" }
29
+ puts line
30
+ end
31
+ answer = metadata.read.strip.gsub!(/\[([^\]]+)\]/) { "#{@fields[$1]}" }
32
+
33
+ print "> "
34
+ attempt = STDIN.gets().strip
35
+
36
+ if( attempt == answer ) then
37
+ puts "Correct."
38
+ score = 1.0
39
+ else
40
+ puts answer
41
+
42
+ for i in 0..0
43
+ puts "Was your answer: [h] Correct, [j] Close, [k] Wrong, or [l] Very Wrong?"
44
+ print "> "
45
+
46
+ case STDIN.gets().strip
47
+ when "h"
48
+ score = 1.0
49
+ when "j"
50
+ score = 0.8
51
+ when "k"
52
+ score = 0.4
53
+ when "l"
54
+ score = 0.0
55
+ else
56
+ redo
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ puts "You scored: " + score.to_s
63
+
64
+ return score
65
+ end
66
+
67
+ def load(datafile)
68
+ @fields = {}
69
+ File.open(datafile, "r") do |file|
70
+ while( line = file.gets() ) do
71
+ if line.strip.empty? then
72
+ break
73
+ end
74
+
75
+ keyval = line.split(':').map{|e| e.strip}
76
+ key = keyval[0]
77
+ val = keyval[1]
78
+
79
+ @fields[key] = val
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,72 @@
1
+ # Based on the SuperMemo 2 algorithm as described here:
2
+ # http://www.supermemo.com/english/ol/sm2.htm
3
+ require 'date'
4
+
5
+ module SRS
6
+ module Schedulers
7
+ class SuperMemo2
8
+ DEFAULT_EF = 2.5
9
+ MIN_EF = 1.3
10
+ FIRST_INTERVAL = 1
11
+ SECOND_INTERVAL = 6
12
+ ITERATION_RESET_BOUNDARY = 3.0 / 5.0
13
+ REPEAT_BOUNDARY = 4.0 / 5.0
14
+
15
+ def initialize()
16
+ end
17
+
18
+ def first_rep(score)
19
+ fields = {
20
+ "Due" => (Date.today + FIRST_INTERVAL).to_time,
21
+ "Repeat" => score < REPEAT_BOUNDARY ? true : false,
22
+ "E-Factor" => adjust_efactor(DEFAULT_EF, score),
23
+ "Interval" => FIRST_INTERVAL,
24
+ "Iteration" => 1
25
+ }
26
+
27
+ return fields
28
+ end
29
+
30
+ def rep(score, fields)
31
+ ef = fields["E-Factor"].to_f
32
+ interval = fields["Interval"].to_i
33
+ iteration = fields["Iteration"].to_i
34
+ repeat = (fields["Repeat"] == "true")
35
+
36
+ if not repeat then
37
+ iteration = 0 if score < ITERATION_RESET_BOUNDARY
38
+ case iteration
39
+ when 0
40
+ interval = FIRST_INTERVAL
41
+ when 1
42
+ interval = SECOND_INTERVAL
43
+ else
44
+ interval = adjust_interval(interval, ef)
45
+ end
46
+
47
+ ef = adjust_efactor(ef, score)
48
+ end
49
+
50
+ fields["Due"] = (Date.today + interval).to_time
51
+ fields["Repeat"] = score < REPEAT_BOUNDARY ? true : false
52
+ fields["E-Factor"] = ef
53
+ fields["Interval"] = interval
54
+ fields["Iteration"] = iteration + 1
55
+
56
+ return fields
57
+ end
58
+
59
+ def adjust_efactor(ef, score)
60
+ q = score * 5
61
+ adjusted_efactor = ef + (0.1-(5.0 - q) * (0.08 + (5.0 - q) * 0.02))
62
+
63
+ adjusted_efactor < MIN_EF ? MIN_EF : adjusted_efactor
64
+ end
65
+
66
+ def adjust_interval(interval, ef)
67
+ (interval * ef).round
68
+ end
69
+ end
70
+ end
71
+ end
72
+
@@ -0,0 +1,3 @@
1
+ module SRS
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,46 @@
1
+ require 'fileutils'
2
+
3
+ module SRS
4
+ class Workspace
5
+ class AlreadyInitialisedError < StandardError
6
+ end
7
+ class FolderNotEmptyError < StandardError
8
+ end
9
+
10
+ attr_reader :root, :dotsrs
11
+
12
+ def initialize(dirname=".")
13
+ if not SRS::Workspace.initialised?(dirname) then return nil end
14
+ @root = dirname
15
+ @dotsrs = File.join(dirname,'.srs')
16
+ self
17
+ end
18
+
19
+ def self.create(dirname, force=false)
20
+ dotsrs_dir = File.join(dirname,'.srs/')
21
+
22
+ if( SRS::Workspace.initialised?(dirname) ) then
23
+ raise AlreadyInitialisedError
24
+ return nil
25
+ end
26
+
27
+ FileUtils.mkdir_p(dirname)
28
+
29
+ if( !force ) then
30
+ if( Dir.entries(dirname).length > 2 ) then
31
+ raise FolderNotEmptyError
32
+ return nil
33
+ end
34
+ end
35
+
36
+ Dir.mkdir(dotsrs_dir)
37
+ Dir.mkdir(File.join(dirname, "data"))
38
+
39
+ return SRS::Workspace.new(dirname)
40
+ end
41
+
42
+ def self.initialised?(dirname=".")
43
+ Dir.exists?(File.join(dirname,'.srs/'))
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,24 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'srs'
3
+ s.version = '0.1.1'
4
+ s.date = '2011-07-07'
5
+ s.authors = ["Daniel P. Wright"]
6
+ s.email = 'dani@dpwright.com'
7
+ s.homepage = 'https://github.com/dpwright/srs'
8
+ s.license = 'Simplified BSD'
9
+
10
+ s.summary = "A highly extensible command-line spaced repetition system"
11
+ s.description = <<-EOF
12
+ A Spaced Repetition System is a study tool which works by spacing out
13
+ exercises so as to learn in the most efficient manner possible.
14
+
15
+ srs is a command-line based implementation of the spaced repetition system.
16
+ It is designed to be highly extensible and to promote the sharing of data
17
+ for study by others.
18
+ EOF
19
+
20
+ s.files = `git ls-files`.split("\n").reject {|path| path =~ /\.gitignore$/ }
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.rdoc_options = ["--charset=UTF-8"]
23
+ s.require_path = "lib"
24
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: srs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Daniel P. Wright
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-07-07 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ! "\tA Spaced Repetition System is a study tool which works by spacing
15
+ out\n\texercises so as to learn in the most efficient manner possible.\n\n\tsrs
16
+ is a command-line based implementation of the spaced repetition system.\n\tIt is
17
+ designed to be highly extensible and to promote the sharing of data\n\tfor study
18
+ by others.\n"
19
+ email: dani@dpwright.com
20
+ executables:
21
+ - srs
22
+ extensions: []
23
+ extra_rdoc_files: []
24
+ files:
25
+ - .rspec
26
+ - LICENCE.md
27
+ - README.md
28
+ - bin/srs
29
+ - lib/srs.rb
30
+ - lib/srs/cli.rb
31
+ - lib/srs/cli/cat.rb
32
+ - lib/srs/cli/do-exercise.rb
33
+ - lib/srs/cli/get-field.rb
34
+ - lib/srs/cli/help.rb
35
+ - lib/srs/cli/init.rb
36
+ - lib/srs/cli/insert-into.rb
37
+ - lib/srs/cli/next-due.rb
38
+ - lib/srs/cli/next-new.rb
39
+ - lib/srs/cli/queue.rb
40
+ - lib/srs/cli/reschedule.rb
41
+ - lib/srs/cli/schedule.rb
42
+ - lib/srs/models/SimpleFlashcard.rb
43
+ - lib/srs/schedulers/SuperMemo2.rb
44
+ - lib/srs/version.rb
45
+ - lib/srs/workspace.rb
46
+ - srs.gemspec
47
+ homepage: https://github.com/dpwright/srs
48
+ licenses:
49
+ - Simplified BSD
50
+ post_install_message:
51
+ rdoc_options:
52
+ - --charset=UTF-8
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 1.8.23
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: A highly extensible command-line spaced repetition system
73
+ test_files: []