Pickaxe 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []