drgdsl 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,269 @@
1
+ require 'pp'
2
+
3
+ module DrgDSL
4
+ class UnknownCstError < StandardError
5
+ attr_reader :cst, :result
6
+
7
+ # @param cst [Hash] CST obtained by parser
8
+ # @param result [Object] whatever the AstBuilder was able to generate
9
+ def initialize(cst, result)
10
+ @cst = cst
11
+ @result = result
12
+ end
13
+
14
+ def message
15
+ <<~EOM
16
+ Don't know how to build AST from this CST:
17
+
18
+ #{pretty cst}
19
+
20
+ Intermediate result:
21
+
22
+ #{pretty result}
23
+ EOM
24
+ end
25
+
26
+ private
27
+
28
+ # @return [String] nicely formatted string for enhanced readability.
29
+ def pretty(object)
30
+ PP.pp object, ''
31
+ end
32
+ end
33
+
34
+ class AstBuilder < Parslet::Transform
35
+
36
+ # @param cst [Hash] CST-ish hash obtained from DrgParser.
37
+ # @return [Node] AST
38
+ # @raise [UnknownCstError] when CST could not be converted to a correct
39
+ # AST.
40
+ def self.build(cst)
41
+ ast = new.apply(cst)
42
+ raise UnknownCstError.new(cst, ast) if broken_ast?(ast)
43
+ ast
44
+ end
45
+
46
+ # Did we manage to build a correct AST?
47
+ #
48
+ # @return [Boolean]
49
+ def self.broken_ast?(ast)
50
+ !ast.is_a?(Ast::Node)
51
+ end
52
+
53
+ # Parslet transformation rules that transform a nested CST-ish hash
54
+ # generated by DrgParser into a AST.
55
+ #
56
+ # Check out https://kschiess.github.io/parslet/transform.html for more
57
+ # details on transformations.
58
+
59
+ # Only one and-expression means there's no RHS of an "oder" expression, so
60
+ # we just go downwards.
61
+ rule(exp: subtree(:and_exp)) { and_exp }
62
+
63
+ # Two or more and-expressions will be joined with an "oder". So and has
64
+ # higher precedence.
65
+ rule(exp: sequence(:and_exp)) do
66
+ Ast::Expression.new(and_exp)
67
+ end
68
+
69
+ # In case of no RHS we simply go downwards.
70
+ rule(and_exp: subtree(:simple)) do
71
+ simple
72
+ end
73
+
74
+ rule(and_exp: sequence(:simple)) do
75
+ Ast::AndExpression.new(simple)
76
+ end
77
+
78
+ rule(simple: subtree(:simple)) { simple }
79
+
80
+ rule(paren_exp: subtree(:exp)) do
81
+ Ast::ParenExpression.new(exp)
82
+ end
83
+
84
+ # "NOT ( ... )" -> "nicht [...]"
85
+ rule(not_exp: subtree(:exp)) do
86
+ Ast::NotExpression.new(exp)
87
+ end
88
+
89
+ # "EMPTY()" -> "leer"
90
+ rule(empty: simple(:empty)) do
91
+ Ast::Empty.new
92
+ end
93
+
94
+ # "NOT IN TABLE ... -> "nicht in Tabelle ...""
95
+ rule(
96
+ unary_condition: {
97
+ keyword: simple(:keyword),
98
+ condition: subtree(:condition)
99
+ }
100
+ ) do
101
+ Ast::UnaryCondition.new(op: keyword, condition: condition)
102
+ end
103
+
104
+ rule(
105
+ variable: subtree(:inner_variable)
106
+ ) do
107
+ inner_variable
108
+ end
109
+
110
+ # "AGEYEARS" --> "Alter"
111
+ rule(inner_variable: subtree(:variable)) do
112
+ Ast::Variable.new(variable)
113
+ end
114
+
115
+ # "42" -> "42"
116
+ rule(constant: simple(:constant)) do
117
+ Ast::Constant.new(constant)
118
+ end
119
+
120
+ # "DRG (F89)" -> "Definition der DRG (F98)"
121
+ rule(
122
+ basic_or_drg_link: {
123
+ variable: subtree(:var),
124
+ drg_link: {
125
+ drg: simple(:drg)
126
+ }
127
+ }
128
+ ) do
129
+ Ast::DrgLink.new(name: drg, variable: var)
130
+ end
131
+
132
+ # "PDX IN TABLE ..." -> "Hauptdiagnose in Tabelle ..."
133
+ rule(
134
+ basic_or_drg_link: {
135
+ variable: subtree(:var),
136
+ condition: subtree(:condition)
137
+ }
138
+ ) do
139
+ Ast::BasicExpression.new(variable: var, condition: condition)
140
+ end
141
+
142
+ # Go down to in-table, in-tables or all-in-tables condition.
143
+ rule(table_condition: subtree(:table_condition)) { table_condition }
144
+
145
+ # "in table (<reftable>)" -> "in Tabelle <external table name>"
146
+ rule(in_table: simple(:table)) do
147
+ Ast::TableCondition.new(
148
+ op: Ast::TableCondition::IN_TABLE,
149
+ tables: table
150
+ )
151
+ end
152
+
153
+ # Multi-tables expression is deliberately mapped to one external table for
154
+ # enhanced readability.
155
+ #
156
+ # "in tables (<ref1>, <ref2>, ...)" -> "in Tabelle <external table name>"
157
+ rule(
158
+ in_tables: {
159
+ tables: sequence(:tables)
160
+ }
161
+ ) do
162
+ Ast::TableCondition.new(
163
+ op: Ast::TableCondition::IN_TABLES,
164
+ tables: tables
165
+ )
166
+ end
167
+
168
+ rule(
169
+ comparison: subtree(:comparison)
170
+ ) do
171
+ comparison
172
+ end
173
+
174
+ rule(
175
+ op: simple(:op),
176
+ value: subtree(:value),
177
+ table_condition: subtree(:table_condition)
178
+ ) do
179
+ Ast::Comparison.new(
180
+ op: op,
181
+ value: value,
182
+ table_condition: table_condition
183
+ )
184
+ end
185
+
186
+ # Handle dangling comparison after table expressions.
187
+ rule(
188
+ in_table: {
189
+ table: simple(:table),
190
+ comparison: subtree(:comparison)
191
+ }
192
+ ) do
193
+ Ast::TableCondition.new(
194
+ op: Ast::TableCondition::IN_TABLE,
195
+ tables: table,
196
+ comparison: comparison
197
+ )
198
+ end
199
+
200
+ rule(
201
+ in_tables: {
202
+ tables: sequence(:tables),
203
+ comparison: subtree(:comparison)
204
+ }
205
+ ) do
206
+ Ast::TableCondition.new(
207
+ op: Ast::TableCondition::IN_TABLES,
208
+ tables: tables,
209
+ comparison: comparison
210
+ )
211
+ end
212
+
213
+ rule(table: simple(:table)) { table.to_s.strip }
214
+
215
+ # "SRGLRB IN TABLE( CO3021ORS )" -> "Beidseitige Prozedur in Tabelle CO3021ORS"
216
+ rule(
217
+ srglrb_table_condition: {
218
+ variable: subtree(:var),
219
+ condition: subtree(:condition)
220
+ }
221
+ ) do
222
+ Ast::SrglrbTableCondition.new(variable: var, condition: condition)
223
+ end
224
+
225
+ # "OPD2 in (SRG in table ...) > 0" -> "Mindestens zwei Behandlungen, die mindestens einen Tag auseinander liegen in [Prozedur in Tabelle ...]" ¯\_(ツ)_/¯
226
+ rule(
227
+ date_exp: {
228
+ opd: simple(:opd),
229
+ variable: subtree(:left_var),
230
+ left_table_condition: subtree(:left_table_condition),
231
+ and_table_condition: {
232
+ variable: subtree(:right_var),
233
+ right_table_condition: subtree(:right_table_condition)
234
+ },
235
+ comparison: subtree(:comparison)
236
+ }
237
+ ) do
238
+ Ast::DateExpression.new(
239
+ left_variable: left_var,
240
+ right_variable: right_var,
241
+ left_condition: left_table_condition,
242
+ right_condition: right_table_condition,
243
+ comparison: comparison,
244
+ opd: opd
245
+ )
246
+ end
247
+
248
+ rule(
249
+ date_exp: {
250
+ opd: simple(:opd),
251
+ variable: subtree(:left_var),
252
+ left_table_condition: subtree(:left_table_condition),
253
+ comparison: subtree(:comparison)
254
+ }
255
+ ) do
256
+ Ast::DateExpression.new(
257
+ left_variable: left_var,
258
+ left_condition: left_table_condition,
259
+ comparison: comparison,
260
+ opd: opd
261
+ )
262
+ end
263
+
264
+ # "Vierzeitige_bestimmte_OR" -> "Vierzeitige bestimmte OR-Prozeduren"
265
+ rule(function_call: simple(:fname)) do
266
+ Ast::FunctionCall.new(fname)
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,256 @@
1
+ module DrgDSL
2
+ class ParserError < StandardError
3
+ attr_reader :parslet_error, :input
4
+
5
+ def initialize(parslet_error:, input:)
6
+ @parslet_error = parslet_error
7
+ @input = input
8
+ end
9
+
10
+ def message
11
+ %{Failed to parse "#{input}"\nParslet::ParseFailed: #{parslet_error}}
12
+ end
13
+ end
14
+
15
+ # Used to parse logic expressions in flowchart decision tree. Based on the
16
+ # Grouper's DRG parser[0], only difference being that this parser doesn't
17
+ # know about statements, since they do not occur in the decision nodes.
18
+ #
19
+ # To learn more about Parslet see the parser[1] and transform[2] docs.
20
+ #
21
+ # The full syntax of the logic is described in
22
+ # documents/Spec-Handbuch_v2.2.3.pdf in chapter 5.5.
23
+ #
24
+ # [0] https://github.com/swissdrg/grouper/blob/master/src/main/common/org/swissdrg/grouper/parser/DrgParser.jj
25
+ # [1] https://kschiess.github.io/parslet/parser.html
26
+ # [2] https://kschiess.github.io/parslet/transform.html
27
+ class Parser < Parslet::Parser
28
+
29
+ # @param expression [String]
30
+ # @return [Ast::Node]
31
+ def self.parse(expression)
32
+ AstBuilder.build new.parse(expression.to_s.strip)
33
+ rescue Parslet::ParseFailed => e
34
+ raise ParserError.new(parslet_error: e, input: expression)
35
+ end
36
+
37
+ # Case-insensitive string matching
38
+ #
39
+ # https://kschiess.github.io/parslet/tricks.html
40
+ def stri(str)
41
+ key_chars = str.split(//)
42
+ key_chars.map { |c| match["#{c.upcase}#{c.downcase}"] }.inject(:>>)
43
+ end
44
+
45
+ # Tokens
46
+ rule(:star) { str('*') >> sp? }
47
+ rule(:k_or) { stri('or') }
48
+ rule(:k_and) { stri('and') }
49
+ rule(:k_not) { stri('not') }
50
+ rule(:k_different) { stri('different') }
51
+ rule(:k_empty) { stri('empty') }
52
+ rule(:k_in) { stri('in') }
53
+ rule(:mdc) { stri('mdc') }
54
+ rule(:in_table) { stri('in table') }
55
+ rule(:in_tables) { stri('in tables') }
56
+ rule(:all_in_table) { stri('all in table') }
57
+ rule(:lpar) { str('(') >> sp? }
58
+ rule(:rpar) { sp? >> str(')') }
59
+ rule(:underscore) { str('_') }
60
+ rule(:comma) { str(',') }
61
+ rule(:quote) { str("'") }
62
+ rule(:assign) { str(':=') }
63
+ rule(:letter) { match('[a-zA-Z]') }
64
+ rule(:digit) { match('[0-9]') }
65
+ rule(:number) { digit.repeat(1) }
66
+
67
+ rule(:comparison_operator) do
68
+ stri(">=") |
69
+ stri(">") |
70
+ stri("<=") |
71
+ stri("<") |
72
+ stri("=")
73
+ end
74
+
75
+ # name
76
+ # ::= letter (letter | number | underscore)*
77
+ rule(:name) { letter >> (letter | number | underscore).repeat }
78
+
79
+ rule(:variable_name) do
80
+ stri("PDX") |
81
+ stri("SDX") >> number.maybe |
82
+ stri("DDX") >> number.maybe |
83
+ stri("SRG") >> number.maybe |
84
+ stri("SRGB") >> number.maybe |
85
+ stri("SRGR") >> number.maybe |
86
+ stri("SRGL") >> number.maybe |
87
+ stri("SRGN") >> number.maybe |
88
+ stri("SRGU") >> number.maybe |
89
+ stri("AGEYEARS") |
90
+ stri("AGEDAYS") |
91
+ stri("SEX") |
92
+ stri("ADM_WT") |
93
+ stri("HMV") |
94
+ stri("MDC") |
95
+ stri("DRG") |
96
+ stri("ADRG") |
97
+ stri("GST") |
98
+ stri("LOS") |
99
+ stri("PCCL") |
100
+ stri("SEP") |
101
+ stri("ENTRY") |
102
+ stri("ADM_MODE") |
103
+ stri("VOID") |
104
+ stri("GESTAGE")
105
+ end
106
+
107
+ # srglrb
108
+ # ::= 'SRGLRB' number{0,1}
109
+ rule(:srglrb) { stri('SRGLRB') >> number.maybe }
110
+
111
+ # opd
112
+ # ::= 'OPD' number{0,1}
113
+ rule(:opd) { stri('OPD') >> number.maybe }
114
+
115
+ # By "double nesting" the variable, the AST builder (a parslet transformer)
116
+ # can use subtree(:variable) to retrieve a variable node, given there's a
117
+ # rule for inner_variable that creates said node.
118
+ #
119
+ # variable
120
+ # ::= variable_name
121
+ rule(:variable) { (star.maybe >> variable_name).as(:inner_variable).as(:variable) }
122
+
123
+ # constant
124
+ # ::= quote{0,1} (number | name) name{0,1} quote{0,1}
125
+ rule(:constant) do
126
+ (quote.maybe >> ((number | name) >> name.maybe) >> quote.maybe).as(:constant)
127
+ end
128
+
129
+ # value
130
+ # ::= variable | constant
131
+ rule(:value) { (variable | constant).as(:value) }
132
+
133
+ # function_call
134
+ # ::= name
135
+ rule(:function_call) do
136
+ name.as(:function_call)
137
+ end
138
+
139
+ # empty
140
+ # ::= empty '()'
141
+ rule(:empty) do
142
+ k_empty >> sp? >> (lpar >> rpar).maybe
143
+ end
144
+
145
+ # table_condition
146
+ # ::= (in_table '(' name ')' comparison{0,1})
147
+ # | in_tables '(' name (comma name)* ')' comparison{0,1}
148
+ # | all_in_table '(' name ')'
149
+ rule(:table_condition) do
150
+ (in_table >> sp? >> lpar >> name.as(:table) >> rpar >> sp? >> comparison.as(:comparison).maybe).as(:in_table) |
151
+ (in_tables >> sp? >> lpar >> (name.as(:table) >> (sp? >> comma >> sp? >> name.as(:table)).repeat).as(:tables) >> rpar >> sp? >> comparison.as(:comparison).maybe).as(:in_tables) |
152
+ (all_in_table >> sp? >> lpar >> name.as(:table) >> rpar).as(:all_in_table)
153
+ end
154
+
155
+ # comparison
156
+ # ::= comparison_operator value table_condition{0,1}
157
+ rule(:comparison) do
158
+ comparison_operator.as(:op) >> sp? >>
159
+ value >> sp? >> table_condition.maybe.as(:table_condition)
160
+ end
161
+
162
+ # unary_condition
163
+ # ::= (not | different) condition
164
+ rule(:unary_condition) do
165
+ (k_not | k_different).as(:keyword) >> sp? >> condition.as(:condition)
166
+ end
167
+
168
+ # condition
169
+ # ::= comparison
170
+ # | unary_operator
171
+ # | empty
172
+ # | table_condition
173
+ rule(:condition) do
174
+ comparison.as(:comparison) | unary_condition.as(:unary_condition) | empty.as(:empty) | table_condition.as(:table_condition)
175
+ end
176
+
177
+ # expression
178
+ # ::= and_expression (or and_expression)*
179
+ rule(:expression) do
180
+ (sp? >> and_expression.as(:and_exp) >> (sp? >> k_or >> sp? >> and_expression.as(:and_exp)).repeat >> sp?).as(:exp)
181
+ end
182
+
183
+ # and_expression
184
+ # ::= simple_expression (and simple_expression)*
185
+ rule(:and_expression) do
186
+ simple_expression.as(:simple) >> (sp? >> k_and >> sp? >> simple_expression.as(:simple)).repeat
187
+ end
188
+
189
+ # simple_expression
190
+ # ::= '(' expression ')'
191
+ # | basic_or_drg_link
192
+ # | srglrb_table_condition
193
+ # | date_expression
194
+ # | not_expression
195
+ # | function_call
196
+ rule(:simple_expression) do
197
+ (lpar >> expression >> rpar).as(:paren_exp) |
198
+ basic_or_drg_link |
199
+ srglrb_table_condition |
200
+ date_expression |
201
+ not_expression |
202
+ function_call
203
+ end
204
+
205
+ # date_expression
206
+ # ::= opd in '(' variable table_condition (and variable table_ondition){0,1} ')' comparison
207
+ rule(:date_expression) do
208
+ (opd.as(:opd) >> sp? >> k_in >> sp? >> lpar >> variable >> sp? >> table_condition.as(:left_table_condition) >> (sp? >> k_and >> sp? >> variable >> sp? >> table_condition.as(:right_table_condition)).as(:and_table_condition).maybe >> rpar >> sp? >> comparison.as(:comparison)).as(:date_exp)
209
+ end
210
+
211
+ # srglrb_table_condition
212
+ # ::= star{0,1} srglrb condition
213
+ rule(:srglrb_table_condition) do
214
+ ((star.maybe >> srglrb).as(:inner_variable).as(:variable) >> sp? >> condition.as(:condition)).as(:srglrb_table_condition)
215
+ end
216
+
217
+ # not_expression
218
+ # ::= not '(' expression ')'
219
+ rule(:not_expression) do
220
+ (k_not >> sp? >> lpar >> expression >> rpar).as(:not_exp)
221
+ end
222
+
223
+ # basic_or_drg_link
224
+ # ::= variable (basic_expression | drg_link)
225
+ rule(:basic_or_drg_link) do
226
+ (variable >> sp? >> (basic_expression | drg_link)).as(:basic_or_drg_link)
227
+ end
228
+
229
+ # basic_expression
230
+ # ::= condition
231
+ rule(:basic_expression) do
232
+ condition.as(:condition)
233
+ end
234
+
235
+ # drg_link
236
+ # ::= '(' name ')'
237
+ rule(:drg_link) do
238
+ (lpar >> name.as(:drg) >> rpar).as(:drg_link)
239
+ end
240
+
241
+ rule(:comment) do
242
+ str('/*') >> (str('*/').absent? >> any).repeat >> str('*/')
243
+ end
244
+
245
+ rule(:sp) do
246
+ (match("[ \t\r\n]") | comment).repeat(1)
247
+ end
248
+
249
+ # Optional whitespace rule. Unfortunately, skipping whitespace can't be
250
+ # automated:
251
+ # https://github.com/kschiess/parslet/issues/4://github.com/kschiess/parslet/issues/49
252
+ rule(:sp?) { sp.repeat }
253
+
254
+ rule(:root) { expression }
255
+ end
256
+ end
@@ -0,0 +1,3 @@
1
+ module DrgDSL
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,17 @@
1
+ module DrgDSL
2
+ module Visitor
3
+ def visit(n)
4
+ send("visit_#{n.type}", n)
5
+ end
6
+
7
+ Ast::Node.node_classes.each do |node_class|
8
+ define_method("visit_#{Ast::Node.type(node_class)}") do |n|
9
+ default_value
10
+ end
11
+ end
12
+
13
+ # @return [Object] what the visit_<node_class> methods return by default.
14
+ def default_value
15
+ end
16
+ end
17
+ end
data/lib/drgdsl.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'oj'
2
+ require 'parslet'
3
+
4
+ require_relative "./drgdsl/version"
5
+ require_relative "./drgdsl/ast"
6
+ require_relative "./drgdsl/ast_builder"
7
+ require_relative "./drgdsl/parser"
8
+ require_relative "./drgdsl/visitor"
9
+
10
+ module DrgDSL
11
+
12
+ # @param input [String]
13
+ # @return [Ast::Node]
14
+ def self.parse(input)
15
+ Parser.parse(input)
16
+ end
17
+
18
+ # @param input [String]
19
+ # @return [String]
20
+ def self.json(input)
21
+ Oj.dump parse(input).to_hash, mode: :compat
22
+ end
23
+ end