optimus-ep 0.5
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.
- data/Rakefile +9 -0
- data/bin/eprime2tabfile +165 -0
- data/bin/stim.times +5 -0
- data/bin/stim1.times +5 -0
- data/bin/stim1_b.times +5 -0
- data/bin/stim1_c.times +5 -0
- data/bin/stim1_d.times +5 -0
- data/bin/test_data.txt +278 -0
- data/bin/test_data2.txt +277 -0
- data/bin/test_eprime_stimfile.rb +20 -0
- data/lib/calculator.rb +49 -0
- data/lib/column_calculator.rb +308 -0
- data/lib/eprime.rb +23 -0
- data/lib/eprime_data.rb +154 -0
- data/lib/eprime_reader.rb +105 -0
- data/lib/eprimetab_parser.rb +21 -0
- data/lib/excel_parser.rb +21 -0
- data/lib/log_file_parser.rb +208 -0
- data/lib/row_filter.rb +40 -0
- data/lib/tabfile_parser.rb +55 -0
- data/lib/tabfile_writer.rb +44 -0
- data/lib/writers/stimtimes_writer.rb +97 -0
- data/spec/calculator_spec.rb +56 -0
- data/spec/column_calculator_spec.rb +368 -0
- data/spec/eprime_data_spec.rb +202 -0
- data/spec/eprime_reader_spec.rb +115 -0
- data/spec/eprimetab_parser_spec.rb +23 -0
- data/spec/excel_parser_spec.rb +26 -0
- data/spec/log_file_parser_spec.rb +156 -0
- data/spec/row_filter_spec.rb +32 -0
- data/spec/samples/bad_excel_tsv.txt +4 -0
- data/spec/samples/corrupt_log_file.txt +116 -0
- data/spec/samples/eprime_tsv.txt +7 -0
- data/spec/samples/excel_tsv.txt +5 -0
- data/spec/samples/optimus_log.txt +110 -0
- data/spec/samples/short_columns.txt +1 -0
- data/spec/samples/sorted_columns.txt +1 -0
- data/spec/samples/std_columns.txt +1 -0
- data/spec/samples/unknown_type.txt +2 -0
- data/spec/samples/unreadable_file +1 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/tabfile_parser_spec.rb +62 -0
- data/spec/tabfile_writer_spec.rb +91 -0
- data/spec/writers/stimtimes_writer_spec.rb +16 -0
- metadata +106 -0
@@ -0,0 +1,208 @@
|
|
1
|
+
# Part of the Optimus package for managing E-Prime data
|
2
|
+
#
|
3
|
+
# Copyright (C) 2008 Board of Regents of the University of Wisconsin System
|
4
|
+
#
|
5
|
+
# Written by Nathan Vack <njvack@wisc.edu>, at the Waisman Laborotory for Brain
|
6
|
+
# Imaging and Behavior, University of Wisconsin - Madison
|
7
|
+
|
8
|
+
module Eprime
|
9
|
+
class Reader
|
10
|
+
|
11
|
+
# Reads and parses E-Prime log files (the ones that start with
|
12
|
+
# *** Header Start ***) and transforms them into an Eprime::Data structure
|
13
|
+
|
14
|
+
class LogfileParser
|
15
|
+
# Handles parsing eprime log files, which are essentially a blow-by-blow
|
16
|
+
# log of everything that happened during an eprime run.
|
17
|
+
|
18
|
+
FRAME_START = '*** LogFrame Start ***'
|
19
|
+
FRAME_END = '*** LogFrame End ***'
|
20
|
+
HEADER_START = '*** Header Start ***'
|
21
|
+
HEADER_END = '*** Header End ***'
|
22
|
+
LEVEL_KEY = 'Level'
|
23
|
+
LEVEL_NAME_KEY = 'LevelName'
|
24
|
+
|
25
|
+
attr_reader :frames
|
26
|
+
attr_reader :levels
|
27
|
+
attr_reader :top_level
|
28
|
+
attr_reader :skip_columns
|
29
|
+
|
30
|
+
# Valid things for the options hash:
|
31
|
+
# :columns => an array of strings, predefining the expected columns
|
32
|
+
# (and their order)
|
33
|
+
# :force => true, if you want to ignore things such as column added
|
34
|
+
# warnings and if the file is incomplete
|
35
|
+
|
36
|
+
def initialize(file, options = {})
|
37
|
+
@columns = options[:columns]
|
38
|
+
@force = options[:force]
|
39
|
+
@file = file
|
40
|
+
@levels = [''] # The 0 index should be blank.
|
41
|
+
@top_level = 0 # This is the level of the frame that'll generate output rows
|
42
|
+
@skip_columns = {} # A hash of columns we *don't* want to add -- just define the strings
|
43
|
+
end
|
44
|
+
|
45
|
+
def make_frames!
|
46
|
+
read_levels(@file)
|
47
|
+
@frames = frameify(@file)
|
48
|
+
set_parents!
|
49
|
+
set_counters!
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_eprime
|
53
|
+
begin
|
54
|
+
if @frames.nil? or @frames.empty?
|
55
|
+
make_frames!
|
56
|
+
end
|
57
|
+
rescue Exception => e
|
58
|
+
unless @force
|
59
|
+
raise e
|
60
|
+
end
|
61
|
+
end
|
62
|
+
if @columns
|
63
|
+
data = Eprime::Data.new(@columns)
|
64
|
+
else
|
65
|
+
data = Eprime::Data.new
|
66
|
+
end
|
67
|
+
self.top_frames.each do |frame|
|
68
|
+
row = data.add_row
|
69
|
+
frame.columns.each do |column, value|
|
70
|
+
begin
|
71
|
+
# Do a check for columns to skip -- this will happen in the case
|
72
|
+
# where you have Procedure[Session] and Procedure[Task] -- we
|
73
|
+
# shouldn't have Procedure, in that case.
|
74
|
+
unless @skip_columns[column]
|
75
|
+
row[column] = value
|
76
|
+
end
|
77
|
+
rescue Exception => e
|
78
|
+
unless @force
|
79
|
+
raise e
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
return data
|
85
|
+
end
|
86
|
+
|
87
|
+
def top_frames
|
88
|
+
return frames.find_all { |frame| frame.level == @top_level }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Define this as a column we *should not* include in out output.
|
92
|
+
def skip_column(col_name)
|
93
|
+
@skip_columns[col_name] = true
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
# iterate over each line, strip it, look for *** LogFrame Start *** and
|
98
|
+
# *** LogFrame End *** -- the content between those goes into a frame array.
|
99
|
+
# If we start a frame but don't end it, raise a DamagedFileError
|
100
|
+
def frameify(file)
|
101
|
+
in_frame = false
|
102
|
+
frames = []
|
103
|
+
frame = Frame.new(self)
|
104
|
+
level = 0
|
105
|
+
file.each_line do |line|
|
106
|
+
# TODO? Refactor this out into its own private function
|
107
|
+
l_s = line.strip
|
108
|
+
key, val = l_s.split(/: */, 2) # There isn't always a space, and values can contain colons
|
109
|
+
|
110
|
+
if !in_frame
|
111
|
+
if key == LEVEL_KEY
|
112
|
+
frame.level = val.to_i
|
113
|
+
@top_level = frame.level if frame.level > @top_level
|
114
|
+
elsif key == FRAME_START
|
115
|
+
in_frame = true
|
116
|
+
end
|
117
|
+
else
|
118
|
+
if key == FRAME_END
|
119
|
+
in_frame = false
|
120
|
+
frames << frame
|
121
|
+
frame = Frame.new(self)
|
122
|
+
else
|
123
|
+
# Add the data to our frame
|
124
|
+
# One more special thing: Experiment gets renamed ExperimentName. WTF?
|
125
|
+
key = "ExperimentName" if key == "Experiment"
|
126
|
+
frame[key] = val
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
raise DamagedFileError.new("Last frame never closed in #{file.path}") if in_frame
|
131
|
+
return frames
|
132
|
+
end
|
133
|
+
|
134
|
+
# Reads through the header and resets the file to its starting position
|
135
|
+
def read_levels(file)
|
136
|
+
in_header = false
|
137
|
+
file.each_line do |line|
|
138
|
+
l_s = line.strip
|
139
|
+
key, val = l_s.split(': ')
|
140
|
+
if !in_header
|
141
|
+
if key == HEADER_START
|
142
|
+
in_header = true
|
143
|
+
end
|
144
|
+
else
|
145
|
+
if key == HEADER_END
|
146
|
+
file.rewind
|
147
|
+
return # Get out of this function!
|
148
|
+
else
|
149
|
+
if key == LEVEL_NAME_KEY
|
150
|
+
@levels << val
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def set_counters!
|
158
|
+
counts = [0] * (@levels.length+1)
|
159
|
+
@frames.each do |frame|
|
160
|
+
counts[frame.level] += 1
|
161
|
+
key = @levels[frame.level]
|
162
|
+
frame[key] = counts[frame.level]
|
163
|
+
counts.fill(0, (frame.level+1)..@levels.length)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def set_parents!
|
168
|
+
parents = []
|
169
|
+
@frames.reverse_each do |frame|
|
170
|
+
parents[frame.level] = frame
|
171
|
+
frame.parent = parents[frame.level-1] # This will be nil for empty slots.
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
class Frame
|
176
|
+
attr_accessor :level
|
177
|
+
attr_accessor :parent
|
178
|
+
def initialize(parser)
|
179
|
+
@level = nil
|
180
|
+
@parent = nil
|
181
|
+
@data = Hash.new
|
182
|
+
@parser = parser
|
183
|
+
end
|
184
|
+
|
185
|
+
def columns
|
186
|
+
my_data = @data.dup
|
187
|
+
return my_data if @parent.nil?
|
188
|
+
parent_data = @parent.columns
|
189
|
+
parent_data.each do |key, val|
|
190
|
+
if my_data.has_key?(key)
|
191
|
+
@parser.skip_column(key)
|
192
|
+
# Append a string like "[Session]" or "[Block]" to the key name
|
193
|
+
my_data["#{key}[#{@parser.levels[@level]}]"] = my_data[key]
|
194
|
+
my_data["#{key}[#{@parser.levels[@parent.level]}]"] = val
|
195
|
+
else
|
196
|
+
my_data[key] = parent_data[key]
|
197
|
+
end
|
198
|
+
end
|
199
|
+
return my_data
|
200
|
+
end
|
201
|
+
|
202
|
+
def method_missing(meth, *args)
|
203
|
+
@data.send meth, *args
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end # Class LogfileParser
|
207
|
+
end # Class Reader
|
208
|
+
end # Module Eprime
|
data/lib/row_filter.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Part of the Optimus package for managing E-Prime data
|
2
|
+
#
|
3
|
+
# Copyright (C) 2008 Board of Regents of the University of Wisconsin System
|
4
|
+
#
|
5
|
+
# Written by Nathan Vack <njvack@wisc.edu>, at the Waisman Laborotory for Brain
|
6
|
+
# Imaging and Behavior, University of Wisconsin - Madison
|
7
|
+
|
8
|
+
module Eprime
|
9
|
+
|
10
|
+
# Implements a row-wise filter for eprime data.
|
11
|
+
# Right now it requires a proc; I'll do something better with a little
|
12
|
+
# DSL later.
|
13
|
+
class RowFilter
|
14
|
+
include Enumerable
|
15
|
+
|
16
|
+
def initialize(data, filter)
|
17
|
+
@data = data
|
18
|
+
@filter = filter
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
@data.each do |row|
|
23
|
+
yield row if match?(row)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def match?(row)
|
28
|
+
if @filter.is_a? Proc
|
29
|
+
return @filter.call(row)
|
30
|
+
elsif @filter.is_a? Array
|
31
|
+
# @filter will be of the form [col_name, comparator, [value]]
|
32
|
+
# only 'equals' is supported for comparators
|
33
|
+
if @filter[1].downcase != 'equals'
|
34
|
+
raise ArgumentError.new('Only equals is supported in filtering')
|
35
|
+
end
|
36
|
+
return row[@filter[0]].to_s == @filter[2].to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# Part of the Optimus package for managing E-Prime data
|
2
|
+
#
|
3
|
+
# Copyright (C) 2008 Board of Regents of the University of Wisconsin System
|
4
|
+
#
|
5
|
+
# Written by Nathan Vack <njvack@wisc.edu>, at the Waisman Laborotory for Brain
|
6
|
+
# Imaging and Behavior, University of Wisconsin - Madison
|
7
|
+
|
8
|
+
|
9
|
+
module Eprime
|
10
|
+
class Reader
|
11
|
+
|
12
|
+
# This class is for reading tab-delimited Eprime files. (Or, really, any tab-delimited file).
|
13
|
+
# The main option of interest is the :skip_lines option, which specifies how many lines
|
14
|
+
# to skip before finding column names. For example:
|
15
|
+
#
|
16
|
+
# TabfileParser.new(stream, :skip_lines => 1)
|
17
|
+
#
|
18
|
+
# is what you'd use for skipping the filename line in a standard eprime Excel file.
|
19
|
+
#
|
20
|
+
# Note: you'll generally be using subclasses of this, and not manually specifying skip_lines.
|
21
|
+
|
22
|
+
class TabfileParser
|
23
|
+
def initialize(file, options = {})
|
24
|
+
@file = file
|
25
|
+
@skip_lines = options[:skip_lines] || 0
|
26
|
+
@columns = options[:columns]
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_eprime
|
30
|
+
lines = @file.readlines
|
31
|
+
@skip_lines.times do
|
32
|
+
lines.shift
|
33
|
+
end
|
34
|
+
|
35
|
+
file_columns = lines.shift.split("\t").map {|elt| elt.strip }
|
36
|
+
expected_size = file_columns.size
|
37
|
+
columns = file_columns
|
38
|
+
data = Eprime::Data.new(columns)
|
39
|
+
current_line = @skip_lines+1
|
40
|
+
lines.each do |line|
|
41
|
+
current_line += 1
|
42
|
+
row = data.add_row
|
43
|
+
col_data = line.split("\t").map {|e| e.strip }
|
44
|
+
if col_data.size != expected_size
|
45
|
+
raise DamagedFileError.new("In #{@file.path}, line #{current_line} should have #{expected_size} columns but had #{col_data.size}.")
|
46
|
+
end
|
47
|
+
col_data.each_index do |i|
|
48
|
+
row[i] = col_data[i]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
return data
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Part of the Optimus package for managing E-Prime data
|
2
|
+
#
|
3
|
+
# Copyright (C) 2008 Board of Regents of the University of Wisconsin System
|
4
|
+
#
|
5
|
+
# Written by Nathan Vack <njvack@wisc.edu>, at the Waisman Laborotory for Brain
|
6
|
+
# Imaging and Behavior, University of Wisconsin - Madison
|
7
|
+
|
8
|
+
# Just use the standard Ruby CSV processing library; it'll make our lives
|
9
|
+
# way easier (by handling in-band tabs, etc)
|
10
|
+
require 'csv'
|
11
|
+
|
12
|
+
module Eprime
|
13
|
+
|
14
|
+
# Writes an Eprime::Data object as a tab-delmited file -- hopefully exactly
|
15
|
+
# like E-DataAid.
|
16
|
+
class TabfileWriter
|
17
|
+
|
18
|
+
# Create a writer, but don't actually write the output.
|
19
|
+
# Valid things in the options hash:
|
20
|
+
# :write_top_line => true, if you want to include the filename
|
21
|
+
# (if it's a file output stream) as the first line output
|
22
|
+
def initialize(eprime_data, outstream, options = {})
|
23
|
+
@eprime = eprime_data
|
24
|
+
@outstream = outstream
|
25
|
+
@write_top_line = options[:write_top_line]
|
26
|
+
@columns = options[:columns] || @eprime.columns
|
27
|
+
end
|
28
|
+
|
29
|
+
# Write to the output stream.
|
30
|
+
def write
|
31
|
+
CSV::Writer.generate(@outstream, "\t") do |tsv|
|
32
|
+
if @write_top_line
|
33
|
+
name = @outstream.respond_to?(:path) ? File.expand_path(@outstream.path.to_s) : ''
|
34
|
+
tsv << [name]
|
35
|
+
end
|
36
|
+
tsv << @columns
|
37
|
+
@eprime.each do |row|
|
38
|
+
vals = @columns.map { |col_name| row[col_name] }
|
39
|
+
tsv << vals
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# Part of the Optimus package for managing E-Prime data
|
2
|
+
#
|
3
|
+
# Copyright (C) 2008 Board of Regents of the University of Wisconsin System
|
4
|
+
#
|
5
|
+
# Written by Nathan Vack <njvack@wisc.edu>, at the Waisman Laborotory for Brain
|
6
|
+
# Imaging and Behavior, University of Wisconsin - Madison
|
7
|
+
|
8
|
+
# This class is a bit ugly around the edges -- I'm not quite sure how to
|
9
|
+
# architect it, yet.
|
10
|
+
|
11
|
+
require 'eprime'
|
12
|
+
require 'column_calculator'
|
13
|
+
require 'row_filter'
|
14
|
+
|
15
|
+
module Eprime
|
16
|
+
class StimtimesWriter
|
17
|
+
@@computed_columns = []
|
18
|
+
@@counter_columns = []
|
19
|
+
@@copydown_columns = []
|
20
|
+
@@runs = 0
|
21
|
+
@@run_column = ''
|
22
|
+
@@output_files = []
|
23
|
+
|
24
|
+
def initialize(argv)
|
25
|
+
# Look through our necessary class variables and do some odd stuff
|
26
|
+
edata = Eprime::Data.new
|
27
|
+
argv.each do |filename|
|
28
|
+
File.open(filename, 'r') do |f|
|
29
|
+
reader = Eprime::Reader.new(f)
|
30
|
+
edata.merge!(reader.eprime_data)
|
31
|
+
end
|
32
|
+
|
33
|
+
@calc = Eprime::ColumnCalculator.new
|
34
|
+
@calc.data = edata
|
35
|
+
@@computed_columns.each do |coldata|
|
36
|
+
@calc.computed_column *coldata
|
37
|
+
end
|
38
|
+
|
39
|
+
@@counter_columns.each do |coldata|
|
40
|
+
@calc.counter_column *coldata
|
41
|
+
end
|
42
|
+
|
43
|
+
@@copydown_columns.each do |coldata|
|
44
|
+
@calc.copydown_column *coldata
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
@@output_files.each do |output|
|
49
|
+
filename, filter, output_column = output
|
50
|
+
self.output_file(filename, filter, output_column)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def output_file(filename, filter, output_column)
|
56
|
+
File.open(filename, 'w') do |file|
|
57
|
+
filtered = Eprime::RowFilter.new(@calc, filter)
|
58
|
+
|
59
|
+
1.upto(@@runs) do |run|
|
60
|
+
run_rows = filtered.find_all {|row| row[@@run_column].to_s == run.to_s}.to_a
|
61
|
+
vals = run_rows.map { |r| r[output_column] }
|
62
|
+
if vals.size == 0
|
63
|
+
file.puts "**"
|
64
|
+
else
|
65
|
+
file.puts((vals << "*").join(' '))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class << self
|
72
|
+
def computed_column(*args)
|
73
|
+
@@computed_columns << args
|
74
|
+
end
|
75
|
+
|
76
|
+
def counter_column(*args)
|
77
|
+
@@counter_columns << args
|
78
|
+
end
|
79
|
+
|
80
|
+
def copydown_column(*args)
|
81
|
+
@@copydown_columns << args
|
82
|
+
end
|
83
|
+
|
84
|
+
def runs(runs)
|
85
|
+
@@runs = runs
|
86
|
+
end
|
87
|
+
|
88
|
+
def run_column(col_name)
|
89
|
+
@@run_column = col_name
|
90
|
+
end
|
91
|
+
|
92
|
+
def output_file(*args)
|
93
|
+
@@output_files << args
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Part of the Optimus package for managing E-Prime data
|
2
|
+
#
|
3
|
+
# Copyright (C) 2008 Board of Regents of the University of Wisconsin System
|
4
|
+
#
|
5
|
+
# Written by Nathan Vack <njvack@wisc.edu>, at the Waisman Laborotory for Brain
|
6
|
+
# Imaging and Behavior, University of Wisconsin - Madison
|
7
|
+
|
8
|
+
require File.join(File.dirname(__FILE__),'spec_helper')
|
9
|
+
require File.join(File.dirname(__FILE__), '../lib/eprime')
|
10
|
+
require 'calculator'
|
11
|
+
|
12
|
+
include EprimeTestHelper
|
13
|
+
|
14
|
+
describe Eprime::Calculator do
|
15
|
+
before :all do
|
16
|
+
@calc = Eprime::Calculator.new
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should compute constants" do
|
20
|
+
@calc.compute(es(:const)).should == ev(:const)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should add" do
|
24
|
+
@calc.compute(es(:add)).should == ev(:add)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should multiply" do
|
28
|
+
@calc.compute(es(:mul)).should == ev(:mul)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should handle negation" do
|
32
|
+
@calc.compute(es(:add_neg)).should == ev(:add_neg)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should handle grouping" do
|
36
|
+
@calc.compute(es(:add_mul_group)).should == ev(:add_mul_group)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should handle fdiv" do
|
40
|
+
@calc.compute(es(:fdiv)).should == ev(:fdiv)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should handle fmul" do
|
44
|
+
@calc.compute(es(:fmul)).should == ev(:fmul)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should handle mod" do
|
48
|
+
@calc.compute(es(:mod)).should == ev(:mod)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should fail with infix garbage" do
|
52
|
+
lambda {
|
53
|
+
@calc.compute("1 broken 2")
|
54
|
+
}.should raise_error(RParsec::ParserException)
|
55
|
+
end
|
56
|
+
end
|