blue_button_parser 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "shoulda", ">= 0"
10
+ gem "bundler", "~> 1.0.0"
11
+ gem "jeweler", "~> 1.6.4"
12
+ gem "rcov", ">= 0"
13
+ gem "json"
14
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,22 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ git (1.2.5)
5
+ jeweler (1.6.4)
6
+ bundler (~> 1.0)
7
+ git (>= 1.2.5)
8
+ rake
9
+ json (1.5.4)
10
+ rake (0.9.2.2)
11
+ rcov (0.9.11)
12
+ shoulda (2.11.3)
13
+
14
+ PLATFORMS
15
+ ruby
16
+
17
+ DEPENDENCIES
18
+ bundler (~> 1.0.0)
19
+ jeweler (~> 1.6.4)
20
+ json
21
+ rcov
22
+ shoulda
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 PatientsLikeMe
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,114 @@
1
+ = BlueButtonParser
2
+
3
+ BlueButtonParser parses a BlueButton free-text personal health data file and translates it into a structured hash suitable for computational purposes.
4
+
5
+ BlueButton[http://www.va.gov/bluebutton/] is the initiative from the U.S. Department of Veterans Affairs
6
+ to allow veterans to download their information from the "My HealtheVet" personal health record
7
+ into a very simple text file. Because this file was meant to be human readable, not computationally readable, the file contains almost no markup or delimiters for sections, keys, or values.
8
+
9
+ BlueButtonParser was created by reverse-engineering the one
10
+ {sample data file provided by the VA}[http://www.va.gov/BLUEBUTTON/docs/VA_My_HealtheVet_Blue_Button_Sample_Version_12_All_Data.txt]
11
+ and creating some ad-hoc rules for how to parse the document. BlueButtonParser will attempt to find all the sections in the file, all the key-value pairs within that section, and even find collections of items within a section when applicable (e.g. the array of facilities in the section "TREATMENT FACILITIES").
12
+
13
+ <b>Example free text data</b>
14
+
15
+ ----------------------------- DEMOGRAPHICS ----------------------------
16
+
17
+ Source: Self-Entered
18
+
19
+ First Name: ONE
20
+ Middle Initial: A
21
+ Last Name: MHVVETERAN
22
+ Suffix:
23
+ Alias: MHVVET
24
+ Relationship to VA: Patient, Veteran, Employee
25
+
26
+ Gender: Male Blood Type: AB+ Organ Donor: Yes
27
+
28
+ Date of Birth: 01 Mar 1948
29
+ Marital Status: Married
30
+ Current Occupation: Truck Driver
31
+
32
+ <b>Example parsed data (JSON)</b>
33
+
34
+ "DEMOGRAPHICS": {
35
+ "Source": "Self-Entered",
36
+ "First Name": "ONE",
37
+ "Middle Initial": "A",
38
+ "Last Name": "MHVVETERAN",
39
+ "Suffix": null,
40
+ "Alias": "MHVVET",
41
+ "Relationship to VA": "Patient, Veteran, Employee",
42
+ "Gender": "Male",
43
+ "Blood Type": "AB+",
44
+ "Organ Donor": "Yes",
45
+ "Date of Birth": "01 Mar 1948",
46
+ "Marital Status": "Married",
47
+ "Current Occupation": "Truck Driver"
48
+ }
49
+
50
+ == Install
51
+
52
+ sudo gem install blue_button_parser
53
+
54
+ == Usage
55
+ require 'blue_button_parser'
56
+
57
+ # parse your downloaded BlueButton data file
58
+ my_bb_file = File.read("test/data/blue_button_example_data.txt")
59
+ bbp = BlueButtonParser.new(my_bb_file)
60
+
61
+ # access the parsed data as a Hash
62
+ parsed_data_hash = bbp.data
63
+
64
+ # example: to see the list of sections parsed:
65
+ parsed_data_hash.keys
66
+ # => ["MY HEALTHEVET ACCOUNT SUMMARY", "VITALS AND READINGS", "HEALTH INSURANCE", ... ]
67
+
68
+ # example: to acccess the "MY HEALTHEVET ACCOUNT SUMMARY" section:
69
+ summary = parsed_data_hash["MY HEALTHEVET ACCOUNT SUMMARY"]
70
+ # => {"Authentication Facility Name"=>"SLC10 TEST LAB", "Authentication Date"=>"19 Aug 2010", ... }
71
+
72
+ = Caveats
73
+
74
+ BlueButtonParser was reverse engineered based on the single sample data file provided by the VA (latest version: v12, 02 Dec 2011). Because there are no rules as to how the document should be formatted,
75
+
76
+ * A later version of the BlueButton output could break the parser
77
+ * An instance of BlueButton output for a second patient might have slightly different formatting--for example, line-wrapped values for fields that were not wrapped in the current reference file, or new fields that were not applicable to the patient in the current reference file
78
+ * As new sections get added, there is no guarantee that they will conform to the formatting for other sections
79
+
80
+ To keep the BlueButtonParser up-to-date, the test data file (test/data/blue_button_example_data.txt) and expect parsed output (test/data/expected_json_output.js) should be updated every time a new version of the BlueButtonData file is released.
81
+
82
+ Note however that as far as I know, there is no formal notification that a new version of the sample data file has been released, so I guess developers will just need to be vigilant. :)
83
+
84
+ After updates, make sure the tests still work and any applicable new tests get added.
85
+
86
+ = Prior work
87
+
88
+ Somebody took a stab at this in the past:
89
+ http://rest-developer-edition.na8.force.com/BlueConverter
90
+
91
+ I believe the code for this example is found here:
92
+ https://github.com/joshbirk/BlueConverter
93
+
94
+ BlueConverter great first pass implementation, but it needs a few corrections:
95
+
96
+ * it doesn't deal with multiple key-values on single line, e.g. "Gender: Male Blood Type: AB+ Organ Donor: Yes"
97
+ * it doesn't deal with values that wrap lines
98
+ * it doesn't recognize that some of the key-values in a section should be grouped into a collection of items, e.g. in the "TREATMENT FACILITIES" section is actually comprised of a series of facilities
99
+ * it doesn't parse the tables, e.g. the "VA Treating Facility" in the "MY HEALTHEVET ACCOUNT SUMMARY"
100
+
101
+ == Contributing to BlueButtonParser
102
+
103
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
104
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
105
+ * Fork the project
106
+ * Start a feature/bugfix branch
107
+ * Commit and push until you are happy with your contribution
108
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
109
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
110
+
111
+ == Copyright
112
+
113
+ Copyright (c) 2012 PatientsLikeMe. See LICENSE.txt for further details.
114
+
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "blue_button_parser"
18
+ gem.homepage = "http://github.com/patientslikeme/blue_button_parser"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Converts a BlueButton free text data file to a structured data Hash}
21
+ gem.description = %Q{Converts a BlueButton free text data file to a structured data Hash}
22
+ gem.email = "open_source@patientslikeme.com"
23
+ gem.authors = ["PatientsLikeMe"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/test_*.rb'
39
+ test.verbose = true
40
+ test.rcov_opts << '--exclude "gems/*"'
41
+ end
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "blue_button_parser #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,64 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{blue_button_parser}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["PatientsLikeMe"]
12
+ s.date = %q{2011-12-22}
13
+ s.description = %q{Converts a BlueButton free text data file to a structured data Hash}
14
+ s.email = %q{open_source@patientslikeme.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "Gemfile",
22
+ "Gemfile.lock",
23
+ "LICENSE.txt",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "blue_button_parser.gemspec",
28
+ "lib/blue_button_parser.rb",
29
+ "test/data/blue_button_example_data.txt",
30
+ "test/data/expected_json_output.js",
31
+ "test/helper.rb",
32
+ "test/test_blue_button_parser.rb"
33
+ ]
34
+ s.homepage = %q{http://github.com/patientslikeme/blue_button_parser}
35
+ s.licenses = ["MIT"]
36
+ s.require_paths = ["lib"]
37
+ s.rubygems_version = %q{1.4.1}
38
+ s.summary = %q{Converts a BlueButton free text data file to a structured data Hash}
39
+
40
+ if s.respond_to? :specification_version then
41
+ s.specification_version = 3
42
+
43
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
44
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
45
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
46
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
47
+ s.add_development_dependency(%q<rcov>, [">= 0"])
48
+ s.add_development_dependency(%q<json>, [">= 0"])
49
+ else
50
+ s.add_dependency(%q<shoulda>, [">= 0"])
51
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
52
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
53
+ s.add_dependency(%q<rcov>, [">= 0"])
54
+ s.add_dependency(%q<json>, [">= 0"])
55
+ end
56
+ else
57
+ s.add_dependency(%q<shoulda>, [">= 0"])
58
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
59
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
60
+ s.add_dependency(%q<rcov>, [">= 0"])
61
+ s.add_dependency(%q<json>, [">= 0"])
62
+ end
63
+ end
64
+
@@ -0,0 +1,282 @@
1
+ class BlueButtonParser
2
+
3
+ attr_reader :text, :data, :config
4
+
5
+ ALWAYS_SKIP_LINES = ["^[-]+$", "^[-]+[ ]+$", "^[ ]+[-]+", "^[=]+$", "^(- ){5,}", "END OF MY HEALTHEVET"]
6
+
7
+ DEFAULT_CONFIG = {
8
+ "MY HEALTHEVET PERSONAL INFORMATION REPORT" => {
9
+ :same_line_keys => ["Name", "Date of Birth"],
10
+ },
11
+ "DOWNLOAD REQUEST SUMMARY" => {},
12
+ "MY HEALTHEVET ACCOUNT SUMMARY" => {
13
+ :collection => {"Facilities" => {:table_columns => ["VA Treating Facility", " Type"]}}
14
+ },
15
+ "DEMOGRAPHICS" => {
16
+ :collection => {"EMERGENCY CONTACTS" => {:item_starts_with => "Contact First Name"}},
17
+ :same_line_keys => [["Gender", "Blood Type", "Organ Donor"], ["Work Phone Number", "Extension"]],
18
+ },
19
+ "HEALTH CARE PROVIDERS" => {
20
+ :collection => {"Providers" => {:item_starts_with => "Provider Name"}},
21
+ :same_line_keys => ["Phone Number", "Ext"],
22
+ },
23
+ "TREATMENT FACILITIES" => {
24
+ :collection => {"Facilities" => {:item_starts_with => "Facility Name"}},
25
+ :same_line_keys => [["Facility Type", "VA Home Facility"], ["Phone Number", "Ext"]],
26
+ },
27
+ "HEALTH INSURANCE" => {
28
+ :collection => {"Companies" => {:item_starts_with => "Health Insurance Company"}},
29
+ :same_line_keys => [["ID Number", "Group Number"], ["Start Date", "Stop Date"]],
30
+ },
31
+ "VA WELLNESS REMINDERS" => {
32
+ :collection => {"Reminders" => {:table_columns => ["Wellness Reminder", "Due Date", "Last Completed", "Location"]}}
33
+ },
34
+ "VA APPOINTMENTS" => {
35
+ :collection => {"Appointments" => {:item_starts_with => "Date/Time"}},
36
+ :skip_lines => ["^FUTURE APPOINTMENTS:", "^PAST APPOINTMENTS:"]
37
+ },
38
+ "VA MEDICATION HISTORY" => {
39
+ :collection => {"Medications" => {:item_starts_with => "Medication"}},
40
+ },
41
+ "MEDICATIONS AND SUPPLEMENTS" => {
42
+ :collection => {"Medications" => {:item_starts_with => "Category"}},
43
+ :same_line_keys => [["Start Date", "Stop Date"], ["Pharmacy Name", "Pharmacy Phone"]],
44
+ },
45
+ "VA ALLERGIES" => {
46
+ :collection => {"Allergies" => {:item_starts_with => "Allergy Name"}},
47
+ },
48
+ "ALLERGIES/ADVERSE REACTIONS" => {
49
+ :collection => {"Allergies" => {:item_starts_with => "Allergy Name"}},
50
+ },
51
+ "MEDICAL EVENTS" => {
52
+ :collection => {"Event" => {:item_starts_with => "Medical Event"}},
53
+ },
54
+ "IMMUNIZATIONS" => {
55
+ :collection => {"Immunizations" => {:item_starts_with => "Immunization"}},
56
+ },
57
+ "VA LABORATORY RESULTS" => {
58
+ :collection => {"Labs" => {:item_starts_with => "Lab Test"}},
59
+ },
60
+ "LABS AND TESTS" => {
61
+ :collection => {"Labs" => {:item_starts_with => "Test Name"}},
62
+ },
63
+ "VITALS AND READINGS" => {
64
+ :collection => {"Reading" => {:item_starts_with => "Measurement Type"}},
65
+ },
66
+ "FAMILY HEALTH HISTORY" => {
67
+ :collection => {"Relation" => {:item_starts_with => "Relationship"}},
68
+ },
69
+ "MILITARY HEALTH HISTORY" => {
70
+ :same_line_keys => [["Service Branch", "Rank"],["Location of Service", "Onboard Ship"]]
71
+ },
72
+ "DOD MILITARY SERVICE INFORMATION" => {
73
+ :collection => {
74
+ "Regular Active Service" => {:table_starts_with => "-- Regular Active Service", :table_columns => ["Service", "Begin Date", "End Date", "Character of Service", "Rank"] },
75
+ "Reserve/Guard Association Periods" => {:table_starts_with => "-- Reserve/Guard Association Periods", :table_columns => ["Service", "Begin Date", "End Date", "Character of Service", "Rank"] },
76
+ "DoD MOS/Occupation Codes" => {:table_starts_with => "-- Note: Both Service and DoD Generic codes", :table_columns => ["Service", "Begin Date", "Enl/Off", "Type", "Svc Occ Code", "DoD Occ Code"]}
77
+ },
78
+ :skip_lines => ["^Translations of Codes Used in this Section"]
79
+ }
80
+ }
81
+
82
+ def initialize(bb_data_text, config=DEFAULT_CONFIG, newline="\n")
83
+ @text = bb_data_text
84
+ @config= config
85
+ @data = parse_text(@text, newline)
86
+ end
87
+
88
+ private
89
+
90
+ def new_section?(line)
91
+ new_section = line.match(/^[-]+ (.*) [-]+/)
92
+ new_section = new_section[1] if new_section
93
+ return new_section
94
+ end
95
+
96
+ def new_collection?(line, last_line, current_section)
97
+ new_collection = nil
98
+
99
+ if collections = sect_config(current_section, :collection)
100
+ collections.each_pair do |name, collection_config|
101
+ if starts_with = collection_config[:item_starts_with]
102
+ if line.match(Regexp.new("^#{starts_with}:"))
103
+ new_collection = name
104
+ break
105
+ end
106
+ elsif table_columns = collection_config[:table_columns]
107
+ new_table = false
108
+
109
+ if table_starts_with = collection_config[:table_starts_with]
110
+ if last_line.match(Regexp.new("^#{table_starts_with}"))
111
+ new_table = true
112
+ end
113
+ else
114
+ new_table = true
115
+ end
116
+
117
+ if new_table
118
+ regexp_str = ".?#{table_columns.join('.*')}.?"
119
+ if line.match(Regexp.new(regexp_str))
120
+ new_collection = name
121
+ break
122
+ end
123
+ end
124
+
125
+ end
126
+ end
127
+ end
128
+
129
+ return new_collection
130
+ end
131
+
132
+ def get_multi_key_values(line, current_section)
133
+ key_values = nil
134
+
135
+ if key_sets = sect_config(current_section, :same_line_keys)
136
+ unless key_sets.first.is_a?(Array)
137
+ key_sets = [key_sets]
138
+ end
139
+
140
+ key_sets.each do |keys|
141
+ regexp_str = keys.collect{|k| "#{k}: (.*)"}.join
142
+ regexp = Regexp.new(regexp_str)
143
+ if keys_match = line.match(regexp)
144
+ key_values = Hash.new
145
+ keys.each_with_index do |key, index|
146
+ key_values[key] = keys_match[index + 1]
147
+ end
148
+ break
149
+ end
150
+ end
151
+ end
152
+
153
+ return key_values
154
+ end
155
+
156
+ def get_single_key_value(line)
157
+ if key_match = line.match(/(.*)\: (.*)?/)
158
+ key_values = {key_match[1] => key_match[2]}
159
+ elsif key_match = line.match(/(.*):$/)
160
+ key_values = {key_match[1] => nil}
161
+ else
162
+ key_values = nil
163
+ end
164
+
165
+ return key_values
166
+ end
167
+
168
+ def get_key_values(line, current_section)
169
+ key_values = get_multi_key_values(line, current_section)
170
+ key_values = get_single_key_value(line) if key_values.nil?
171
+ return key_values
172
+ end
173
+
174
+ def key_ended?(line)
175
+ # either an empty line or a line starting with a key (e.g. "Status:") means we're done with multi-line
176
+ line.empty? or line.match(/^\S(.*): (\S*)/)
177
+ end
178
+
179
+ def sect_config(current_section, key)
180
+ if @config[current_section]
181
+ @config[current_section][key]
182
+ else
183
+ nil
184
+ end
185
+ end
186
+
187
+ def parse_table_line(line, columns, column_widths)
188
+ row = Hash.new
189
+ columns.each_with_index do |column, index|
190
+ start = column_widths[index]
191
+ finish = if index == column_widths.size - 1
192
+ line.size
193
+ else
194
+ column_widths[index + 1]
195
+ end
196
+ val_str = (line[start, finish-start]).strip
197
+ val_str = val_str.empty? ? nil : val_str
198
+ row[column] = val_str
199
+ end
200
+ return row
201
+ end
202
+
203
+ def table_columns(current_section, current_collection)
204
+ sect_config(current_section, :collection)[current_collection][:table_columns]
205
+ end
206
+
207
+ def column_widths(line, columns)
208
+ columns.collect{|c| line.index(c)}
209
+ end
210
+
211
+ def parse_text(text, newline="\n")
212
+ # parse text line by line
213
+ lines = text.split(newline)
214
+
215
+ # state variables
216
+ current_section = nil
217
+ current_collection = nil
218
+ current_key = nil
219
+ current_table = nil
220
+
221
+ # put parsed data into this hash
222
+ data = Hash.new
223
+
224
+ # start parsing
225
+ lines.each_with_index do |line, index|
226
+ skip_regexps = (ALWAYS_SKIP_LINES + (sect_config(current_section, :skip_lines) || [])).compact
227
+ skip_regexps = skip_regexps.collect{|r| Regexp.new(r)}
228
+ next if skip_regexps.find{|re| re.match(line)}
229
+
230
+ if collection = new_collection?(line, lines[index - 1], current_section)
231
+ current_collection = collection
232
+ current_key = nil
233
+ data[current_section][current_collection] ||= []
234
+ if columns = table_columns(current_section, current_collection)
235
+ current_table = {:columns => columns, :widths => column_widths(line, columns)}
236
+ next
237
+ else
238
+ data[current_section][current_collection] << Hash.new
239
+ end
240
+ end
241
+
242
+ if key_ended?(line)
243
+ current_key = nil
244
+ end
245
+
246
+ if current_section and current_key
247
+ value = line.rstrip
248
+ if current_collection.nil?
249
+ data[current_section][current_key] = [data[current_section][current_key], value].compact.join(" \n")
250
+ else
251
+ (data[current_section][current_collection].last)[current_key] = [(data[current_section][current_collection].last)[current_key], value].compact.join(" \n")
252
+ end
253
+ end
254
+
255
+ if section = new_section?(line)
256
+ current_section = section
257
+ current_collection = nil
258
+ data[current_section] ||= {}
259
+ elsif current_table
260
+ if line.empty?
261
+ current_collection = nil
262
+ current_table = nil
263
+ else
264
+ data[current_section][current_collection] << parse_table_line(line, current_table[:columns], current_table[:widths])
265
+ end
266
+ elsif (!current_key and current_section and key_values = get_key_values(line, current_section))
267
+ key_values.each_pair do |key, value|
268
+ val = (value.nil? or value.strip.empty?) ? nil : value.strip #empty strings should be converted to nils
269
+ if current_collection.nil?
270
+ data[current_section][key] = val
271
+ else
272
+ (data[current_section][current_collection].last)[key] = val
273
+ end
274
+ current_key = key
275
+ end
276
+ end
277
+ end
278
+
279
+ return data
280
+ end
281
+
282
+ end