1use crate::input::{Input, Options};
11use crate::limits::{self, bounded, strict};
12use crate::pn;
13use crate::uid;
14use crate::{MwlError, StudyUid, Warning, WorklistItem};
15use dicom_core::value::DataSetSequence;
16use dicom_core::{DataElement, PrimitiveValue, Tag, VR};
17use dicom_dictionary_std::tags;
18use dicom_object::InMemDicomObject;
19use hl7kit::{Message, Segment};
20
21pub fn worklist_item(input: &Input<'_>, options: &Options) -> Result<WorklistItem, MwlError> {
23 let msg = input.message;
24 let mut ds = InMemDicomObject::new_empty();
25 let mut warnings = Vec::new();
26
27 character_set(&mut ds, msg, options, &mut warnings);
28 patient(&mut ds, msg, options, &mut warnings)?;
29 imaging_service_request(&mut ds, input, options, &mut warnings)?;
30 let study_uid = requested_procedure(&mut ds, input, options, &mut warnings)?;
31 scheduled_procedure_steps(&mut ds, input, options, &mut warnings)?;
32 visit(&mut ds, msg);
33 warnings.dedup();
35
36 Ok(WorklistItem {
37 dataset: ds,
38 study_uid,
39 warnings,
40 })
41}
42
43fn character_set(
51 ds: &mut InMemDicomObject,
52 msg: &Message,
53 options: &Options,
54 w: &mut Vec<Warning>,
55) {
56 if let Some(cs) = &options.character_set {
57 put_str(ds, tags::SPECIFIC_CHARACTER_SET, VR::CS, cs);
58 return;
59 }
60 let msh18 = field(msg, "MSH-18.1");
61 let mapped = msh18.as_deref().map(|v| v.trim().to_ascii_uppercase());
62 let term = match mapped.as_deref() {
63 Some("ASCII") => return,
64 Some("8859/1") => Some("ISO_IR 100"),
65 Some("8859/2") => Some("ISO_IR 101"),
66 Some("8859/5") => Some("ISO_IR 144"),
67 Some("8859/15") => Some("ISO_IR 203"),
68 Some("UNICODE UTF-8") | Some("UNICODE") | Some("UTF-8") => Some("ISO_IR 192"),
69 _ => None,
70 };
71 match term {
72 Some(t) => put_str(ds, tags::SPECIFIC_CHARACTER_SET, VR::CS, t),
73 None => {
74 w.push(Warning::CharacterSetAssumed {
75 msh18,
76 used: "ISO_IR 192".into(),
77 });
78 put_str(ds, tags::SPECIFIC_CHARACTER_SET, VR::CS, "ISO_IR 192");
79 }
80 }
81}
82
83fn patient(
88 ds: &mut InMemDicomObject,
89 msg: &Message,
90 options: &Options,
91 w: &mut Vec<Warning>,
92) -> Result<(), MwlError> {
93 let id = field(msg, "PID-3.1").ok_or(MwlError::MissingPatientId)?;
95 let id = bounded(tags::PATIENT_ID, limits::LO, id, options.length_policy, w)?;
96 put_str(ds, tags::PATIENT_ID, VR::LO, &id);
97 if let Some(issuer) = field(msg, "PID-3.4") {
99 let issuer = bounded(
100 tags::ISSUER_OF_PATIENT_ID,
101 limits::LO,
102 issuer,
103 options.length_policy,
104 w,
105 )?;
106 put_str(ds, tags::ISSUER_OF_PATIENT_ID, VR::LO, &issuer);
107 }
108
109 match field(msg, "PID-5").and_then(|v| pn::from_xpn(&v)) {
111 Some(name) => {
112 if name.reordered {
113 w.push(Warning::NameComponentsReordered);
114 }
115 let pn = bounded(
116 tags::PATIENT_NAME,
117 limits::PN,
118 name.pn,
119 options.length_policy,
120 w,
121 )?;
122 put_str(ds, tags::PATIENT_NAME, VR::PN, &pn);
123 }
124 None => put_empty(ds, tags::PATIENT_NAME, VR::PN),
125 }
126
127 match field(msg, "PID-7").and_then(|v| hl7_date(&v)) {
129 Some(d) => put_str(ds, tags::PATIENT_BIRTH_DATE, VR::DA, &d),
130 None => put_empty(ds, tags::PATIENT_BIRTH_DATE, VR::DA),
131 }
132
133 let sex = field(msg, "PID-8").map(|s| s.to_ascii_uppercase());
138 match sex.as_deref() {
139 Some("M") | Some("F") | Some("O") => {
140 put_str(ds, tags::PATIENT_SEX, VR::CS, sex.as_deref().unwrap_or(""));
141 }
142 Some("U") | None => put_empty(ds, tags::PATIENT_SEX, VR::CS),
143 Some(other) => {
144 w.push(Warning::SexMappedToOther {
145 hl7: other.to_string(),
146 });
147 put_empty(ds, tags::PATIENT_SEX, VR::CS);
148 }
149 }
150 Ok(())
154}
155
156fn imaging_service_request(
161 ds: &mut InMemDicomObject,
162 input: &Input<'_>,
163 options: &Options,
164 w: &mut Vec<Warning>,
165) -> Result<(), MwlError> {
166 let msg = input.message;
167 match &input.order.accession {
171 Some(acc) => {
172 let acc = bounded(
173 tags::ACCESSION_NUMBER,
174 limits::SH,
175 acc.clone(),
176 options.length_policy,
177 w,
178 )?;
179 put_str(ds, tags::ACCESSION_NUMBER, VR::SH, &acc);
180 }
181 None => put_empty(ds, tags::ACCESSION_NUMBER, VR::SH),
182 }
183 if let Some(v) = field(msg, "ORC-2.1") {
185 let v = bounded(
186 tags::PLACER_ORDER_NUMBER_IMAGING_SERVICE_REQUEST,
187 limits::LO,
188 v,
189 options.length_policy,
190 w,
191 )?;
192 put_str(
193 ds,
194 tags::PLACER_ORDER_NUMBER_IMAGING_SERVICE_REQUEST,
195 VR::LO,
196 &v,
197 );
198 }
199 if let Some(v) = field(msg, "ORC-3.1") {
201 let v = bounded(
202 tags::FILLER_ORDER_NUMBER_IMAGING_SERVICE_REQUEST,
203 limits::LO,
204 v,
205 options.length_policy,
206 w,
207 )?;
208 put_str(
209 ds,
210 tags::FILLER_ORDER_NUMBER_IMAGING_SERVICE_REQUEST,
211 VR::LO,
212 &v,
213 );
214 }
215 let referring = field(msg, "PV1-8")
218 .or_else(|| field(msg, "ORC-12"))
219 .and_then(|v| pn::from_xcn(&v));
220 match referring {
221 Some(name) => {
222 let v = bounded(
223 tags::REFERRING_PHYSICIAN_NAME,
224 limits::PN,
225 name.pn,
226 options.length_policy,
227 w,
228 )?;
229 put_str(ds, tags::REFERRING_PHYSICIAN_NAME, VR::PN, &v);
230 }
231 None => put_empty(ds, tags::REFERRING_PHYSICIAN_NAME, VR::PN),
232 }
233 if let Some(name) = field(msg, "OBR-16")
236 .or_else(|| field(msg, "ORC-12"))
237 .and_then(|v| pn::from_xcn(&v))
238 {
239 let v = bounded(
240 tags::REQUESTING_PHYSICIAN,
241 limits::PN,
242 name.pn,
243 options.length_policy,
244 w,
245 )?;
246 put_str(ds, tags::REQUESTING_PHYSICIAN, VR::PN, &v);
247 }
248 Ok(())
254}
255
256fn requested_procedure(
261 ds: &mut InMemDicomObject,
262 input: &Input<'_>,
263 options: &Options,
264 w: &mut Vec<Warning>,
265) -> Result<StudyUid, MwlError> {
266 let msg = input.message;
267 let rp_id = match &input.order.procedure_id {
272 Some(v) => v.clone(),
273 None => {
274 let placer = field(msg, "ORC-2.1").or_else(|| field(msg, "OBR-2.1"));
275 w.push(Warning::MissingType1 {
276 tag: tags::REQUESTED_PROCEDURE_ID,
277 substituted: placer
278 .as_ref()
279 .map(|p| format!("placer order number {p} (ORC-2.1)")),
280 });
281 placer.ok_or(MwlError::MissingRequestedProcedureId)?
282 }
283 };
284 let rp_id = bounded(
285 tags::REQUESTED_PROCEDURE_ID,
286 limits::SH,
287 rp_id,
288 options.length_policy,
289 w,
290 )?;
291 put_str(ds, tags::REQUESTED_PROCEDURE_ID, VR::SH, &rp_id);
292
293 let (value, origin, warning) = uid::resolve(input.order, &options.uid_policy)?;
295 let value = strict(tags::STUDY_INSTANCE_UID, limits::UI, value)?;
296 if let Some(warning) = warning {
297 w.push(warning);
298 }
299 put_str(ds, tags::STUDY_INSTANCE_UID, VR::UI, &value);
300
301 if let Some(desc) = field(msg, "OBR-4.2") {
304 let desc = bounded(
305 tags::REQUESTED_PROCEDURE_DESCRIPTION,
306 limits::LO,
307 desc,
308 options.length_policy,
309 w,
310 )?;
311 put_str(ds, tags::REQUESTED_PROCEDURE_DESCRIPTION, VR::LO, &desc);
312 }
313 if let Some(item) = code_item(msg, "OBR-4", options, w)? {
317 put_seq(ds, tags::REQUESTED_PROCEDURE_CODE_SEQUENCE, vec![item]);
318 }
319 let priority = field(msg, "OBR-27.6").or_else(|| field(msg, "TQ1-9.1"));
324 if let Some(p) = priority.map(|p| p.to_ascii_uppercase()) {
325 let dicom = match p.as_str() {
326 "S" => Some("STAT"),
327 "A" | "T" => Some("HIGH"),
328 "R" => Some("ROUTINE"),
329 "P" => Some("MEDIUM"),
330 _ => None,
331 };
332 if let Some(d) = dicom {
333 put_str(ds, tags::REQUESTED_PROCEDURE_PRIORITY, VR::CS, d);
334 }
335 }
336 ds.put(DataElement::new(
338 tags::REFERENCED_STUDY_SEQUENCE,
339 VR::SQ,
340 DataSetSequence::<InMemDicomObject>::empty(),
341 ));
342
343 Ok(StudyUid { value, origin })
344}
345
346fn scheduled_procedure_steps(
352 ds: &mut InMemDicomObject,
353 input: &Input<'_>,
354 options: &Options,
355 w: &mut Vec<Warning>,
356) -> Result<(), MwlError> {
357 let msg = input.message;
358 let ipcs: Vec<Segment<'_>> = msg.segments_named("IPC").collect();
359 let mut items = Vec::new();
360 if ipcs.is_empty() {
361 items.push(sps_item(input, None, 1, options, w)?);
362 } else {
363 for (i, ipc) in ipcs.iter().enumerate() {
364 items.push(sps_item(input, Some(ipc), i + 1, options, w)?);
365 }
366 }
367 put_seq(ds, tags::SCHEDULED_PROCEDURE_STEP_SEQUENCE, items);
368 Ok(())
369}
370
371fn sps_item(
372 input: &Input<'_>,
373 ipc: Option<&Segment<'_>>,
374 index: usize,
375 options: &Options,
376 w: &mut Vec<Warning>,
377) -> Result<InMemDicomObject, MwlError> {
378 let msg = input.message;
379 let ipc_field = |n: usize, c: usize| -> Option<String> {
380 let seg = ipc?;
381 let comp = seg.field(n)?.component(c)?;
382 let v = msg.decode(comp.value()).trim().to_string();
383 (!v.is_empty()).then_some(v)
384 };
385 let mut item = InMemDicomObject::new_empty();
386
387 let modality = match ipc_field(5, 1) {
393 Some(m) => modality_code(&m, w).ok_or(MwlError::MissingModality { found: Some(m) })?,
394 None => {
395 let obr24 = field(msg, "OBR-24");
396 match &obr24 {
397 Some(v) => modality_code(v, w).ok_or(MwlError::MissingModality {
398 found: obr24.clone(),
399 })?,
400 None => return Err(MwlError::MissingModality { found: None }),
401 }
402 }
403 };
404 put_str(&mut item, tags::MODALITY, VR::CS, &modality);
405
406 let ae = match ipc_field(9, 1) {
410 Some(ae) => ae,
411 None => {
412 let ae = options
413 .default_station_ae
414 .clone()
415 .ok_or(MwlError::MissingStationAe)?;
416 w.push(Warning::StationAeDefaulted(ae.clone()));
417 ae
418 }
419 };
420 let ae = strict(
421 tags::SCHEDULED_STATION_AE_TITLE,
422 limits::AE,
423 ae.trim().to_string(),
424 )?;
425 put_str(&mut item, tags::SCHEDULED_STATION_AE_TITLE, VR::AE, &ae);
426
427 let start = field(msg, "OBR-27.4").or_else(|| field(msg, "TQ1-7"));
430 let (date, time) = match start.as_deref().map(|s| (hl7_date(s), hl7_time(s))) {
431 Some((Some(d), Some(t))) => (d, t),
432 _ => return Err(MwlError::MissingStartDateTime),
433 };
434 put_str(
435 &mut item,
436 tags::SCHEDULED_PROCEDURE_STEP_START_DATE,
437 VR::DA,
438 &date,
439 );
440 put_str(
441 &mut item,
442 tags::SCHEDULED_PROCEDURE_STEP_START_TIME,
443 VR::TM,
444 &time,
445 );
446
447 let performing = field(msg, "OBR-34.1").and_then(|cnn| {
451 let xcn = cnn.split('&').collect::<Vec<_>>().join("^");
452 pn::from_xcn(&xcn)
453 });
454 match performing {
455 Some(name) => {
456 let v = bounded(
457 tags::SCHEDULED_PERFORMING_PHYSICIAN_NAME,
458 limits::PN,
459 name.pn,
460 options.length_policy,
461 w,
462 )?;
463 put_str(
464 &mut item,
465 tags::SCHEDULED_PERFORMING_PHYSICIAN_NAME,
466 VR::PN,
467 &v,
468 );
469 }
470 None => put_empty(&mut item, tags::SCHEDULED_PERFORMING_PHYSICIAN_NAME, VR::PN),
471 }
472
473 if let Some(desc) = field(msg, "OBR-4.2") {
476 let desc = bounded(
477 tags::SCHEDULED_PROCEDURE_STEP_DESCRIPTION,
478 limits::LO,
479 desc,
480 options.length_policy,
481 w,
482 )?;
483 put_str(
484 &mut item,
485 tags::SCHEDULED_PROCEDURE_STEP_DESCRIPTION,
486 VR::LO,
487 &desc,
488 );
489 }
490 if ipc.is_some() {
493 if let (Some(code), Some(scheme)) = (ipc_field(6, 1), ipc_field(6, 3)) {
494 let mut c = InMemDicomObject::new_empty();
495 put_str(
496 &mut c,
497 tags::CODE_VALUE,
498 VR::SH,
499 &bounded(tags::CODE_VALUE, limits::SH, code, options.length_policy, w)?,
500 );
501 put_str(
502 &mut c,
503 tags::CODING_SCHEME_DESIGNATOR,
504 VR::SH,
505 &bounded(
506 tags::CODING_SCHEME_DESIGNATOR,
507 limits::SH,
508 scheme,
509 options.length_policy,
510 w,
511 )?,
512 );
513 if let Some(meaning) = ipc_field(6, 2) {
514 put_str(
515 &mut c,
516 tags::CODE_MEANING,
517 VR::LO,
518 &bounded(
519 tags::CODE_MEANING,
520 limits::LO,
521 meaning,
522 options.length_policy,
523 w,
524 )?,
525 );
526 }
527 put_seq(&mut item, tags::SCHEDULED_PROTOCOL_CODE_SEQUENCE, vec![c]);
528 }
529 }
530
531 let sps_id = match ipc_field(4, 1).or_else(|| field(msg, "OBR-20")) {
535 Some(v) => v,
536 None => {
537 let rp = input
538 .order
539 .procedure_id
540 .clone()
541 .or_else(|| field(msg, "ORC-2.1"))
542 .unwrap_or_else(|| "SPS".to_string());
543 let id = format!("{rp}-{index}");
544 w.push(Warning::MissingType1 {
545 tag: tags::SCHEDULED_PROCEDURE_STEP_ID,
546 substituted: Some(id.clone()),
547 });
548 id
549 }
550 };
551 let sps_id = bounded(
552 tags::SCHEDULED_PROCEDURE_STEP_ID,
553 limits::SH,
554 sps_id,
555 options.length_policy,
556 w,
557 )?;
558 put_str(
559 &mut item,
560 tags::SCHEDULED_PROCEDURE_STEP_ID,
561 VR::SH,
562 &sps_id,
563 );
564
565 match ipc_field(7, 1) {
567 Some(v) => put_str(
568 &mut item,
569 tags::SCHEDULED_STATION_NAME,
570 VR::SH,
571 &bounded(
572 tags::SCHEDULED_STATION_NAME,
573 limits::SH,
574 v,
575 options.length_policy,
576 w,
577 )?,
578 ),
579 None => put_empty(&mut item, tags::SCHEDULED_STATION_NAME, VR::SH),
580 }
581 match ipc_field(8, 1) {
583 Some(v) => put_str(
584 &mut item,
585 tags::SCHEDULED_PROCEDURE_STEP_LOCATION,
586 VR::SH,
587 &bounded(
588 tags::SCHEDULED_PROCEDURE_STEP_LOCATION,
589 limits::SH,
590 v,
591 options.length_policy,
592 w,
593 )?,
594 ),
595 None => put_empty(&mut item, tags::SCHEDULED_PROCEDURE_STEP_LOCATION, VR::SH),
596 }
597 if field(msg, "ORC-1")
600 .map(|s| s.to_ascii_uppercase())
601 .as_deref()
602 == Some("NW")
603 {
604 put_str(
605 &mut item,
606 tags::SCHEDULED_PROCEDURE_STEP_STATUS,
607 VR::CS,
608 "SCHEDULED",
609 );
610 }
611 Ok(item)
612}
613
614fn modality_code(value: &str, w: &mut Vec<Warning>) -> Option<String> {
618 const DICOM: &[&str] = &[
619 "AR", "AU", "BDUS", "BI", "BMD", "CR", "CT", "DG", "DX", "ECG", "EPS", "ES", "GM", "HC",
620 "HD", "IO", "IVOCT", "IVUS", "KER", "LEN", "LS", "MG", "MR", "NM", "OAM", "OCT", "OP",
621 "OPM", "OPT", "OPV", "OSS", "OT", "PT", "PX", "REG", "RF", "RG", "RTIMAGE", "SM", "SR",
622 "TG", "US", "VA", "XA", "XC",
623 ];
624 const TABLE_0074: &[(&str, &str)] = &[
627 ("MRI", "MR"),
628 ("NMR", "MR"),
629 ("NMS", "NM"),
630 ("RUS", "US"),
631 ("VUS", "US"),
632 ("XRC", "XA"),
633 ("OTH", "OT"),
634 ];
635 let v = value.trim().to_ascii_uppercase();
636 if DICOM.contains(&v.as_str()) {
637 return Some(v);
638 }
639 let to = TABLE_0074.iter().find(|(from, _)| *from == v)?.1;
640 w.push(Warning::ModalityTranslated {
641 from: value.trim().to_string(),
642 to: to.to_string(),
643 });
644 Some(to.to_string())
645}
646
647fn visit(ds: &mut InMemDicomObject, msg: &Message) {
654 match field(msg, "PV1-19.1") {
655 Some(v) => put_str(
656 ds,
657 tags::ADMISSION_ID,
658 VR::LO,
659 &v.chars().take(limits::LO).collect::<String>(),
660 ),
661 None => put_empty(ds, tags::ADMISSION_ID, VR::LO),
662 }
663 let location: Vec<String> = ["PV1-3.1", "PV1-3.2", "PV1-3.3"]
664 .iter()
665 .filter_map(|p| field(msg, p))
666 .collect();
667 if location.is_empty() {
668 put_empty(ds, tags::CURRENT_PATIENT_LOCATION, VR::LO);
669 } else {
670 let joined = location.join(" ");
671 put_str(
672 ds,
673 tags::CURRENT_PATIENT_LOCATION,
674 VR::LO,
675 &joined.chars().take(limits::LO).collect::<String>(),
676 );
677 }
678}
679
680fn code_item(
687 msg: &Message,
688 path: &str,
689 options: &Options,
690 w: &mut Vec<Warning>,
691) -> Result<Option<InMemDicomObject>, MwlError> {
692 let code = field(msg, &format!("{path}.1"));
693 let scheme = field(msg, &format!("{path}.3"));
694 let (Some(code), Some(scheme)) = (code, scheme) else {
695 return Ok(None);
696 };
697 let mut item = InMemDicomObject::new_empty();
698 put_str(
699 &mut item,
700 tags::CODE_VALUE,
701 VR::SH,
702 &bounded(tags::CODE_VALUE, limits::SH, code, options.length_policy, w)?,
703 );
704 put_str(
705 &mut item,
706 tags::CODING_SCHEME_DESIGNATOR,
707 VR::SH,
708 &bounded(
709 tags::CODING_SCHEME_DESIGNATOR,
710 limits::SH,
711 scheme,
712 options.length_policy,
713 w,
714 )?,
715 );
716 if let Some(meaning) = field(msg, &format!("{path}.2")) {
717 put_str(
718 &mut item,
719 tags::CODE_MEANING,
720 VR::LO,
721 &bounded(
722 tags::CODE_MEANING,
723 limits::LO,
724 meaning,
725 options.length_policy,
726 w,
727 )?,
728 );
729 }
730 Ok(Some(item))
731}
732
733fn field(msg: &Message, path: &str) -> Option<String> {
735 msg.get_decoded(path)
736 .map(|v| v.trim().to_string())
737 .filter(|v| !v.is_empty())
738}
739
740pub fn hl7_date(dtm: &str) -> Option<String> {
742 let d: String = dtm.trim().chars().take(8).collect();
743 if d.len() != 8 || !d.chars().all(|c| c.is_ascii_digit()) {
744 return None;
745 }
746 let month: u8 = d[4..6].parse().ok()?;
747 let day: u8 = d[6..8].parse().ok()?;
748 ((1..=12).contains(&month) && (1..=31).contains(&day)).then_some(d)
749}
750
751pub fn hl7_time(dtm: &str) -> Option<String> {
754 let s = dtm.trim();
755 let end = s.find(['+', '-', '.']).unwrap_or(s.len());
756 let digits = &s[..end];
757 if digits.len() < 10 || !digits.chars().all(|c| c.is_ascii_digit()) {
758 return None;
759 }
760 let t = &digits[8..digits.len().min(14)];
761 let mut t = t.to_string();
762 while t.len() < 6 {
763 t.push('0');
764 }
765 let h: u8 = t[..2].parse().ok()?;
766 let m: u8 = t[2..4].parse().ok()?;
767 let sec: u8 = t[4..6].parse().ok()?;
768 (h < 24 && m < 60 && sec < 61).then_some(t)
769}
770
771fn put_str(ds: &mut InMemDicomObject, tag: Tag, vr: VR, value: &str) {
772 ds.put(DataElement::new(tag, vr, PrimitiveValue::from(value)));
773}
774
775fn put_empty(ds: &mut InMemDicomObject, tag: Tag, vr: VR) {
776 ds.put(DataElement::new(tag, vr, PrimitiveValue::Empty));
777}
778
779fn put_seq(ds: &mut InMemDicomObject, tag: Tag, items: Vec<InMemDicomObject>) {
780 ds.put(DataElement::new(tag, VR::SQ, DataSetSequence::from(items)));
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 #[test]
788 fn dates_and_times() {
789 assert_eq!(hl7_date("20260905113000").as_deref(), Some("20260905"));
790 assert_eq!(hl7_date("2026090"), None);
791 assert_eq!(hl7_date("20261305"), None);
792 assert_eq!(hl7_time("20260905113000").as_deref(), Some("113000"));
793 assert_eq!(hl7_time("202609051130").as_deref(), Some("113000"));
794 assert_eq!(hl7_time("2026090511").as_deref(), Some("110000"));
795 assert_eq!(hl7_time("20260905113000+0200").as_deref(), Some("113000"));
796 assert_eq!(hl7_time("20260905"), None);
797 assert_eq!(hl7_time("2026090525"), None);
798 }
799
800 #[test]
801 fn modality_codes() {
802 let mut w = Vec::new();
803 assert_eq!(modality_code("MR", &mut w).as_deref(), Some("MR"));
804 assert_eq!(modality_code("ct", &mut w).as_deref(), Some("CT"));
805 assert!(w.is_empty());
806 assert_eq!(modality_code("MRI", &mut w).as_deref(), Some("MR"));
807 assert_eq!(w.len(), 1);
808 assert_eq!(
809 modality_code("RAD", &mut w),
810 None,
811 "ambiguous table 0074 code"
812 );
813 assert_eq!(modality_code("", &mut w), None);
814 }
815}