canql 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ module Canql
2
+ grammar TestResult
3
+ rule test_results
4
+ and_keyword? existance_modifier:test_result_no_keyword? natal_period:test_result_natal_period? test_results_keyword <Nodes::TestResult::Exists>
5
+ end
6
+
7
+ rule test_result_no_keyword
8
+ space ('no' / 'some') word_break
9
+ end
10
+
11
+ rule test_result_natal_period
12
+ (prenatal_keyword / postnatal_keyword)
13
+ end
14
+
15
+ rule test_results_keyword
16
+ space 'tests' word_break
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ # These file contain custom mixins for treetop
3
+ # nodes that enable them to generate meta_data_items, etc.
4
+
5
+ # Treetop documentation seems to be out of date;
6
+ # it says to create subclasses of nodes. However,
7
+ # treetop 1.4.10 expects to extend the node with
8
+ # the specified <MODULE> from the grammar.
9
+ require 'canql/treetop/extensions'
10
+ require 'canql/constants'
11
+
12
+ require 'canql/nodes/age'
13
+ require 'canql/nodes/dates'
14
+ require 'canql/nodes/e_base_records'
15
+ require 'canql/nodes/batch_types'
16
+ require 'canql/nodes/anomaly'
17
+ require 'canql/nodes/test_result'
18
+ require 'canql/nodes/patient'
19
+ require 'canql/nodes/main'
20
+ require 'canql/nodes/registry'
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module Age
5
+ module BirthDateNode
6
+ def meta_data_item
7
+ subject = reverse_scan_for_marker(:subject) == 'mother' ? 'mother' : 'patient'
8
+ range = fuzzy_date.to_daterange
9
+ {
10
+ "#{subject}.birthdate" => {
11
+ Canql::LIMITS => [
12
+ range.date1.try(:to_date).try(:iso8601), range.date2.try(:to_date).try(:iso8601)
13
+ ]
14
+ }
15
+ }
16
+ end
17
+ end
18
+
19
+ module DeathDateNode
20
+ def meta_data_item
21
+ subject = reverse_scan_for_marker(:subject) == 'mother' ? 'mother' : 'patient'
22
+ range = fuzzy_date.to_daterange
23
+ {
24
+ "#{subject}.deathdate" => {
25
+ Canql::LIMITS => [
26
+ range.date1.try(:to_date).try(:iso8601), range.date2.try(:to_date).try(:iso8601)
27
+ ]
28
+ }
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module Anomaly
5
+ module Exists
6
+ def anomaly_type
7
+ text_value = natal_period.text_value.strip
8
+ return '' if '' == text_value
9
+
10
+ ".#{text_value}"
11
+ end
12
+
13
+ def meta_data_item
14
+ {
15
+ "anomaly#{anomaly_type}.exists" => {
16
+ Canql::EQUALS => existance_modifier.text_value.strip != 'no'
17
+ }
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module BatchTypeNode
5
+ def to_type
6
+ string = respond_to?(:normalise) ? normalise : text_value
7
+ string.upcase
8
+ end
9
+ end
10
+
11
+ module PaediatricNode
12
+ def normalise
13
+ 'paediatric'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require 'chronic'
3
+ require 'ndr_support/daterange'
4
+
5
+ module Canql #:nodoc: all
6
+ module Nodes
7
+ module FuzzyDateNode
8
+ delegate :to_daterange, to: :date
9
+ end
10
+
11
+ module SpecificDateNode
12
+ delegate :to_daterange, to: :date_fragment
13
+ end
14
+
15
+ module FragmentedDateRangeNode
16
+ def to_daterange
17
+ d1 = start.to_daterange.date1
18
+ d2 = finish.to_daterange.date2
19
+
20
+ Daterange.new(d1, d2)
21
+ end
22
+ end
23
+
24
+ module YearQuarterNode
25
+ def to_daterange
26
+ quarter = text_value[0..1]
27
+ year = text_value[3..-1]
28
+ quarters = {
29
+ 'q1': "01-04-#{year}",
30
+ 'q2': "01-07-#{year}",
31
+ 'q3': "01-10-#{year}",
32
+ 'q4': "01-01-#{year.to_i + 1}"
33
+ }
34
+ Daterange.new(quarters[quarter.to_sym].to_date,
35
+ quarters[quarter.to_sym].to_date + 3.months - 1.day)
36
+ end
37
+ end
38
+
39
+ module DateFragmentNode
40
+ delegate :to_daterange, to: :fragment
41
+ end
42
+
43
+ module DateRangeNode
44
+ def to_daterange
45
+ Daterange.new(text_value.to_s)
46
+ end
47
+ end
48
+
49
+ module ChronicDateNode
50
+ def to_daterange
51
+ chronic = Chronic.parse(text_value.to_s, context: :past, guess: false)
52
+ if chronic.instance_of?(Chronic::Span)
53
+ Daterange.new(chronic.begin, chronic.end - 1.day)
54
+ else
55
+ Daterange.new(chronic)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module EBaseRecordsNode
5
+ def meta_data_item
6
+ filter =
7
+ if types.empty?
8
+ { Canql::ALL => true }
9
+ else
10
+ { Canql::EQUALS => types.to_list }
11
+ end
12
+ { 'unprocessed_records.sources' => filter }
13
+ end
14
+ end
15
+
16
+ module BatchTypesNode
17
+ delegate :to_list, to: :allowed_types
18
+ end
19
+
20
+ module AllowedTypesNode
21
+ def to_list
22
+ list = [batch_type.to_type]
23
+ list.concat types.elements.map(&:extract_type)
24
+ end
25
+ end
26
+
27
+ module MoreTypesNode
28
+ def extract_type
29
+ batch_type.to_type
30
+ end
31
+ end
32
+
33
+ module ActionsNode
34
+ def meta_data_item
35
+ { 'action.actioninitiated' => { Canql::EQUALS => action_type.text_value.upcase.strip } }
36
+ end
37
+ end
38
+
39
+ module ActionProviderCodeNode
40
+ def meta_data_item
41
+ # default to provider
42
+ key = 'providercode'
43
+ { "action.#{key}" => { Canql::EQUALS => code.text_value.upcase } }
44
+ end
45
+ end
46
+
47
+ module ActionProviderNameNode
48
+ def meta_data_item
49
+ # default to provider
50
+ key = provider_type.text_value == 'cancer network' ? 'cn_ukacrname' : 'providername'
51
+ {
52
+ "action.#{key}" => {
53
+ Canql::BEGINS => short_desc.text_value.upcase,
54
+ :interval => interval
55
+ }
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ # require 'active_support/core_ext/object/blank'
3
+
4
+ module Canql #:nodoc: all
5
+ module Nodes
6
+ module RecordCountNode
7
+ def meta_data_item
8
+ { 'limit' => { Canql::EQUALS => number.text_value.to_i } }
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module Patient
5
+ module GenderNode
6
+ def meta_data_item
7
+ { 'patient.sex' => { Canql::EQUALS => gender.text_value == 'male' ? '1' : '2' } }
8
+ end
9
+ end
10
+
11
+ module OutcomeNode
12
+ def meta_data_item
13
+ { 'patient.outcome' => { Canql::EQUALS => outcome.text_value } }
14
+ end
15
+ end
16
+
17
+ module FieldExists
18
+ FIELDS = {
19
+ 'date of birth': { patient: 'birthdate', mother: 'birthdate' },
20
+ 'dob': { patient: 'birthdate', mother: 'birthdate' },
21
+ 'postcode': { patient: 'postcode', mother: 'postcode' },
22
+ 'nhs number': { patient: 'nhsnumber', mother: 'nhsnumber' },
23
+ 'birth weight': { patient: 'weight' },
24
+ 'place of delivery': { patient: 'placeofdelivery' },
25
+ 'sex': { patient: 'sex' },
26
+ 'outcome': { patient: 'outcome' }
27
+ }.freeze
28
+
29
+ def meta_data_item
30
+ subject = reverse_scan_for_marker(:subject) == 'mother' ? 'mother' : 'patient'
31
+ field_names = actual_field_names(
32
+ patient_field_list.text_values_for_marker(:patient_field_name), subject.to_sym
33
+ )
34
+ modifer = field_existance_modifier.text_value.strip
35
+ existance = modifer != 'missing' ? 'fields_populated' : 'fields_missing'
36
+ { "#{subject}.#{existance}" => { Canql::EQUALS => field_names } }
37
+ end
38
+
39
+ def actual_field_names(fields, subject)
40
+ actual_fields = []
41
+ fields.each do |f|
42
+ f = f.downcase.to_sym
43
+ actual_fields << FIELDS[f][subject] unless FIELDS[f].nil? || FIELDS[f][subject].nil?
44
+ end
45
+ actual_fields
46
+ end
47
+ end
48
+
49
+ module ExpectedDateRangeNode
50
+ def meta_data_item
51
+ range = fuzzy_date.to_daterange
52
+ {
53
+ 'patient.expecteddeliverydate' => {
54
+ Canql::LIMITS => [
55
+ range.date1.try(:to_date).try(:iso8601),
56
+ range.date2.try(:to_date).try(:iso8601)
57
+ ]
58
+ }
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module RegistryNode
5
+ def meta_data_item
6
+ { 'patient.registry' => { Canql::EQUALS => registry.to_registrycode } }
7
+ end
8
+ end
9
+
10
+ module RegistryCodeNode
11
+ def to_registrycode
12
+ text_value.upcase
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ module Canql #:nodoc: all
3
+ module Nodes
4
+ module TestResult
5
+ module Exists
6
+ def test_result_type
7
+ text_value = natal_period.text_value.strip
8
+ return '' if '' == text_value
9
+
10
+ ".#{text_value}"
11
+ end
12
+
13
+ def meta_data_item
14
+ { "testresults#{test_result_type}.exists" => {
15
+ Canql::EQUALS => existance_modifier.text_value.strip != 'no'
16
+ } }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ require 'canql/nodes'
3
+ require 'canql/grammars'
4
+
5
+ module Canql
6
+ # This class simplifies CANQL queries by wrapping them in a little syntactic sugar.
7
+ class Parser
8
+ attr_reader :parser
9
+
10
+ def initialize(query)
11
+ raise ArgumentError unless query.is_a?(String)
12
+
13
+ @parser = CanqlParser.new
14
+ @result = @parser.parse(query.downcase)
15
+
16
+ return if valid?
17
+ # FIXME: should log "Parser failed parsing \"#{query}\": #{@parser.failure_reason} " \
18
+ # "(line: #{@parser.failure_line}, column: #{@parser.failure_column})"
19
+ end
20
+
21
+ def valid?
22
+ !@result.nil?
23
+ end
24
+
25
+ def failure_reason
26
+ valid? ? nil : @parser.failure_reason
27
+ end
28
+
29
+ def failure_line
30
+ valid? ? nil : @parser.failure_line
31
+ end
32
+
33
+ def failure_column
34
+ valid? ? nil : @parser.failure_column
35
+ end
36
+
37
+ def meta_data
38
+ valid? ? @result.meta_data : {}
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ module Treetop
3
+ module Runtime
4
+ # We extend the SyntaxNode class to include a hash of meta data.
5
+ # Individual rules can define meta_data_item method that must return
6
+ # a hash that is merged with the hash data for the entire query.
7
+ class SyntaxNode
8
+ def meta_data(hash = {})
9
+ hash.merge!(meta_data_item) if respond_to?(:meta_data_item)
10
+
11
+ if nonterminal?
12
+ elements.each do |element|
13
+ element.meta_data(hash)
14
+ end
15
+ end
16
+
17
+ hash
18
+ end
19
+
20
+ def text_values_for_marker(marker)
21
+ list = []
22
+ text_values_for_marker_scanner(self, marker, list)
23
+ list
24
+ end
25
+
26
+ def reverse_scan_for_marker(marker)
27
+ marker_value = reverse_marker_scanner(self, marker)
28
+ return if marker_value.nil? || marker_value.empty?
29
+ marker_value
30
+ end
31
+
32
+ private
33
+
34
+ def text_values_for_marker_scanner(root, marker, list)
35
+ return if root.elements.nil?
36
+ root.elements.each do |e|
37
+ list << e.send(marker).text_value if e.respond_to?(marker)
38
+ text_values_for_marker_scanner(e, marker, list)
39
+ end
40
+ list
41
+ end
42
+
43
+ def reverse_marker_scanner(root, marker)
44
+ return if root.nil? || root.elements.nil?
45
+ marker_value = root.send(marker).text_value if root.respond_to?(marker)
46
+ if marker_value.nil? || marker_value.empty?
47
+ marker_value = reverse_marker_scanner(root.parent, marker)
48
+ end
49
+ marker_value
50
+ end
51
+ end
52
+ end
53
+ end