Skip to content

Commit f2a4686

Browse files
committed
fix: validate response data inputs
1 parent 3047d22 commit f2a4686

9 files changed

Lines changed: 83 additions & 3 deletions

lib/irt_ruby.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require "irt_ruby/version"
44
require "matrix"
5+
require "irt_ruby/response_data_validator"
56
require "irt_ruby/rasch_model"
67
require "irt_ruby/two_parameter_model"
78
require "irt_ruby/three_parameter_model"

lib/irt_ruby/rasch_model.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def initialize(data,
2020
# missing_strategy: :ignore (skip), :treat_as_incorrect, :treat_as_correct
2121

2222
@data = data
23-
@data_array = data.to_a
23+
@data_array = ResponseDataValidator.validate!(data)
2424
num_rows = @data_array.size
2525
num_cols = @data_array.first.size
2626

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
module IrtRuby
4+
# Validates response data accepted by IRT model constructors.
5+
module ResponseDataValidator
6+
VALID_RESPONSES = [0, 1, nil].freeze
7+
8+
module_function
9+
10+
def validate!(data)
11+
raise ArgumentError, "response data must be a Matrix or array of arrays" unless data.respond_to?(:to_a)
12+
13+
data_array = data.to_a
14+
15+
raise ArgumentError, "response data must have at least one row" unless data_array.is_a?(Array) && data_array.any?
16+
17+
validate_rows!(data_array)
18+
validate_values!(data_array)
19+
20+
data_array
21+
end
22+
23+
def validate_rows!(data_array)
24+
first_row = data_array.first
25+
26+
raise ArgumentError, "response data must be a Matrix or array of arrays" unless first_row.is_a?(Array)
27+
28+
expected_columns = first_row.size
29+
raise ArgumentError, "response data must have at least one column" if expected_columns.zero?
30+
31+
data_array.each_with_index do |row, index|
32+
raise ArgumentError, "response data row #{index} must be an Array" unless row.is_a?(Array)
33+
34+
next if row.size == expected_columns
35+
36+
raise ArgumentError, "response data must be rectangular; row #{index} has #{row.size} columns, expected #{expected_columns}"
37+
end
38+
end
39+
40+
def validate_values!(data_array)
41+
data_array.each_with_index do |row, row_index|
42+
row.each_with_index do |value, column_index|
43+
next if VALID_RESPONSES.include?(value)
44+
45+
raise ArgumentError,
46+
"response data contains invalid value #{value.inspect} at row #{row_index}, column #{column_index}; allowed values are 0, 1, and nil"
47+
end
48+
end
49+
end
50+
end
51+
end

lib/irt_ruby/three_parameter_model.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def initialize(data,
1919
decay_factor: 0.5,
2020
missing_strategy: :ignore)
2121
@data = data
22-
@data_array = data.to_a
22+
@data_array = ResponseDataValidator.validate!(data)
2323
num_rows = @data_array.size
2424
num_cols = @data_array.first.size
2525

lib/irt_ruby/two_parameter_model.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def initialize(data, max_iter: 1000, tolerance: 1e-6, param_tolerance: 1e-6,
1515
learning_rate: 0.01, decay_factor: 0.5,
1616
missing_strategy: :ignore)
1717
@data = data
18-
@data_array = data.to_a
18+
@data_array = ResponseDataValidator.validate!(data)
1919
num_rows = @data_array.size
2020
num_cols = @data_array.first.size
2121

spec/irt_ruby/rasch_model_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
require "spec_helper"
44

55
RSpec.describe IrtRuby::RaschModel do
6+
it_behaves_like "response data validation"
7+
68
let(:data_array) do
79
[
810
[1, 1, 0],

spec/irt_ruby/three_parameter_model_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
require "spec_helper"
44

55
RSpec.describe IrtRuby::ThreeParameterModel do
6+
it_behaves_like "response data validation"
7+
68
let(:data_array) do
79
[
810
[1, 1, 0],

spec/irt_ruby/two_parameter_model_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
require "spec_helper"
44

55
RSpec.describe IrtRuby::TwoParameterModel do
6+
it_behaves_like "response data validation"
7+
68
let(:data_array) do
79
[
810
[1, 1, 0],

spec/spec_helper.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@
22

33
require "irt_ruby"
44

5+
RSpec.shared_examples "response data validation" do
6+
it "rejects empty data" do
7+
expect { described_class.new([]) }.to raise_error(ArgumentError, /at least one row/)
8+
end
9+
10+
it "rejects empty rows" do
11+
expect { described_class.new([[]]) }.to raise_error(ArgumentError, /at least one column/)
12+
end
13+
14+
it "rejects ragged rows" do
15+
expect { described_class.new([[1, 0], [1]]) }.to raise_error(ArgumentError, /rectangular/)
16+
end
17+
18+
it "rejects invalid response values" do
19+
expect { described_class.new([[1, 2], [0, nil]]) }.to raise_error(ArgumentError, /invalid value 2/)
20+
end
21+
22+
it "rejects non-numeric truthy, falsey, and string responses" do
23+
expect { described_class.new([[1, "1"], [false, nil]]) }.to raise_error(ArgumentError, /invalid value/)
24+
end
25+
end
26+
527
RSpec.configure do |config|
628
# Enable flags like --only-failures and --next-failure
729
config.example_status_persistence_file_path = ".rspec_status"

0 commit comments

Comments
 (0)