engine 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/README.md +1 -0
- data/Rakefile +12 -0
- data/engine.gemspec +24 -0
- data/lib/engine/card.rb +23 -0
- data/lib/engine/card_state.rb +58 -0
- data/lib/engine/cli.rb +36 -0
- data/lib/engine/deck.rb +23 -0
- data/lib/engine/fixtures.rb +15 -0
- data/lib/engine/rating.rb +18 -0
- data/lib/engine/strategies.rb +66 -0
- data/lib/engine/version.rb +3 -0
- data/lib/engine.rb +20 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 668d7179c24a01408a2a5a9662ef97e34f541438
|
4
|
+
data.tar.gz: af96c0c16a180cfc759f17659486a4272df85bc9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8a05758d9e2d029ed925ab197b4f8c30ed29d2b0a42348ef58e2c7b84f79bcf3ad5f770e7c7102924fb7756f658eb69efd25dda8b474d7bc2d339c66a4bd178a
|
7
|
+
data.tar.gz: a83d69cef23e40c28762d4633083082b7116968759923bf508ba00aee8c532b2d694902f385cedfc41c4e818e8d16d71cef571a25c2b7c9a172a8858f083c1fb
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pkg
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Flashcard Engine with Spaced Repetition
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rubygems/package_task'
|
2
|
+
|
3
|
+
spec = Gem::Specification.load(File.expand_path('../engine.gemspec', __FILE__))
|
4
|
+
gem = Gem::PackageTask.new(spec)
|
5
|
+
gem.define
|
6
|
+
|
7
|
+
desc "Push gem to rubygems.org"
|
8
|
+
task :push => :gem do
|
9
|
+
#sh "git tag v#{Engine::VERSION}"
|
10
|
+
sh "git push --tags"
|
11
|
+
sh "gem push pkg/engine-#{Engine::VERSION}.gem"
|
12
|
+
end
|
data/engine.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.expand_path('../lib/engine/version', __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = 'engine'
|
7
|
+
gem.version = Engine::VERSION
|
8
|
+
gem.authors = [ 'Arne Brasseur' ]
|
9
|
+
gem.email = [ 'arne@arnebrasseur.net' ]
|
10
|
+
gem.description = 'Flashcard Engine with Spaced Repetition.'
|
11
|
+
gem.summary = gem.description
|
12
|
+
gem.homepage = 'https://github.com/plexus/engine'
|
13
|
+
gem.license = 'MIT'
|
14
|
+
|
15
|
+
gem.require_paths = %w[lib]
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.test_files = `git ls-files -- spec`.split($/)
|
18
|
+
gem.extra_rdoc_files = %w[README.md]
|
19
|
+
|
20
|
+
gem.add_runtime_dependency 'hexp', '~> 0.2.0'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rake', '~> 10.1'
|
23
|
+
gem.add_development_dependency 'rspec', '~> 2.14'
|
24
|
+
end
|
data/lib/engine/card.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module XFlash
|
2
|
+
class Card < Struct.new(:data, :card_state)
|
3
|
+
extend Forwardable
|
4
|
+
def_delegators :card_state, :factor, :iteration, :streak, :interval, :expired?, :expired_for_seconds, :last_shown
|
5
|
+
|
6
|
+
def new?
|
7
|
+
card_state.empty?
|
8
|
+
end
|
9
|
+
|
10
|
+
def rate(rating)
|
11
|
+
next_card_state = card_state << Rating.new(Time.now, rating)
|
12
|
+
self.class.new(data, next_card_state)
|
13
|
+
end
|
14
|
+
|
15
|
+
def lapsed?
|
16
|
+
streak == 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
"<Card #{data.inspect} #{card_state.inspect}>"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module XFlash
|
2
|
+
|
3
|
+
# CardStates form a linked list, each pointing to the previous state plus
|
4
|
+
# the new data point. The actual factor/interval, streak are calculated
|
5
|
+
# recursively based on the previous value, and the new data point.
|
6
|
+
class CardState < Struct.new(:parent, :data_point)
|
7
|
+
STRATEGY = AnkiStrategy
|
8
|
+
EMPTY = CardState.new
|
9
|
+
|
10
|
+
START = {
|
11
|
+
iteration: 0,
|
12
|
+
streak: 0,
|
13
|
+
factor: 2.5,
|
14
|
+
interval: 1
|
15
|
+
}
|
16
|
+
|
17
|
+
def << data_point
|
18
|
+
self.class.new(self, data_point)
|
19
|
+
end
|
20
|
+
|
21
|
+
def strategy
|
22
|
+
STRATEGY.new(parent, data_point)
|
23
|
+
end
|
24
|
+
|
25
|
+
def empty?
|
26
|
+
parent.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def data_points(&blk)
|
30
|
+
return to_enum(__method__) unless block_given?
|
31
|
+
unless empty?
|
32
|
+
parent.data_points(&blk)
|
33
|
+
yield data_point
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def last_shown
|
38
|
+
data_points.to_a.last.timestamp
|
39
|
+
end
|
40
|
+
|
41
|
+
def expired?(time)
|
42
|
+
(!empty? && data_point.fail?) || expired_for_seconds(time) > 0
|
43
|
+
end
|
44
|
+
|
45
|
+
def expired_for_seconds(time)
|
46
|
+
empty? ? 0 : [time - last_shown - interval * 60, 0].max
|
47
|
+
end
|
48
|
+
|
49
|
+
def iteration ; empty? ? START[:iteration] : parent.iteration + 1 end
|
50
|
+
def streak ; empty? ? START[:streak] : strategy.next_streak end
|
51
|
+
def factor ; empty? ? START[:factor] : strategy.next_factor end
|
52
|
+
def interval ; empty? ? START[:interval] : strategy.next_interval end
|
53
|
+
|
54
|
+
def inspect
|
55
|
+
"<#{self.class} iteration: %d, streak: %d, factor: %.2f, interval: %.2f>" % [iteration, streak, factor, interval]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/engine/cli.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module XFlash
|
2
|
+
class CLI
|
3
|
+
attr_reader :card, :cards
|
4
|
+
def initialize(cards)
|
5
|
+
@cards = Deck.new(cards)
|
6
|
+
end
|
7
|
+
|
8
|
+
def next_card
|
9
|
+
@card = cards.sample
|
10
|
+
end
|
11
|
+
|
12
|
+
def rate_card(score)
|
13
|
+
puts "Before : " + card.inspect
|
14
|
+
card.rate(score).tap do |new_card|
|
15
|
+
@cards = @cards.update_card(card, new_card)
|
16
|
+
@card = new_card
|
17
|
+
end
|
18
|
+
puts "After : " + card.inspect
|
19
|
+
end
|
20
|
+
|
21
|
+
def readline_loop
|
22
|
+
next_card
|
23
|
+
loop do
|
24
|
+
input = Readline.readline("#{card.data.first} > ", true)
|
25
|
+
exit unless input
|
26
|
+
case input
|
27
|
+
when /s/, ""
|
28
|
+
puts card.data.last
|
29
|
+
when /[0-5]/
|
30
|
+
rate_card(input.to_i)
|
31
|
+
next_card
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/engine/deck.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module XFlash
|
2
|
+
class Deck < DelegateClass(Array)
|
3
|
+
%w[select map reject grep reverse sort_by sort].each do |array_method|
|
4
|
+
define_method array_method do |*args, &blk|
|
5
|
+
self.class.new(super(*args, &blk))
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def expired_cards(time)
|
10
|
+
select {|card| card.expired?(time) }.sort_by do |card|
|
11
|
+
[card.expired_for_seconds(time), -card.last_shown.to_i]
|
12
|
+
end.reverse
|
13
|
+
end
|
14
|
+
|
15
|
+
def new_cards
|
16
|
+
select(&:new?)
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_card(old, new)
|
20
|
+
self.class.new(take(index(old)) + [new] + drop(index(old) + 1))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
module XFlash
|
3
|
+
FIXTURES = [
|
4
|
+
[ '入選', "(入选) [ru4 xuan3] /to be chosen/to be elected as/" ],
|
5
|
+
[ '報名', "(报名) [bao4 ming2] /to sign up/to enter one's name/to apply/to register/to enroll/to enlist/" ],
|
6
|
+
[ '訊息', "(讯息) [xun4 xi1] /information/news/message/text message or SMS/" ],
|
7
|
+
[ '詢問', "(询问) [xun2 wen4] /to inquire/" ],
|
8
|
+
[ '導致', "(导致) [dao3 zhi4] /to lead to/to create/to cause/to bring about/" ],
|
9
|
+
[ '琴', "[qin2] /guqin or zither, cf 古琴[gu3 qin2]/musical instrument in general/" ],
|
10
|
+
[ '提供', "[ti2 gong1] /to offer/to supply/to provide/to furnish/" ],
|
11
|
+
[ '更新', "[geng1 xin1] /to replace the old with new/to renew/to renovate/to upgrade/to update/to regenerate/" ],
|
12
|
+
[ '出力', "[chu1 li4] /to exert oneself/" ],
|
13
|
+
[ '拌蒜', "[ban4 suan4] /to stagger (walk unsteadily)/" ],
|
14
|
+
].map{|args| Card.new(args, CardState::EMPTY)}
|
15
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module XFlash
|
2
|
+
class Rating < Struct.new(:timestamp, :rating)
|
3
|
+
MAX_RATING = 3
|
4
|
+
FAIL = 0
|
5
|
+
HARD = 1
|
6
|
+
GOOD = 2
|
7
|
+
EASY = 3
|
8
|
+
|
9
|
+
def neg_rating
|
10
|
+
MAX_RATING - rating
|
11
|
+
end
|
12
|
+
|
13
|
+
def fail? ; rating == FAIL end
|
14
|
+
def hard? ; rating == HARD end
|
15
|
+
def good? ; rating == GOOD end
|
16
|
+
def easy? ; rating == EASY end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module XFlash
|
2
|
+
class BaseStrategy < Struct.new(:card_state, :data_point)
|
3
|
+
extend Forwardable
|
4
|
+
def_delegators :card_state, :data_points, :iteration, :streak, :factor, :interval
|
5
|
+
def_delegators :data_point, :fail?, :rating, :neg_rating
|
6
|
+
end
|
7
|
+
|
8
|
+
class SuperMemoStrategy < BaseStrategy
|
9
|
+
INITIAL_INTERVALS = [1, 6]
|
10
|
+
|
11
|
+
def next_streak
|
12
|
+
fail? ? 0 : streak + 1
|
13
|
+
end
|
14
|
+
|
15
|
+
def next_factor
|
16
|
+
[factor + (0.1 - neg_rating * (0.28 + neg_rating * 0.02)), 1.3].max
|
17
|
+
end
|
18
|
+
|
19
|
+
def next_interval
|
20
|
+
INITIAL_INTERVALS.fetch(next_streak) do
|
21
|
+
interval * next_factor
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class AnkiStrategy < BaseStrategy
|
27
|
+
LEARNING_INTERVALS = [0, 1, 10, 25].map(&:to_f)
|
28
|
+
INITIAL_INTERVALS = [1, 2].map {|min| min*60*24 }.map(&:to_f)
|
29
|
+
LEARNING_STEPS = LEARNING_INTERVALS.length - 1
|
30
|
+
|
31
|
+
def learning?
|
32
|
+
steps_to_graduation > 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def steps_to_graduation
|
36
|
+
data_points.map(&:rating).inject(LEARNING_STEPS, :-)
|
37
|
+
end
|
38
|
+
|
39
|
+
def next_streak
|
40
|
+
fail? || learning? ? 0 : streak + 1
|
41
|
+
end
|
42
|
+
|
43
|
+
def next_factor
|
44
|
+
if learning?
|
45
|
+
factor
|
46
|
+
else
|
47
|
+
[factor + [0.15, 0, -0.15, -0.3].fetch(neg_rating) {0}, 1.3].max
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Good enough for our purposes, and stable for a given card-state
|
52
|
+
def pseudo_rand
|
53
|
+
data_point.timestamp.to_f % 1
|
54
|
+
end
|
55
|
+
|
56
|
+
def next_interval
|
57
|
+
if learning?
|
58
|
+
LEARNING_INTERVALS.fetch(-steps_to_graduation)
|
59
|
+
else
|
60
|
+
INITIAL_INTERVALS.fetch(next_streak) do
|
61
|
+
interval * next_factor
|
62
|
+
end
|
63
|
+
end * (0.8 + pseudo_rand * 0.4) # 0.8 - 1.2
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/engine.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'yaml'
|
5
|
+
#require 'readline'
|
6
|
+
require 'forwardable'
|
7
|
+
require 'delegate'
|
8
|
+
|
9
|
+
require_relative 'engine/rating'
|
10
|
+
require_relative 'engine/strategies'
|
11
|
+
require_relative 'engine/card_state'
|
12
|
+
require_relative 'engine/card'
|
13
|
+
require_relative 'engine/fixtures'
|
14
|
+
require_relative 'engine/deck'
|
15
|
+
#require_relative 'engine/cli'
|
16
|
+
|
17
|
+
module Engine
|
18
|
+
end
|
19
|
+
|
20
|
+
#Engine::CLI.new(Engine::FIXTURES).readline_loop
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: engine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Arne Brasseur
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: hexp
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.1'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.14'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.14'
|
55
|
+
description: Flashcard Engine with Spaced Repetition.
|
56
|
+
email:
|
57
|
+
- arne@arnebrasseur.net
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files:
|
61
|
+
- README.md
|
62
|
+
files:
|
63
|
+
- ".gitignore"
|
64
|
+
- README.md
|
65
|
+
- Rakefile
|
66
|
+
- engine.gemspec
|
67
|
+
- lib/engine.rb
|
68
|
+
- lib/engine/card.rb
|
69
|
+
- lib/engine/card_state.rb
|
70
|
+
- lib/engine/cli.rb
|
71
|
+
- lib/engine/deck.rb
|
72
|
+
- lib/engine/fixtures.rb
|
73
|
+
- lib/engine/rating.rb
|
74
|
+
- lib/engine/strategies.rb
|
75
|
+
- lib/engine/version.rb
|
76
|
+
homepage: https://github.com/plexus/engine
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata: {}
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 2.2.2
|
97
|
+
signing_key:
|
98
|
+
specification_version: 4
|
99
|
+
summary: Flashcard Engine with Spaced Repetition.
|
100
|
+
test_files: []
|