rust 0.12 → 0.15
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/lib/rust/core/csv.rb +4 -4
- data/lib/rust/core/manual.rb +89 -0
- data/lib/rust/core/rust.rb +28 -5
- data/lib/rust/core/types/dataframe.rb +71 -2
- data/lib/rust/core/types/datatype.rb +13 -3
- data/lib/rust/core/types/factor.rb +4 -0
- data/lib/rust/core/types/language.rb +32 -0
- data/lib/rust/core/types/list.rb +2 -0
- data/lib/rust/core/types/matrix.rb +8 -0
- data/lib/rust/core.rb +50 -0
- data/lib/rust/external/ggplot2/plot_builder.rb +112 -8
- data/lib/rust/external/ggplot2/scale.rb +12 -0
- data/lib/rust/external/ggplot2/themes.rb +26 -3
- data/lib/rust/external/ggplot2.rb +112 -1
- data/lib/rust/forms/all.rb +4 -0
- data/lib/rust/forms/google_forms.rb +283 -0
- data/lib/rust/jobs/all.rb +4 -0
- data/lib/rust/jobs/jobs.rb +144 -0
- data/lib/rust/models/regression.rb +175 -11
- data/lib/rust/stats/probabilities.rb +22 -1
- data/lib/rust.rb +1 -0
- metadata +9 -7
- data/lib/rust/external/ggplot2/helper.rb +0 -122
|
@@ -53,7 +53,7 @@ module Rust::Plots::GGPlot
|
|
|
53
53
|
def to_h
|
|
54
54
|
options = @options.clone
|
|
55
55
|
|
|
56
|
-
options['_starting'] = @starting.sub("theme_", "")
|
|
56
|
+
options['_starting'] = @starting.sub("theme_", "") if @starting
|
|
57
57
|
options = options.map do |key, value|
|
|
58
58
|
[key, value.is_a?(Theme::Element) ? value.to_h : value]
|
|
59
59
|
end.to_h
|
|
@@ -78,6 +78,9 @@ module Rust::Plots::GGPlot
|
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
class ExistingTheme < Layer
|
|
82
|
+
end
|
|
83
|
+
|
|
81
84
|
class Theme::Element
|
|
82
85
|
attr_reader :options
|
|
83
86
|
|
|
@@ -152,6 +155,8 @@ module Rust::Plots::GGPlot
|
|
|
152
155
|
return value
|
|
153
156
|
elsif value.is_a?(Hash)
|
|
154
157
|
return Theme::LineElement.new(**value)
|
|
158
|
+
elsif !value
|
|
159
|
+
return Theme::BlankElement.new
|
|
155
160
|
else
|
|
156
161
|
raise "Expected line or hash"
|
|
157
162
|
end
|
|
@@ -162,6 +167,8 @@ module Rust::Plots::GGPlot
|
|
|
162
167
|
return value
|
|
163
168
|
elsif value.is_a?(Hash)
|
|
164
169
|
return Theme::RectElement.new(**value)
|
|
170
|
+
elsif !value
|
|
171
|
+
return Theme::BlankElement.new
|
|
165
172
|
else
|
|
166
173
|
raise "Expected rect or hash"
|
|
167
174
|
end
|
|
@@ -172,6 +179,8 @@ module Rust::Plots::GGPlot
|
|
|
172
179
|
return value
|
|
173
180
|
elsif value.is_a?(Hash)
|
|
174
181
|
return Theme::TextElement.new(**value)
|
|
182
|
+
elsif !value
|
|
183
|
+
return Theme::BlankElement.new
|
|
175
184
|
else
|
|
176
185
|
raise "Expected text or hash"
|
|
177
186
|
end
|
|
@@ -225,7 +234,7 @@ module Rust::Plots::GGPlot
|
|
|
225
234
|
end
|
|
226
235
|
|
|
227
236
|
class ThemeBuilder < ThemeComponentBuilder
|
|
228
|
-
def initialize(starting =
|
|
237
|
+
def initialize(starting = nil)
|
|
229
238
|
super("plot")
|
|
230
239
|
@starting = starting
|
|
231
240
|
end
|
|
@@ -417,7 +426,21 @@ module Rust::Plots::GGPlot
|
|
|
417
426
|
end
|
|
418
427
|
end
|
|
419
428
|
|
|
420
|
-
|
|
429
|
+
class ThemeCollection
|
|
430
|
+
def self.ggtech(name = "google")
|
|
431
|
+
Rust.prerequisite("ricardo-bion/ggtech", true)
|
|
432
|
+
|
|
433
|
+
return ExistingTheme.new("theme_tech", theme: name)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def self.ggdark(style = "classic")
|
|
437
|
+
Rust.prerequisite("ggdark")
|
|
438
|
+
|
|
439
|
+
return ExistingTheme.new("dark_theme_#{style}")
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
self.default_theme = ThemeBuilder.new("bw").
|
|
421
444
|
title(face: 'bold', size: 12).
|
|
422
445
|
legend do |legend|
|
|
423
446
|
legend.background(fill: 'white', size: 4, colour: 'white')
|
|
@@ -2,4 +2,115 @@ require_relative 'ggplot2/core'
|
|
|
2
2
|
require_relative 'ggplot2/geoms'
|
|
3
3
|
require_relative 'ggplot2/themes'
|
|
4
4
|
require_relative 'ggplot2/plot_builder'
|
|
5
|
-
require_relative 'ggplot2/
|
|
5
|
+
require_relative 'ggplot2/scale'
|
|
6
|
+
|
|
7
|
+
Rust::Manual.register(:ggplot2, "ggplot2", "Informations on the wrapper of the popular ggplot2 plotting library for R.")
|
|
8
|
+
|
|
9
|
+
Rust::Manual.for(:ggplot2).register("Introduction", /intro/,
|
|
10
|
+
<<-EOS
|
|
11
|
+
bind_ggplot! # Avoid using long module names to reach Rust::Plots::GGPlot (simply includes this module)
|
|
12
|
+
|
|
13
|
+
# Best with a dataframe, but not necessary. If you have it...
|
|
14
|
+
df = Rust.toothgrowth
|
|
15
|
+
plot = PlotBuilder.for_dataframe(df). # Use a dataframe (symbols will be variable names)
|
|
16
|
+
labeled("Example plot"). # "labeled" sets the label to the last set aesthetic item (x, y, or title, in this case)
|
|
17
|
+
with_x(:len).labeled("X data from df"). # Set all the aesthetics (x, y, ...)
|
|
18
|
+
with_y(:dose).labeled("Y data from df").
|
|
19
|
+
draw_points. # Set the geometries to plot (based on the plot type)
|
|
20
|
+
build # Returns the plot ready to use
|
|
21
|
+
plot.show # Show the plot in a window
|
|
22
|
+
plot.save("output.pdf", width: 5, height: 4) # Save the plot, width, height etc. are optional
|
|
23
|
+
|
|
24
|
+
# If you don't have a dataframe...
|
|
25
|
+
plot2 = PlotBuilder.new.
|
|
26
|
+
with_x([1,2,3]).labeled("X data from df").
|
|
27
|
+
with_y([3,4,5]).labeled("Y data from df").
|
|
28
|
+
draw_points.
|
|
29
|
+
build
|
|
30
|
+
plot2.show
|
|
31
|
+
EOS
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
Rust::Manual.for(:ggplot2).register("Scatter plots", /scatter/,
|
|
35
|
+
<<-EOS
|
|
36
|
+
bind_ggplot!
|
|
37
|
+
df = Rust.toothgrowth
|
|
38
|
+
plot = PlotBuilder.for_dataframe(df).
|
|
39
|
+
with_x(:len).labeled("X data").
|
|
40
|
+
with_y(:dose).labeled("Y data").
|
|
41
|
+
draw_points. # To draw points
|
|
42
|
+
draw_lines. # To draw lines (keep both to draw both)
|
|
43
|
+
build
|
|
44
|
+
plot.show
|
|
45
|
+
EOS
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
Rust::Manual.for(:ggplot2).register("Bar plots", /bar/,
|
|
49
|
+
<<-EOS
|
|
50
|
+
bind_ggplot!
|
|
51
|
+
df = Rust.toothgrowth
|
|
52
|
+
plot = PlotBuilder.for_dataframe(df).
|
|
53
|
+
with_x(:len).labeled("X data").
|
|
54
|
+
with_fill(:supp).labeled("Legend"). # Use with_fill or with_color for stacked plots
|
|
55
|
+
draw_bars. # To draw bars
|
|
56
|
+
build
|
|
57
|
+
plot.show
|
|
58
|
+
EOS
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
Rust::Manual.for(:ggplot2).register("Box plots", /box/,
|
|
62
|
+
<<-EOS
|
|
63
|
+
bind_ggplot!
|
|
64
|
+
df = Rust.toothgrowth
|
|
65
|
+
plot = PlotBuilder.for_dataframe(df).
|
|
66
|
+
with_y(:len).labeled("Data to boxplot").
|
|
67
|
+
with_group(:supp).labeled("Groups"). # Groups to plot
|
|
68
|
+
draw_boxplot.
|
|
69
|
+
build
|
|
70
|
+
plot.show
|
|
71
|
+
EOS
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
Rust::Manual.for(:ggplot2).register("Histograms", /hist/,
|
|
75
|
+
<<-EOS
|
|
76
|
+
bind_ggplot!
|
|
77
|
+
df = Rust.toothgrowth
|
|
78
|
+
plot = PlotBuilder.for_dataframe(df).
|
|
79
|
+
with_x(:len).labeled("Data to plot").
|
|
80
|
+
with_fill(:supp).labeled("Color"). # Use with_fill or with_color for multiple plots
|
|
81
|
+
draw_histogram.
|
|
82
|
+
build
|
|
83
|
+
plot.show
|
|
84
|
+
EOS
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
Rust::Manual.for(:ggplot2).register("Themes", /them/,
|
|
88
|
+
<<-EOS
|
|
89
|
+
bind_ggplot!
|
|
90
|
+
df = Rust.toothgrowth
|
|
91
|
+
# The method with_theme allows to change theme options. The method can be called
|
|
92
|
+
# several times, each time the argument does not overwrite the previous options,
|
|
93
|
+
# unless they are specified again (in that case, the last specified ones win).
|
|
94
|
+
plot = PlotBuilder.for_dataframe(df).
|
|
95
|
+
with_x(:len).labeled("X data").
|
|
96
|
+
with_y(:dose).labeled("Y data").
|
|
97
|
+
draw_points.
|
|
98
|
+
with_theme(
|
|
99
|
+
ThemeBuilder.new('bw').
|
|
100
|
+
title(face: 'bold', size: 12). # Each method sets the property for the related element
|
|
101
|
+
legend do |legend| # Legend and other parts can be set like this
|
|
102
|
+
legend.position(:left) # Puts the legend on the left
|
|
103
|
+
end.
|
|
104
|
+
axis do |axis| # Modifies the axes
|
|
105
|
+
axis.line(Theme::BlankElement.new) # Hides the lines for the axes
|
|
106
|
+
axis.text_x(size: 3) # X axis labels
|
|
107
|
+
end.
|
|
108
|
+
panel do |panel|
|
|
109
|
+
panel.grid_major(colour: 'grey70', size: 0.2) # Sets the major ticks grid
|
|
110
|
+
panel.grid_minor(Theme::BlankElement.new) # Hides the minor ticks grid
|
|
111
|
+
end.
|
|
112
|
+
build
|
|
113
|
+
).build
|
|
114
|
+
plot.show
|
|
115
|
+
EOS
|
|
116
|
+
)
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
require_relative '../core'
|
|
2
|
+
require 'time'
|
|
3
|
+
|
|
4
|
+
module Rust
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
# Class that allows to read CSVs exported from Google Forms.
|
|
8
|
+
class GoogleFormMapping
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# Loads a mapping from a CSV file that can be used in the constructor of GoogleForm given the CSV +filename+ and the
|
|
12
|
+
# keys for defining what should be transformed (+key_from+) in what (+key_to+). Returns a hash with the mapping
|
|
13
|
+
# from -> to.
|
|
14
|
+
|
|
15
|
+
def self.load(filename, key_from="from", key_to="to", **options)
|
|
16
|
+
raise TypeError, "Expected string for filename" unless filename.is_a?(String)
|
|
17
|
+
raise TypeError, "Expected string for key_from" unless key_from.is_a?(String)
|
|
18
|
+
raise TypeError, "Expected string for key_to" unless key_to.is_a?(String)
|
|
19
|
+
|
|
20
|
+
result = {}
|
|
21
|
+
mapping = Rust::CSV.read(filename, headers: true)
|
|
22
|
+
mapping.each do |r|
|
|
23
|
+
result[r[key_from]] = r[key_to]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
return GoogleFormMapping.new(result, **options)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(hash, **options)
|
|
30
|
+
raise TypeError, "Hash should be an hash" unless hash.is_a?(Hash)
|
|
31
|
+
raise TypeError, "Mapping for question #{question} must have either all String keys or all Regexp keys." if !hash.keys.all? { |m| m.is_a?(Regexp) } && !hash.keys.all? { |m| m.is_a?(String) }
|
|
32
|
+
raise "Unsupported options: #{options.keys - [:strip, :downcase]}" if (options.keys - [:strip, :downcase]).size > 0
|
|
33
|
+
|
|
34
|
+
if hash.keys.all? { |m| m.is_a?(Regexp) }
|
|
35
|
+
@type = :regexp
|
|
36
|
+
else
|
|
37
|
+
@type = :direct
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@strip = options[:strip]
|
|
41
|
+
@downcase = options[:downcase]
|
|
42
|
+
|
|
43
|
+
if @type == :direct
|
|
44
|
+
@hash = {}
|
|
45
|
+
hash.each do |k, v|
|
|
46
|
+
@hash[normalize(k)] = v
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
@hash = hash
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get(from)
|
|
54
|
+
if @type == :regexp
|
|
55
|
+
@hash.each do |k, v|
|
|
56
|
+
if from.match(k)
|
|
57
|
+
return v
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
return from
|
|
61
|
+
elsif @type == :direct
|
|
62
|
+
return @hash[normalize(from)] || from
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
def normalize(string)
|
|
68
|
+
string = string.downcase if @downcase
|
|
69
|
+
string = string.strip if @strip
|
|
70
|
+
return string
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class GoogleForm
|
|
75
|
+
ALLOWED_TYPES = [:multiple, :checkbox, :scale, :text]
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
# Reads the CSV at +filename+ and returns a GoogleForm. The schema must be a hash that contains, for each question number or name,
|
|
79
|
+
# the type of answer (:multiple, :checkbox, :scale, or :text). For the other options, see Rust::CSV.read.
|
|
80
|
+
|
|
81
|
+
def self.read(filename, schema, mappings={}, **options)
|
|
82
|
+
data_frame = Rust::CSV.read(filename, **options)
|
|
83
|
+
|
|
84
|
+
return GoogleForm.new(data_frame, schema, mappings)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def initialize(data_frame, schema, mappings={})
|
|
88
|
+
raise TypeError, "Expected Rust::DataFrame" unless data_frame.is_a?(Rust::DataFrame)
|
|
89
|
+
raise TypeError, "Expected Hash or Array" if !schema.is_a?(Hash) && !schema.is_a?(Array)
|
|
90
|
+
raise TypeError, "Schema keys must all be numbers or strings" if schema.is_a?(Hash) && !schema.keys.all? { |k| k.is_a?(String) }
|
|
91
|
+
raise TypeError, "Mappings should be an hash [String, Integer] -> GoogleFormMapping" if !mappings.is_a?(Hash) || !mappings.keys.all? { |k| k.is_a?(String) || k.is_a?(Integer) } || !mappings.values.all? { |v| v.is_a?(GoogleFormMapping) }
|
|
92
|
+
if schema.is_a?(Array)
|
|
93
|
+
new_schema = {}
|
|
94
|
+
for i in 0...schema.size
|
|
95
|
+
new_schema[index_to_title(i+1, data_frame)] = schema[i]
|
|
96
|
+
end
|
|
97
|
+
schema = new_schema
|
|
98
|
+
end
|
|
99
|
+
raise TypeError, "Schema values must all be #{ALLOWED_TYPES}; #{schema.values.uniq - ALLOWED_TYPES} given instead" if !schema.values.all? { |v| ALLOWED_TYPES.include?(v)}
|
|
100
|
+
raise TypeError, "Schema must include types for all the questions" if schema.size != (data_frame.columns - 1)
|
|
101
|
+
|
|
102
|
+
@data_frame = data_frame
|
|
103
|
+
@questions = data_frame.colnames
|
|
104
|
+
@schema = schema
|
|
105
|
+
|
|
106
|
+
mappings.each do |question, mapping|
|
|
107
|
+
raise "Mappings can not be defined for :scale questions" if schema[title_to_index(question)] == :scale
|
|
108
|
+
end
|
|
109
|
+
@mappings = mappings
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def data_frame
|
|
113
|
+
@data_frame
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def mapped_data_frame
|
|
117
|
+
df = Rust::DataFrame.new(@data_frame.colnames)
|
|
118
|
+
self.each_answer do |a|
|
|
119
|
+
df << a
|
|
120
|
+
end
|
|
121
|
+
return df
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def rows
|
|
125
|
+
@data_frame.rows
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def answer(i)
|
|
129
|
+
row = @data_frame.row(i)
|
|
130
|
+
|
|
131
|
+
@questions.each_with_index do |colname, i|
|
|
132
|
+
if i == 0
|
|
133
|
+
row[colname] = Time.parse(row[colname])
|
|
134
|
+
else
|
|
135
|
+
row[colname] = get_value(row[colname], colname)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
return row
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def each_answer
|
|
143
|
+
for i in 0...@data_frame.rows
|
|
144
|
+
yield(self.answer(i))
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def answers
|
|
149
|
+
answers = []
|
|
150
|
+
for i in 0...@data_frame.rows
|
|
151
|
+
answers << self.answer(i)
|
|
152
|
+
end
|
|
153
|
+
return answers
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def filter
|
|
157
|
+
matching = Rust::DataFrame.new(@questions)
|
|
158
|
+
|
|
159
|
+
for i in 0...@data_frame.rows
|
|
160
|
+
matching << @data_frame.row(i) if yield(self.answer(i))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
return GoogleForm.new(matching, @schema, @mappings)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def raw_answers_to(question)
|
|
167
|
+
question = index_to_title(question) if question.is_a?(Integer)
|
|
168
|
+
results = []
|
|
169
|
+
|
|
170
|
+
(@data_frame|question).each do |value|
|
|
171
|
+
value = get_value(value, question)
|
|
172
|
+
results << value
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
return results
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def answers_to(question)
|
|
179
|
+
question = index_to_title(question) if question.is_a?(Integer)
|
|
180
|
+
|
|
181
|
+
results = {}
|
|
182
|
+
|
|
183
|
+
(@data_frame|question).each do |value|
|
|
184
|
+
value = get_value(value, question)
|
|
185
|
+
if value.is_a?(Array)
|
|
186
|
+
value.each do |v|
|
|
187
|
+
results[v] = 0 unless results[v]
|
|
188
|
+
results[v] += 1
|
|
189
|
+
end
|
|
190
|
+
else
|
|
191
|
+
results[value] = 0 unless results[value]
|
|
192
|
+
results[value] += 1
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
results.delete(nil)
|
|
196
|
+
|
|
197
|
+
return results
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def textual_answers_to(question)
|
|
201
|
+
question = index_to_title(question) if question.is_a?(Integer)
|
|
202
|
+
raise TypeError, "Expected textual question, #{@schema[question]} instead" if @schema[question] != :text
|
|
203
|
+
|
|
204
|
+
results = {}
|
|
205
|
+
|
|
206
|
+
(@data_frame|question).each do |value|
|
|
207
|
+
value = get_value(value, question)
|
|
208
|
+
next if value == nil
|
|
209
|
+
|
|
210
|
+
category = yield(value)
|
|
211
|
+
results[category] = 0 unless results[category]
|
|
212
|
+
results[category] += 1
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
return results
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def percentual_answers_to(question, exclude=[])
|
|
219
|
+
answers = answers_to(question)
|
|
220
|
+
|
|
221
|
+
exclude.each do |ex|
|
|
222
|
+
answers.delete(ex)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
tot = answers.values.sum
|
|
226
|
+
answers = answers.map { |k, v| [k, v.to_f/tot] }.to_h
|
|
227
|
+
return answers
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def percentual_textual_answers_to(question, &block)
|
|
231
|
+
answers = textual_answers_to(question, &block)
|
|
232
|
+
|
|
233
|
+
tot = answers.values.sum
|
|
234
|
+
answers = answers.map { |k, v| [k, v.to_f/tot] }.to_h
|
|
235
|
+
return answers
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
def index_to_title(i, data_frame=@data_frame)
|
|
240
|
+
data_frame.colnames[i]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def title_to_index(title, data_frame=@data_frame)
|
|
244
|
+
data_frame.colnames.index(title)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def get_value(value, question, data_frame=@data_frame)
|
|
248
|
+
mapping = @mappings[question]
|
|
249
|
+
|
|
250
|
+
mapped_value = mapping ? mapping.get(value) : value
|
|
251
|
+
|
|
252
|
+
case @schema[question]
|
|
253
|
+
when :multiple
|
|
254
|
+
return nil if mapped_value == ""
|
|
255
|
+
return mapped_value
|
|
256
|
+
|
|
257
|
+
when :checkbox
|
|
258
|
+
return value.split(';').map { |single_value| mapping ? mapping.get(single_value) : single_value }
|
|
259
|
+
|
|
260
|
+
when :scale
|
|
261
|
+
return nil if value == ""
|
|
262
|
+
ordinal = (data_frame|question).uniq.sort
|
|
263
|
+
ordinal.delete("")
|
|
264
|
+
return ordinal.index(value) + 1
|
|
265
|
+
|
|
266
|
+
when :text
|
|
267
|
+
return mapped_value
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
raise TypeError
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
module Rust::RBindings
|
|
276
|
+
def read_csv(filename, **options)
|
|
277
|
+
Rust::CSV.read(filename, **options)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def write_csv(filename, dataframe, **options)
|
|
281
|
+
Rust::CSV.write(filename, dataframe, **options)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
require_relative '../core'
|
|
2
|
+
|
|
3
|
+
module Rust::Jobs
|
|
4
|
+
class TaskHook
|
|
5
|
+
def initialize(task, on_complete, on_error)
|
|
6
|
+
@task = task
|
|
7
|
+
@on_complete = on_complete
|
|
8
|
+
@on_error = on_error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def complete!
|
|
12
|
+
@on_complete.call
|
|
13
|
+
@task.notify
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def error!
|
|
17
|
+
@on_error.call
|
|
18
|
+
@task.notify
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Task
|
|
23
|
+
def initialize(title, &block)
|
|
24
|
+
raise "Expected block to describe the task" unless block_given?
|
|
25
|
+
@title = title
|
|
26
|
+
@todo = block
|
|
27
|
+
@done = false
|
|
28
|
+
|
|
29
|
+
@complete_hook = proc {}
|
|
30
|
+
@error_hook = proc {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def start
|
|
34
|
+
@thread = Thread.start do
|
|
35
|
+
@todo.call(TaskHook.new(@complete_hook, @error_hook))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def notify
|
|
40
|
+
@done = true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def waitfor(granularity=0.1)
|
|
44
|
+
while !@done
|
|
45
|
+
sleep granularity
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def commit
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def on_complete(&block)
|
|
53
|
+
raise "Block expected" unless block_given?
|
|
54
|
+
@complete_hook = block
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def on_error(&block)
|
|
58
|
+
raise "Block expected" unless block_given?
|
|
59
|
+
@error_hook = block
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class Job
|
|
64
|
+
def initialize(name, **options)
|
|
65
|
+
@name = name
|
|
66
|
+
@tasks = []
|
|
67
|
+
|
|
68
|
+
@parallel = false
|
|
69
|
+
|
|
70
|
+
if options['parallel']
|
|
71
|
+
@parallel = true
|
|
72
|
+
@parallel_tasks = 10
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if options['parallel_tasks'].is_a?(Integer)
|
|
76
|
+
@parallel_tasks = options['parallel_tasks'].to_i
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@logger = options['logger'] ? options['logger'] : STDOUT
|
|
80
|
+
|
|
81
|
+
if options[:quiet]
|
|
82
|
+
@logger = File.open(File::NULL, "w")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def log(message, type="INFO")
|
|
87
|
+
@logger << "[#{type}] #{Time.now}: #{message.gsub("\n", " -- ")}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def log_info(message)
|
|
91
|
+
log(message, "INFO")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def log_warning(message)
|
|
95
|
+
log(message, "WARNING")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def log_error(message)
|
|
99
|
+
log(message, "ERROR")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def add_task(task=nil, **options, &block)
|
|
103
|
+
if block_given?
|
|
104
|
+
raise "You gave both a block and a task. Please, choose one" if task
|
|
105
|
+
task = Task.new(options['title'], block)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
raise "Expected a task, #{task.class} given instead" unless task.is_a?(Task)
|
|
109
|
+
|
|
110
|
+
@tasks << task
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def start
|
|
114
|
+
log_info "Job \"#@name\" started"
|
|
115
|
+
if @parallel
|
|
116
|
+
else
|
|
117
|
+
@tasks.each do |t|
|
|
118
|
+
t.on_complete do
|
|
119
|
+
log_info "Task \"#{t.title}\" completed"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
t.on_error do |message|
|
|
123
|
+
log_error "Task \"#{t.title}\" did not complete: #{message}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
log_info "Task \"#{t.title}\" started"
|
|
127
|
+
t.start
|
|
128
|
+
t.waitfor
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
class Resumeable < Job
|
|
135
|
+
def initialize(name, **options)
|
|
136
|
+
super
|
|
137
|
+
# TODO complete here
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def start
|
|
141
|
+
# TODO complete here
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|