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 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: []