openehr 2.1.0 → 2.3.1
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.
- checksums.yaml +4 -4
- data/README.rdoc +19 -6
- data/lib/openehr/am/openehr_profile/data_types/text.rb +21 -0
- data/lib/openehr/aql/engine/contains_resolver.rb +98 -7
- data/lib/openehr/aql/engine/dataset.rb +3 -1
- data/lib/openehr/aql/engine/predicate_evaluator.rb +70 -3
- data/lib/openehr/aql/engine.rb +45 -12
- data/lib/openehr/parser/opt_parser.rb +12 -425
- data/lib/openehr/parser/xml_archetype_parser.rb +255 -0
- data/lib/openehr/parser/xml_constraint_parsing.rb +275 -0
- data/lib/openehr/parser/xml_domain_type_parsing.rb +142 -0
- data/lib/openehr/parser/xml_primitive_parsing.rb +145 -0
- data/lib/openehr/parser.rb +1 -0
- data/lib/openehr/rm/common/generic.rb +4 -7
- data/lib/openehr/rm/composition/content/entry.rb +4 -1
- data/lib/openehr/rm/data_structures/item_structure/representation.rb +12 -2
- data/lib/openehr/rm/data_types/basic.rb +8 -9
- data/lib/openehr/rm/data_types/encapsulated.rb +16 -0
- data/lib/openehr/rm/data_types/quantity/date_time.rb +168 -0
- data/lib/openehr/rm/data_types/quantity.rb +64 -9
- data/lib/openehr/rm/ehr.rb +6 -6
- data/lib/openehr/rm/factory.rb +4 -0
- data/lib/openehr/serializer/adl_serializer.rb +151 -14
- data/lib/openehr/serializer/xml_serializer.rb +345 -99
- data/lib/openehr/version.rb +1 -1
- metadata +6 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
module OpenEHR
|
|
2
|
+
module Parser
|
|
3
|
+
# The 8 C_PRIMITIVE readers (boolean/string/integer/real/date/
|
|
4
|
+
# date_time/time/duration), shared between OPTParser and
|
|
5
|
+
# XMLArchetypeParser - both read a <item xsi:type="C_*">-wrapped
|
|
6
|
+
# element with the same internal shape. Extracted from opt_parser.rb
|
|
7
|
+
# without behavior changes, except C_REAL's range now reads Float
|
|
8
|
+
# bounds instead of silently truncating them to Integer via
|
|
9
|
+
# numeric_interval(real: true) (see xml_constraint_parsing.rb).
|
|
10
|
+
module XMLPrimitiveParsing
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def c_date(xml)
|
|
14
|
+
pattern = xml.at('pattern')
|
|
15
|
+
range = xml.at('range')
|
|
16
|
+
if pattern
|
|
17
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDate.new(pattern: pattern.text)
|
|
18
|
+
elsif range
|
|
19
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDate.new(range: occurrences(range))
|
|
20
|
+
else
|
|
21
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDate.new
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def c_date_time(xml)
|
|
26
|
+
pattern = xml.at('pattern')
|
|
27
|
+
range = xml.at('range')
|
|
28
|
+
if pattern
|
|
29
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDateTime.new(pattern: pattern.text)
|
|
30
|
+
elsif range
|
|
31
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDateTime.new(range: occurrences(range))
|
|
32
|
+
else
|
|
33
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDateTime.new
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def c_integer(xml)
|
|
38
|
+
range = xml.at('range')
|
|
39
|
+
list = xml.xpath('list')
|
|
40
|
+
if range
|
|
41
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CInteger.new(range: occurrences(range))
|
|
42
|
+
elsif !list.empty?
|
|
43
|
+
list_values = list.map { |item| item.text.to_i }
|
|
44
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CInteger.new(list: list_values)
|
|
45
|
+
else
|
|
46
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CInteger.new
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def c_real(xml)
|
|
51
|
+
range = xml.at('range')
|
|
52
|
+
list = xml.xpath('list')
|
|
53
|
+
if range
|
|
54
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new(range: numeric_interval(range, real: true))
|
|
55
|
+
elsif !list.empty?
|
|
56
|
+
list_values = list.map { |item| item.text.to_f }
|
|
57
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new(list: list_values)
|
|
58
|
+
else
|
|
59
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def c_duration(xml)
|
|
64
|
+
pattern = xml.at('pattern')
|
|
65
|
+
range_xml = xml.at('range')
|
|
66
|
+
list = xml.xpath('list')
|
|
67
|
+
if pattern
|
|
68
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(pattern: pattern.text)
|
|
69
|
+
elsif range_xml
|
|
70
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(range: duration_range(range_xml))
|
|
71
|
+
elsif !list.empty?
|
|
72
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(list: list.map(&:text))
|
|
73
|
+
else
|
|
74
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A C_DURATION range's bounds are ISO8601 duration strings (e.g.
|
|
79
|
+
# PT24H), not plain numbers, so occurrences() (built for numeric
|
|
80
|
+
# Interval bounds) doesn't apply here; wrap each bound as a
|
|
81
|
+
# DV_DURATION instead, matching what CDuration#valid_value?
|
|
82
|
+
# already expects its range bounds to be.
|
|
83
|
+
def duration_range(range_xml)
|
|
84
|
+
lower = duration_bound(range_xml.at('lower'), range_xml.at('lower_unbounded'))
|
|
85
|
+
upper = duration_bound(range_xml.at('upper'), range_xml.at('upper_unbounded'))
|
|
86
|
+
return nil if lower.nil? && upper.nil?
|
|
87
|
+
|
|
88
|
+
OpenEHR::AssumedLibraryTypes::Interval.new(
|
|
89
|
+
lower: lower, upper: upper,
|
|
90
|
+
lower_included: lower.nil? ? nil : bool_node(range_xml.at('lower_included'), true),
|
|
91
|
+
upper_included: upper.nil? ? nil : bool_node(range_xml.at('upper_included'), true))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def duration_bound(value_node, unbounded_node)
|
|
95
|
+
return nil if bool_node(unbounded_node, false)
|
|
96
|
+
return nil if value_node.nil? || value_node.text.empty?
|
|
97
|
+
|
|
98
|
+
OpenEHR::RM::DataTypes::Quantity::DateTime::DvDuration.new(value: value_node.text)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def bool_node(node, default)
|
|
102
|
+
node ? to_bool(node.text) : default
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def c_time(xml)
|
|
106
|
+
pattern = xml.at('pattern')
|
|
107
|
+
range = xml.at('range')
|
|
108
|
+
if pattern
|
|
109
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new(pattern: pattern.text)
|
|
110
|
+
elsif range
|
|
111
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new(range: occurrences(range))
|
|
112
|
+
else
|
|
113
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def c_boolean(xml)
|
|
118
|
+
true_valid = xml.at('true_valid')
|
|
119
|
+
false_valid = xml.at('false_valid')
|
|
120
|
+
assumed_value = xml.at('assumed_value')
|
|
121
|
+
|
|
122
|
+
true_valid_value = true_valid ? to_bool(true_valid.text) : nil
|
|
123
|
+
false_valid_value = false_valid ? to_bool(false_valid.text) : nil
|
|
124
|
+
assumed_value_value = assumed_value ? to_bool(assumed_value.text) : nil
|
|
125
|
+
|
|
126
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CBoolean.new(
|
|
127
|
+
true_valid: true_valid_value,
|
|
128
|
+
false_valid: false_valid_value,
|
|
129
|
+
assumed_value: assumed_value_value
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def c_string(attr_xml)
|
|
134
|
+
if attr_xml.at('pattern')
|
|
135
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CString.new(pattern: attr_xml.at('pattern').text)
|
|
136
|
+
else
|
|
137
|
+
list = attr_xml.xpath('.//list').map do |str|
|
|
138
|
+
str.text
|
|
139
|
+
end
|
|
140
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CString.new(list: list)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
data/lib/openehr/parser.rb
CHANGED
|
@@ -167,8 +167,8 @@ module OpenEHR
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
class Participation
|
|
170
|
-
attr_reader :performer, :function
|
|
171
|
-
attr_accessor :time
|
|
170
|
+
attr_reader :performer, :function
|
|
171
|
+
attr_accessor :time, :mode
|
|
172
172
|
|
|
173
173
|
def initialize(args ={ })
|
|
174
174
|
self.performer = args[:performer]
|
|
@@ -186,11 +186,8 @@ module OpenEHR
|
|
|
186
186
|
raise ArgumentError, 'function is mandatory' if function.nil?
|
|
187
187
|
@function = function
|
|
188
188
|
end
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
raise ArgumentError, 'mode is mandatory' if mode.nil?
|
|
192
|
-
@mode = mode
|
|
193
|
-
end
|
|
189
|
+
# mode is optional (0..1) since RM 1.0.4 - attr_accessor above
|
|
190
|
+
# covers it, no validation needed.
|
|
194
191
|
end
|
|
195
192
|
|
|
196
193
|
class Attestation < AuditDetails
|
|
@@ -258,9 +258,12 @@ module OpenEHR
|
|
|
258
258
|
@current_state = current_state
|
|
259
259
|
end
|
|
260
260
|
|
|
261
|
+
# transition is optional (0..1) since RM 1.1.0; when present
|
|
262
|
+
# its code is still validated against the terminology.
|
|
261
263
|
def transition=(transition)
|
|
262
264
|
if transition.nil?
|
|
263
|
-
|
|
265
|
+
@transition = nil
|
|
266
|
+
return
|
|
264
267
|
end
|
|
265
268
|
unless OpenEHR::TerminologyService.has_code_for_group?('ism transition careflow transition', transition.defining_code.code_string)
|
|
266
269
|
raise ArgumentError, 'transition code is invalid'
|
|
@@ -21,12 +21,13 @@ module OpenEHR
|
|
|
21
21
|
NULL_FLAVOUR_CODES = %w[271 272 273 253].freeze
|
|
22
22
|
|
|
23
23
|
attr_accessor :value
|
|
24
|
-
attr_reader :null_flavor
|
|
25
|
-
path_attribute :value, :null_flavour
|
|
24
|
+
attr_reader :null_flavor, :null_reason
|
|
25
|
+
path_attribute :value, :null_flavour, :null_reason
|
|
26
26
|
def initialize(args = {})
|
|
27
27
|
super(args)
|
|
28
28
|
self.value = args[:value]
|
|
29
29
|
self.null_flavor= args[:null_flavor]
|
|
30
|
+
self.null_reason = args[:null_reason]
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def null_flavor=(null_flavor)
|
|
@@ -40,6 +41,15 @@ module OpenEHR
|
|
|
40
41
|
alias null_flavour null_flavor
|
|
41
42
|
alias null_flavour= null_flavor=
|
|
42
43
|
|
|
44
|
+
# null_reason is DV_TEXT [0..1] since RM 1.1.0. Invariant: if
|
|
45
|
+
# set, null_flavor must also be set.
|
|
46
|
+
def null_reason=(null_reason)
|
|
47
|
+
if !null_reason.nil? && @null_flavor.nil?
|
|
48
|
+
raise ArgumentError, 'null_reason requires null_flavor to be set'
|
|
49
|
+
end
|
|
50
|
+
@null_reason = null_reason
|
|
51
|
+
end
|
|
52
|
+
|
|
43
53
|
def is_null?
|
|
44
54
|
return @value.nil?
|
|
45
55
|
end
|
|
@@ -75,17 +75,17 @@ module OpenEHR
|
|
|
75
75
|
self.type = args[:type]
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# issuer/assigner/type are optional (0..1) since RM 1.0.4 - only
|
|
79
|
+
# id is mandatory. A non-nil value must still not be empty.
|
|
78
80
|
def issuer=(issuer)
|
|
79
|
-
if issuer.nil?
|
|
80
|
-
|
|
81
|
-
end
|
|
81
|
+
raise ArgumentError, 'issuer must not be empty' if !issuer.nil? && issuer.empty?
|
|
82
|
+
|
|
82
83
|
@issuer = issuer
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
def assigner=(assigner)
|
|
86
|
-
if assigner.nil?
|
|
87
|
-
|
|
88
|
-
end
|
|
87
|
+
raise ArgumentError, 'assigner must not be empty' if !assigner.nil? && assigner.empty?
|
|
88
|
+
|
|
89
89
|
@assigner = assigner
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -97,9 +97,8 @@ module OpenEHR
|
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
def type=(type)
|
|
100
|
-
if type.nil?
|
|
101
|
-
|
|
102
|
-
end
|
|
100
|
+
raise ArgumentError, 'type must not be empty' if !type.nil? && type.empty?
|
|
101
|
+
|
|
103
102
|
@type = type
|
|
104
103
|
end
|
|
105
104
|
end #end of DvIdentifier
|
|
@@ -65,6 +65,7 @@ module OpenEHR
|
|
|
65
65
|
def initialize(args = {})
|
|
66
66
|
super(args)
|
|
67
67
|
self.media_type = args[:media_type]
|
|
68
|
+
self.size = args[:size]
|
|
68
69
|
self.uri = args[:uri]
|
|
69
70
|
self.data = args[:data]
|
|
70
71
|
self.compression_algorithm = args[:compression_algorithm]
|
|
@@ -81,6 +82,21 @@ module OpenEHR
|
|
|
81
82
|
@media_type = media_type
|
|
82
83
|
end
|
|
83
84
|
|
|
85
|
+
# size is Integer [1..1] since RM 1.1.0 (size in bytes of the raw
|
|
86
|
+
# data content). Left unset (nil), it falls back to the inherited
|
|
87
|
+
# DvEncapsulated#size (@value.size) for backward compatibility.
|
|
88
|
+
def size
|
|
89
|
+
@size.nil? ? super : @size
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def size=(size)
|
|
93
|
+
unless size.nil?
|
|
94
|
+
raise ArgumentError, 'size must be an Integer' unless size.is_a?(Integer)
|
|
95
|
+
raise ArgumentError, 'size must not be negative' if size.negative?
|
|
96
|
+
end
|
|
97
|
+
@size = size
|
|
98
|
+
end
|
|
99
|
+
|
|
84
100
|
def is_external?
|
|
85
101
|
!@uri.nil?
|
|
86
102
|
end
|
|
@@ -30,6 +30,17 @@ module OpenEHR
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
undef magnitude=
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Shared guard for add/subtract on DvDate/DvTime/DvDateTime:
|
|
37
|
+
# DvAbsoluteQuantity#add/subtract assume magnitude=-based
|
|
38
|
+
# reconstruction, which DvTemporal subclasses can't support
|
|
39
|
+
# (see the undef above), so each subclass implements its own
|
|
40
|
+
# add/subtract against a DvDuration directly instead.
|
|
41
|
+
def validate_duration_operand(a_diff)
|
|
42
|
+
raise ArgumentError, 'a_diff must be a DvDuration' unless a_diff.is_a?(DvDuration)
|
|
43
|
+
end
|
|
33
44
|
end
|
|
34
45
|
|
|
35
46
|
class DvDate < DvTemporal
|
|
@@ -74,8 +85,30 @@ module OpenEHR
|
|
|
74
85
|
week.to_s + 'W' + day.to_s + 'D')
|
|
75
86
|
end
|
|
76
87
|
|
|
88
|
+
def add(a_diff)
|
|
89
|
+
validate_duration_operand(a_diff)
|
|
90
|
+
raise ArgumentError, 'a_diff must not have a time component' if time_component?(a_diff)
|
|
91
|
+
|
|
92
|
+
DvDate.new(:value => shift_by(a_diff).iso8601)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def subtract(a_diff)
|
|
96
|
+
add(-a_diff)
|
|
97
|
+
end
|
|
98
|
+
|
|
77
99
|
private
|
|
78
100
|
|
|
101
|
+
def time_component?(a_diff)
|
|
102
|
+
[a_diff.hours, a_diff.minutes, a_diff.seconds].any? { |v| v && !v.zero? }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def shift_by(a_diff)
|
|
106
|
+
sign = a_diff.negative? ? -1 : 1
|
|
107
|
+
months = (((a_diff.years || 0) * MONTH_IN_YEAR) + (a_diff.months || 0)) * sign
|
|
108
|
+
days = (((a_diff.weeks || 0) * DAYS_IN_WEEK) + (a_diff.days || 0)) * sign
|
|
109
|
+
(Date.new(@year, @month, @day) >> months) + days
|
|
110
|
+
end
|
|
111
|
+
|
|
79
112
|
# Returns [month_adjustment, day] for diff: when future's
|
|
80
113
|
# day-of-month is behind past's, a month is borrowed from
|
|
81
114
|
# future's calendar to make day non-negative.
|
|
@@ -130,6 +163,48 @@ module OpenEHR
|
|
|
130
163
|
end
|
|
131
164
|
return DvDuration.new(:value => str)
|
|
132
165
|
end
|
|
166
|
+
|
|
167
|
+
def add(a_diff)
|
|
168
|
+
validate_duration_operand(a_diff)
|
|
169
|
+
raise ArgumentError, 'a_diff must not have a date component' if date_component?(a_diff)
|
|
170
|
+
|
|
171
|
+
DvTime.new(:value => shifted_time_string(a_diff))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def subtract(a_diff)
|
|
175
|
+
add(-a_diff)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def date_component?(a_diff)
|
|
181
|
+
[a_diff.years, a_diff.months, a_diff.weeks, a_diff.days].any? { |v| v && !v.zero? }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def duration_seconds(a_diff)
|
|
185
|
+
((a_diff.hours || 0) * MINUTES_IN_HOUR * SECONDS_IN_MINUTE) +
|
|
186
|
+
((a_diff.minutes || 0) * SECONDS_IN_MINUTE) +
|
|
187
|
+
(a_diff.seconds || 0) +
|
|
188
|
+
(a_diff.fractional_second || 0)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def shifted_time_string(a_diff)
|
|
192
|
+
sign = a_diff.negative? ? -1 : 1
|
|
193
|
+
day_seconds = HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE
|
|
194
|
+
total = (magnitude + (sign * duration_seconds(a_diff))) % day_seconds
|
|
195
|
+
formatted_time(total) + timezone
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def formatted_time(total_seconds)
|
|
199
|
+
hour = (total_seconds / (MINUTES_IN_HOUR * SECONDS_IN_MINUTE)).to_i
|
|
200
|
+
remainder = total_seconds - (hour * MINUTES_IN_HOUR * SECONDS_IN_MINUTE)
|
|
201
|
+
minute = (remainder / SECONDS_IN_MINUTE).to_i
|
|
202
|
+
second_with_fraction = remainder - (minute * SECONDS_IN_MINUTE)
|
|
203
|
+
second = second_with_fraction.to_i
|
|
204
|
+
fractional = (second_with_fraction - second).round(6)
|
|
205
|
+
str = format('%02d:%02d:%02d', hour, minute, second)
|
|
206
|
+
fractional.zero? ? str : str + fractional.to_s[1..]
|
|
207
|
+
end
|
|
133
208
|
end
|
|
134
209
|
|
|
135
210
|
class DvDateTime < DvTemporal
|
|
@@ -204,11 +279,55 @@ module OpenEHR
|
|
|
204
279
|
end
|
|
205
280
|
# rubocop:enable Metrics/AbcSize
|
|
206
281
|
|
|
282
|
+
def add(a_diff)
|
|
283
|
+
validate_duration_operand(a_diff)
|
|
284
|
+
|
|
285
|
+
shifted_date = date_part_shifted_by(a_diff)
|
|
286
|
+
day_carry, wrapped_seconds = time_of_day_seconds_shifted_by(a_diff).divmod(day_seconds)
|
|
287
|
+
DvDateTime.new(:value => combine(shifted_date + day_carry, wrapped_seconds) + timezone)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def subtract(a_diff)
|
|
291
|
+
add(-a_diff)
|
|
292
|
+
end
|
|
293
|
+
|
|
207
294
|
private
|
|
295
|
+
|
|
208
296
|
def split_date_time(date_time)
|
|
209
297
|
/^(.*)T(.*)$/ =~ date_time.as_string
|
|
210
298
|
return DvDate.new(:value => $1), DvTime.new(:value => $2)
|
|
211
299
|
end
|
|
300
|
+
|
|
301
|
+
def day_seconds
|
|
302
|
+
HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def date_part_shifted_by(a_diff)
|
|
306
|
+
sign = a_diff.negative? ? -1 : 1
|
|
307
|
+
months = (((a_diff.years || 0) * MONTH_IN_YEAR) + (a_diff.months || 0)) * sign
|
|
308
|
+
days = (((a_diff.weeks || 0) * DAYS_IN_WEEK) + (a_diff.days || 0)) * sign
|
|
309
|
+
(Date.new(@year, @month, @day) >> months) + days
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def time_of_day_seconds_shifted_by(a_diff)
|
|
313
|
+
sign = a_diff.negative? ? -1 : 1
|
|
314
|
+
own_seconds = (@hour * MINUTES_IN_HOUR * SECONDS_IN_MINUTE) + (@minute * SECONDS_IN_MINUTE) +
|
|
315
|
+
@second + (@fractional_second || 0)
|
|
316
|
+
diff_seconds = ((a_diff.hours || 0) * MINUTES_IN_HOUR * SECONDS_IN_MINUTE) +
|
|
317
|
+
((a_diff.minutes || 0) * SECONDS_IN_MINUTE) + (a_diff.seconds || 0) + (a_diff.fractional_second || 0)
|
|
318
|
+
own_seconds + (sign * diff_seconds)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def combine(date, total_seconds)
|
|
322
|
+
hour = (total_seconds / (MINUTES_IN_HOUR * SECONDS_IN_MINUTE)).to_i
|
|
323
|
+
remainder = total_seconds - (hour * MINUTES_IN_HOUR * SECONDS_IN_MINUTE)
|
|
324
|
+
minute = (remainder / SECONDS_IN_MINUTE).to_i
|
|
325
|
+
second_with_fraction = remainder - (minute * SECONDS_IN_MINUTE)
|
|
326
|
+
second = second_with_fraction.to_i
|
|
327
|
+
fractional = (second_with_fraction - second).round(6)
|
|
328
|
+
str = date.iso8601 + format('T%02d:%02d:%02d', hour, minute, second)
|
|
329
|
+
fractional.zero? ? str : str + fractional.to_s[1..]
|
|
330
|
+
end
|
|
212
331
|
end
|
|
213
332
|
|
|
214
333
|
class DvDuration < DvTemporal
|
|
@@ -261,6 +380,55 @@ module OpenEHR
|
|
|
261
380
|
text = value.start_with?('-') ? value[1..-1] : "-#{value}"
|
|
262
381
|
DvDuration.new(:value => text)
|
|
263
382
|
end
|
|
383
|
+
|
|
384
|
+
# DV_DURATION is a DV_AMOUNT in the openEHR RM even though it
|
|
385
|
+
# inherits DvAbsoluteQuantity's add/subtract/diff method names
|
|
386
|
+
# here (see DvTemporal); those assume magnitude=-based
|
|
387
|
+
# reconstruction, which DvTemporal subclasses can't support,
|
|
388
|
+
# so +/-/multiply are implemented directly instead, going
|
|
389
|
+
# through magnitude (already sign-aware and nominal-month
|
|
390
|
+
# aware) and reconstructing via an ISO8601 string.
|
|
391
|
+
def +(other)
|
|
392
|
+
raise ArgumentError, 'type mismatch' unless is_strictly_comparable_to?(other)
|
|
393
|
+
|
|
394
|
+
self.class.from_seconds(magnitude + other.magnitude)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def -(other)
|
|
398
|
+
raise ArgumentError, 'type mismatch' unless is_strictly_comparable_to?(other)
|
|
399
|
+
|
|
400
|
+
self.class.from_seconds(magnitude - other.magnitude)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def multiply(factor)
|
|
404
|
+
raise ArgumentError, 'factor must be Numeric' unless factor.is_a?(Numeric)
|
|
405
|
+
|
|
406
|
+
self.class.from_seconds(magnitude * factor)
|
|
407
|
+
end
|
|
408
|
+
alias * multiply
|
|
409
|
+
|
|
410
|
+
def self.from_seconds(total_seconds)
|
|
411
|
+
sign = total_seconds.negative? ? '-' : ''
|
|
412
|
+
days, hours, minutes, seconds_part = duration_components(total_seconds.abs)
|
|
413
|
+
new(:value => "#{sign}P0Y0M0W#{days}DT#{hours}H#{minutes}M#{seconds_part}S")
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def self.duration_components(total_seconds)
|
|
417
|
+
days, remainder = total_seconds.divmod(HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE)
|
|
418
|
+
hours, remainder = remainder.divmod(MINUTES_IN_HOUR * SECONDS_IN_MINUTE)
|
|
419
|
+
minutes, remainder = remainder.divmod(SECONDS_IN_MINUTE)
|
|
420
|
+
[days, hours, minutes, seconds_with_fraction(remainder)]
|
|
421
|
+
end
|
|
422
|
+
private_class_method :duration_components
|
|
423
|
+
|
|
424
|
+
def self.seconds_with_fraction(remainder)
|
|
425
|
+
seconds = remainder.to_i
|
|
426
|
+
fractional = (remainder - seconds).round(6)
|
|
427
|
+
seconds += 1 if fractional >= 1.0
|
|
428
|
+
fractional = 0.0 if fractional >= 1.0
|
|
429
|
+
fractional.zero? ? seconds.to_s : "#{seconds}#{fractional.to_s[1..]}"
|
|
430
|
+
end
|
|
431
|
+
private_class_method :seconds_with_fraction
|
|
264
432
|
end
|
|
265
433
|
end # of DateTime
|
|
266
434
|
end # of Quantity
|
|
@@ -237,23 +237,27 @@ module OpenEHR
|
|
|
237
237
|
unless self.is_strictly_comparable_to? other
|
|
238
238
|
raise ArgumentError, 'type mismatch'
|
|
239
239
|
end
|
|
240
|
-
|
|
241
|
-
result.magnitude = @magnitude + other.magnitude
|
|
242
|
-
return result
|
|
240
|
+
build_arithmetic_result(magnitude + other.magnitude)
|
|
243
241
|
end
|
|
244
242
|
|
|
245
243
|
def -(other)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
244
|
+
unless self.is_strictly_comparable_to? other
|
|
245
|
+
raise ArgumentError, 'type mismatch'
|
|
246
|
+
end
|
|
247
|
+
build_arithmetic_result(magnitude - other.magnitude)
|
|
249
248
|
end
|
|
250
249
|
|
|
251
250
|
def -@
|
|
252
|
-
|
|
253
|
-
negated.magnitude = -magnitude
|
|
254
|
-
negated
|
|
251
|
+
build_arithmetic_result(-magnitude)
|
|
255
252
|
end
|
|
256
253
|
|
|
254
|
+
def multiply(factor)
|
|
255
|
+
raise ArgumentError, 'factor must be Numeric' unless factor.is_a?(Numeric)
|
|
256
|
+
|
|
257
|
+
build_arithmetic_result(magnitude * factor)
|
|
258
|
+
end
|
|
259
|
+
alias * multiply
|
|
260
|
+
|
|
257
261
|
def set_accuracy(accuracy, accuracy_percent)
|
|
258
262
|
if accuracy_percent
|
|
259
263
|
raise ArgumentError, 'accuracy invalid' unless self.class.valid_percentage(accuracy)
|
|
@@ -270,6 +274,20 @@ module OpenEHR
|
|
|
270
274
|
def self.valid_percentage(number)
|
|
271
275
|
number >= 0.0 && number <= 100.0
|
|
272
276
|
end
|
|
277
|
+
|
|
278
|
+
private
|
|
279
|
+
|
|
280
|
+
# Overridable hook used by +/-/-@/multiply: reconstructs the
|
|
281
|
+
# result from a new magnitude via dup + magnitude=. Works for
|
|
282
|
+
# any subclass whose magnitude is ivar-backed (DvQuantity,
|
|
283
|
+
# DvCount). DvProportion's magnitude is derived from
|
|
284
|
+
# numerator/denominator instead, so it overrides +/-@/multiply
|
|
285
|
+
# directly rather than relying on this hook.
|
|
286
|
+
def build_arithmetic_result(new_magnitude)
|
|
287
|
+
result = dup
|
|
288
|
+
result.magnitude = new_magnitude
|
|
289
|
+
result
|
|
290
|
+
end
|
|
273
291
|
end
|
|
274
292
|
|
|
275
293
|
class DvQuantity < DvAmount
|
|
@@ -446,6 +464,43 @@ module OpenEHR
|
|
|
446
464
|
return false
|
|
447
465
|
end
|
|
448
466
|
end
|
|
467
|
+
|
|
468
|
+
# DvProportion's magnitude is derived from numerator/denominator
|
|
469
|
+
# rather than ivar-backed, so it cannot use DvAmount's default
|
|
470
|
+
# build_arithmetic_result (magnitude=-based reconstruction) -
|
|
471
|
+
# +/-@/multiply are overridden here at the numerator/denominator
|
|
472
|
+
# level instead. Only same-denominator addition/subtraction is
|
|
473
|
+
# well-defined without inventing a fraction-normalization
|
|
474
|
+
# policy beyond what the RM specifies.
|
|
475
|
+
def +(other)
|
|
476
|
+
unless is_strictly_comparable_to?(other) && denominator == other.denominator
|
|
477
|
+
raise ArgumentError, 'type mismatch: proportions must share type and denominator'
|
|
478
|
+
end
|
|
479
|
+
build_arithmetic_result(numerator + other.numerator)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def -(other)
|
|
483
|
+
self + (-other)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def -@
|
|
487
|
+
build_arithmetic_result(-numerator)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def multiply(factor)
|
|
491
|
+
raise ArgumentError, 'factor must be Numeric' unless factor.is_a?(Numeric)
|
|
492
|
+
|
|
493
|
+
build_arithmetic_result(numerator * factor)
|
|
494
|
+
end
|
|
495
|
+
alias * multiply
|
|
496
|
+
|
|
497
|
+
private
|
|
498
|
+
|
|
499
|
+
def build_arithmetic_result(new_numerator)
|
|
500
|
+
result = dup
|
|
501
|
+
result.numerator = new_numerator
|
|
502
|
+
result
|
|
503
|
+
end
|
|
449
504
|
end # end of DvProportion
|
|
450
505
|
end # of Quantity
|
|
451
506
|
end # of Data_Types
|
data/lib/openehr/rm/ehr.rb
CHANGED
|
@@ -45,6 +45,8 @@ module OpenEHR
|
|
|
45
45
|
@time_created = time_created
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# contributions is optional (0..1) since RM 1.1.0 (was mandatory
|
|
49
|
+
# pre-1.1.0).
|
|
48
50
|
def contributions=(contributions)
|
|
49
51
|
unless contributions.nil?
|
|
50
52
|
contributions.each do |contrib|
|
|
@@ -52,10 +54,8 @@ module OpenEHR
|
|
|
52
54
|
raise ArgumentError, 'contribution type should be CONTRIBUTION'
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
|
-
@contributions = contributions
|
|
56
|
-
else
|
|
57
|
-
raise ArgumentError, 'contributions are mandatory'
|
|
58
57
|
end
|
|
58
|
+
@contributions = contributions
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def ehr_access=(ehr_access)
|
|
@@ -72,6 +72,8 @@ module OpenEHR
|
|
|
72
72
|
@ehr_status = ehr_status
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# compositions is optional (0..1) since RM 1.1.0 (was mandatory
|
|
76
|
+
# pre-1.1.0).
|
|
75
77
|
def compositions=(compositions)
|
|
76
78
|
unless compositions.nil?
|
|
77
79
|
compositions.each do |compo|
|
|
@@ -79,10 +81,8 @@ module OpenEHR
|
|
|
79
81
|
raise ArgumentError, 'composition type should be VERSIONED_COMPOSITION'
|
|
80
82
|
end
|
|
81
83
|
end
|
|
82
|
-
@compositions = compositions
|
|
83
|
-
else
|
|
84
|
-
raise ArgumentError, 'compositions are mandatory'
|
|
85
84
|
end
|
|
85
|
+
@compositions = compositions
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def directory=(directory)
|
data/lib/openehr/rm/factory.rb
CHANGED
|
@@ -597,6 +597,10 @@ module OpenEHR
|
|
|
597
597
|
|
|
598
598
|
class CompositionFactory < Factory
|
|
599
599
|
class << self
|
|
600
|
+
def create(param)
|
|
601
|
+
OpenEHR::RM::Composition::Composition.new(param)
|
|
602
|
+
end
|
|
603
|
+
|
|
600
604
|
def create_from_json(json)
|
|
601
605
|
hash = JSON.parse(json, max_nesting: false, symbolize_names: true)
|
|
602
606
|
OpenEHR::RM::Composition::Composition.new(params(hash))
|