openehr 1.3.0 → 2.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +93 -12
  3. data/lib/openehr/am/archetype/constraint_model/primitive.rb +204 -6
  4. data/lib/openehr/am/archetype/constraint_model.rb +228 -4
  5. data/lib/openehr/am/archetype/ontology.rb +2 -2
  6. data/lib/openehr/am/archetype.rb +101 -2
  7. data/lib/openehr/am/openehr_profile/data_types/basic.rb +28 -0
  8. data/lib/openehr/am/openehr_profile/data_types/quantity.rb +88 -0
  9. data/lib/openehr/am/openehr_profile/data_types/text.rb +23 -0
  10. data/lib/openehr/am/template.rb +25 -3
  11. data/lib/openehr/aql/engine/binding.rb +13 -0
  12. data/lib/openehr/aql/engine/contains_resolver.rb +161 -0
  13. data/lib/openehr/aql/engine/dataset.rb +122 -0
  14. data/lib/openehr/aql/engine/path_evaluator.rb +137 -0
  15. data/lib/openehr/aql/engine/predicate_evaluator.rb +65 -0
  16. data/lib/openehr/aql/engine.rb +119 -0
  17. data/lib/openehr/aql/errors.rb +27 -0
  18. data/lib/openehr/aql/lexer.rb +295 -0
  19. data/lib/openehr/aql/model/containment.rb +45 -0
  20. data/lib/openehr/aql/model/from_clause.rb +35 -0
  21. data/lib/openehr/aql/model/function_call.rb +56 -0
  22. data/lib/openehr/aql/model/identified_path.rb +20 -0
  23. data/lib/openehr/aql/model/literal.rb +15 -0
  24. data/lib/openehr/aql/model/object_path.rb +34 -0
  25. data/lib/openehr/aql/model/order_by_and_limit.rb +50 -0
  26. data/lib/openehr/aql/model/predicate.rb +84 -0
  27. data/lib/openehr/aql/model/query.rb +24 -0
  28. data/lib/openehr/aql/model/select_clause.rb +29 -0
  29. data/lib/openehr/aql/model/where_clause.rb +105 -0
  30. data/lib/openehr/aql/model.rb +14 -0
  31. data/lib/openehr/aql/parser.rb +484 -0
  32. data/lib/openehr/aql/result_set.rb +54 -0
  33. data/lib/openehr/aql.rb +28 -0
  34. data/lib/openehr/assumed_library_types.rb +68 -17
  35. data/lib/openehr/parser/adl_grammar.tt +29 -13
  36. data/lib/openehr/parser/adl_parser.rb +18 -4
  37. data/lib/openehr/parser/archetype_validator.rb +126 -0
  38. data/lib/openehr/parser/exception.rb +13 -0
  39. data/lib/openehr/parser/opt_parser.rb +162 -9
  40. data/lib/openehr/parser.rb +2 -0
  41. data/lib/openehr/path.rb +195 -0
  42. data/lib/openehr/rm/common/archetyped.rb +116 -6
  43. data/lib/openehr/rm/common/change_control.rb +3 -8
  44. data/lib/openehr/rm/common/directory.rb +4 -0
  45. data/lib/openehr/rm/common/generic.rb +24 -10
  46. data/lib/openehr/rm/composition/content/entry.rb +24 -5
  47. data/lib/openehr/rm/composition/content/navigation.rb +2 -1
  48. data/lib/openehr/rm/composition.rb +19 -2
  49. data/lib/openehr/rm/data_structures/history.rb +3 -0
  50. data/lib/openehr/rm/data_structures/item_structure/representation.rb +13 -9
  51. data/lib/openehr/rm/data_structures/item_structure.rb +28 -15
  52. data/lib/openehr/rm/data_types/encapsulated.rb +23 -3
  53. data/lib/openehr/rm/data_types/quantity/date_time.rb +9 -1
  54. data/lib/openehr/rm/data_types/quantity.rb +66 -14
  55. data/lib/openehr/rm/data_types/text.rb +8 -0
  56. data/lib/openehr/rm/data_types/time_specification.rb +5 -4
  57. data/lib/openehr/rm/demographic.rb +4 -3
  58. data/lib/openehr/rm/ehr.rb +20 -1
  59. data/lib/openehr/rm/factory.rb +234 -6
  60. data/lib/openehr/rm/integration.rb +1 -0
  61. data/lib/openehr/rm/support/identification.rb +37 -8
  62. data/lib/openehr/rm/support/measurement.rb +32 -0
  63. data/lib/openehr/rm/type_name.rb +90 -0
  64. data/lib/openehr/rm.rb +3 -0
  65. data/lib/openehr/serializer/adl_serializer.rb +335 -0
  66. data/lib/openehr/serializer/base.rb +20 -0
  67. data/lib/openehr/serializer/opt_serializer.rb +49 -0
  68. data/lib/openehr/serializer/rm_json_serializer.rb +62 -0
  69. data/lib/openehr/serializer/xml_serializer.rb +260 -0
  70. data/lib/openehr/serializer.rb +5 -291
  71. data/lib/openehr/terminology_service.rb +44 -0
  72. data/lib/openehr/version.rb +1 -1
  73. data/lib/openehr.rb +5 -2
  74. metadata +35 -7
  75. data/lib/openehr/parser/validator.rb +0 -18
  76. data/lib/openehr/parser/xml_parser.rb +0 -13
  77. data/lib/openehr/rm/data_types/charset_extract.rb +0 -24
  78. data/lib/openehr/writer.rb +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8b6b9646daa3a967d9addfb4effc70408a402ff746a52fc549bff5ed150593b
4
- data.tar.gz: 9390b872a07e6b3e3291489cfd1dc2ef87cc8eb41f3d2906aab829cfeb317937
3
+ metadata.gz: 6f34e667ae737ff2718bb8d2e676d75711b0753fd2fb39f64cba6e6eb3bef59a
4
+ data.tar.gz: bfede641c16301632b2a3647727a10036dff325b2e8bed288bbca1aa820e075c
5
5
  SHA512:
6
- metadata.gz: a4d0d99c554e80da635c8d4d4bbbef09c5937dede153582e68793611e4708afac008eef063b2ca4a2b57cb7d25f6e18cf0db0914e9f74bf4463b200dd60a764a
7
- data.tar.gz: 2b57b5b3556a2d431e7643936026067438d8d1405a9ad443cc7cd237cc7cd37f9b8dbf83085f3aa4d5407d3e3957a01945664b3e00fffa82299a23ab82387a60
6
+ metadata.gz: 1eabc54006e29b679f16d9d085f097ddb808037a97190611331161b435d1dc77fd7f53ae08f686eda74c3186094a0a15f45351e5fc4d1e1079c13029a10be4e9
7
+ data.tar.gz: 035d2e2a083d64cb84984c20c7362c4cb8a40912406855a9472c0d1fc240ec43e97547bd17fa83f29f3026b7d55483106b53691f45ceefd813ef858f36026fef
data/README.rdoc CHANGED
@@ -1,4 +1,4 @@
1
- {<img src="https://travis-ci.org/skoba/openehr-ruby.png?branch=master" alt="Build Status" />}[https://travis-ci.org/skoba/openehr-ruby]
1
+ {<img src="https://github.com/skoba/openehr-ruby/actions/workflows/ci.yml/badge.svg" alt="CI" />}[https://github.com/skoba/openehr-ruby/actions/workflows/ci.yml]
2
2
  =Synopsis
3
3
 
4
4
  A Ruby implementation of the openEHR specifications
@@ -7,17 +7,17 @@ A Ruby implementation of the openEHR specifications
7
7
 
8
8
  = Requirements
9
9
 
10
- * Supports CRuby 2.5.0 or later.
11
- * Developed with CRuby 3.0.0 on FreeBSD and Linux.
12
- * Ruby 2.4 or earlier are no longer supported.
10
+ * Supports CRuby 3.1 or later.
11
+ * Developed with CRuby 3.4 on Linux; CI covers 3.1-3.4.
12
+ * Ruby 3.0 or earlier are no longer supported.
13
13
 
14
14
  =Description
15
15
 
16
16
  This is a stable release of Ruby openEHR implementation project.
17
- This release is based on openEHR specification release 1.0.2.
18
- We implemented almost of the specifications of the openEHR.
19
- The work is still in progress to implement related tools, such as
20
- archetype validator or serializer. Formerly, we named this package
17
+ This release targets openEHR specification release 1.0.2 as its
18
+ baseline, with a growing set of RM 1.1.0 / BASE 1.2.0 deltas layered on
19
+ top (see "RM 1.0.2 vs RM 1.1.0 semantics" below for exactly which
20
+ classes/attributes are 1.1.0-only). Formerly, we named this package
21
21
  as open_ehr, but changed to openehr from release 1.1.0. 1.0.x versions
22
22
  are obsoleted.
23
23
 
@@ -25,12 +25,18 @@ The intention is to have a sample EHR to deploy quickly with Ruby on Rails for d
25
25
 
26
26
  This package includes:
27
27
  * ADL 1.4 parser
28
- * AM/RM packages based on the openEHR specification 1.0.2
28
+ * AM/RM packages based on the openEHR specification 1.0.2, plus RM 1.1.0 deltas
29
+ * ADL/XML/JSON serializers and an ADL archetype validator
30
+ * AQL (Archetype Query Language): a parser plus an in-memory execution
31
+ engine that evaluates queries directly against RM object graphs you
32
+ already have in memory (see "Querying with AQL" below)
29
33
 
30
34
  This package excludes:
31
- * ADL serializer(in progress)
32
- * ADL validator(in progress)
33
- * Data store/retrieve would be separate to the other project.
35
+ * An XML archetype parser (in progress)
36
+ * Persistent data stores and search indexes - AQL here only ever
37
+ queries in-memory RM object graphs; the moment your data lives in a
38
+ database, that integration (e.g. openehr-rails) owns fetching it out
39
+ and this gem's AQL engine stays completely unaware of how
34
40
  * Terminology service(moved to openehr-terminology package)
35
41
  * Rails plugin(moved to openehr-rails package
36
42
 
@@ -43,6 +49,81 @@ and so on. We have therefore postponed implementations of these classes.
43
49
  Terminology and Demographic servers will be implemented in other
44
50
  projects.
45
51
 
52
+ =RM 1.0.2 vs RM 1.1.0 semantics
53
+
54
+ This gem's baseline is openEHR RM 1.0.2, with the following RM 1.1.0 /
55
+ BASE 1.2.0 deltas incorporated so far. Everything not listed here keeps
56
+ its 1.0.2 meaning; anything listed only exists, or only behaves this
57
+ way, from RM 1.1.0 onward.
58
+
59
+ Class / attribute RM version Notes
60
+ ------------------------------------------ ---------- ---------------------------------------------
61
+ DV_SCALE (new class) 1.1.0 Like DV_ORDINAL, but Real-valued (SPECRM-19)
62
+ DV_QUANTITY.units_system 1.1.0 Optional; identifies a non-UCUM units system
63
+ DV_QUANTITY.units_display_name 1.1.0 Optional displayable form of units (SPECRM-65)
64
+ CODE_PHRASE.preferred_term 1.1.0 Optional preferred term for code_string
65
+ DV_PARAGRAPH deprecated Deprecated since RM 1.0.4; still legal, warns
66
+ DV_DURATION negative values ("-P10D") 1.1.0 Consistently supported (SPECRM-96)
67
+ EHR.folders 1.1.0 List<OBJECT_REF>; directory = folders.item(1)
68
+ FOLDER.details 1.1.0 Optional ITEM_STRUCTURE meta-data
69
+ FEEDER_AUDIT_DETAILS.other_details 1.1.0 Optional ITEM_STRUCTURE meta-data (SPECRM-74)
70
+ ACTIVITY.timing 1.1.0 Now optional (was mandatory pre-1.1.0)
71
+ ISM_TRANSITION.reason 1.1.0 Optional List<DV_TEXT>
72
+ COMPOSITION.category = "episodic" 1.1.0 A terminology value, not a code change (SPECRM-89);
73
+ already supported via the pluggable
74
+ OpenEHR::TerminologyService seam
75
+
76
+ Not yet incorporated from RM 1.1.0 / BASE 1.2.0: ITEM_TAG (still in a
77
+ development branch, not yet in a released RM version) and the finer
78
+ points of DV_DURATION arithmetic beyond negation/unary minus.
79
+
80
+ =Querying with AQL
81
+
82
+ query = OpenEHR::AQL.parse(aql_string) # => a parsed, immutable Query
83
+ result = query.execute(dataset, params: { ehr_id: '...' }) # => a ResultSet (Enumerable, plus #to_json)
84
+ result = OpenEHR::AQL.execute(aql_string, dataset, params: {}) # one-shot
85
+
86
+ The engine evaluates queries against <tt>OpenEHR::AQL::Dataset</tt>, its
87
+ one and only input boundary - and the whole of its independence from
88
+ any particular web framework or O/R mapper. Everything under
89
+ <tt>lib/openehr/aql/</tt> talks to "wherever your data lives"
90
+ exclusively through <tt>Dataset</tt>, which itself depends on nothing
91
+ but Ruby's own <tt>Enumerable</tt>/<tt>#each</tt> protocol and this
92
+ gem's own RM classes - no ActiveRecord, no Rails, no Sequel, anywhere
93
+ in this gem, ever.
94
+
95
+ ==Supplying data to AQL
96
+
97
+ <tt>Dataset</tt> accepts any Enumerable of records shaped like
98
+ <tt>{ehr_id:, compositions:, ehr_status: (optional)}</tt> (a Hash with
99
+ symbol or string keys, or a duck-typed object responding to the same
100
+ methods), or a full <tt>OpenEHR::RM::EHR::EHR</tt> if you already have
101
+ one. Every element yielded by a record's <tt>compositions</tt> must be
102
+ one of this gem's own RM objects (<tt>is_a?(OpenEHR::RM::Common::Archetyped::Pathable)</tt>) -
103
+ anything else raises <tt>OpenEHR::AQL::DatasetError</tt> immediately,
104
+ naming the offending class and index, rather than half-working via
105
+ loose duck typing.
106
+
107
+ Construction never iterates its source, so a lazily-produced
108
+ Enumerable stays lazy all the way through <tt>execute</tt>. A
109
+ hypothetical Rails + ActiveRecord integration (e.g. openehr-rails)
110
+ might look like this - note this is entirely outside this gem, shown
111
+ only to make the boundary concrete:
112
+
113
+ records = Ehr.find_each.lazy.map { |row|
114
+ { ehr_id: row.uid,
115
+ compositions: row.composition_records.lazy.map { |cr|
116
+ OpenEHR::RM::CompositionFactory.create_from_json(cr.canonical_json) } } }
117
+ OpenEHR::AQL.execute(aql, OpenEHR::AQL::Dataset.new(ehrs: records))
118
+
119
+ Swapping to Sinatra + Sequel, or any other framework/ORM, only changes
120
+ the two <tt>.map</tt> bodies above - nothing in this gem's AQL code
121
+ needs to know or care. For quick, non-database scripts:
122
+
123
+ c1 = OpenEHR::RM::CompositionFactory.create_from_json(composition_json) # or build one by hand
124
+ OpenEHR::AQL.execute(aql, OpenEHR::AQL::Dataset.of_compositions([c1, c2]))
125
+ # or even just: OpenEHR::AQL.execute(aql, [c1, c2]) # via Dataset.wrap
126
+
46
127
  =Main codebase authors
47
128
  * Shinji Kobayashi and Akimichi Tatsukawa of http://openehr.jp
48
129
  * ADL parser test cases are derived from Java and Eiffel reference implementation project of the openEHR.
@@ -61,6 +61,10 @@ module OpenEHR
61
61
  return @false_valid
62
62
  end
63
63
 
64
+ def valid_value?(value)
65
+ (value == true && true_valid?) || (value == false && false_valid?)
66
+ end
67
+
64
68
  private
65
69
  def binary_consistency(true_valid, false_valid)
66
70
  if (true_valid == false) && (false_valid == false)
@@ -77,6 +81,7 @@ module OpenEHR
77
81
 
78
82
  class CString < CPrimitive
79
83
  attr_reader :pattern, :list
84
+ attr_accessor :list_open
80
85
 
81
86
  def initialize(args = { })
82
87
  args[:type] = 'String'
@@ -84,6 +89,7 @@ module OpenEHR
84
89
  consistency(args[:pattern], args[:list])
85
90
  @pattern = args[:pattern]
86
91
  @list = args[:list]
92
+ @list_open = args[:list_open]
87
93
  end
88
94
 
89
95
  def pattern=(pattern)
@@ -96,9 +102,20 @@ module OpenEHR
96
102
  @list = list
97
103
  end
98
104
 
105
+ def valid_value?(value)
106
+ return false unless value.is_a?(String)
107
+ return true if pattern.nil? && list.nil?
108
+ return list.include?(value) if list
109
+
110
+ Regexp.new(pattern).match?(value)
111
+ end
112
+
99
113
  private
114
+ # C_STRING may be fully unconstrained (neither pattern nor
115
+ # list, i.e. any_allowed); only having both set
116
+ # simultaneously is a genuine contradiction.
100
117
  def consistency(pattern, list)
101
- if pattern.nil? == list.nil?
118
+ if !pattern.nil? && !list.nil?
102
119
  raise ArgumentError, 'consistency invaild'
103
120
  end
104
121
  end
@@ -125,12 +142,28 @@ module OpenEHR
125
142
  @range = range
126
143
  end
127
144
 
145
+ def valid_value?(value)
146
+ return false unless value.is_a?(Integer)
147
+
148
+ constrained_value?(value)
149
+ end
150
+
128
151
  private
152
+ # C_INTEGER may be fully unconstrained (neither list nor range,
153
+ # i.e. any_allowed); only having both set simultaneously is a
154
+ # genuine contradiction.
129
155
  def consistency(list, range)
130
- if list.nil? == range.nil?
156
+ if !list.nil? && !range.nil?
131
157
  raise ArgumentError, 'consistency invalid'
132
158
  end
133
159
  end
160
+
161
+ def constrained_value?(value)
162
+ return true if list.nil? && range.nil?
163
+ return list.include?(value) if list
164
+
165
+ range.has?(value)
166
+ end
134
167
  end
135
168
 
136
169
  class CReal < CInteger
@@ -138,11 +171,17 @@ module OpenEHR
138
171
  args[:type] = 'Real'
139
172
  super
140
173
  end
174
+
175
+ def valid_value?(value)
176
+ return false unless value.is_a?(Numeric)
177
+
178
+ constrained_value?(value)
179
+ end
141
180
  end
142
181
 
143
182
  module CDateModule
144
183
  attr_accessor :timezone_validity, :day_validity, :range, :list
145
- attr_reader :month_validity, :day_validity, :range, :pattern
184
+ attr_reader :month_validity, :pattern
146
185
 
147
186
  def month_validity=(month_validity)
148
187
  if (month_validity == ValidityKind::OPTIONAL &&
@@ -163,6 +202,13 @@ module OpenEHR
163
202
  return !@range.nil?
164
203
  end
165
204
 
205
+ def valid_value?(value)
206
+ date = as_date(value)
207
+ return false if date.nil?
208
+
209
+ date_fields_valid?(date) && constrained_date?(date)
210
+ end
211
+
166
212
  protected
167
213
  def valid_pattern?(pattern)
168
214
  if /^([Yy?X]{4})(-([Mm?X]{2})(-([Dd?X]{2}))?)?$/ =~ pattern
@@ -172,9 +218,43 @@ module OpenEHR
172
218
  end
173
219
  end
174
220
 
221
+ def date_fields_valid?(date)
222
+ validity_satisfied?(month_validity, !date.month_unknown?) &&
223
+ validity_satisfied?(day_validity, !date.day_unknown?)
224
+ end
225
+
226
+ def validity_satisfied?(validity, field_present)
227
+ case validity
228
+ when ValidityKind::DISALLOWED then !field_present
229
+ when ValidityKind::MANDATORY then field_present
230
+ else
231
+ true
232
+ end
233
+ end
234
+
175
235
  private
176
236
  def consistency_validity(month_validity, day_validity)
177
237
  end
238
+
239
+ # A String value is parsed as a DV_DATE (matching the
240
+ # concrete type this codebase's range/list bounds actually
241
+ # use, so Comparable/Interval#has? compare like with like);
242
+ # a malformed string surfaces as ArgumentError, caught below.
243
+ def as_date(value)
244
+ return value if value.respond_to?(:month_unknown?)
245
+ return nil unless value.is_a?(String)
246
+
247
+ OpenEHR::RM::DataTypes::Quantity::DateTime::DvDate.new(:value => value)
248
+ rescue ArgumentError
249
+ nil
250
+ end
251
+
252
+ def constrained_date?(date)
253
+ return true if range.nil? && list.nil?
254
+ return list.any? { |item| as_date(item) == date } if list
255
+
256
+ range.has?(date)
257
+ end
178
258
  end
179
259
 
180
260
  class CDate < CPrimitive
@@ -212,7 +292,7 @@ module OpenEHR
212
292
  end
213
293
 
214
294
  module CTimeModule
215
- attr_accessor :range, :list
295
+ attr_accessor :range, :list, :timezone_validity
216
296
  attr_reader :minute_validity, :second_validity,
217
297
  :millisecond_validity, :pattern
218
298
  def pattern=(pattern)
@@ -247,6 +327,13 @@ module OpenEHR
247
327
  return !@range.nil?
248
328
  end
249
329
 
330
+ def valid_value?(value)
331
+ time = as_time(value)
332
+ return false if time.nil?
333
+
334
+ time_fields_valid?(time) && constrained_time?(time)
335
+ end
336
+
250
337
  protected
251
338
  def valid_pattern?(pattern)
252
339
  if /^([Hh?X]{2})(:([Mm?X]{2})(:([Ss?X]{2}))?)?$/ =~ pattern
@@ -255,6 +342,43 @@ module OpenEHR
255
342
  false
256
343
  end
257
344
  end
345
+
346
+ def time_fields_valid?(time)
347
+ validity_satisfied?(minute_validity, !time.minute_unknown?) &&
348
+ validity_satisfied?(second_validity, !time.second_unknown?) &&
349
+ validity_satisfied?(millisecond_validity, time.has_fractional_second?)
350
+ end
351
+
352
+ def validity_satisfied?(validity, field_present)
353
+ case validity
354
+ when ValidityKind::DISALLOWED then !field_present
355
+ when ValidityKind::MANDATORY then field_present
356
+ else
357
+ true
358
+ end
359
+ end
360
+
361
+ private
362
+
363
+ # A String value is parsed as a DV_TIME (matching the
364
+ # concrete type this codebase's range/list bounds actually
365
+ # use, so Comparable/Interval#has? compare like with like);
366
+ # a malformed string surfaces as ArgumentError, caught below.
367
+ def as_time(value)
368
+ return value if value.respond_to?(:minute_unknown?)
369
+ return nil unless value.is_a?(String)
370
+
371
+ OpenEHR::RM::DataTypes::Quantity::DateTime::DvTime.new(:value => value)
372
+ rescue ArgumentError
373
+ nil
374
+ end
375
+
376
+ def constrained_time?(time)
377
+ return true if range.nil? && list.nil?
378
+ return list.any? { |item| as_time(item) == time } if list
379
+
380
+ range.has?(time)
381
+ end
258
382
  end
259
383
 
260
384
  class CTime < CPrimitive
@@ -286,9 +410,9 @@ module OpenEHR
286
410
  self.millisecond_validity = args[:millisecond_validity]
287
411
  self.second_validity = args[:second_validity]
288
412
  self.minute_validity = args[:minute_validity]
289
- self.hour_validity = args[:hour_vaildity]
413
+ self.hour_validity = args[:hour_validity]
290
414
  self.day_validity = args[:day_validity]
291
- self.month_validity = args[:day_validity]
415
+ self.month_validity = args[:month_validity]
292
416
  end
293
417
 
294
418
  def hour_validity=(hour_validity)
@@ -313,6 +437,15 @@ module OpenEHR
313
437
  @day_validity = day_validity
314
438
  end
315
439
 
440
+ def valid_value?(value)
441
+ datetime = as_datetime(value)
442
+ return false if datetime.nil?
443
+
444
+ date_fields_valid?(datetime) && time_fields_valid?(datetime) &&
445
+ validity_satisfied?(hour_validity, !datetime.hour_unknown?) &&
446
+ constrained_datetime?(datetime)
447
+ end
448
+
316
449
  protected
317
450
  def valid_pattern?(pattern)
318
451
  if /^([Yy?X]{4})(-([Mm?X]{2})(-([Dd?X]{2}))?)?[T ]?([Hh?X]{2})(:([Mm?X]{2})(:([Ss?X]{2}))?)?$/ =~ pattern
@@ -321,6 +454,28 @@ module OpenEHR
321
454
  false
322
455
  end
323
456
  end
457
+
458
+ private
459
+
460
+ # A String value is parsed as a DV_DATE_TIME (matching the
461
+ # concrete type this codebase's range/list bounds actually
462
+ # use, so Comparable/Interval#has? compare like with like);
463
+ # a malformed string surfaces as ArgumentError, caught below.
464
+ def as_datetime(value)
465
+ return value if value.respond_to?(:hour_unknown?)
466
+ return nil unless value.is_a?(String)
467
+
468
+ OpenEHR::RM::DataTypes::Quantity::DateTime::DvDateTime.new(:value => value)
469
+ rescue ArgumentError
470
+ nil
471
+ end
472
+
473
+ def constrained_datetime?(datetime)
474
+ return true if range.nil? && list.nil?
475
+ return list.any? { |item| as_datetime(item) == datetime } if list
476
+
477
+ range.has?(datetime)
478
+ end
324
479
  end
325
480
 
326
481
  class CDuration < CPrimitive
@@ -335,6 +490,49 @@ module OpenEHR
335
490
  self.pattern = args[:pattern]
336
491
  self.list = args[:list]
337
492
  self.range = args[:range]
493
+ self.years_allowed = args[:years_allowed]
494
+ self.months_allowed = args[:months_allowed]
495
+ self.weeks_allowed = args[:weeks_allowed]
496
+ self.days_allowed = args[:days_allowed]
497
+ self.hours_allowed = args[:hours_allowed]
498
+ self.minutes_allowed = args[:minutes_allowed]
499
+ self.seconds_allowed = args[:seconds_allowed]
500
+ self.fractional_seconds_allowed = args[:fractional_seconds_allowed]
501
+ end
502
+
503
+ def valid_value?(value)
504
+ duration = as_duration(value)
505
+ return false if duration.nil?
506
+ return false unless fields_allowed?(duration)
507
+ return true if list.nil? && range.nil?
508
+ return list.any? { |item| as_duration(item).to_seconds == duration.to_seconds } if list
509
+
510
+ range.has?(duration)
511
+ end
512
+
513
+ private
514
+
515
+ # A String value is parsed as a DV_DURATION (matching the
516
+ # concrete type this codebase's range/list bounds actually
517
+ # use, so Comparable/Interval#has? compare like with like);
518
+ # a malformed string surfaces as ArgumentError, caught below.
519
+ def as_duration(value)
520
+ return value if value.respond_to?(:to_seconds)
521
+ return nil unless value.is_a?(String)
522
+
523
+ OpenEHR::RM::DataTypes::Quantity::DateTime::DvDuration.new(:value => value)
524
+ rescue ArgumentError
525
+ nil
526
+ end
527
+
528
+ def fields_allowed?(duration)
529
+ [[years_allowed, duration.years], [months_allowed, duration.months],
530
+ [weeks_allowed, duration.weeks], [days_allowed, duration.days],
531
+ [hours_allowed, duration.hours], [minutes_allowed, duration.minutes],
532
+ [seconds_allowed, duration.seconds],
533
+ [fractional_seconds_allowed, duration.fractional_second]].all? do |allowed, field|
534
+ allowed != false || field.nil?
535
+ end
338
536
  end
339
537
  end
340
538
  end # of Primitive