hqmf-parser 1.0.0

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 (49) hide show
  1. data/Gemfile +23 -0
  2. data/README.md +903 -0
  3. data/Rakefile +19 -0
  4. data/VERSION +1 -0
  5. data/lib/hqmf-generator/hqmf-generator.rb +308 -0
  6. data/lib/hqmf-model/attribute.rb +35 -0
  7. data/lib/hqmf-model/data_criteria.rb +322 -0
  8. data/lib/hqmf-model/document.rb +172 -0
  9. data/lib/hqmf-model/population_criteria.rb +90 -0
  10. data/lib/hqmf-model/precondition.rb +85 -0
  11. data/lib/hqmf-model/types.rb +318 -0
  12. data/lib/hqmf-model/utilities.rb +52 -0
  13. data/lib/hqmf-parser.rb +54 -0
  14. data/lib/hqmf-parser/1.0/attribute.rb +68 -0
  15. data/lib/hqmf-parser/1.0/comparison.rb +34 -0
  16. data/lib/hqmf-parser/1.0/data_criteria.rb +105 -0
  17. data/lib/hqmf-parser/1.0/document.rb +209 -0
  18. data/lib/hqmf-parser/1.0/expression.rb +52 -0
  19. data/lib/hqmf-parser/1.0/population_criteria.rb +79 -0
  20. data/lib/hqmf-parser/1.0/precondition.rb +89 -0
  21. data/lib/hqmf-parser/1.0/range.rb +65 -0
  22. data/lib/hqmf-parser/1.0/restriction.rb +157 -0
  23. data/lib/hqmf-parser/1.0/utilities.rb +41 -0
  24. data/lib/hqmf-parser/2.0/data_criteria.rb +319 -0
  25. data/lib/hqmf-parser/2.0/document.rb +165 -0
  26. data/lib/hqmf-parser/2.0/population_criteria.rb +53 -0
  27. data/lib/hqmf-parser/2.0/precondition.rb +44 -0
  28. data/lib/hqmf-parser/2.0/types.rb +223 -0
  29. data/lib/hqmf-parser/2.0/utilities.rb +30 -0
  30. data/lib/hqmf-parser/converter/pass1/data_criteria_converter.rb +254 -0
  31. data/lib/hqmf-parser/converter/pass1/document_converter.rb +183 -0
  32. data/lib/hqmf-parser/converter/pass1/population_criteria_converter.rb +135 -0
  33. data/lib/hqmf-parser/converter/pass1/precondition_converter.rb +164 -0
  34. data/lib/hqmf-parser/converter/pass1/precondition_extractor.rb +159 -0
  35. data/lib/hqmf-parser/converter/pass1/simple_data_criteria.rb +35 -0
  36. data/lib/hqmf-parser/converter/pass1/simple_operator.rb +89 -0
  37. data/lib/hqmf-parser/converter/pass1/simple_population_criteria.rb +10 -0
  38. data/lib/hqmf-parser/converter/pass1/simple_precondition.rb +63 -0
  39. data/lib/hqmf-parser/converter/pass1/simple_restriction.rb +64 -0
  40. data/lib/hqmf-parser/converter/pass2/comparison_converter.rb +91 -0
  41. data/lib/hqmf-parser/converter/pass2/operator_converter.rb +169 -0
  42. data/lib/hqmf-parser/converter/pass3/specific_occurrence_converter.rb +86 -0
  43. data/lib/hqmf-parser/converter/pass3/specific_occurrence_converter_bak.rb +70 -0
  44. data/lib/hqmf-parser/parser.rb +22 -0
  45. data/lib/hqmf-parser/value_sets/value_set_parser.rb +206 -0
  46. data/lib/tasks/coverme.rake +8 -0
  47. data/lib/tasks/hqmf.rake +141 -0
  48. data/lib/tasks/value_sets.rake +23 -0
  49. metadata +159 -0
@@ -0,0 +1,169 @@
1
+ module HQMF
2
+ # Class for converting an HQMF 1.0 representation to an HQMF 2.0 representation
3
+ class OperatorConverter
4
+
5
+ def self.apply_temporal(data_criteria, precondition, restriction, data_criteria_converter)
6
+ data_criteria.temporal_references ||= []
7
+ value = restriction.operator.value
8
+ type = restriction.operator.type
9
+ temporal_reference = nil
10
+ if (restriction.single_target?)
11
+ # multiple targets appears to be the result of restrictions with restrictions
12
+ target = restriction.target
13
+ if (restriction.multi_target?)
14
+ found = false
15
+ # restrictions with restrictions can have a target that is modified by the child restrcitons
16
+ restriction.preconditions.each do |precondition|
17
+ if precondition.reference.id.start_with? target
18
+ found = true
19
+ target = precondition.reference.id
20
+ end
21
+ end
22
+ unless found
23
+ puts "\tmultiple targets... need to check this" if restriction.multi_target?
24
+ end
25
+ end
26
+ temporal_reference = HQMF::TemporalReference.new(type, HQMF::Reference.new(target),value)
27
+ data_criteria_converter.validate_not_deleted(target)
28
+ elsif (restriction.multi_target?)
29
+
30
+ children_criteria = HQMF::DataCriteriaConverter.extract_data_criteria(restriction.preconditions, data_criteria_converter)
31
+
32
+ if (children_criteria.length == 1)
33
+ target = children_criteria[0].id
34
+ temporal_reference = HQMF::TemporalReference.new(type, HQMF::Reference.new(target),value)
35
+ data_criteria_converter.validate_not_deleted(target)
36
+ else
37
+ parent_id = "GROUP"
38
+ if restriction.generated_data_criteria.nil?
39
+ # we pass in restriction.preconditions here rather than children_criteria because we need to be able to create grouping data criteria for and and or preconditions in a tree
40
+ group_criteria = data_criteria_converter.create_group_data_criteria(restriction.preconditions, "#{type}_CHILDREN", value, parent_id, @@ids.next, "grouping", "temporal")
41
+ # save the generated grouping criteria so that we can reference it from other locations
42
+ restriction.generated_data_criteria = group_criteria
43
+ else
44
+ # we have already processed this restriction and have a grouping criteria for it. Take the one we have previously generated
45
+ group_criteria = restriction.generated_data_criteria
46
+ end
47
+ temporal_reference = HQMF::TemporalReference.new(type, HQMF::Reference.new(group_criteria.id), value)
48
+ end
49
+ else
50
+ raise "no target for temporal restriction"
51
+ end
52
+ restriction.converted=true
53
+
54
+ # add temporal reference to data criteria
55
+ data_criteria.temporal_references << temporal_reference unless data_criteria.has_temporal(temporal_reference)
56
+ end
57
+
58
+
59
+ def self.apply_summary(data_criteria, precondition, restriction, data_criteria_converter)
60
+ value = restriction.operator.value
61
+ type = restriction.operator.type
62
+ subset_operator = HQMF::SubsetOperator.new(type, value)
63
+
64
+ if (restriction.multi_target?)
65
+ children_criteria = HQMF::DataCriteriaConverter.extract_data_criteria(restriction.preconditions, data_criteria_converter)
66
+
67
+ data_criteria = nil
68
+ if (children_criteria.length == 1)
69
+ data_criteria = children_criteria[0]
70
+ # this block used to set the order of operations of values after subset operators.
71
+ # if data_criteria and !data_criteria.value.nil?
72
+ # subset_operator.value ||= data_criteria.value
73
+ # data_criteria.value = nil
74
+ # end
75
+ data_criteria.subset_operators ||= []
76
+ # add subset operator to data criteria
77
+ data_criteria.subset_operators << subset_operator unless data_criteria.has_subset(subset_operator)
78
+ else
79
+ parent_id = "GROUP"
80
+
81
+ # this block of code used to be used to pull the value off of a data criteria and shove it onto a subset operator to evaluate MIN: lab result(result > 10) as MIN > 10: lab result
82
+ # the decision was made by NQF to not support this order of operations based on previous guidance to the measure developers
83
+ #
84
+ # unless subset_operator.value
85
+ # # scalar comparisons are used for MIN>90 etc. The value is on a REFR restriction. We need to store it on the data criteria since the value is processed before the operator is created.
86
+ # scalar_comparison = nil
87
+ #
88
+ # # check to see if we have different values referenced accross the children
89
+ # # this happens on things like most recent blood pressure reading
90
+ # multiple_differing_values = false
91
+ # children_criteria.each do |criteria|
92
+ # if scalar_comparison.nil?
93
+ # scalar_comparison = criteria.value
94
+ # else
95
+ # if scalar_comparison != criteria.value
96
+ # multiple_differing_values = true
97
+ # end
98
+ # end
99
+ # end
100
+ #
101
+ # # if we have multiple differing values, we want to keep the preconditions from the restriction, and attach them to the precondition
102
+ # # we can then apply the subset operator to all of the children individually
103
+ # if (multiple_differing_values)
104
+ # precondition.preconditions.delete(restriction)
105
+ # precondition.preconditions.concat(restriction.preconditions)
106
+ # children_criteria.each do |criteria|
107
+ # subset_operator = HQMF::SubsetOperator.new(type, value)
108
+ # subset_operator.value = criteria.value
109
+ # criteria.value = nil
110
+ # criteria.subset_operators ||= []
111
+ # criteria.subset_operators << subset_operator unless criteria.has_subset(subset_operator)
112
+ # end
113
+ # restriction.converted=true
114
+ # # we want to return since we have applied the subset to the children. We no longer want to create a grouping data critiera
115
+ # return;
116
+ # end
117
+ #
118
+ # # all the children have the same value, apply the value to the subset operator
119
+ # children_criteria.each do |criteria|
120
+ # if scalar_comparison.nil?
121
+ # scalar_comparison = criteria.value
122
+ # else
123
+ # raise "multiple different scalar comparisons for a grouping data criteria" if scalar_comparison != criteria.value
124
+ # end
125
+ # criteria.value = nil
126
+ # end
127
+ # subset_operator.value ||= scalar_comparison
128
+ # end
129
+
130
+ if restriction.generated_data_criteria.nil?
131
+ # we pass in restriction.preconditions here rather than children_criteria because we need to be able to create grouping data criteria for and and or preconditions in a tree
132
+ data_criteria = data_criteria_converter.create_group_data_criteria(restriction.preconditions, type, value, parent_id, @@ids.next, "grouping", "summary")
133
+ # save the generated grouping criteria so that we can reference it from other locations
134
+ restriction.generated_data_criteria = data_criteria
135
+ else
136
+ # we have already processed this restriction and have a grouping criteria for it. Take the one we have previously generated
137
+ data_criteria = restriction.generated_data_criteria
138
+ end
139
+
140
+ data_criteria.subset_operators ||= []
141
+ # add subset operator to data criteria
142
+ data_criteria.subset_operators << subset_operator unless data_criteria.has_subset(subset_operator)
143
+ end
144
+ precondition.reference = HQMF::Reference.new(data_criteria.id)
145
+ elsif (restriction.single_target?)
146
+ subset_operator = HQMF::SubsetOperator.new(type, value)
147
+ data_criteria.subset_operators ||= []
148
+ # add subset operator to data criteria
149
+ data_criteria.subset_operators << subset_operator unless data_criteria.has_subset(subset_operator)
150
+ end
151
+
152
+ restriction.converted=true
153
+ end
154
+
155
+ # Simple class to issue monotonically increasing integer identifiers
156
+ class Counter
157
+ def initialize
158
+ @count = 0
159
+ end
160
+
161
+ def next
162
+ @count+=1
163
+ end
164
+ end
165
+ @@ids = Counter.new
166
+
167
+
168
+ end
169
+ end
@@ -0,0 +1,86 @@
1
+ module HQMF
2
+ # Class for cleaning up references to specific occurrences in the tree
3
+ class SpecificOccurrenceConverter
4
+
5
+ def initialize(data_criteria_converter)
6
+ @data_criteria_converter = data_criteria_converter
7
+ end
8
+
9
+ def convert_specific_occurrences(population_criteria)
10
+ population_criteria.each do |population|
11
+ walk_up_tree(population.preconditions)
12
+ end
13
+ end
14
+
15
+ def walk_up_tree(preconditions)
16
+ preconditions.each do |precondition|
17
+ if (has_child_comparison(precondition))
18
+ walk_up_tree(precondition.preconditions)
19
+ end
20
+ unless (precondition.comparison?)
21
+ occurrence_map = get_grouped_occurrences(precondition.preconditions)
22
+ occurrence_map.each do |key, value|
23
+
24
+ ##
25
+ ##### figure out negations
26
+ ##
27
+
28
+ if (precondition.conjunction_code == 'atLeastOneTrue')
29
+
30
+ group = @data_criteria_converter.build_group_data_criteria(value, "#{key}_union", precondition.id, HQMF::DataCriteria::UNION)
31
+ binding.pry
32
+ elsif (precondition.conjunction_code == 'allTrue')
33
+ binding.pry
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def get_grouped_occurrences(preconditions)
42
+
43
+ result = {}
44
+ preconditions.each do |precondition|
45
+ if (precondition.preconditions and !precondition.preconditions.empty?)
46
+ get_grouped_occurrences(precondition.preconditions).each do |key, value|
47
+ if (result[key])
48
+ result[key].concat(value)
49
+ else
50
+ result[key] = value
51
+ end
52
+ end
53
+ else
54
+ data_criteria = @data_criteria_converter.v2_data_criteria_by_id[precondition.reference.id]
55
+ if is_specific_occurrence(data_criteria)
56
+ binding.pry
57
+ result[data_criteria.source_data_criteria] ||= []
58
+ result[data_criteria.source_data_criteria] << data_criteria
59
+ preconditions.delete(precondition)
60
+ binding.pry
61
+ end
62
+ end
63
+ end
64
+ result
65
+
66
+ end
67
+
68
+ def is_specific_occurrence(data_criteria)
69
+ !data_criteria.specific_occurrence.nil?
70
+ end
71
+
72
+ def has_child_comparison(node)
73
+ value = false
74
+ node.preconditions.each do |precondition|
75
+ if (precondition.comparison?)
76
+ value ||= true
77
+ elsif precondition.has_preconditions?
78
+ value ||= has_child_comparison(precondition)
79
+ end
80
+ end if node.preconditions
81
+ value
82
+ end
83
+
84
+
85
+ end
86
+ end
@@ -0,0 +1,70 @@
1
+ module HQMF
2
+ # Class for cleaning up references to specific occurrences in the tree
3
+ class SpecificOccurrenceConverter
4
+
5
+ def initialize(data_criteria_converter)
6
+ @data_criteria_converter = data_criteria_converter
7
+ end
8
+
9
+ def convert_specific_occurrences(population_criteria)
10
+ create_precondition_map(population_criteria)
11
+
12
+ @specific_occurrence_key_mapper = {}
13
+
14
+ specific_occurrences = {}
15
+ @data_criteria_converter.v2_data_criteria.each do |data_criteria|
16
+ if (is_specific_occurrence(data_criteria))
17
+ if (data_criteria.respond_to? :precondition_id and @precondition_map[data_criteria.precondition_id])
18
+ specific_occurrences[data_criteria.source_data_criteria] ||= []
19
+ specific_occurrences[data_criteria.source_data_criteria] << data_criteria
20
+ elsif (data_criteria.children_criteria or data_criteria.subset_operators or data_criteria.temporal_references)
21
+ binding.pry
22
+ end
23
+ end
24
+ end
25
+ specific_occurrences.each do |key, occurrences|
26
+
27
+ occurrences.sort! do |left, right|
28
+ @precondition_map[right.precondition_id].depth <=> @precondition_map[left.precondition_id].depth
29
+ end
30
+
31
+ occurrences.each_cons(2) do |pair|
32
+ parent = get_common_parent(@precondition_map[pair[0].precondition_id], @precondition_map[pair[1].precondition_id])
33
+ binding.pry
34
+ end
35
+
36
+ end
37
+ end
38
+
39
+ def get_common_parent(left, right)
40
+ return left if left == right
41
+ get_common_parent(@precondition_parent_map[left.id], @precondition_parent_map[right.id])
42
+ end
43
+
44
+ def create_precondition_map(population_criteria)
45
+ @precondition_map = {}
46
+ @precondition_parent_map = {}
47
+ depth = 0
48
+ population_criteria.each do |population|
49
+ recurse_precondition_map(population, depth)
50
+ end
51
+ end
52
+
53
+ def recurse_precondition_map(node, depth)
54
+ node.preconditions.each do |precondition|
55
+ precondition.depth = depth
56
+ @precondition_map[precondition.id] = precondition
57
+ @precondition_parent_map[precondition.id] = node
58
+ if precondition.has_preconditions?
59
+ depth += 1
60
+ recurse_precondition_map(precondition, depth)
61
+ end
62
+ end if node.preconditions
63
+ end
64
+
65
+ def is_specific_occurrence(data_criteria)
66
+ !data_criteria.specific_occurrence.nil?
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,22 @@
1
+ module HQMF
2
+ class Parser
3
+
4
+ HQMF_VERSION_1 = "1.0"
5
+ HQMF_VERSION_2 = "2.0"
6
+
7
+ def self.parse(hqmf_contents, version, codes = nil)
8
+
9
+
10
+ case version
11
+ when HQMF_VERSION_1
12
+ puts("\tCodes not passed in, cannot backfill properties like gender") unless codes
13
+ HQMF::DocumentConverter.convert(HQMF1::Document.new(hqmf_contents).to_json, codes)
14
+ when HQMF_VERSION_2
15
+ HQMF2::Document.new(hqmf_contents).to_model
16
+ else
17
+ raise "Unsupported HQMF version specified: #{version}"
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,206 @@
1
+ require 'zip/zipfilesystem'
2
+ require 'spreadsheet'
3
+ require 'google_spreadsheet'
4
+ require 'roo'
5
+ require 'iconv'
6
+
7
+ module HQMF
8
+ module ValueSet
9
+ class Parser
10
+
11
+ GROUP_CODE_SET = "GROUPING"
12
+
13
+ ORGANIZATION_TITLE = "Value Set Developer"
14
+ OID_TITLE = "Value Set OID"
15
+ CONCEPT_TITLE = "Value Set Name"
16
+ CATEGORY_TITLE = "QDM Category"
17
+ CODE_SET_TITLE ="Code System"
18
+ VERSION_TITLE = "Code System Version"
19
+ CODE_TITLE = "Code"
20
+ DESCRIPTION_TITLE = "Descriptor"
21
+
22
+ CODE_SYSTEM_NORMALIZER = {
23
+ 'ICD-9'=>'ICD-9-CM',
24
+ 'ICD-10'=>'ICD-10-CM',
25
+ 'HL7 (2.16.840.1.113883.5.1)'=>'HL7'
26
+ }
27
+ IGNORED_CODE_SYSTEM_NAMES = ['Grouping', 'GROUPING' ,'HL7', "Administrative Sex"]
28
+
29
+ def initialize()
30
+ end
31
+
32
+ # import an excel matrix array into mongo
33
+ def parse(file, options={})
34
+ sheet_array = file_to_array(file, options)
35
+ by_oid_ungrouped = cells_to_hashs_by_oid(sheet_array)
36
+ collapse_groups(by_oid_ungrouped)
37
+ end
38
+
39
+ def collapse_groups(by_oid_ungrouped)
40
+
41
+ final = []
42
+
43
+ # select the grouped code sets and fill in the children... also remove the children that are a
44
+ # member of a group. We remove the children so that we can create parent groups for the orphans
45
+ (by_oid_ungrouped.select {|key,value| value["code_set"].upcase == GROUP_CODE_SET}).each do |key, value|
46
+ # remove the group so that it is not in the orphan list
47
+ by_oid_ungrouped.delete(value["oid"])
48
+ codes = []
49
+ value["codes"].each do |child_oid|
50
+ # codes << by_oid_ungrouped.delete(child_oid)
51
+ # do not delete the children of a group. These may be referenced by other groups or directly by the measure
52
+ code = by_oid_ungrouped[child_oid]
53
+ puts "\tcode could not be found: #{child_oid}" unless code
54
+ codes << code if code
55
+ # for hierarchies we need to probably have codes be a hash that we select from if we don't find the
56
+ # element in by_oid_ungrouped we may need to look for it in final
57
+ end
58
+ value["code_sets"] = codes
59
+ value.delete("codes")
60
+ value.delete("code_set")
61
+ final << value
62
+ end
63
+
64
+ # fill out the orphans
65
+ by_oid_ungrouped.each do |key, orphan|
66
+ final << adopt_orphan(orphan)
67
+ end
68
+
69
+ deleted = []
70
+ final.delete_if {|x| to_delete = x['code_sets'].nil? || x['code_sets'].empty?; deleted << x if to_delete; to_delete }
71
+ deleted.each do |value|
72
+ puts "\tDeleted value set with no code sets: #{value['oid']}"
73
+ end
74
+ final
75
+
76
+ end
77
+
78
+ def adopt_orphan(orphan)
79
+ parent = orphan.dup
80
+ parent["code_sets"] = [orphan]
81
+ parent.delete("codes")
82
+ parent.delete("code_set")
83
+ parent
84
+ end
85
+
86
+ # take an excel matrix array and turn it into an array of db models
87
+ def cells_to_hashs_by_oid(array)
88
+ a = Array.new(array) # new variable for reentrant
89
+ headers = a.shift.map {|i| i.to_s } # because of this shift
90
+ string_data = a.map {|row| row.map {|cell| cell.to_s } }
91
+ array_of_hashes = string_data.map {|row| Hash[*headers.zip(row).flatten] }
92
+
93
+ by_oid = {}
94
+ array_of_hashes.each do |row|
95
+ entry = convert_row(row)
96
+
97
+ existing = by_oid[entry["oid"]]
98
+ if (existing)
99
+ existing["codes"].concat(entry["codes"])
100
+ else
101
+ by_oid[entry["oid"]] = entry
102
+ end
103
+ end
104
+
105
+ by_oid
106
+ end
107
+
108
+ def self.get_format(file_path)
109
+ if file_path =~ /xls$/
110
+ :xls
111
+ elsif file_path =~ /xlsx$/
112
+ :xlsx
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def convert_row(row)
119
+ # Value Set Developer
120
+ # Value Set OID
121
+ # Value Set Name
122
+ # QDM Category
123
+ # Code System
124
+ # Code System Version
125
+ # Code
126
+ # Descriptor
127
+ value = {
128
+ "key" => normalize_names(row[CATEGORY_TITLE],row[CONCEPT_TITLE]),
129
+ "organization" => row[ORGANIZATION_TITLE],
130
+ "oid" => row[OID_TITLE].strip.gsub(/[^0-9\.]/i, ''),
131
+ "concept" => normalize_names(row[CONCEPT_TITLE]),
132
+ "category" => normalize_names(row[CATEGORY_TITLE]),
133
+ "code_set" => normalize_code_system(row[CODE_SET_TITLE]),
134
+ "version" => row[VERSION_TITLE],
135
+ "codes" => extract_code(row[CODE_TITLE], row[CODE_SET_TITLE]),
136
+ "description" => row[DESCRIPTION_TITLE]
137
+ }
138
+ value['codes'].map! {|code| code.strip.gsub(/[^0-9\.]/i, '')} if (value['code_set'].upcase == GROUP_CODE_SET)
139
+ value
140
+ end
141
+
142
+ # Break all the supplied strings into separate words and return the resulting list as a
143
+ # new string with each word separated with '_'
144
+ def normalize_names(*components)
145
+ name = []
146
+ components.each do |component|
147
+ name.concat component.gsub(/\W/,' ').split.collect { |word| word.strip.downcase }
148
+ end
149
+ name.join '_'
150
+ end
151
+
152
+ def normalize_code_system(code_system_name)
153
+ code_system_name = CODE_SYSTEM_NORMALIZER[code_system_name] if CODE_SYSTEM_NORMALIZER[code_system_name]
154
+ return code_system_name if IGNORED_CODE_SYSTEM_NAMES.include? code_system_name
155
+ oid = HealthDataStandards::Util::CodeSystemHelper.oid_for_code_system(code_system_name)
156
+ puts "\tbad code system name: #{code_system_name}" unless oid
157
+ code_system_name
158
+ end
159
+
160
+ def extract_code(code, set)
161
+
162
+ code.strip!
163
+ if set=='CPT' && code.include?('-')
164
+ eval(code.strip.gsub('-','..')).to_a.collect { |i| i.to_s }
165
+ else
166
+ [code]
167
+ end
168
+
169
+ end
170
+
171
+ def file_to_array(file_path, options)
172
+ defaults = {
173
+ :columns => 2, # range of import
174
+ :sheet => 1 # only one sheet at a time can be worked on
175
+ }
176
+ options = defaults.merge(options)
177
+
178
+ book = book_by_format(file_path, options[:format])
179
+ book.default_sheet=book.sheets[options[:sheet]]
180
+
181
+ # catch double byte encoding problems in spreadsheet files
182
+ # Encoding::InvalidByteSequenceError: "\x9E\xDE" on UTF-16LE
183
+ begin
184
+ book.to_matrix.to_a
185
+ rescue Encoding::InvalidByteSequenceError => e
186
+ raise "Spreadsheet encoding problem: #{e}"
187
+ end
188
+ end
189
+
190
+ def book_by_format(file_path, format)
191
+ format = HQMF::ValueSet::Parser.get_format(file_path) unless format
192
+
193
+ if format == :xls
194
+ book = Excel.new(file_path, nil, :ignore)
195
+ elsif format == :xlsx
196
+ book = Excelx.new(file_path, nil, :ignore)
197
+ else
198
+ raise "File does not end in .xls or .xlsx"
199
+ end
200
+ book
201
+ end
202
+
203
+
204
+ end
205
+ end
206
+ end