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.
Files changed (45) hide show
  1. data/Rakefile +9 -0
  2. data/bin/eprime2tabfile +165 -0
  3. data/bin/stim.times +5 -0
  4. data/bin/stim1.times +5 -0
  5. data/bin/stim1_b.times +5 -0
  6. data/bin/stim1_c.times +5 -0
  7. data/bin/stim1_d.times +5 -0
  8. data/bin/test_data.txt +278 -0
  9. data/bin/test_data2.txt +277 -0
  10. data/bin/test_eprime_stimfile.rb +20 -0
  11. data/lib/calculator.rb +49 -0
  12. data/lib/column_calculator.rb +308 -0
  13. data/lib/eprime.rb +23 -0
  14. data/lib/eprime_data.rb +154 -0
  15. data/lib/eprime_reader.rb +105 -0
  16. data/lib/eprimetab_parser.rb +21 -0
  17. data/lib/excel_parser.rb +21 -0
  18. data/lib/log_file_parser.rb +208 -0
  19. data/lib/row_filter.rb +40 -0
  20. data/lib/tabfile_parser.rb +55 -0
  21. data/lib/tabfile_writer.rb +44 -0
  22. data/lib/writers/stimtimes_writer.rb +97 -0
  23. data/spec/calculator_spec.rb +56 -0
  24. data/spec/column_calculator_spec.rb +368 -0
  25. data/spec/eprime_data_spec.rb +202 -0
  26. data/spec/eprime_reader_spec.rb +115 -0
  27. data/spec/eprimetab_parser_spec.rb +23 -0
  28. data/spec/excel_parser_spec.rb +26 -0
  29. data/spec/log_file_parser_spec.rb +156 -0
  30. data/spec/row_filter_spec.rb +32 -0
  31. data/spec/samples/bad_excel_tsv.txt +4 -0
  32. data/spec/samples/corrupt_log_file.txt +116 -0
  33. data/spec/samples/eprime_tsv.txt +7 -0
  34. data/spec/samples/excel_tsv.txt +5 -0
  35. data/spec/samples/optimus_log.txt +110 -0
  36. data/spec/samples/short_columns.txt +1 -0
  37. data/spec/samples/sorted_columns.txt +1 -0
  38. data/spec/samples/std_columns.txt +1 -0
  39. data/spec/samples/unknown_type.txt +2 -0
  40. data/spec/samples/unreadable_file +1 -0
  41. data/spec/spec_helper.rb +98 -0
  42. data/spec/tabfile_parser_spec.rb +62 -0
  43. data/spec/tabfile_writer_spec.rb +91 -0
  44. data/spec/writers/stimtimes_writer_spec.rb +16 -0
  45. 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