quiz_master 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ module QuizMaster
2
+ # This class allows us to build a QuizVariant which is a Quiz whose Questions
3
+ # and thier Answers have been re-ordered in the way specified by the orderings
4
+ # supplied to new.
5
+ #
6
+ # Once a QuizVariant has been made it is not intended to be mutated, it is an
7
+ # "end result" which can be used to generate output.
8
+ class QuizVariant
9
+ # Get the questions for this QuizVariant.
10
+ attr_reader :questions
11
+ # Get the header for this QuizVariant.
12
+ attr_reader :header
13
+ # Get the footer for this QuizVariant.
14
+ attr_reader :footer
15
+
16
+ # Public: Initialize this QuizVariant. The Answers are reordered before the
17
+ # Questions.
18
+ #
19
+ # source - A Quiz to be "shuffled" according to the reorderings.
20
+ # answer_orderings - An Array of ReorderingVectors which specify how each
21
+ # Question's Answers are to be reordered.
22
+ # question_ordering - A ReorderingVector which specifies how the Questions
23
+ # are to be reordered.
24
+ #
25
+ # Returns a QuizVariant.
26
+ def initialize(source, answer_orderings, question_ordering)
27
+ @header = source.header.dup
28
+ @footer = source.footer.dup
29
+ @questions = source
30
+ .reorder_answers(answer_orderings)
31
+ .reorder_questions(question_ordering)
32
+ .questions
33
+ .map(&:render_answers)
34
+ end
35
+ end
36
+ end
@@ -1,5 +1,5 @@
1
- require_relative 'question'
2
- require_relative 'answers_renderer'
1
+ require_relative "question"
2
+ require_relative "answers_renderer"
3
3
 
4
4
  module QuizMaster
5
5
  # This class is a question which has its answers rendered.
@@ -0,0 +1,68 @@
1
+ module QuizMaster
2
+ # This is responsible for reordering an Array. It is a place to put any
3
+ # logic and error checking.
4
+ #
5
+ # Examples
6
+ #
7
+ # r = Reorderer.new(0, 2, 1, 3)
8
+ # r.reorder(%w(a b c d)) # => %w(a c b d)
9
+ class Reorderer
10
+ # Public: Initialize a Reorderer.
11
+ #
12
+ # args - Specify an Array of indices which must not be empty and must
13
+ # contain the values 0 ... size of array.
14
+ #
15
+ # Examples
16
+ #
17
+ # r = Reorderer.new(2, 0, 1)
18
+ #
19
+ # Raises ArgumentError if the args can't be validated.
20
+ def initialize(*args)
21
+ @indices = args.to_a
22
+ indices_invalid? &&
23
+ fail(ArgumentError, "#{args.inspect} is a bad set of indices")
24
+ end
25
+
26
+ # Public: Reorder the passed array based on the indices we were initialized
27
+ # with.
28
+ #
29
+ # array - An Array containing elements you're interested in reordering.
30
+ #
31
+ # Examples
32
+ #
33
+ # r.reorder([1, 3, 5])
34
+ #
35
+ # Raises an ArgumentError if the size of the Array isn't the same as the
36
+ # size of the indices specified in new.
37
+ #
38
+ # Returns a new Array containing the reordered elements of array.
39
+ def reorder(array)
40
+ fail(ArgumentError, "array should have #{@indices.size} elements") if
41
+ array.size != @indices.size
42
+ array.values_at(*@indices)
43
+ end
44
+
45
+ # Determines which elements of a list will be moved when the list is
46
+ # reordered.
47
+ #
48
+ # Examples
49
+ #
50
+ # r = Reorderer.new(3,0,2,1)
51
+ # r.moving_indices # => [0, 1, 3]
52
+ # # ... so element 2 of an Array will be element 2 of the result
53
+ # # of the re-order, and elements 0, 1, and 3 will be moved in the
54
+ # # result:
55
+ # r.reorder(%w(zero one two three)) # => ["three", "zero", "two", "one"]
56
+ #
57
+ # Retruns an Array of source indices.
58
+ def moving_indices
59
+ @indices.each_with_index
60
+ .select { |destination, source| source != destination }
61
+ .map { |_, source| source }
62
+ end
63
+
64
+ private def indices_invalid?
65
+ @indices.empty? || @indices.sort != (0 ... @indices.size).to_a
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,95 @@
1
+ require "forwardable"
2
+
3
+ module QuizMaster
4
+ # A ReorderingVector is a way of specifying how an array should be reordered.
5
+ # It contains a mapping of source indices so you can say:
6
+ #
7
+ # reordered = original.values_at(*reordering_vector)
8
+ #
9
+ # The reordering is constructed "randomly" at initialization, and has the idea
10
+ # of "anchored" indices which are guaranteed to remain in their original
11
+ # place.
12
+ class ReorderingVector
13
+ extend Forwardable
14
+ include Comparable
15
+
16
+ # Public: Initialize a ReorderingVector.
17
+ #
18
+ # anchored - An Array of "truthy" values which indicate the size of the
19
+ # Array to reorder and which elements should be "anchored"
20
+ # at their current position in the reordering (indicated by
21
+ # a +true+ value).
22
+ # shuffle_block - A block used to shuffle the "un-anchored" indices. It is
23
+ # called with an Array as an argument and should return an
24
+ # Array containing the same elements. Optional
25
+ def initialize(anchored, &shuffle_block)
26
+ shuffle_block ||= ->(a) { a.shuffle }
27
+ @indices = create_indices(anchored, &shuffle_block)
28
+ end
29
+
30
+ ##
31
+ # :method: []
32
+ # :args: index
33
+ #
34
+ # Public: Get the element at the specified index.
35
+ #
36
+ # Returns the element at the specified index.
37
+ def_delegator :@indices, :[]
38
+
39
+ ##
40
+ # :method: size
41
+ #
42
+ # Public: Get the size of the ReorderingVector.
43
+ #
44
+ # Returns an Integer representing the size of the ReorderingVector.
45
+ def_delegator :@indices, :size
46
+
47
+ ##
48
+ # :method: each
49
+ #
50
+ # Public: Allows us to enumerate the elements of the ReorderingVector.
51
+ #
52
+ # Yields each element of the Reordering Vector.
53
+ #
54
+ # Returns an Enumerator if no block given.
55
+ def_delegator :@indices, :each
56
+
57
+ # Public: Convert a ReorderingVector to an Array
58
+ #
59
+ # Examples
60
+ #
61
+ # r = ReorderingVector.new([true, false, false, true])
62
+ # r.to_a # => [0, 2, 1, 3]
63
+ #
64
+ # Returns an Array representation of this object.
65
+ def to_a
66
+ @indices.dup
67
+ end
68
+
69
+ alias_method :to_ary, :to_a
70
+
71
+ # Public: Compare two ReorderingVectors. We include the Comparable module
72
+ # so this gives us all of the comparisons. We compare ReorderingVectors as
73
+ # Arrays.
74
+ #
75
+ # other - An other objecy which can be compared to an Array.
76
+ #
77
+ # Returns +1 / 0 / -1 for successful comparisons, and nil for incomparable
78
+ # values.
79
+ def <=>(other)
80
+ to_a <=> other.to_a
81
+ end
82
+
83
+ private def create_indices(anchored, &shuffle_block)
84
+ anchored_indices, floating_indices =
85
+ anchored.each_index.partition { |i| anchored[i] }
86
+
87
+ result = anchored_indices.each_with_object([]) { |i, a| a[i] = i }
88
+
89
+ shuffled = shuffle_block.call(floating_indices)
90
+ floating_indices.zip(shuffled).each_with_object(result) do |(to, from), a|
91
+ a[to] = from
92
+ end
93
+ end
94
+ end
95
+ end
@@ -13,7 +13,7 @@ module QuizMaster
13
13
  # This generates an infinite range which we can use for numbering the
14
14
  # questions.
15
15
  numbers = (1 .. Float::INFINITY)
16
- quiz.questions.zip(numbers).each_with_object('') do |(q, number), result|
16
+ quiz.questions.zip(numbers).each_with_object("") do |(q, number), result|
17
17
  result << "#{number}) #{q.correct_answer.tag}\n"
18
18
  end
19
19
  end
@@ -21,7 +21,7 @@ module QuizMaster
21
21
  # Public: Create a TextAssembler object
22
22
  def initialize
23
23
  @tags = []
24
- @format_string = ''
24
+ @format_string = ""
25
25
  end
26
26
 
27
27
  # Public: Add a tag reference to the text we are assembling.
@@ -35,7 +35,7 @@ module QuizMaster
35
35
  # Returns nothing.
36
36
  def add_tag_reference(tag)
37
37
  @tags << tag
38
- @format_string << '(%s)'
38
+ @format_string << "(%s)"
39
39
  end
40
40
 
41
41
  # Public: Add some literal text to the text we are assembling.
@@ -48,7 +48,7 @@ module QuizMaster
48
48
  #
49
49
  # Returns nothing.
50
50
  def add_text(text)
51
- @format_string << text.gsub('%', '%%')
51
+ @format_string << text.gsub("%", "%%")
52
52
  end
53
53
 
54
54
  # Public: Get the String representation of the TextAssembler.
@@ -36,7 +36,7 @@ module QuizMaster
36
36
  #
37
37
  # Returns a String containing the formatted Question.
38
38
  def format(question, number)
39
- answers = AnswersRenderer.new(answers: question.answers, first_tag: 'a')
39
+ answers = AnswersRenderer.new(answers: question.answers, first_tag: "a")
40
40
 
41
41
  answers.each_with_object("#{number}) #{question.prompt}\n\n") do |a, text|
42
42
  text << " #{a.tag}) #{a.text}\n"
@@ -1,8 +1,9 @@
1
- require_relative 'text_question_formatter'
1
+ require_relative "text_question_formatter"
2
2
 
3
3
  module QuizMaster
4
4
  # This is responsible for generating a text version of Quizes.
5
5
  class TextQuestionSheetFormatter
6
+ # Initialiize a TextQuestionSheetFormatter.
6
7
  def initialize
7
8
  @question_formatter = TextQuestionFormatter.new
8
9
  end
@@ -1,5 +1,5 @@
1
1
  #
2
2
  module QuizMaster
3
3
  # The Gem version. We will use semantic versioning: http://semver.org
4
- VERSION = '0.0.2'
4
+ VERSION = "0.0.3"
5
5
  end
@@ -1,17 +1,19 @@
1
1
  # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
2
+ lib = File.expand_path("../lib", __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'quiz_master/version'
4
+ require "quiz_master/version"
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = 'quiz_master'
7
+ spec.name = "quiz_master"
8
8
  spec.version = QuizMaster::VERSION
9
- spec.authors = ['Mike Stok']
10
- spec.email = ['mike@stok.ca']
11
- spec.summary = 'Simple manager for multiple choice quizzes'
9
+ spec.authors = ["Mike Stok"]
10
+ spec.email = ["mike@stok.ca"]
11
+ spec.summary = "Simple manager for multiple choice quizzes"
12
12
  # spec.description = %q{TODO: Write a longer description. Optional.}
13
- spec.homepage = 'https://github.com/mikestok/quiz_master'
14
- spec.license = 'MIT'
13
+ spec.homepage = "https://github.com/mikestok/quiz_master"
14
+ spec.license = "MIT"
15
+
16
+ spec.required_ruby_version = "~> 2.1"
15
17
 
16
18
  # Because there are likely to be / characters in the regular expressions
17
19
  # we relax the rubocop checks for a bit...
@@ -19,13 +21,13 @@ Gem::Specification.new do |spec|
19
21
  spec.files = `git ls-files -z`.split("\x0")
20
22
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
23
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
- spec.require_paths = ['lib']
24
+ spec.require_paths = ["lib"]
23
25
  # rubocop:enable Style/RegexpLiteral
24
26
 
25
- spec.add_development_dependency 'bundler', '~> 1.6'
26
- spec.add_development_dependency 'rake'
27
- spec.add_development_dependency 'rubocop'
28
- spec.add_development_dependency 'rdoc'
29
- spec.add_development_dependency 'inch'
30
- spec.add_development_dependency 'sparkr' # inch should have included this...
27
+ spec.add_development_dependency "bundler", "~> 1.6"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "rubocop"
30
+ spec.add_development_dependency "rdoc"
31
+ spec.add_development_dependency "inch"
32
+ spec.add_development_dependency "sparkr" # inch should have included this...
31
33
  end
@@ -0,0 +1,55 @@
1
+ require "spec_helper"
2
+
3
+ describe Question do
4
+ describe "reorder_answers" do
5
+ it "can reorder its answers" do
6
+ q = example_questions.first
7
+ reordered = q.reorder_answers(Reorderer.new(2, 0, 1, 3))
8
+ reordered.answers.map(&:text).must_equal(
9
+ [
10
+ "This is the third answer",
11
+ "This is the first answer",
12
+ "This is the second answer",
13
+ "{{3}} and {{5}}"
14
+ ]
15
+ )
16
+ end
17
+
18
+ it 'raises an error if asked to move "anchored" answers' do
19
+ q = example_questions[2]
20
+ lambda do
21
+ q.reorder_answers(Reorderer.new(0, 7, 2, 3, 4, 5, 6, 1))
22
+ end.must_raise(MovingAnchoredAnswerError)
23
+ end
24
+ end
25
+
26
+ describe "reordering_vector" do
27
+ q = example_questions[2]
28
+ rv = q.reordering_vector { |a| a.reverse }
29
+
30
+ it "must be a ReorderingVector" do
31
+ rv.must_be_kind_of(ReorderingVector)
32
+ end
33
+
34
+ it "must have the expected order" do
35
+ rv.to_a.must_equal([5, 4, 3, 2, 1, 0, 6, 7])
36
+ end
37
+ end
38
+
39
+ describe "render_answers" do
40
+ first_tag = "a"
41
+ q = example_questions.first.render_answers(first_tag: first_tag)
42
+
43
+ it "must have the correct first answer and tag" do
44
+ af = q.answers.first
45
+ af.tag.must_equal first_tag
46
+ af.text.must_equal "This is the first answer"
47
+ end
48
+
49
+ it "must have the correct last answer and tag" do
50
+ al = q.answers.last
51
+ al.tag.must_equal "d"
52
+ al.text.must_equal "(a) and (c)"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ require "spec_helper"
2
+
3
+ describe Quiz do
4
+ describe "answer_reordering_vectors" do
5
+ it "should generate expected answer reordering vectors" do
6
+ quiz_source = Quiz.new(example_questions)
7
+ vectors = quiz_source.answer_reordering_vectors { |a| a.reverse }
8
+
9
+ vectors.must_equal([
10
+ ReorderingVector([3, 2, 1, 0]),
11
+ ReorderingVector([3, 2, 1, 0]),
12
+ ReorderingVector([5, 4, 3, 2, 1, 0, 6, 7])
13
+ ])
14
+ end
15
+ end
16
+
17
+ describe "question_reordering_vector" do
18
+ it "should generate expected reordering vector" do
19
+ quiz_source = Quiz.new(example_questions)
20
+ v = quiz_source.question_reordering_vector { |a| a.reverse }
21
+
22
+ v.must_equal(ReorderingVector([2, 1, 0]))
23
+ end
24
+ end
25
+
26
+ describe "reorder_answers" do
27
+ quiz_source = Quiz.new(example_questions)
28
+ vectors = quiz_source.answer_reordering_vectors { |a| a.reverse }
29
+ quiz = quiz_source.reorder_answers(vectors)
30
+
31
+ it "should *not* have shuffled the questions" do
32
+ quiz.questions.first.prompt.must_equal(
33
+ EXAMPLE_QUESTION_PARAMS.first[:prompt]
34
+ )
35
+ end
36
+
37
+ it "should have shuffled the answers within the question" do
38
+ quiz.questions.first.answers.first.text.must_equal(
39
+ EXAMPLE_QUESTION_PARAMS.first[:answers].last.text
40
+ )
41
+ end
42
+ end
43
+
44
+ describe "reorder_questions" do
45
+ it "should return a quiz with questions re-ordered" do
46
+ quiz_source = Quiz.new(example_questions)
47
+ vector = quiz_source.question_reordering_vector { |a| a.reverse }
48
+ quiz = quiz_source.reorder_questions(vector)
49
+
50
+ quiz.questions.first.prompt.must_equal(
51
+ EXAMPLE_QUESTION_PARAMS.last[:prompt]
52
+ )
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,34 @@
1
+ require "spec_helper"
2
+
3
+ describe QuizVariant do
4
+ describe "initialize" do
5
+ quiz_source = Quiz.new(example_questions)
6
+
7
+ answer_orderings = [
8
+ ReorderingVector([3, 1, 0, 2]),
9
+ ReorderingVector([1, 0, 2, 3]),
10
+ ReorderingVector([4, 0, 1, 3, 5, 2, 6, 7])
11
+ ]
12
+ question_ordering = ReorderingVector([0, 2, 1])
13
+
14
+ variant = QuizVariant.new(quiz_source, answer_orderings, question_ordering)
15
+
16
+ it "has questions in the right order" do
17
+ variant.questions[0].prompt.must_equal "Example 1 prompt"
18
+ variant.questions[1].prompt.must_equal "What is the colour of a Banana?"
19
+ variant.questions[2].prompt.must_equal "Example 2 prompt"
20
+ end
21
+
22
+ it "has answers in the right order" do
23
+ banana_question = variant.questions[1]
24
+ banana_question.answers[0].text.must_equal "Brown"
25
+ banana_question.answers[1].text.must_equal "Green"
26
+ banana_question.answers[6].text.must_equal "All of the colours above"
27
+ end
28
+
29
+ it "has the tags assigned" do
30
+ banana_question = variant.questions[1]
31
+ banana_question.answers[0].tag.must_equal "a"
32
+ end
33
+ end
34
+ end