sapor 0.1b1
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 +7 -0
- data/Area Class Diagram.dia +0 -0
- data/Area Class Diagram.png +0 -0
- data/Class Diagram.dia +0 -0
- data/Class Diagram.png +0 -0
- data/Examples.md +361 -0
- data/LICENSE +674 -0
- data/README.md +70 -0
- data/Rakefile +18 -0
- data/Technical Documentation.md +14 -0
- data/bin/create_installation_package.sh +49 -0
- data/bin/install.sh +45 -0
- data/bin/sapor.rb +22 -0
- data/bin/sapor.sh +105 -0
- data/lib/sapor.rb +44 -0
- data/lib/sapor/binomials_cache.rb +45 -0
- data/lib/sapor/combinations_distribution.rb +180 -0
- data/lib/sapor/dichotomies.rb +98 -0
- data/lib/sapor/dichotomy.rb +138 -0
- data/lib/sapor/first_past_the_post.rb +78 -0
- data/lib/sapor/leveled_proportional.rb +64 -0
- data/lib/sapor/log4r_logger.rb +49 -0
- data/lib/sapor/log_facade.rb +40 -0
- data/lib/sapor/number_formatter.rb +45 -0
- data/lib/sapor/poll.rb +137 -0
- data/lib/sapor/polychotomy.rb +359 -0
- data/lib/sapor/proportional.rb +128 -0
- data/lib/sapor/pseudorandom_multirange_enumerator.rb +87 -0
- data/lib/sapor/regional_data/area.rb +80 -0
- data/lib/sapor/regional_data/catalonia-2012-2015.psv +100 -0
- data/lib/sapor/regional_data/catalonia-2012.psv +87 -0
- data/lib/sapor/regional_data/catalonia.rb +90 -0
- data/lib/sapor/regional_data/norway.rb +408 -0
- data/lib/sapor/regional_data/united_kingdom.rb +1075 -0
- data/lib/sapor/regional_data/utopia.rb +66 -0
- data/sapor.gemspec +35 -0
- data/spec/integration/area_spec.rb +28 -0
- data/spec/integration/poll_spec.rb +107 -0
- data/spec/integration/sample.poll +7 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/unit/area_spec.rb +115 -0
- data/spec/unit/binomials_cache_spec.rb +34 -0
- data/spec/unit/catalonia_spec.rb +82 -0
- data/spec/unit/combinations_distribution_spec.rb +241 -0
- data/spec/unit/denominators_spec.rb +34 -0
- data/spec/unit/dichotomies_spec.rb +154 -0
- data/spec/unit/dichotomy_spec.rb +320 -0
- data/spec/unit/first_past_the_post_spec.rb +53 -0
- data/spec/unit/leveled_proportional_spec.rb +51 -0
- data/spec/unit/norway_spec.rb +47 -0
- data/spec/unit/number_formatter_spec.rb +173 -0
- data/spec/unit/poll_spec.rb +105 -0
- data/spec/unit/polychotomy_spec.rb +332 -0
- data/spec/unit/proportional_spec.rb +86 -0
- data/spec/unit/pseudorandom_multirange_enumerator_spec.rb +82 -0
- metadata +119 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Statistical Analysis of Polling Results (SAPoR)
|
4
|
+
# Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
|
5
|
+
#
|
6
|
+
# This file is part of SAPoR.
|
7
|
+
#
|
8
|
+
# SAPoR is free software: you can redistribute it and/or modify it under the
|
9
|
+
# terms of the GNU General Public License as published by the Free Software
|
10
|
+
# Foundation, either version 3 of the License, or (at your option) any later
|
11
|
+
# version.
|
12
|
+
#
|
13
|
+
# SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
|
14
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
15
|
+
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You can find a copy of the GNU General Public License in /doc/gpl.txt
|
18
|
+
#
|
19
|
+
|
20
|
+
module Sapor
|
21
|
+
#
|
22
|
+
# Represents a set of dichotomies.
|
23
|
+
#
|
24
|
+
class Dichotomies
|
25
|
+
include NumberFormatter
|
26
|
+
|
27
|
+
def initialize(results, population_size, threshold = nil)
|
28
|
+
sample_size = results.values.inject(:+)
|
29
|
+
@dichotomy_hash = {}
|
30
|
+
results.each_pair do |choice, number|
|
31
|
+
@dichotomy_hash[choice] = Dichotomy.new(number, sample_size,
|
32
|
+
population_size)
|
33
|
+
end
|
34
|
+
@threshold = threshold
|
35
|
+
end
|
36
|
+
|
37
|
+
def refine
|
38
|
+
@dichotomy_hash.values.each(&:refine)
|
39
|
+
end
|
40
|
+
|
41
|
+
def error_estimate
|
42
|
+
@dichotomy_hash.values.map(&:error_estimate).max
|
43
|
+
end
|
44
|
+
|
45
|
+
def confidence_interval_values(choice, level)
|
46
|
+
@dichotomy_hash[choice].confidence_interval_values(level)
|
47
|
+
end
|
48
|
+
|
49
|
+
def report
|
50
|
+
choice_lengths = @dichotomy_hash.keys.map(&:length)
|
51
|
+
choice_lengths << 6
|
52
|
+
max_choice_width = choice_lengths.max
|
53
|
+
sorted_choices = sort_choices_by_label_and_mpv
|
54
|
+
lines = sorted_choices.map do |choice|
|
55
|
+
create_report_line(choice, @dichotomy_hash[choice], max_choice_width)
|
56
|
+
end
|
57
|
+
"Most probable fractions and 95% confidence intervals:\n" +
|
58
|
+
'Choice'.ljust(max_choice_width) + ' MPF CI(95%)' +
|
59
|
+
(@threshold.nil? ? '' : ' P(≥' + (100 * @threshold).to_i.to_s +
|
60
|
+
'%)') +
|
61
|
+
"\n" + lines.join("\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
def progress_report
|
65
|
+
size = @dichotomy_hash.values.first.values.size
|
66
|
+
"Number of data points: #{with_thousands_separator(size)}."
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def compare_choices_by_label_and_mpv(a, b)
|
72
|
+
if a == OTHER
|
73
|
+
1
|
74
|
+
elsif b == OTHER
|
75
|
+
-1
|
76
|
+
else
|
77
|
+
mpv_a = @dichotomy_hash[a].most_probable_value
|
78
|
+
mpv_b = @dichotomy_hash[b].most_probable_value
|
79
|
+
mpv_a == mpv_b ? a <=> b : mpv_b <=> mpv_a
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def sort_choices_by_label_and_mpv
|
84
|
+
@dichotomy_hash.keys.sort do |a, b|
|
85
|
+
compare_choices_by_label_and_mpv(a, b)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def create_report_line(choice, dichotomy, max_choice_width)
|
90
|
+
choice.ljust(max_choice_width) + ' ' + \
|
91
|
+
six_char_percentage(dichotomy.most_probable_fraction) + ' ' + \
|
92
|
+
six_char_percentage(dichotomy.confidence_interval.first) + '–' + \
|
93
|
+
six_char_percentage(dichotomy.confidence_interval.last) +
|
94
|
+
(@threshold.nil? ? '' : ' ' +
|
95
|
+
six_char_percentage(dichotomy.threshold_probability(@threshold)))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Statistical Analysis of Polling Results (SAPoR)
|
4
|
+
# Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
|
5
|
+
#
|
6
|
+
# This file is part of SAPoR.
|
7
|
+
#
|
8
|
+
# SAPoR is free software: you can redistribute it and/or modify it under the
|
9
|
+
# terms of the GNU General Public License as published by the Free Software
|
10
|
+
# Foundation, either version 3 of the License, or (at your option) any later
|
11
|
+
# version.
|
12
|
+
#
|
13
|
+
# SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
|
14
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
15
|
+
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You can find a copy of the GNU General Public License in /doc/gpl.txt
|
18
|
+
#
|
19
|
+
|
20
|
+
module Sapor
|
21
|
+
#
|
22
|
+
# Represents a dichotomy.
|
23
|
+
#
|
24
|
+
class Dichotomy
|
25
|
+
def initialize(number, sample_size, population_size)
|
26
|
+
@number = number
|
27
|
+
@sample_size = sample_size
|
28
|
+
@population_size = population_size
|
29
|
+
@distribution = CombinationsDistribution.new
|
30
|
+
middle = @population_size / 2
|
31
|
+
@distribution[middle] = combinations_for(middle)
|
32
|
+
end
|
33
|
+
|
34
|
+
def combinations(value)
|
35
|
+
@distribution[value]
|
36
|
+
end
|
37
|
+
|
38
|
+
def confidence_interval(level = 0.95)
|
39
|
+
value_confidence_interval(level).map { |x| x.to_f / @population_size }
|
40
|
+
end
|
41
|
+
|
42
|
+
def confidence_interval_values(level = 0.95)
|
43
|
+
@distribution.confidence_interval_values(level)
|
44
|
+
end
|
45
|
+
|
46
|
+
def error_estimate
|
47
|
+
estimate_error
|
48
|
+
end
|
49
|
+
|
50
|
+
def most_probable_fraction
|
51
|
+
most_probable_value.to_f / @population_size
|
52
|
+
end
|
53
|
+
|
54
|
+
def most_probable_value
|
55
|
+
@distribution.most_probable_value
|
56
|
+
end
|
57
|
+
|
58
|
+
def refine
|
59
|
+
new_values = find_refinement_values
|
60
|
+
new_values.each do |value|
|
61
|
+
@distribution[value] = combinations_for(value)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def threshold_probability(threshold)
|
66
|
+
@distribution.threshold_probability(threshold, @population_size)
|
67
|
+
end
|
68
|
+
|
69
|
+
def value_confidence_interval(level = 0.95)
|
70
|
+
@distribution.confidence_interval(level, @population_size)
|
71
|
+
end
|
72
|
+
|
73
|
+
def values
|
74
|
+
@distribution.values
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def combinations_for(value)
|
80
|
+
dual_value = @population_size - value
|
81
|
+
dual_number = @sample_size - @number
|
82
|
+
if value < @number || dual_value < dual_number
|
83
|
+
0.to_lf
|
84
|
+
else
|
85
|
+
BinomialsCache.binomial(value, @number) * \
|
86
|
+
BinomialsCache.binomial(dual_value, dual_number)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def find_refinement_value_at_bottom
|
91
|
+
bottom = values.min
|
92
|
+
if bottom == 0
|
93
|
+
[]
|
94
|
+
else
|
95
|
+
[(bottom.to_f / 3).round]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def find_refinement_values_between(low, high)
|
100
|
+
new_low = ((2 * low + high).to_f / 3).round
|
101
|
+
new_high = ((2 * high + low).to_f / 3).round
|
102
|
+
refinement_values = []
|
103
|
+
refinement_values << new_high unless new_high == high
|
104
|
+
refinement_values << new_low unless new_low == new_high || new_low == low
|
105
|
+
refinement_values
|
106
|
+
end
|
107
|
+
|
108
|
+
def find_refinement_values_in_between
|
109
|
+
sorted_values = values.sort
|
110
|
+
refinement_values = (0..(@distribution.size - 2)).to_a.map do |i|
|
111
|
+
find_refinement_values_between(sorted_values[i], sorted_values[i + 1])
|
112
|
+
end
|
113
|
+
refinement_values.flatten
|
114
|
+
end
|
115
|
+
|
116
|
+
def find_refinement_value_at_top
|
117
|
+
top = values.max
|
118
|
+
if top == @population_size
|
119
|
+
[]
|
120
|
+
else
|
121
|
+
[((2.to_f * @population_size + top) / 3).round]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def find_refinement_values
|
126
|
+
find_refinement_value_at_bottom + find_refinement_values_in_between +
|
127
|
+
find_refinement_value_at_top
|
128
|
+
end
|
129
|
+
|
130
|
+
def estimate_error
|
131
|
+
if @distribution.size == @population_size + 1
|
132
|
+
0
|
133
|
+
else
|
134
|
+
1.to_f / @distribution.size
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Statistical Analysis of Polling Results (SAPoR)
|
4
|
+
# Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
|
5
|
+
#
|
6
|
+
# This file is part of SAPoR.
|
7
|
+
#
|
8
|
+
# SAPoR is free software: you can redistribute it and/or modify it under the
|
9
|
+
# terms of the GNU General Public License as published by the Free Software
|
10
|
+
# Foundation, either version 3 of the License, or (at your option) any later
|
11
|
+
# version.
|
12
|
+
#
|
13
|
+
# SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
|
14
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
15
|
+
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You can find a copy of the GNU General Public License in /doc/gpl.txt
|
18
|
+
#
|
19
|
+
|
20
|
+
module Sapor
|
21
|
+
#
|
22
|
+
# Class representing an electoral system based on the First-Past-The-Post
|
23
|
+
# system.
|
24
|
+
#
|
25
|
+
class FirstPastThePost
|
26
|
+
def initialize(last_election_result, last_detailed_election_result)
|
27
|
+
@last_election_result = last_election_result
|
28
|
+
@last_detailed_election_result = last_detailed_election_result
|
29
|
+
end
|
30
|
+
|
31
|
+
def project(simulation)
|
32
|
+
multiplicators = calculate_multiplicators(simulation)
|
33
|
+
result = create_empty_result(simulation)
|
34
|
+
@last_detailed_election_result.each_value do |local_last_result|
|
35
|
+
winner = local_winner(local_last_result, multiplicators)
|
36
|
+
result[winner] += 1
|
37
|
+
end
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def calculate_multiplicators(simulation)
|
44
|
+
simulation_sum = simulation.values.inject(:+)
|
45
|
+
last_election_sum = @last_election_result.values.inject(:+)
|
46
|
+
multiplicators = {}
|
47
|
+
simulation.each_key do |choice|
|
48
|
+
new_fraction = simulation[choice].to_f / simulation_sum
|
49
|
+
last_fraction = @last_election_result[choice].to_f / last_election_sum
|
50
|
+
multiplicators[choice] = new_fraction / last_fraction
|
51
|
+
end
|
52
|
+
multiplicators
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_empty_result(simulation)
|
56
|
+
result = {}
|
57
|
+
simulation.each_key do |choice|
|
58
|
+
result[choice] = 0
|
59
|
+
end
|
60
|
+
result[OTHER] = 0
|
61
|
+
result
|
62
|
+
end
|
63
|
+
|
64
|
+
def local_winner(local_last_result, multiplicators)
|
65
|
+
new_local_result = {}
|
66
|
+
local_last_result.each_pair do |choice, votes|
|
67
|
+
if multiplicators.key?(choice)
|
68
|
+
new_local_result[choice] = votes * multiplicators[choice]
|
69
|
+
else
|
70
|
+
new_local_result[choice] = votes
|
71
|
+
end
|
72
|
+
end
|
73
|
+
max_votes = new_local_result.values.max
|
74
|
+
winner = new_local_result.select { |_k, v| v.equal?(max_votes) }.keys.first
|
75
|
+
multiplicators.key?(winner) ? winner : OTHER
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Statistical Analysis of Polling Results (SAPoR)
|
4
|
+
# Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
|
5
|
+
#
|
6
|
+
# This file is part of SAPoR.
|
7
|
+
#
|
8
|
+
# SAPoR is free software: you can redistribute it and/or modify it under the
|
9
|
+
# terms of the GNU General Public License as published by the Free Software
|
10
|
+
# Foundation, either version 3 of the License, or (at your option) any later
|
11
|
+
# version.
|
12
|
+
#
|
13
|
+
# SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
|
14
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
15
|
+
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You can find a copy of the GNU General Public License in /doc/gpl.txt
|
18
|
+
#
|
19
|
+
|
20
|
+
module Sapor
|
21
|
+
#
|
22
|
+
# Class representing a proportional electoral system with a second round of
|
23
|
+
# leveling seats.
|
24
|
+
#
|
25
|
+
class LeveledProportional
|
26
|
+
def initialize(last_election_result, last_detailed_election_result,
|
27
|
+
seat_distribution, leveling_seats, leveling_threshold,
|
28
|
+
denominators_class)
|
29
|
+
@proportional = Proportional.new(last_election_result,
|
30
|
+
last_detailed_election_result,
|
31
|
+
seat_distribution, denominators_class)
|
32
|
+
@leveling_seats = leveling_seats
|
33
|
+
@leveling_threshold = leveling_threshold
|
34
|
+
@denominators_class = denominators_class
|
35
|
+
end
|
36
|
+
|
37
|
+
def project(simulation)
|
38
|
+
result = @proportional.project(simulation)
|
39
|
+
threshold = @leveling_threshold * simulation.values.inject(:+)
|
40
|
+
quotients = []
|
41
|
+
simulation.each_pair do |choice, votes|
|
42
|
+
next if votes < threshold
|
43
|
+
denominators(result[choice]).each do |d|
|
44
|
+
quotients << [choice, votes.to_f / d]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
seats = quotients.sort { |a, b| b.last <=> a.last }.map(&:first).slice(0, @leveling_seats)
|
48
|
+
seats.each do |seat|
|
49
|
+
if result.key?(seat)
|
50
|
+
result[seat] += 1
|
51
|
+
else
|
52
|
+
result[seat] = 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
result
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def denominators(seats)
|
61
|
+
@denominators_class.get(seats + @leveling_seats).reverse_each.take(@leveling_seats)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Statistical Analysis of Polling Results (SAPoR)
|
4
|
+
# Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
|
5
|
+
#
|
6
|
+
# This file is part of SAPoR.
|
7
|
+
#
|
8
|
+
# SAPoR is free software: you can redistribute it and/or modify it under the
|
9
|
+
# terms of the GNU General Public License as published by the Free Software
|
10
|
+
# Foundation, either version 3 of the License, or (at your option) any later
|
11
|
+
# version.
|
12
|
+
#
|
13
|
+
# SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
|
14
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
15
|
+
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You can find a copy of the GNU General Public License in /doc/gpl.txt
|
18
|
+
#
|
19
|
+
|
20
|
+
module Sapor
|
21
|
+
#
|
22
|
+
# Builder to create Log4rLoggers.
|
23
|
+
#
|
24
|
+
class Log4rLoggerBuilder
|
25
|
+
def create_logger
|
26
|
+
Log4rLogger.new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
require 'rubygems'
|
31
|
+
require 'log4r'
|
32
|
+
#
|
33
|
+
# Logger using Log4r.
|
34
|
+
#
|
35
|
+
class Log4rLogger
|
36
|
+
def initialize
|
37
|
+
@logger = Log4r::Logger.new 'stdout'
|
38
|
+
stdout_outputter = Log4r::Outputter.stdout
|
39
|
+
@logger.outputters = stdout_outputter
|
40
|
+
stdout_format = Log4r::PatternFormatter.new(pattern: '%d %l: %m')
|
41
|
+
stdout_outputter.formatter = stdout_format
|
42
|
+
@logger.level = Log4r::INFO
|
43
|
+
end
|
44
|
+
|
45
|
+
def info(message)
|
46
|
+
@logger.info(message)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Statistical Analysis of Polling Results (SAPoR)
|
4
|
+
# Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
|
5
|
+
#
|
6
|
+
# This file is part of SAPoR.
|
7
|
+
#
|
8
|
+
# SAPoR is free software: you can redistribute it and/or modify it under the
|
9
|
+
# terms of the GNU General Public License as published by the Free Software
|
10
|
+
# Foundation, either version 3 of the License, or (at your option) any later
|
11
|
+
# version.
|
12
|
+
#
|
13
|
+
# SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
|
14
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
15
|
+
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You can find a copy of the GNU General Public License in /doc/gpl.txt
|
18
|
+
#
|
19
|
+
|
20
|
+
module Sapor
|
21
|
+
#
|
22
|
+
# A facade for logging.
|
23
|
+
#
|
24
|
+
class LogFacade
|
25
|
+
@logger_builder = nil
|
26
|
+
|
27
|
+
class << self
|
28
|
+
attr_writer :logger_builder
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.builder
|
32
|
+
@logger_builder = Log4rLoggerBuilder.new if @logger_builder.nil?
|
33
|
+
@logger_builder
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.create_logger
|
37
|
+
builder.create_logger
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|