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 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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Engine
2
+ VERSION = '0.0.0'
3
+ 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: []