# 3. Data types
Requirement prefix: `D3`.
The `DATA_VALUE` hierarchy is every leaf value a clinical record can hold. Two
themes run through this section: **precision is preserved**, and **comparison
refuses more often than it answers**.
## Where the quantity invariants come from
The rendered RM 1.1.0 Data Types page does not include the class-definition
tables — the specification sources `include::` them from a UML export that is
generated at build time and is not in the repository. The invariants in the
Quantity part of this section were therefore read from the **Release 1.0.2
publication** (*Data Types Information Model*, Rev 2.1.1, 20 Nov 2008,
§6.2.1–6.2.12), and the RM amendment record was then checked for every change
since:
| 1.0.3 | `SPECRM-32` — `DV_PROPORTION` gains the zero-denominator invariant |
| 1.1.0 | `SPECRM-19` — `DV_SCALE` added; `DV_ORDINAL.limits` removed |
| 1.1.0 | `SPECRM-65` — `units_system`, `units_display_name` added to `DV_QUANTITY` |
| 1.1.0 | `SPECRM-94`, `SPECRM-96` — arithmetic, comparison, negative durations |
None of them altered `precision`, `accuracy`, `magnitude_status`, or the
`DV_ORDERED` range invariants, so the 1.0.2 text governs them. `DV_SCALE`, new
in 1.1.0, is taken to inherit `DV_ORDERED`'s invariants unchanged — it is a
`DV_ORDERED` descendant and the amendment describes it as `DV_ORDINAL` with a
`Real` value. That last inference is the only one not read from a source.
## Coverage
- **D3.1** The crate MUST model every concrete `DATA_VALUE` descendant in
openEHR RM 1.1.0: `DV_BOOLEAN`, `DV_STATE`, `DV_IDENTIFIER`, `DV_TEXT`,
`DV_CODED_TEXT`, `DV_PARAGRAPH`, `DV_ORDINAL`, `DV_SCALE`, `DV_QUANTITY`,
`DV_COUNT`, `DV_PROPORTION`, `DV_DATE`, `DV_TIME`, `DV_DATE_TIME`,
`DV_DURATION`, `DV_MULTIMEDIA`, `DV_PARSABLE`, `DV_URI`, `DV_EHR_URI`,
`DV_INTERVAL`, `DV_PERIODIC_TIME_SPECIFICATION`, and
`DV_GENERAL_TIME_SPECIFICATION`.
- **D3.2** `DV_PARAGRAPH` MUST be readable and MUST NOT be recommended. openEHR
deprecates it in favour of markdown-formatted `DV_TEXT`, and instances written
before the deprecation must still round-trip.
## Text and terminology
- **D3.3** `DV_TEXT.value` MUST be non-empty. A field that is present and empty
is indistinguishable from one that is absent, and openEHR already has a way to
say absent.
- **D3.4** `DV_TEXT.formatting` MUST be non-empty when present, and a value of
`plain_no_newlines` MUST be refused when `value` contains a newline.
- **D3.5** `CODE_PHRASE` MUST require a non-empty `code_string` and a
well-formed `TERMINOLOGY_ID`, and MUST NOT check the code against any
terminology other than openEHR's own (`S1.10`).
- **D3.6** `TERM_MAPPING.match` MUST be one of `>`, `=`, `<`, `?`, modelled as a
closed set rather than a character.
- **D3.7** `DV_CODED_TEXT.value` is required by openEHR to be the rubric of
`defining_code`. The crate MUST check this **only** for codes in the openEHR
support terminology, and MUST report *not checked* as an outcome distinct from
*valid*. Reporting an unchecked external code as valid would be a claim about
a terminology the crate cannot see.
- **D3.8** Where a code appears in more than one openEHR terminology group with
different rubrics, agreement with **any** group MUST be accepted: a bare
`DV_CODED_TEXT` does not know which attribute it sits on.
## Dates, times, and durations
- **D3.9** Partial precision MUST be preserved. `2024`, `2024-05`, and
`2024-05-17` are three different dates and MUST NOT be completed to one
another. A completed date is a fabricated clinical fact that no downstream
reader can distinguish from a real one.
- **D3.10** The lexical form MUST be preserved exactly, including the choice
between `Z` and `+00:00`, and including the number of digits in a fractional
second.
- **D3.11** Date, time, and date-time MUST be validated on construction:
component ranges, month lengths, and the full Gregorian leap rule including
the century exceptions.
- **D3.12** A leap second (`:60`) MUST be accepted. A record that captured one
must not be rejected on read-back.
- **D3.13** `DV_DURATION` MUST accept openEHR's leading `-`, which plain
ISO 8601 does not, and MUST refuse designators that are out of order or
repeated.
- **D3.13a** The extended ISO 8601 format is required; the basic format
(`20240517`) MUST be refused. It does not appear in openEHR canonical JSON,
and accepting it would make `2024` ambiguous. Recorded as a limitation in
[`audit.md`](audit.md).
## Comparison
- **D3.14** Comparison MUST be partial. Where an ordering is not established,
the answer MUST be "not comparable", never a default. Specifically:
- two dates or times whose known components agree but whose precisions differ,
- a local time and one carrying a UTC offset,
- two durations whose approximations agree and whose calendar components
differ,
- values of different `DATA_VALUE` classes.
- **D3.15** `DV_QUANTITY` values MUST be comparable only when their `units`
strings are equal (`S1.9`).
- **D3.16** `DV_ORDINAL` and `DV_SCALE` values MUST be comparable only when
their symbols come from the same terminology. Ordinal `2` on a pain scale and
ordinal `2` on a sedation scale are unrelated.
- **D3.17** `DV_PROPORTION` values MUST be comparable only when their kinds
match.
- **D3.18** Times carrying UTC offsets MUST be normalised to UTC before
comparison.
## Quantities and proportions
- **D3.19** `DV_QUANTITY` MUST require a finite magnitude and a non-empty
`units`. A quantity with no units is a number, and openEHR has `DV_COUNT`.
- **D3.19a** The non-empty requirement in `D3.19` is a **declared narrowing** of
openEHR's `Units_valid: units /= void`, which forbids only a missing value and
not an empty one. What survives: every `DV_QUANTITY` this crate accepts has a
units string that names a measured property, which is what
`is_strictly_comparable_to` depends on (`D3.15`). What it costs: an instance
carrying `"units": ""` is refused rather than read.
- **D3.20** `precision` MUST satisfy openEHR's `Precision_valid: precision >= -1`.
`0` means an integral quantity; **`-1` means no limit** — any number of
decimal places — and is a *stated* precision, distinct from the attribute
being absent. Requiring `precision >= 0` would reject conformant data.
- **D3.20a** `is_integral` MUST be exactly `precision = 0`, as openEHR defines
it, and MUST answer "unknown" when no precision is recorded. A magnitude that
happens to be whole is not a quantity *declared* integral.
- **D3.21** `accuracy` MUST be finite and MUST satisfy both openEHR rules:
`Accuracy_validity` — a percentage accuracy lies in 0–100 — and
`Accuracy_is_percent_validity` — an accuracy of **0** must not be flagged as a
percentage, because 0 means 100% accurate and "0%" reads as the opposite.
- **D3.21a** `-1.0` MUST be honoured as openEHR's `unknown_accuracy_value`: it
records that accuracy was not measured, and a reader treating it as an error
of minus one is wrong by the width of the scale. `accuracy_unknown` MUST
answer true for it **and** for an absent accuracy, because both mean the same
thing to a reader and only one of them is obvious.
- **D3.22** `magnitude_status` MUST be one of `=`, `<`, `>`, `<=`, `>=`, `~`,
modelled as a closed set — openEHR's `valid_magnitude_status`. A result below
an assay's detection limit is a real result and recording it as the limit
loses the only part that matters.
- **D3.23** `DV_PROPORTION` MUST refuse a zero denominator, MUST require
denominator 1 for `pk_unitary` and 100 for `pk_percent`, and MUST require
integral parts for `pk_fraction` and `pk_integer_fraction`. These comparisons
are exact: a tolerance would accept a denominator of 99.999 and then render it
as a percentage.
- **D3.23a** `DV_PROPORTION.precision` MUST satisfy
`Precision_validity: precision = 0 implies is_integral`. Note the direction:
declaring precision `0` **asserts** the parts are whole numbers. It does not
forbid an integral *kind* from carrying a precision, which is the reading an
implementation usually arrives at and which rejects valid data.
- **D3.24** `DV_ORDERED` reference ranges MUST be modelled, and "outside the
normal range" MUST be distinguishable from "no normal range recorded".
- **D3.24a** `normal_status` MUST come from the openEHR `normal statuses` code
set (`Normal_status_validity`). A renderer prints this letter verbatim beside
a result, so an invented one reaches a clinician.
- **D3.24b** Where both `normal_range` and `normal_status` are present, they
MUST agree: `N` if and only if the value lies inside the range
(`Normal_range_and_status_consistency`). They disagree when a result is copied
from one system and its flag from another — which is the case where a
clinician sees a normal flag beside an abnormal number.
## Encapsulated data
- **D3.25** `DV_MULTIMEDIA` MUST NOT render its bytes in any `Display` or
`Debug` output. `Debug` MUST print media type and length instead. This is the
one place the crate's usual "`Debug` may show everything" rule is overridden,
because "everything" is a megabyte of base64 that ends up in a ticket.
- **D3.26** `DV_MULTIMEDIA.integrity_check` MUST be verifiable, and the
verification MUST distinguish *passed*, *failed*, *not recorded*, *no inline
data*, and *unsupported algorithm*. A field saying "SHA-256: …" that nothing
checks is worse than no field, because it reads as an assurance.
- **D3.27** The crate MUST read an integrity check declared as `SHA-1` and MUST
NOT emit one. openEHR lists SHA-1; a clinical record may outlive it, and it
has already been outlived.
- **D3.28** `DV_PARSABLE` MUST require both `value` and `formalism` to be
non-empty. An empty formalism leaves content nothing can decide how to read.
- **D3.29** Base64 decoding MUST tolerate embedded whitespace, which
XML-derived payloads carry and which conveys nothing.
## URIs
- **D3.30** `DV_URI` MUST require a well-formed scheme and MUST refuse spaces
and control characters.
- **D3.31** `DV_EHR_URI` MUST require the scheme `ehr`. `LINK.target` is typed
`DV_EHR_URI` precisely so that a link cannot point out of the record without
saying so.
- **D3.32** Validity of a `DV_EHR_URI` is **structural only**. The crate cannot
resolve it, and documentation MUST NOT let "valid" be read as "resolvable".
## Identifiers as values
- **D3.33** `DV_IDENTIFIER.id` MUST be non-empty.
- **D3.34** `DV_IDENTIFIER`'s `Display` MUST render the type and issuer and MUST
NOT render the identifier (`X11.6`).