srs 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +2 -0
- data/LICENCE.md +22 -0
- data/README.md +292 -0
- data/bin/srs +11 -0
- data/lib/srs.rb +2 -0
- data/lib/srs/cli.rb +43 -0
- data/lib/srs/cli/cat.rb +39 -0
- data/lib/srs/cli/do-exercise.rb +73 -0
- data/lib/srs/cli/get-field.rb +56 -0
- data/lib/srs/cli/help.rb +44 -0
- data/lib/srs/cli/init.rb +58 -0
- data/lib/srs/cli/insert-into.rb +48 -0
- data/lib/srs/cli/next-due.rb +44 -0
- data/lib/srs/cli/next-new.rb +29 -0
- data/lib/srs/cli/queue.rb +68 -0
- data/lib/srs/cli/reschedule.rb +94 -0
- data/lib/srs/cli/schedule.rb +69 -0
- data/lib/srs/models/SimpleFlashcard.rb +85 -0
- data/lib/srs/schedulers/SuperMemo2.rb +72 -0
- data/lib/srs/version.rb +3 -0
- data/lib/srs/workspace.rb +46 -0
- data/srs.gemspec +24 -0
- metadata +73 -0
data/.rspec
ADDED
data/LICENCE.md
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
data/lib/srs.rb
ADDED
data/lib/srs/cli.rb
ADDED
@@ -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
|
+
|
data/lib/srs/cli/cat.rb
ADDED
@@ -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
|
+
|
data/lib/srs/cli/help.rb
ADDED
@@ -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
|
+
|
data/lib/srs/cli/init.rb
ADDED
@@ -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
|
+
|
data/lib/srs/version.rb
ADDED
@@ -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
|
data/srs.gemspec
ADDED
@@ -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: []
|