srs 0.1.1
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 +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: []
|