typingtutor 1.0.5 → 1.0.6
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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +40 -0
- data/lib/typingtutor.rb +3 -0
- data/lib/typingtutor/dictionary.rb +2 -1
- data/lib/typingtutor/exercise.rb +106 -0
- data/lib/typingtutor/line.rb +8 -6
- data/lib/typingtutor/runner.rb +15 -53
- data/lib/typingtutor/stats.rb +88 -0
- data/lib/typingtutor/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bff0c5f22219fd895592101fcbd84132de5d1886
|
4
|
+
data.tar.gz: e3d3fae0c2971d3220866cfddd4d842337a707d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59e91a49e45ed199c9ac7ed9d1096701ed9ffda2d910462f486b8d23fbe0fdeea92c01c43c456bf7dbf62f5d0e5eb236bdabb1d38e73254f19622a4a7909dec1
|
7
|
+
data.tar.gz: 5b4b6558abd1da914f24035e103de878ef64f8b5c029f93c41035e1e34da07f237ba78b3a75b992df2ff682c21314dc42e2ace95149418dad50626ca38a99e63
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
* 1.0.6: Added 'improve' exercise and overall stats
|
4
|
+
* 1.0.5: Added 'training' exercise based on random dictionary words
|
5
|
+
* 1.0.4: Added 'biglebowski' exercise
|
6
|
+
* 1.0.3: First version with gem executable working properly
|
7
|
+
* 1.0.0/1/2: Test publish gem
|
data/README.md
CHANGED
@@ -20,3 +20,43 @@ $ typingtutor biglebowski
|
|
20
20
|
```
|
21
21
|
|
22
22
|

|
23
|
+
|
24
|
+
Generic training, generates 250 random english words
|
25
|
+
|
26
|
+
```
|
27
|
+
$ typingtutor training
|
28
|
+
```
|
29
|
+
|
30
|
+
Improve on letters you struggle the most:
|
31
|
+
|
32
|
+
```
|
33
|
+
$ typingtutor improve
|
34
|
+
```
|
35
|
+
|
36
|
+
See overall stats about your typing:
|
37
|
+
|
38
|
+
```
|
39
|
+
$ typingtutor stats
|
40
|
+
ccuracy per letter:
|
41
|
+
6: 67%
|
42
|
+
7: 75%
|
43
|
+
9: 83%
|
44
|
+
m: 89%
|
45
|
+
b: 91%
|
46
|
+
w: 92%
|
47
|
+
j: 92%
|
48
|
+
y: 92%
|
49
|
+
h: 92%
|
50
|
+
n: 92%
|
51
|
+
...
|
52
|
+
|
53
|
+
Exercises:
|
54
|
+
- fox: 7 runs, 56 wpm
|
55
|
+
- improve: 3 runs, 36 wpm
|
56
|
+
|
57
|
+
Exercises played : 10
|
58
|
+
Time played: 17m 25s
|
59
|
+
Avg speed: 36 wpm
|
60
|
+
Max speed: 68 wpm
|
61
|
+
|
62
|
+
```
|
data/lib/typingtutor.rb
CHANGED
@@ -26,6 +26,7 @@ module Typingtutor
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def pick_word_with_letter(letter)
|
29
|
+
return nil if @letters[letter].nil?
|
29
30
|
@letters[letter][rand(@letters[letter].size)]
|
30
31
|
end
|
31
32
|
|
@@ -36,7 +37,7 @@ module Typingtutor
|
|
36
37
|
def training_exercise
|
37
38
|
lines = []
|
38
39
|
line = ""
|
39
|
-
|
40
|
+
250.times do
|
40
41
|
line << (pick_word+" ")
|
41
42
|
if line.size > 70
|
42
43
|
lines << line.strip
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Typingtutor
|
2
|
+
class Exercise
|
3
|
+
attr_accessor :name, :body, :stats, :start, :time
|
4
|
+
attr_accessor :chars, :correct_chars, :words, :keystrokes, :typing_accuracy, :word_accuracy, :gross_wpm
|
5
|
+
|
6
|
+
# class methods
|
7
|
+
|
8
|
+
def self.load(name:, stats:)
|
9
|
+
return nil if name.nil?
|
10
|
+
body = load_body(name:name, stats:stats)
|
11
|
+
return body.nil? ? nil : Exercise.new(name:name, body:body, stats:stats)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.load_body(name:, stats:)
|
15
|
+
gem_file_name = File.join(File.dirname(__FILE__), '..', '..', "exercises", "#{name}.txt")
|
16
|
+
|
17
|
+
lines ||= dictionary.training_exercise if name == 'training'
|
18
|
+
lines ||= improve_exercise(stats) if name == 'improve'
|
19
|
+
# load from exercise folder in the gem
|
20
|
+
lines ||= IO.read(name).lines if File.exists?(name)
|
21
|
+
lines ||= IO.read("#{name}.txt").lines if File.exists?("#{name}.txt")
|
22
|
+
lines ||= IO.read(gem_file_name).lines if File.exists?(gem_file_name)
|
23
|
+
|
24
|
+
return if lines.nil?
|
25
|
+
|
26
|
+
lines.each {|line| line.strip!} # remove extra spaces
|
27
|
+
lines.reject! {|line| line.strip.to_s == ""} # remove empty lines
|
28
|
+
return lines
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.available_exercises
|
32
|
+
files = Dir[File.join(File.dirname(__FILE__), '..', '..', "exercises", "*.txt")]
|
33
|
+
files.map! do |name|
|
34
|
+
name = name.split(File::SEPARATOR).last
|
35
|
+
name = name.gsub /\.txt/, ''
|
36
|
+
name
|
37
|
+
end
|
38
|
+
files
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.improve_exercise(stats)
|
42
|
+
lines = []
|
43
|
+
index = 0
|
44
|
+
stats.worst_letters.to_h.each do |letter, accuracy|
|
45
|
+
next if dictionary.pick_word_with_letter(letter).nil? # no samples to use
|
46
|
+
lines << "You have a #{accuracy}% accuracy on letter #{letter}, let's work on that:"
|
47
|
+
2.times do
|
48
|
+
lines << 8.times.map {|i| dictionary.pick_word_with_letter(letter)}.join(" #{letter * (rand(2)+1)} ").strip
|
49
|
+
end
|
50
|
+
index += 1
|
51
|
+
break if index > 5
|
52
|
+
end
|
53
|
+
lines
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.dictionary
|
57
|
+
@dictionary ||= Dictionary.new
|
58
|
+
end
|
59
|
+
|
60
|
+
# instance methods
|
61
|
+
|
62
|
+
def initialize(name:, body:, stats:)
|
63
|
+
self.name = name
|
64
|
+
self.body = body
|
65
|
+
self.stats = stats
|
66
|
+
end
|
67
|
+
|
68
|
+
def play
|
69
|
+
@start = Time.now
|
70
|
+
results = body.map { |line| Line.new(line:line, stats:stats).play unless line == ""}
|
71
|
+
self.time = Time.now - @start
|
72
|
+
self.chars = results.map {|s| s[:chars] }.inject(:+)
|
73
|
+
self.correct_chars = results.map {|s| s[:correct_chars] }.inject(:+)
|
74
|
+
self.words = results.map {|s| s[:words] }.inject(:+)
|
75
|
+
self.keystrokes = results.map {|s| s[:keystrokes] }.inject(:+)
|
76
|
+
self.typing_accuracy = (correct_chars.to_f / keystrokes.to_f * 100).to_i
|
77
|
+
self.word_accuracy = (correct_chars.to_f / chars.to_f * 100).to_i # TODO
|
78
|
+
self.gross_wpm = words / (time / 60)
|
79
|
+
stats.record_exercise(self)
|
80
|
+
return results
|
81
|
+
end
|
82
|
+
|
83
|
+
def results
|
84
|
+
{
|
85
|
+
time: time,
|
86
|
+
chars: chars,
|
87
|
+
correct_chars: correct_chars,
|
88
|
+
words: words,
|
89
|
+
keystrokes: keystrokes,
|
90
|
+
typing_accuracy: typing_accuracy,
|
91
|
+
word_accuracy: word_accuracy,
|
92
|
+
gross_wpm: gross_wpm
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
def print
|
97
|
+
puts
|
98
|
+
puts "------------------------"
|
99
|
+
puts "Time: #{time.round.divmod(60).join('m ')}s (#{words} words)"
|
100
|
+
puts "Speed: #{gross_wpm.round} wpm"
|
101
|
+
# puts "Word accuracy: #{word_accuracy}%"
|
102
|
+
puts "Typing accuracy: #{typing_accuracy}%"
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
data/lib/typingtutor/line.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
module Typingtutor
|
2
2
|
class Line
|
3
|
-
def initialize(
|
4
|
-
@original =
|
3
|
+
def initialize(line:, stats:)
|
4
|
+
@original = line
|
5
5
|
@actual = ""
|
6
6
|
@position = 0
|
7
7
|
@keystrokes = 0
|
8
|
+
@stats = stats
|
8
9
|
end
|
9
10
|
|
10
11
|
def play
|
@@ -28,7 +29,7 @@ module Typingtutor
|
|
28
29
|
@keystrokes +=1
|
29
30
|
end
|
30
31
|
end
|
31
|
-
return
|
32
|
+
return results
|
32
33
|
end
|
33
34
|
|
34
35
|
def print_char
|
@@ -37,6 +38,7 @@ module Typingtutor
|
|
37
38
|
else
|
38
39
|
print HighLine.color(expected_char, :plain)
|
39
40
|
end
|
41
|
+
@stats.record_letter(expected_char, ok?)
|
40
42
|
end
|
41
43
|
|
42
44
|
def print_backspace
|
@@ -47,11 +49,11 @@ module Typingtutor
|
|
47
49
|
def actual_char; @actual[@position]; end
|
48
50
|
def ok?; expected_char == actual_char; end
|
49
51
|
|
50
|
-
def
|
51
|
-
{
|
52
|
+
def results
|
53
|
+
{ chars: @original.length,
|
52
54
|
correct_chars: @original.length.times.select {|i| @original[i] == @actual[i] }.size,
|
53
55
|
keystrokes: @keystrokes,
|
54
|
-
|
56
|
+
words: @actual.split(' ').size
|
55
57
|
}
|
56
58
|
end
|
57
59
|
|
data/lib/typingtutor/runner.rb
CHANGED
@@ -3,13 +3,19 @@ module Typingtutor
|
|
3
3
|
include HighLine::SystemExtensions
|
4
4
|
|
5
5
|
def run(exercise_name)
|
6
|
-
|
6
|
+
if exercise_name == 'stats'
|
7
|
+
stats.print_full
|
8
|
+
exit(0)
|
9
|
+
end
|
10
|
+
|
11
|
+
exercise = Exercise.load(name:exercise_name, stats:stats)
|
7
12
|
if exercise
|
8
13
|
setup_color_scheme
|
9
14
|
play_intro
|
10
|
-
|
11
|
-
|
12
|
-
|
15
|
+
exercise.play
|
16
|
+
exercise.print
|
17
|
+
stats.print
|
18
|
+
stats.save
|
13
19
|
else
|
14
20
|
puts "Typingtutor v#{Typingtutor::VERSION}"
|
15
21
|
puts
|
@@ -17,8 +23,9 @@ module Typingtutor
|
|
17
23
|
puts "$ typingtutor path/to/localfile.txt"
|
18
24
|
puts
|
19
25
|
puts "built in exercises:"
|
20
|
-
puts "- training (
|
21
|
-
|
26
|
+
puts "- training (250 random english words)"
|
27
|
+
puts "- improve (focus on letters you miss the most)"
|
28
|
+
Exercise.available_exercises.each {|e| puts "- #{e}"}
|
22
29
|
puts
|
23
30
|
puts "eg: typingtutor biglebowski"
|
24
31
|
end
|
@@ -41,54 +48,9 @@ module Typingtutor
|
|
41
48
|
HighLine.color_scheme = cs
|
42
49
|
end
|
43
50
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
lines ||= dictionary.training_exercise if name == 'training'
|
48
|
-
# load from exercise folder in the gem
|
49
|
-
lines ||= IO.read(name).lines if File.exists?(name)
|
50
|
-
lines ||= IO.read("#{name}.txt").lines if File.exists?("#{name}.txt")
|
51
|
-
lines ||= IO.read(gem_file_name).lines if File.exists?(gem_file_name)
|
52
|
-
|
53
|
-
return if lines.nil?
|
54
|
-
|
55
|
-
lines.each {|line| line.strip!} # remove extra spaces
|
56
|
-
lines.reject! {|line| line.strip.to_s == ""} # remove empty lines
|
57
|
-
return lines
|
58
|
-
end
|
59
|
-
|
60
|
-
def exercises
|
61
|
-
files = Dir[File.join(File.dirname(__FILE__), '..', '..', "exercises", "*.txt")]
|
62
|
-
files.map! do |name|
|
63
|
-
name = name.split(File::SEPARATOR).last
|
64
|
-
name = name.gsub /\.txt/, ''
|
65
|
-
name
|
66
|
-
end
|
67
|
-
files
|
68
|
-
end
|
69
|
-
|
70
|
-
def dictionary
|
71
|
-
@dictionary ||= Dictionary.new
|
51
|
+
def stats
|
52
|
+
@stats ||= Stats.new
|
72
53
|
end
|
73
54
|
|
74
|
-
def print_stats(stats)
|
75
|
-
time = Time.now - @start
|
76
|
-
total_chars = stats.map {|s| s[:total_chars] }.inject(:+)
|
77
|
-
correct_chars = stats.map {|s| s[:correct_chars] }.inject(:+)
|
78
|
-
total_words = stats.map {|s| s[:total_words] }.inject(:+)
|
79
|
-
keystrokes = stats.map {|s| s[:keystrokes] }.inject(:+)
|
80
|
-
|
81
|
-
typing_accuracy = (correct_chars.to_f / keystrokes.to_f * 100).to_i
|
82
|
-
word_accuracy = (correct_chars.to_f / total_chars.to_f * 100).to_i # TODO
|
83
|
-
|
84
|
-
gross_wpm = total_words / (time / 60)
|
85
|
-
|
86
|
-
puts
|
87
|
-
puts "------------------------"
|
88
|
-
puts "Time: #{time.round(1)}s (#{total_words} words)"
|
89
|
-
puts "Speed: #{gross_wpm.round} wpm"
|
90
|
-
puts "Word accuracy: #{word_accuracy}%"
|
91
|
-
puts "Typing accuracy: #{typing_accuracy}%"
|
92
|
-
end
|
93
55
|
end # Runner
|
94
56
|
end # Typingtutor
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Typingtutor
|
2
|
+
class Stats
|
3
|
+
FILE = File.expand_path('~/.typingtutor')
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@stats = {}
|
7
|
+
@stats.merge!(YAML.load(IO.read(FILE))) if File.exists?(FILE)
|
8
|
+
@stats[:created_at] ||= Time.now
|
9
|
+
@stats[:total] ||= {}
|
10
|
+
@stats[:exercises] ||= {}
|
11
|
+
@stats[:words] ||= {}
|
12
|
+
@stats[:letters] ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def save
|
16
|
+
@stats[:updated_at] = Time.now
|
17
|
+
IO.write(FILE, YAML.dump(@stats))
|
18
|
+
end
|
19
|
+
|
20
|
+
def record_exercise(exercise)
|
21
|
+
@stats[:exercises][exercise.name] ||= {}
|
22
|
+
@stats[:exercises][exercise.name][:runs] ||= 0
|
23
|
+
@stats[:exercises][exercise.name][:runs] += 1
|
24
|
+
@stats[:exercises][exercise.name][:last_run] = Time.now
|
25
|
+
@stats[:exercises][exercise.name][:last_run_results] = exercise.results
|
26
|
+
|
27
|
+
# update totals
|
28
|
+
@stats[:total][:runs] ||= 0
|
29
|
+
@stats[:total][:runs] += 1
|
30
|
+
|
31
|
+
[:time, :chars, :correct_chars, :words, :keystrokes].each do |metric|
|
32
|
+
@stats[:total][metric] ||= 0
|
33
|
+
@stats[:total][metric] += exercise.results[metric]
|
34
|
+
end
|
35
|
+
|
36
|
+
@stats[:total][:max_wpm] ||= 0
|
37
|
+
@stats[:total][:max_wpm] = [@stats[:total][:max_wpm], exercise.results[:gross_wpm]].max
|
38
|
+
@stats[:total][:avg_wpm] = @stats[:total][:words] / (@stats[:total][:time] / 60)
|
39
|
+
end
|
40
|
+
|
41
|
+
def record_word(word)
|
42
|
+
# TODO
|
43
|
+
end
|
44
|
+
|
45
|
+
def record_letter(letter, ok)
|
46
|
+
return if letter == ' '
|
47
|
+
@stats[:letters][letter] ||= {}
|
48
|
+
@stats[:letters][letter][:total] ||= {}
|
49
|
+
@stats[:letters][letter][:total][:count] ||= 0
|
50
|
+
@stats[:letters][letter][:total][:count] += 1
|
51
|
+
@stats[:letters][letter][:total][:correct] ||= 0
|
52
|
+
@stats[:letters][letter][:total][:correct] += 1 if ok
|
53
|
+
@stats[:letters][letter][:total][:accuracy] = (@stats[:letters][letter][:total][:correct].to_f / @stats[:letters][letter][:total][:count].to_f * 100).round
|
54
|
+
end
|
55
|
+
|
56
|
+
def print
|
57
|
+
puts "------------------------"
|
58
|
+
puts "Your avg speed: #{@stats[:total][:avg_wpm].round} wpm"
|
59
|
+
puts "Your max speed: #{@stats[:total][:max_wpm].round} wpm"
|
60
|
+
end
|
61
|
+
|
62
|
+
def print_full
|
63
|
+
puts "Accuracy per letter:"
|
64
|
+
worst_letters.each {|letter, accuracy| puts "#{letter}: #{accuracy.round}%"}
|
65
|
+
|
66
|
+
if @stats[:exercises].size > 0
|
67
|
+
puts
|
68
|
+
puts "Exercises:"
|
69
|
+
@stats[:exercises].each do |name, data|
|
70
|
+
puts "- #{name}: #{data[:runs]} runs, #{data[:last_run_results][:gross_wpm].round} wpm"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
puts
|
74
|
+
puts "Exercises played: #{@stats[:total][:runs] || 0}"
|
75
|
+
puts "Time played: #{(@stats[:total][:time] || 0).round.divmod(60).join('m ')}s"
|
76
|
+
puts "Avg speed: #{(@stats[:total][:avg_wpm] || 0).round} wpm"
|
77
|
+
puts "Max speed: #{(@stats[:total][:max_wpm] || 0).round} wpm"
|
78
|
+
end
|
79
|
+
|
80
|
+
def worst_letters
|
81
|
+
accuracy = @stats[:letters].map {|letter, data| [letter, data[:total][:accuracy]]}.to_h # { "a" => 100, "b" => 98 }
|
82
|
+
accuracy.reject! {|key, value| value == 100}
|
83
|
+
accuracy.reject! {|key, value| key !~ /\w/ }
|
84
|
+
accuracy = accuracy.sort_by {|key, value| value }.to_h
|
85
|
+
accuracy
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/typingtutor/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typingtutor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paolo Dona
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-09-
|
11
|
+
date: 2016-09-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: highline
|
@@ -48,6 +48,7 @@ extra_rdoc_files: []
|
|
48
48
|
files:
|
49
49
|
- ".DS_Store"
|
50
50
|
- ".gitignore"
|
51
|
+
- CHANGELOG.md
|
51
52
|
- Gemfile
|
52
53
|
- Gemfile.lock
|
53
54
|
- LICENSE
|
@@ -210,8 +211,10 @@ files:
|
|
210
211
|
- exercises/starwars.txt
|
211
212
|
- lib/typingtutor.rb
|
212
213
|
- lib/typingtutor/dictionary.rb
|
214
|
+
- lib/typingtutor/exercise.rb
|
213
215
|
- lib/typingtutor/line.rb
|
214
216
|
- lib/typingtutor/runner.rb
|
217
|
+
- lib/typingtutor/stats.rb
|
215
218
|
- lib/typingtutor/version.rb
|
216
219
|
- typingtutor.gemspec
|
217
220
|
homepage: https://github.com/paolodona/typingtutor
|