Pickaxe 0.1.3 → 0.2.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.
@@ -2,3 +2,5 @@
2
2
 
3
3
  Pickaxe provides a simple way to load, solve and rate tests
4
4
  (bundle of questions) written in simple text format.
5
+
6
+ Documentation can be read [here](http://dejw.github.com/pickaxe/).
@@ -30,7 +30,7 @@ END_OF_BANNER
30
30
  options[:sorted] = true
31
31
  end
32
32
 
33
- opts.on("--select [NUMBER]", "Select certain amount of questions") do |v|
33
+ opts.on("--select [NUMBER]", "Select certain number of questions") do |v|
34
34
  options[:select] = Integer(v)
35
35
  end
36
36
 
@@ -0,0 +1,25 @@
1
+ require "rubygems"
2
+ require "bundler"
3
+
4
+ Bundler.setup(:default)
5
+
6
+ require 'active_support/all'
7
+
8
+ module Pickaxe
9
+ VERSION = "0.2.0"
10
+
11
+ class PickaxeError < StandardError
12
+ attr_reader :status_code
13
+ def self.status_code(code = nil)
14
+ define_method(:status_code) { code }
15
+ end
16
+ end
17
+
18
+ autoload :Shell, 'pickaxe/shell'
19
+ autoload :Color, 'pickaxe/color'
20
+ autoload :Main, 'pickaxe/main'
21
+ autoload :Test, 'pickaxe/test'
22
+ end
23
+
24
+ require 'pickaxe/extensions'
25
+
@@ -0,0 +1,54 @@
1
+ module Pickaxe
2
+ # Extracted from https://github.com/wycats/thor/blob/master/lib/thor/shell/color.rb
3
+ module Color
4
+ # Embed in a String to clear all previous ANSI sequences.
5
+ CLEAR = "\e[0m"
6
+ # The start of an ANSI bold sequence.
7
+ BOLD = "\e[1m"
8
+
9
+ # Set the terminal's foreground ANSI color to black.
10
+ BLACK = "\e[30m"
11
+ # Set the terminal's foreground ANSI color to red.
12
+ RED = "\e[31m"
13
+ # Set the terminal's foreground ANSI color to green.
14
+ GREEN = "\e[32m"
15
+ # Set the terminal's foreground ANSI color to yellow.
16
+ YELLOW = "\e[33m"
17
+ # Set the terminal's foreground ANSI color to blue.
18
+ BLUE = "\e[34m"
19
+ # Set the terminal's foreground ANSI color to magenta.
20
+ MAGENTA = "\e[35m"
21
+ # Set the terminal's foreground ANSI color to cyan.
22
+ CYAN = "\e[36m"
23
+ # Set the terminal's foreground ANSI color to white.
24
+ WHITE = "\e[37m"
25
+
26
+ # Set the terminal's background ANSI color to black.
27
+ ON_BLACK = "\e[40m"
28
+ # Set the terminal's background ANSI color to red.
29
+ ON_RED = "\e[41m"
30
+ # Set the terminal's background ANSI color to green.
31
+ ON_GREEN = "\e[42m"
32
+ # Set the terminal's background ANSI color to yellow.
33
+ ON_YELLOW = "\e[43m"
34
+ # Set the terminal's background ANSI color to blue.
35
+ ON_BLUE = "\e[44m"
36
+ # Set the terminal's background ANSI color to magenta.
37
+ ON_MAGENTA = "\e[45m"
38
+ # Set the terminal's background ANSI color to cyan.
39
+ ON_CYAN = "\e[46m"
40
+ # Set the terminal's background ANSI color to white.
41
+ ON_WHITE = "\e[47m"
42
+
43
+ # Set color by using a string or one of the defined constants. If a third
44
+ # option is set to true, it also adds bold to the string. This is based
45
+ # on Highline implementation and it automatically appends CLEAR to the end
46
+ # of the returned String.
47
+ #
48
+ def self.set_color(string, color, bold=false)
49
+ color = self.const_get(color.to_s.upcase) if color.is_a?(Symbol)
50
+ bold = bold ? BOLD : ""
51
+ "#{bold}#{color}#{string}#{CLEAR}"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ class String
2
+ def word_wrap(*args)
3
+ options = args.extract_options!
4
+ unless args.blank?
5
+ options[:line_width] = args[0] || Pickaxe::Shell.dynamic_width || 80
6
+ end
7
+ options.reverse_merge!(:line_width => Pickaxe::Shell.dynamic_width || 80)
8
+
9
+ self.split("\n").collect do |line|
10
+ line.length > options[:line_width] ? line.gsub(/(.{1,#{options[:line_width]}})(\s+|$)/, "\\1\n").strip : line
11
+ end * "\n"
12
+ end
13
+
14
+ def color(name, bold=false)
15
+ Pickaxe::Color.set_color(self, name, bold)
16
+ end
17
+ end
18
+
19
+ class Fixnum
20
+ def to_duration
21
+ s = self
22
+ m = s / 60
23
+ s -= m * 60
24
+
25
+ [m, s].compact.zip(["m", "s"]).collect(&:join).join(" ")
26
+ end
27
+ end
@@ -0,0 +1,112 @@
1
+ module Pickaxe
2
+ class Main
3
+ class NoTests < PickaxeError; status_code(1) ; end
4
+
5
+ def initialize(paths, options = {})
6
+ raise NoTests, "no tests to run" if paths.empty?
7
+ @test = Test.new(options, *paths)
8
+ @questions = @test.shuffled_questions
9
+ @answers = Hash.new([])
10
+
11
+ @started_at = Time.now
12
+ @current_index = 0
13
+ while @current_index < @questions.length do
14
+ @question = @questions[@current_index]
15
+
16
+ puts "#{@current_index+1} / #{@questions.length}\t\tFrom: #{@question.file}\t\tTime spent: #{spent?}"
17
+ puts @question.answered(@answers[@question])
18
+
19
+ until (line = prompt?).nil? or command(line)
20
+ # empty
21
+ end
22
+
23
+ break if puts or line.nil?
24
+ end
25
+
26
+ statistics!
27
+ end
28
+
29
+ #
30
+ # Available commands
31
+ # ^ question jumps to given question
32
+ # <+ moves back one question
33
+ # >+ moves forward one question
34
+ # ! a [ b ...] answers the question and forces to show correct answers
35
+ # a [ b ...] answers the question
36
+ # ? shows help
37
+ #
38
+ def command(line)
39
+ case line
40
+ when /^\s*@\s*(.+)/ then # @ question
41
+ @current_index = Integer($1) -1
42
+ true
43
+ when /<+/ then
44
+ if @current_index > 0
45
+ @current_index -= 1
46
+ true
47
+ else
48
+ error "You are at first question"
49
+ end
50
+ when />+/ then
51
+ if @current_index < (@questions.length - 1)
52
+ @current_index += 1
53
+ true
54
+ else
55
+ error "You are at last question"
56
+ end
57
+ when "\n" then
58
+ @current_index += 1
59
+ true
60
+ when /^\s*!\s*(.+)/ then
61
+ raise NotImplementedError
62
+ when /\?/ then
63
+ puts <<END_OF_HELP
64
+
65
+ Available commands (whitespace does not matter):
66
+ @ question jumps to given question
67
+ < moves back one question
68
+ > moves forward one question
69
+ a [ b ...] answers the question
70
+ ! a [ b ...] answers the question and forces reveal correct answers
71
+ ? shows help
72
+
73
+ END_OF_HELP
74
+ false
75
+ else
76
+ @answers[@question] = line.split(/\s+/).collect(&:strip)
77
+ @current_index += 1
78
+ true
79
+ end
80
+ end
81
+
82
+ def statistics!
83
+ @stats = @test.statistics!(@answers)
84
+
85
+ puts
86
+ puts "Time: #{spent?}"
87
+ puts "All: #{@questions.length}"
88
+ stat :correct, :green
89
+ stat :unanswered, :yellow
90
+ stat :incorrect, :red
91
+ end
92
+
93
+ def error(msg)
94
+ $stderr.puts(("! " + msg).color(:red))
95
+ false
96
+ end
97
+
98
+ def prompt?(p = "? ")
99
+ print p
100
+ $stdin.gets
101
+ end
102
+
103
+ def spent?
104
+ (Time.now - @started_at).to_i.to_duration
105
+ end
106
+ protected
107
+ def stat(name, color)
108
+ value = @stats[name.to_s.downcase.to_sym]
109
+ puts "#{name.to_s.capitalize}: #{value} (#{value/@questions.length.to_f * 100}%%)".color(color)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,16 @@
1
+ module Pickaxe
2
+ module Shell
3
+ # Extracted from https://github.com/wycats/thor/blob/master/lib/thor/shell/basic.rb
4
+ def self.dynamic_width
5
+ (dynamic_width_stty.nonzero? || dynamic_width_tput)
6
+ end
7
+
8
+ def self.dynamic_width_stty
9
+ %x{stty size 2>/dev/null}.split[1].to_i
10
+ end
11
+
12
+ def self.dynamic_width_tput
13
+ %x{tput cols 2>/dev/null}.to_i
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,128 @@
1
+ module Pickaxe
2
+ #
3
+ # Test is a file in which questions are separated by a blank line.
4
+ # Each question has content (first line), and answers remaining lines.
5
+ # Answers are listed one per line which starts with optional >> (means answer
6
+ # is correct), followed by index in parenthesis (index) and followed by text.
7
+ #
8
+ # Example:
9
+ #
10
+ # 1. To be or not to be?
11
+ # (a) To be.
12
+ # (b) Not to be.
13
+ # >> (c) I do not know.
14
+ #
15
+ class Test
16
+ attr_reader :questions
17
+
18
+ include Enumerable
19
+
20
+ # Ruby-comments and C-comments
21
+ COMMENTS_RE = /^#.*|^\/\/.*/
22
+
23
+ class PathError < PickaxeError; status_code(1) ; end
24
+ class MissingAnswers < PickaxeError; status_code(2) ; end
25
+ class BadAnswer < PickaxeError; status_code(3) ; end
26
+
27
+ def initialize(options, *files)
28
+ @options = options
29
+ @files = files.collect do |file_or_directory|
30
+ raise PathError, "file or directory '#{file_or_directory}' does not exist" unless File.exist?(file_or_directory)
31
+ if File.file?(file_or_directory)
32
+ file_or_directory
33
+ else
34
+ Dir.glob("#{file_or_directory}/*.#{@options[:extension] || "txt"}")
35
+ end
36
+ end.flatten
37
+
38
+ @questions = []
39
+ @files.each do |file|
40
+ File.open(file) do |f|
41
+ lines = f.readlines.collect(&:strip)
42
+ lines = lines.reject {|line| line =~ COMMENTS_RE }
43
+ lines.split("").reject(&:blank?).each do |question|
44
+ @questions.push(Question.parse(file, question))
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ # Yields questions randomly
51
+ def each(&block)
52
+ shuffled_questions.each(&block)
53
+ end
54
+
55
+ def shuffled_questions
56
+ questions = if @options[:sorted]
57
+ @questions
58
+ else
59
+ @questions.shuffle
60
+ end
61
+
62
+ @selected = if @options[:select]
63
+ questions[0...(@options[:select])]
64
+ else
65
+ questions
66
+ end
67
+ end
68
+
69
+ def selected
70
+ @selected ||= @questions
71
+ end
72
+
73
+ def statistics!(answers)
74
+ Hash.new(0).tap do |statistics|
75
+ selected.each do |question|
76
+ given = answers[question]
77
+ if question.correct?(given)
78
+ statistics[:correct] += 1
79
+ elsif given.blank?
80
+ statistics[:unanswered] += 1
81
+ else
82
+ statistics[:incorrect] += 1
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ class Question < Struct.new(:file, :content, :answers)
90
+ def self.parse(file, answers)
91
+ content = answers.shift
92
+ raise MissingAnswers, "question '#{content.truncate(20)}' has no answers'" if answers.blank?
93
+ Question.new(file, content, answers.collect {|answer| Answer.parse(answer) })
94
+ end
95
+
96
+ def answered(indices)
97
+ "#{self.content.word_wrap}\n\n" + self.answers.collect do |answer|
98
+ selected = indices.include?(answer.index)
99
+ line = (selected ? ">> " : " ") + answer.to_s
100
+ unless indices.blank?
101
+ if selected and answer.correctness
102
+ line.color(:green)
103
+ elsif not selected and answer.correctness
104
+ line.color(:yellow)
105
+ elsif selected and not answer.correctness
106
+ line.color(:red)
107
+ end
108
+ end || line
109
+ end.join("\n") + "\n\n"
110
+ end
111
+
112
+ def correct?(given)
113
+ given.sort == answers.select(&:correctness).collect(&:index).sort
114
+ end
115
+ end
116
+
117
+ class Answer < Struct.new(:content, :index, :correctness)
118
+ RE = /^\s*(>>)?(\?\?)?\s*\((\w+)\)\s*(.+)$/
119
+ def self.parse(line)
120
+ raise BadAnswer, "'#{line.truncate(20)}' does not look like answer" if (m = RE.match(line)).nil?
121
+ Answer.new(m[m.size-1].strip, m[m.size-2].strip, m[1] == ">>")
122
+ end
123
+
124
+ def to_s
125
+ "(#{self.index}) #{self.content}".word_wrap
126
+ end
127
+ end
128
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: Pickaxe
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 3
10
- version: 0.1.3
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Dawid Fatyga
@@ -62,7 +62,10 @@ dependencies:
62
62
  name: i18n
63
63
  prerelease: false
64
64
  type: :runtime
65
- description: " Pickaxe provides a simple way to load, solve and rate tests (bundle of questions)\n written in simple text format.\n"
65
+ description: |
66
+ Pickaxe provides a simple way to load, solve and rate tests (bundle of questions)
67
+ written in simple text format.
68
+
66
69
  email: dawid.fatyga@gmail.com
67
70
  executables:
68
71
  - pickaxe
@@ -71,8 +74,14 @@ extensions: []
71
74
  extra_rdoc_files:
72
75
  - README.markdown
73
76
  files:
74
- - README.markdown
77
+ - lib/pickaxe.rb
78
+ - lib/pickaxe/test.rb
79
+ - lib/pickaxe/color.rb
80
+ - lib/pickaxe/extensions.rb
81
+ - lib/pickaxe/shell.rb
82
+ - lib/pickaxe/main.rb
75
83
  - bin/pickaxe
84
+ - README.markdown
76
85
  has_rdoc: true
77
86
  homepage: https://github.com/dejw/pickaxe
78
87
  licenses: []