-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathget_meds
More file actions
executable file
·292 lines (269 loc) · 10.8 KB
/
get_meds
File metadata and controls
executable file
·292 lines (269 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
#!/usr/bin/env python3
import click
import csv
import re
import sys
import xlrd
@click.command()
@click.option(
'-c', '--code-column', default=1,
help='medication code column number (counted from 1)'
)
@click.option(
'-e', '--en-desc-column', default=2,
help='English description column number (counted from 1)'
)
@click.option(
'-E', '--en-adjust-column', default=None, type=int,
help='English adjustment column number (counted from 1)'
)
@click.option(
'-f', '--fr-desc-column', default=None, type=int,
help='French description column number (counted from 1)'
)
@click.option(
'-F', '--fr-adjust-column', default=None, type=int,
help='French adjustment column number (counted from 1)'
)
@click.option(
'-s', '--sheet', default=1,
help='sheet name or number (counted from 1)'
)
@click.argument('filename')
@click.argument('target', required=False)
def get_meds(code_column,
en_desc_column, fr_desc_column,
en_adjust_column, fr_adjust_column,
sheet, filename, target):
book = xlrd.open_workbook(filename)
code_col = code_column - 1
en_col = en_desc_column - 1
fr_col = fr_desc_column - 1 if fr_desc_column else None
en_adj = en_adjust_column - 1 if en_adjust_column else None
fr_adj = fr_adjust_column - 1 if fr_adjust_column else None
try:
s = book.sheet_by_name(sheet)
except:
s = book.sheets()[int(sheet) - 1]
drugs = set()
drug_list = []
formats = {}
units = {}
items = read_items(s, code_col, en_col, fr_col, en_adj, fr_adj)
for code, en_desc, fr_desc in items:
if not code.startswith('D'):
continue
drug = code[:8]
if drug not in drugs:
drugs.add(drug)
drug_list.append(drug)
if '$' in en_desc:
en_desc, unit = en_desc.split('$', 1)
units[code] = unit.strip()
if '$' in fr_desc:
fr_desc, unit = fr_desc.split('$', 1)
units[code] = unit.strip()
en_desc = normalize(en_desc)
fr_desc = normalize(fr_desc)
formats.setdefault(drug, []).append((code, en_desc, fr_desc))
if target:
begin_marker = '==== BEGIN GENERATED OUTPUT ===='
end_marker = '==== END GENERATED OUTPUT ===='
with open(target) as file:
template = file.read()
if not (begin_marker in template and end_marker in template):
raise SystemExit('Markers %r and %r not found in %s' % (begin_marker, end_marker, target))
prologue = template.split(begin_marker, 1)[0]
epilogue = template.rsplit(end_marker, 1)[1]
prologue = re.sub(r'.*$', '', prologue)
epilogue = re.sub(r'^.*\n', '', epilogue)
with open(target, 'w') as file:
file.write(prologue)
file.write('''\
// %s
// Produced by executing: get_meds %s
''' % (begin_marker, ' '.join(sys.argv[1:-1])))
write_items(file, drug_list, formats, units, items)
file.write('''\
// %s
''' % end_marker)
file.write(epilogue)
else:
write_items(sys.stdout, drug_list, formats, units, items)
def write_items(file, drug_list, formats, units, items):
for stock_prefix, category_expr in [
('DORA', 'Category ORAL = new Category("DORA", "oral", false, PO)'),
('DINJ', 'Category INJECTABLE = new Category("DINJ", "injectable", false, IV, SC, IM, IO)'),
('DINF', 'Category PERFUSION = new Category("DINF", "perfusion", true)'),
('DEX', 'Category EXTERNAL = new Category("DEXT", "external", false, UNSPECIFIED, OC)'),
('DVAC', 'Category VACCINE = new Category("DVAC", "vaccines/immunoglobulins", false)')
]:
en_names = set()
fr_names = set()
drug_exprs = []
for drug in drug_list:
if drug.startswith(stock_prefix):
en_name = clean(get_drug_name([en for code, en, fr in formats[drug]]))
fr_name = clean(get_drug_name([fr for code, en, fr in formats[drug]]))
if not en_name:
warn('Could not separate drug name: %r' % formats[drug][0][1])
if not fr_name:
warn('Could not separate drug name: %r' % formats[drug][0][2])
if en_name and en_name in en_names:
warn('Duplicate drug name: %r' % en_name)
if fr_name and fr_name in fr_names:
warn('Duplicate drug name: %r' % fr_name)
en_names.add(en_name)
fr_names.add(fr_name)
name = pack_en_fr(en_name, fr_name)
format_exprs = []
for code, en_desc, fr_desc in formats[drug]:
en_format = clean(en_desc[len(en_name):].lstrip(' ^'))
fr_format = clean(fr_desc[len(fr_name):].lstrip(' ^'))
format = pack_en_fr(en_format, fr_format)
unit = units.get(code) or ('Unit.%s' % guess_unit(en_desc))
format_exprs.append('''\
new Format("%s", "%s", %s)''' %
(code.strip('-'), format, unit))
drug_exprs.append('''\
new Drug("%s", "%s").withFormats(
%s
)''' % (drug, name, ',\n'.join(format_exprs)))
file.write('''\
%s.withDrugs(
%s
);
''' % (category_expr, ',\n'.join(drug_exprs)))
def pack_en_fr(en_text, fr_text):
if fr_text.strip():
return '%s [fr:%s]' % (en_text, fr_text)
else:
return en_text
def get_drug_name(descs):
if len(descs) == 0:
return ''
if len(descs) == 1:
return split_med(descs[0])[0]
words = [desc.split() for desc in descs]
sets = [set(group) for group in zip(*words)]
w = 0
while w < len(sets) - 1 and len(sets[w]) == 1:
next = sets[w]
word = list(next)[0]
if '^' in word: break
if re.match(r'^[0-9.% ]+$', word): break
w += 1
result = ' '.join(set.pop() for set in sets[:w])
return re.sub(r',* *[eé]q\..*$', '', result)
def read_items(s, code_col, en_col, fr_col, en_adj, fr_adj):
for row in range(s.nrows):
code = s.cell(row, code_col).value.strip()
if ' ' in code:
continue
extra_code, extra_en_desc, extra_fr_desc = None, '', ''
en_desc = s.cell(row, en_col).value
if en_adj is not None:
en_adjust = s.cell(row, en_adj).value
if en_adjust.strip().startswith('add '):
add, adjust = en_adjust.split('|', 1)
extra_code = add.split()[1]
extra_en_desc = apply_adjustments(extra_en_desc, adjust)
else:
en_desc = apply_adjustments(en_desc, en_adjust)
fr_desc = '' if fr_col is None else s.cell(row, fr_col).value
if fr_adj is not None:
fr_adjust = s.cell(row, fr_adj).value
if fr_adjust == '=':
fr_adjust = en_adj and en_adjust or ''
if fr_adjust.strip().startswith('add '):
add, adjust = fr_adjust.split('|', 1)
extra_code = add.split()[1]
extra_fr_desc = apply_adjustments(extra_fr_desc, adjust)
else:
fr_desc = apply_adjustments(fr_desc, fr_adjust)
yield (code, en_desc, fr_desc)
if extra_code is not None:
yield (extra_code, extra_en_desc, extra_fr_desc)
def apply_adjustments(text, adjustments):
if not adjustments.strip():
return text
for clause in adjustments.split('|'):
clause = clause.strip()
action, args = clause.split(' ', 1)
if action == 'sub':
old, new = args.split('>', 1)
text = text.replace(old.strip(), new.strip(), 1)
elif action == 'drug':
drug = args.strip()
format = text
for word in drug.split():
if re.search(r'\w', word):
word = re.sub(r'^\W+|\W+$', '', word)
format = re.sub(r'\b' + word + r'\b', '', format, 1)
text = drug + ' ^ ' + format
elif action == 'format':
format = args.strip()
drug, old = text.split('^', 1)
text = drug + ' ^ ' + format
elif action == 'unit':
unit = args.strip()
text = text + ' $ ' + unit
return text
def split_med(med):
if '^' in med:
return med.split('^', 1)
m = re.match(r'(.*), *(([eé]q[\. ]*)\d.*)', med)
if m:
return m.group(1), m.group(2).lstrip(', ')
m = re.match(r'([A-Za-z ]+) ([0-9.%]+ .*)', med)
if m:
return m.group(1), m.group(2).lstrip(', ')
m = re.match(r'([^,]+), *(.*)', med)
if m:
return m.group(1), m.group(2).lstrip(', ')
return med, ''
def normalize(desc):
for search, replace in [
(r'([a-zA-Z])\.', r'\1. '), # period after a letter is a word break
(r'(\d),(\d)', r'\1.\2'), # turn commas into decimal points
(r'(\d)(g|mg|µg|l|ml|IU|UI)\b', r'\1 \2'), # space before any unit
(r'(\d)x(\d)', r'\1 x \2'), # spaces around x as multiplication sign
(r'\bx(\d)', r'x \1'), # spaces around x as multiplication sign
(r'(\d)x\b', r'\1 x'), # spaces around x as multiplication sign
(r'(\d) *%', r'\1%'), # no spaces between number and percent sign
(r'%', '% '), # end of a percentage is a word break
(r'[ ,]000', '000'), # remove thousands separators
(r'[ ,]*/[ ,]*', ' / '), # one space before and after any slash
(r'(\d) / (\d)', r'\1/\2'), # no spaces around slash for fractions
(r',( *,)*', ','), # collapse multiple commas to single comma
(r' *, *', ' , '), # one space before and after any comma
(r'\s+', ' '), # collapse whitespace to single space
(r'^[ ,]*|[ ,]*$', ''), # remove leading/trailing spaces/commas
]:
desc = re.sub(search, replace, desc)
return desc
def clean(desc):
for search, replace in [
(r'[ ,]*/[ ,]*', ' / '), # one space before and after any slash
(r'(\d) / (\d)', r'\1/\2'), # no spaces around slash for fractions
(r',( *,)*', ','), # collapse multiple commas to single comma
(r' *, *', ', '), # no space before and one space after any comma
(r'\s+', ' '), # collapse whitespace to single space
(r'^[ ,]*|[ ,]*$', ''), # remove leading/trailing spaces/commas
]:
desc = re.sub(search, replace, desc)
return desc
def guess_unit(desc):
if re.findall(r'\btab\.?\b', desc):
return 'TABLET'
if re.findall(r'\bcaps?\.?\b', desc):
return 'CAPSULE'
if re.findall(r'/ *puff\b', desc):
return 'PUFF'
if re.findall(r' *[0-9.]+ *m?l\b', desc) or re.findall(r'\d%', desc):
return 'ML'
return 'MG'
def warn(message):
print(message, file=sys.stderr)
if __name__ == '__main__':
get_meds()