1#![doc(test(attr(deny(warnings))))]
10
11pub mod api;
12pub mod cache;
13pub mod entity;
14pub mod expression;
15
16pub const MMS: &str = "http://id.who.int/icd/release/11/mms";
18pub const ICF: &str = "http://id.who.int/icd/release/11/icf";
20pub const FOUNDATION: &str = "http://id.who.int/icd/entity";
22pub const SCHEMA: &str = "http://id.who.int/icd/schema/";
24pub const CANONICAL: &str = "http://id.who.int";
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
29pub enum Linearization {
30 Mms,
32 Icf,
34 Foundation,
36}
37
38impl Linearization {
39 pub const ALL: [Self; 3] = [Self::Mms, Self::Icf, Self::Foundation];
41
42 #[must_use]
44 pub const fn system(self) -> &'static str {
45 match self {
46 Self::Mms => MMS,
47 Self::Icf => ICF,
48 Self::Foundation => FOUNDATION,
49 }
50 }
51
52 #[must_use]
54 pub const fn name(self) -> &'static str {
55 match self {
56 Self::Mms => "mms",
57 Self::Icf => "icf",
58 Self::Foundation => "entity",
59 }
60 }
61
62 #[must_use]
64 pub fn parse(name: &str) -> Option<Self> {
65 Self::ALL.into_iter().find(|l| l.name() == name)
66 }
67
68 #[must_use]
70 pub fn root_path(self, release: &str) -> String {
71 match self {
72 Self::Foundation => String::from("/icd/entity"),
73 other => format!("/icd/release/11/{release}/{}", other.name()),
74 }
75 }
76
77 #[must_use]
79 pub fn uri(self, id: &str) -> String {
80 format!("{}/{id}", self.system())
81 }
82
83 #[must_use]
85 pub fn versioned_uri(self, release: &str, id: &str) -> String {
86 match self {
87 Self::Foundation => self.uri(id),
88 other => format!(
89 "http://id.who.int/icd/release/11/{release}/{}/{id}",
90 other.name()
91 ),
92 }
93 }
94
95 #[must_use]
99 pub fn id_of(self, uri: &str) -> Option<String> {
100 let rest = uri.strip_prefix(CANONICAL)?;
101 let tail = match self {
102 Self::Foundation => rest.strip_prefix("/icd/entity/")?,
103 other => {
104 let after = rest.strip_prefix("/icd/release/11/")?;
105 let (head, remainder) = after.split_once('/')?;
106 if head == other.name() {
107 remainder
108 } else if is_release(head) {
109 let (name, tail) = remainder.split_once('/')?;
110 if name != other.name() {
111 return None;
112 }
113 tail
114 } else {
115 return None;
116 }
117 }
118 };
119 let tail = tail.trim_end_matches('/');
120 let mut parts = tail.split('/');
121 let id = parts.next()?;
122 if id.is_empty() || !id.bytes().all(|b| b.is_ascii_digit()) {
123 return None;
124 }
125 match parts.next() {
126 None => Some(id.to_owned()),
127 Some(residual @ ("other" | "unspecified")) if parts.next().is_none() => {
128 Some(format!("{id}/{residual}"))
129 }
130 Some(_) => None,
131 }
132 }
133
134 #[must_use]
137 pub fn key_of(id: &str) -> Option<u64> {
138 let (number, residual) = match id.split_once('/') {
139 None => (id, 0),
140 Some((n, "other")) => (n, 1),
141 Some((n, "unspecified")) => (n, 2),
142 Some(_) => return None,
143 };
144 let number: u64 = number.parse().ok()?;
145 number.checked_mul(4)?.checked_add(residual)
146 }
147}
148
149#[must_use]
151pub fn is_release(text: &str) -> bool {
152 let bytes = text.as_bytes();
153 bytes.len() == 7
154 && bytes
155 .get(..4)
156 .is_some_and(|y| y.iter().all(u8::is_ascii_digit))
157 && bytes.get(4) == Some(&b'-')
158 && bytes
159 .get(5..)
160 .is_some_and(|m| m.iter().all(u8::is_ascii_digit))
161}
162
163#[cfg(test)]
164mod tests {
165 use super::{Linearization, is_release};
166
167 #[test]
168 fn uris_name_entities_in_both_forms() {
169 let mms = Linearization::Mms;
170 assert_eq!(
171 mms.id_of("http://id.who.int/icd/release/11/mms/257068234")
172 .as_deref(),
173 Some("257068234")
174 );
175 assert_eq!(
176 mms.id_of("http://id.who.int/icd/release/11/2026-01/mms/1363559646/other")
177 .as_deref(),
178 Some("1363559646/other")
179 );
180 assert_eq!(mms.id_of("http://id.who.int/icd/release/11/icf/12"), None);
181 assert_eq!(mms.id_of("http://id.who.int/icd/release/11/mms/abc"), None);
182 assert_eq!(
183 mms.id_of("http://id.who.int/icd/release/11/mms/1/2/3"),
184 None
185 );
186 assert_eq!(
187 Linearization::Foundation
188 .id_of("http://id.who.int/icd/entity/257068234")
189 .as_deref(),
190 Some("257068234")
191 );
192 assert_eq!(
193 Linearization::Foundation.id_of("http://id.who.int/icd/release/11/mms/257068234"),
194 None
195 );
196 assert_eq!(mms.uri("1A"), "http://id.who.int/icd/release/11/mms/1A");
197 assert_eq!(
198 mms.versioned_uri("2026-01", "5"),
199 "http://id.who.int/icd/release/11/2026-01/mms/5"
200 );
201 assert_eq!(
202 Linearization::key_of("1363559646/other"),
203 Some(1_363_559_646 * 4 + 1)
204 );
205 assert_eq!(Linearization::key_of("12/x"), None);
206 assert!(is_release("2026-01"));
207 assert!(!is_release("mms"));
208 }
209}