drgdsl 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.
@@ -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