openehr 2.0.2 → 2.3.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.
- checksums.yaml +4 -4
- data/README.rdoc +19 -6
- data/lib/openehr/am/archetype/constraint_model.rb +1 -1
- 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/assumed_library_types.rb +20 -17
- data/lib/openehr/parser/adl_parser.rb +0 -4
- data/lib/openehr/parser/opt_parser.rb +12 -429
- data/lib/openehr/parser/xml_archetype_parser.rb +255 -0
- data/lib/openehr/parser/xml_constraint_parsing.rb +264 -0
- data/lib/openehr/parser/xml_domain_type_parsing.rb +128 -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 +6 -9
- data/lib/openehr/rm/common/resource.rb +0 -1
- 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_structures/item_structure.rb +0 -1
- data/lib/openehr/rm/data_types/basic.rb +11 -21
- data/lib/openehr/rm/data_types/encapsulated.rb +18 -2
- data/lib/openehr/rm/data_types/quantity/date_time.rb +203 -28
- data/lib/openehr/rm/data_types/quantity.rb +66 -10
- data/lib/openehr/rm/data_types/time_specification.rb +101 -27
- data/lib/openehr/rm/data_types/uri.rb +0 -8
- data/lib/openehr/rm/ehr.rb +6 -6
- data/lib/openehr/rm/factory.rb +22 -0
- data/lib/openehr/rm/security.rb +3 -1
- data/lib/openehr/rm/type_name.rb +0 -1
- data/lib/openehr/serializer/adl_serializer.rb +239 -84
- data/lib/openehr/serializer/base.rb +2 -0
- data/lib/openehr/serializer/rm_json_serializer.rb +0 -1
- data/lib/openehr/serializer/xml_serializer.rb +351 -103
- data/lib/openehr/version.rb +1 -1
- metadata +9 -144
|
@@ -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,16 +186,13 @@ 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
|
|
197
|
-
attr_reader :reason
|
|
198
|
-
attr_accessor :proof, :attested_view, :is_pending
|
|
194
|
+
attr_reader :reason, :items
|
|
195
|
+
attr_accessor :proof, :attested_view, :is_pending
|
|
199
196
|
|
|
200
197
|
def initialize(args = { })
|
|
201
198
|
super(args)
|
|
@@ -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
|
|
@@ -25,20 +25,11 @@ module OpenEHR
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
class DvBoolean < DataValue
|
|
28
|
-
def initialize(args)
|
|
29
|
-
super(args)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
28
|
def value=(value)
|
|
33
29
|
raise ArgumentError, "value must not be nil" if value.nil?
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@value = false
|
|
38
|
-
elsif /TRUE/i =~ value
|
|
39
|
-
@value = true
|
|
40
|
-
elsif /FALSE/i =~ value
|
|
41
|
-
@value = false
|
|
30
|
+
case value
|
|
31
|
+
when true, /TRUE/i then @value = true
|
|
32
|
+
when false, /FALSE/i then @value = false
|
|
42
33
|
end
|
|
43
34
|
end
|
|
44
35
|
|
|
@@ -84,17 +75,17 @@ module OpenEHR
|
|
|
84
75
|
self.type = args[:type]
|
|
85
76
|
end
|
|
86
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.
|
|
87
80
|
def issuer=(issuer)
|
|
88
|
-
if issuer.nil?
|
|
89
|
-
|
|
90
|
-
end
|
|
81
|
+
raise ArgumentError, 'issuer must not be empty' if !issuer.nil? && issuer.empty?
|
|
82
|
+
|
|
91
83
|
@issuer = issuer
|
|
92
84
|
end
|
|
93
85
|
|
|
94
86
|
def assigner=(assigner)
|
|
95
|
-
if assigner.nil?
|
|
96
|
-
|
|
97
|
-
end
|
|
87
|
+
raise ArgumentError, 'assigner must not be empty' if !assigner.nil? && assigner.empty?
|
|
88
|
+
|
|
98
89
|
@assigner = assigner
|
|
99
90
|
end
|
|
100
91
|
|
|
@@ -106,9 +97,8 @@ module OpenEHR
|
|
|
106
97
|
end
|
|
107
98
|
|
|
108
99
|
def type=(type)
|
|
109
|
-
if type.nil?
|
|
110
|
-
|
|
111
|
-
end
|
|
100
|
+
raise ArgumentError, 'type must not be empty' if !type.nil? && type.empty?
|
|
101
|
+
|
|
112
102
|
@type = type
|
|
113
103
|
end
|
|
114
104
|
end #end of DvIdentifier
|
|
@@ -37,10 +37,10 @@ module OpenEHR
|
|
|
37
37
|
@charset = charset
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
private
|
|
41
|
-
|
|
42
40
|
CHARSET_LIST_PATH = File.expand_path('charset.lst', __dir__)
|
|
43
41
|
|
|
42
|
+
private
|
|
43
|
+
|
|
44
44
|
def charset_valid?(charset)
|
|
45
45
|
result = false
|
|
46
46
|
File.open(CHARSET_LIST_PATH) do |file|
|
|
@@ -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
|
|
@@ -55,20 +66,8 @@ module OpenEHR
|
|
|
55
66
|
else
|
|
56
67
|
past, future = self, other
|
|
57
68
|
end
|
|
58
|
-
year
|
|
59
|
-
|
|
60
|
-
day = future.day - past.day
|
|
61
|
-
else
|
|
62
|
-
month = -1
|
|
63
|
-
previous_month = future.month - 1
|
|
64
|
-
if previous_month == 0
|
|
65
|
-
previous_month = 12
|
|
66
|
-
end
|
|
67
|
-
day = DAYS_IN_MONTH[previous_month] + future.day - past.day
|
|
68
|
-
if leapyear?(future.year) && (previous_month == 2)
|
|
69
|
-
day += 1
|
|
70
|
-
end
|
|
71
|
-
end
|
|
69
|
+
year = 0
|
|
70
|
+
month, day = day_borrow(past, future)
|
|
72
71
|
week = day / 7
|
|
73
72
|
if (future.month >= past.month)
|
|
74
73
|
month += future.month - past.month
|
|
@@ -82,11 +81,48 @@ module OpenEHR
|
|
|
82
81
|
end
|
|
83
82
|
year += future.year - past.year
|
|
84
83
|
return DvDuration.new(:value =>
|
|
85
|
-
'P' + year.to_s + 'Y' + month.to_s + 'M' +
|
|
84
|
+
'P' + year.to_s + 'Y' + month.to_s + 'M' +
|
|
86
85
|
week.to_s + 'W' + day.to_s + 'D')
|
|
87
86
|
end
|
|
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
|
+
|
|
99
|
+
private
|
|
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
|
+
|
|
112
|
+
# Returns [month_adjustment, day] for diff: when future's
|
|
113
|
+
# day-of-month is behind past's, a month is borrowed from
|
|
114
|
+
# future's calendar to make day non-negative.
|
|
115
|
+
def day_borrow(past, future)
|
|
116
|
+
return [0, future.day - past.day] if future.day >= past.day
|
|
117
|
+
|
|
118
|
+
previous_month = future.month - 1
|
|
119
|
+
previous_month = 12 if previous_month == 0
|
|
120
|
+
day = DAYS_IN_MONTH[previous_month] + future.day - past.day
|
|
121
|
+
day += 1 if leapyear?(future.year) && previous_month == 2
|
|
122
|
+
[-1, day]
|
|
123
|
+
end
|
|
88
124
|
end
|
|
89
|
-
|
|
125
|
+
|
|
90
126
|
class DvTime < DvTemporal
|
|
91
127
|
include OpenEHR::AssumedLibraryTypes::ISO8601TimeModule
|
|
92
128
|
|
|
@@ -104,19 +140,19 @@ module OpenEHR
|
|
|
104
140
|
if @fractional_second.nil? && @second.nil? && @minute.nil?
|
|
105
141
|
return @hour * 60 * 60
|
|
106
142
|
elsif @fractional_second.nil? && @second.nil?
|
|
107
|
-
return @hour * 60 * 60 + @minute * 60
|
|
143
|
+
return (@hour * 60 * 60) + (@minute * 60)
|
|
108
144
|
elsif @fractional_second.nil?
|
|
109
|
-
return @hour * 60 * 60 + @minute * 60 + @second
|
|
145
|
+
return (@hour * 60 * 60) + (@minute * 60) + @second
|
|
110
146
|
else
|
|
111
|
-
return @hour*60*60
|
|
147
|
+
return (@hour*60*60)+(@minute* 60) + @second + @fractional_second
|
|
112
148
|
end
|
|
113
149
|
end
|
|
114
150
|
|
|
115
151
|
def diff(other)
|
|
116
152
|
diff = (other.magnitude - self.magnitude).abs
|
|
117
153
|
hour = (diff / 60 / 60).to_i
|
|
118
|
-
minute = ((diff - hour*60*60)/60).to_i
|
|
119
|
-
second = (diff - hour * 60 * 60 - minute * 60).to_i
|
|
154
|
+
minute = ((diff - (hour*60*60))/60).to_i
|
|
155
|
+
second = (diff - (hour * 60 * 60) - (minute * 60)).to_i
|
|
120
156
|
fractional_second = ((diff - diff.to_i)*1000000.0).to_i/1000000.0
|
|
121
157
|
str = 'P0Y0M0W0DT' + hour.to_s + 'H' +
|
|
122
158
|
minute.to_s + 'M' + second.to_s
|
|
@@ -127,6 +163,48 @@ module OpenEHR
|
|
|
127
163
|
end
|
|
128
164
|
return DvDuration.new(:value => str)
|
|
129
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
|
|
130
208
|
end
|
|
131
209
|
|
|
132
210
|
class DvDateTime < DvTemporal
|
|
@@ -156,11 +234,11 @@ module OpenEHR
|
|
|
156
234
|
minute += @timezone.minute
|
|
157
235
|
end
|
|
158
236
|
end
|
|
159
|
-
seconds = (((@year * 365.24 +
|
|
160
|
-
@month * 30.42 +
|
|
161
|
-
@day) * 24 +
|
|
162
|
-
hour) * 60 +
|
|
163
|
-
minute) * 60 + @second
|
|
237
|
+
seconds = (((((((@year * 365.24) +
|
|
238
|
+
(@month * 30.42) +
|
|
239
|
+
@day) * 24) +
|
|
240
|
+
hour) * 60) +
|
|
241
|
+
minute) * 60) + @second
|
|
164
242
|
if @fractional_second.nil?
|
|
165
243
|
return seconds
|
|
166
244
|
else
|
|
@@ -168,6 +246,9 @@ module OpenEHR
|
|
|
168
246
|
end
|
|
169
247
|
end
|
|
170
248
|
|
|
249
|
+
# rubocop:disable Metrics/AbcSize -- dense calendar arithmetic; refactoring
|
|
250
|
+
# without broader leap-year/borrow coverage (only 1 example exercises this
|
|
251
|
+
# method) is riskier than the lint value gained
|
|
171
252
|
def diff(other)
|
|
172
253
|
if self.magnitude >= other.magnitude
|
|
173
254
|
past, future = other, self
|
|
@@ -183,8 +264,8 @@ module OpenEHR
|
|
|
183
264
|
end
|
|
184
265
|
date_duration = past_date.diff(future_date)
|
|
185
266
|
hour = (time_diff / 60 / 60).to_i
|
|
186
|
-
minute = ((time_diff - hour*60*60)/60).to_i
|
|
187
|
-
second = (time_diff - hour * 60 * 60 - minute * 60).to_i
|
|
267
|
+
minute = ((time_diff - (hour*60*60))/60).to_i
|
|
268
|
+
second = (time_diff - (hour * 60 * 60) - (minute * 60)).to_i
|
|
188
269
|
str = date_duration.value + 'T' + hour.to_s + 'H' +
|
|
189
270
|
minute.to_s + 'M' + second.to_s
|
|
190
271
|
if @fractional_second.nil?
|
|
@@ -196,12 +277,57 @@ module OpenEHR
|
|
|
196
277
|
fractional_second.to_s[1..-1] + 'S')
|
|
197
278
|
end
|
|
198
279
|
end
|
|
280
|
+
# rubocop:enable Metrics/AbcSize
|
|
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
|
|
199
293
|
|
|
200
294
|
private
|
|
295
|
+
|
|
201
296
|
def split_date_time(date_time)
|
|
202
297
|
/^(.*)T(.*)$/ =~ date_time.as_string
|
|
203
298
|
return DvDate.new(:value => $1), DvTime.new(:value => $2)
|
|
204
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
|
|
205
331
|
end
|
|
206
332
|
|
|
207
333
|
class DvDuration < DvTemporal
|
|
@@ -254,6 +380,55 @@ module OpenEHR
|
|
|
254
380
|
text = value.start_with?('-') ? value[1..-1] : "-#{value}"
|
|
255
381
|
DvDuration.new(:value => text)
|
|
256
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
|
|
257
432
|
end
|
|
258
433
|
end # of DateTime
|
|
259
434
|
end # of Quantity
|