Skip to main content

automapper_validation/
syntax.rs

1//! The MIG syntax check a CONTRL reports: one message against its MIG alone.
2//!
3//! No Prüfidentifikator, AHB or condition takes part. A CONTRL reports syntax
4//! errors only (CONTRL AHB 1.0 §2.1); a code wrong only in its context is an
5//! AHB matter and goes to the APERAK. What is checked:
6//!
7//! - **UNH and UNT** (UCM): their elements, the segment count (UNT 0074, code
8//!   29) and the message reference (UNT 0062 against UNH 0062, code 28). An
9//!   error here stops the check of that message: nothing else is reported.
10//! - **Segments** (UCS): a segment the MIG marks `M`/`R` missing (13), one the
11//!   MIG has no place for (15), more data elements than the MIG defines (16),
12//!   a segment or group repeating beyond its `MaxRep` (35, 36).
13//! - **Data elements** (UCD): an `M`/`R` element or component empty (13), a
14//!   value outside the MIG's code list (12), too many components (16), a bad
15//!   decimal notation (19), characters outside the syntax level (21), a
16//!   letter in a numeric element (37), no digit before the decimal mark (38),
17//!   too long (39), too short (40).
18//!
19//! The codes are those the CONTRL MIG 2.0b allows in UCM/UCS/UCD 0085. Every
20//! finding carries its segment counter from UNH = 1 (UCS 0096); a missing
21//! segment or group is reported at its predecessor. Element positions follow
22//! S011: the segment tag is 0098 = 1, the first component 0104 = 1.
23//!
24//! The message is placed with the assembler on the MIG with every segment
25//! number kept, i.e. as if a Prüfidentifikator allowed all the MIG allows.
26
27use std::collections::{HashMap, HashSet};
28
29use mig_assembly::assembler::{AssembledGroup, AssembledSegment, Assembler, AssemblerConfig};
30use mig_assembly::{pid_filter, structure_check, StructureDiagnosticKind};
31use mig_types::schema::common::CodeDefinition;
32use mig_types::schema::mig::{MigDataElement, MigSchema, MigSegment, MigSegmentGroup};
33use mig_types::segment::OwnedSegment;
34use serde::{Deserialize, Serialize};
35
36/// Which CONTRL segment reports a finding.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub enum SyntaxScope {
39    /// UCM: an error in UNH or UNT.
40    Message,
41    /// UCS: an error of a whole segment (0085 in UCS).
42    Segment,
43    /// UCD: an error of a data element in a segment.
44    Element,
45}
46
47/// One syntax error of a message.
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct SyntaxFinding {
50    /// The CONTRL segment that reports it.
51    pub scope: SyntaxScope,
52    /// The syntax error code, DE0085 (`"13"`, `"35"`, …).
53    pub code: String,
54    /// The segment the error is in; for a missing one, the missing segment
55    /// (a missing group: its first segment).
56    pub segment_tag: String,
57    /// The segment counter within the message, UNH = 1 (UCS 0096). A missing
58    /// segment or group is reported at its predecessor.
59    pub message_segment_number: u32,
60    /// The data element's position in the segment, the tag being 1 (0098).
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub element_position: Option<u32>,
63    /// The component's position in its composite, from 1 (0104).
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub component_position: Option<u32>,
66    /// What is wrong, for people.
67    pub detail: String,
68}
69
70/// Values accepted beyond the MIG's own code lists, by MIG segment `Number`,
71/// element index and component index (0-based). A MIG's code list can lag
72/// its AHBs — a Prüfidentifikator an AHB correction adds, an answer code an
73/// AHB takes from a Codeliste the MIG predates — and what an AHB allows is
74/// never a syntax error.
75pub type AcceptedCodes = HashMap<(String, usize, usize), Accepted>;
76
77/// What an element accepts beyond its MIG code list.
78#[derive(Debug, Clone, Default, PartialEq, Eq)]
79pub struct Accepted {
80    /// Any value: an AHB leaves the element open.
81    pub any: bool,
82    /// These codes.
83    pub codes: HashSet<String>,
84}
85
86impl Accepted {
87    fn accepts(&self, value: &str) -> bool {
88        self.any || self.codes.contains(value)
89    }
90}
91
92/// The character repertoire of a syntax level (UNB S001 0001).
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Charset {
95    /// UNOA: upper-case letters, digits, space and a few signs.
96    Unoa,
97    /// UNOB: printable ASCII.
98    Unob,
99    /// UNOC: ISO 8859-1, the energy market's level.
100    Unoc,
101    /// Any other level: every character but control characters.
102    Other,
103}
104
105impl Charset {
106    /// The repertoire of syntax identifier `id` (`"UNOC"`).
107    pub fn from_syntax_identifier(id: &str) -> Self {
108        match id {
109            "UNOA" => Self::Unoa,
110            "UNOB" => Self::Unob,
111            "UNOC" => Self::Unoc,
112            _ => Self::Other,
113        }
114    }
115
116    fn allows(self, c: char) -> bool {
117        match self {
118            Self::Unoa => {
119                c.is_ascii_uppercase() || c.is_ascii_digit() || " .,-()/='+:?!\"%&*;<>".contains(c)
120            }
121            Self::Unob => (' '..='~').contains(&c),
122            Self::Unoc => (' '..='~').contains(&c) || ('\u{A0}'..='\u{FF}').contains(&c),
123            Self::Other => !c.is_control(),
124        }
125    }
126}
127
128/// The interchange's syntax settings a value is judged by.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub struct SyntaxSettings {
131    pub charset: Charset,
132    /// The decimal mark (UNA, default `.`).
133    pub decimal_mark: char,
134}
135
136impl Default for SyntaxSettings {
137    fn default() -> Self {
138        Self {
139            charset: Charset::Unoc,
140            decimal_mark: '.',
141        }
142    }
143}
144
145/// The placements of a MIG the syntax check assembles a message on: the
146/// whole MIG kept apart, with its group variants merged, and with them
147/// apart. They depend on the MIG alone, and building them is most of the
148/// check's cost (about 10 ms for UTILMD), so a caller checking many messages
149/// builds them once ([`check_message_syntax_prepared`]).
150#[derive(Debug, Clone)]
151pub struct MigPlacements {
152    unmerged: MigSchema,
153    merged: MigSchema,
154    apart: MigSchema,
155}
156
157impl MigPlacements {
158    pub fn new(mig: &MigSchema) -> Self {
159        let numbers = all_numbers(mig);
160        Self {
161            unmerged: pid_filter::filter_mig_for_pid_unmerged(mig, &numbers),
162            merged: pid_filter::filter_mig_for_pid(mig, &numbers),
163            apart: pid_filter::filter_mig_variant_aware(mig, &numbers),
164        }
165    }
166}
167
168/// Check one message against `mig`.
169///
170/// `segments` are the message's segments as the assembler takes them for
171/// `mig` (`MessageChunk::segments_for_mig`): UNH…UNT, with the envelope
172/// before them where the MIG covers it. Only UNH…UNT is judged.
173pub fn check_message_syntax(
174    segments: &[OwnedSegment],
175    mig: &MigSchema,
176    settings: SyntaxSettings,
177    accepted: &AcceptedCodes,
178) -> Vec<SyntaxFinding> {
179    check_message_syntax_prepared(segments, mig, &MigPlacements::new(mig), settings, accepted)
180}
181
182/// [`check_message_syntax`] with the MIG's placements built beforehand.
183/// `placements` must be [`MigPlacements::new`] of `mig`.
184pub fn check_message_syntax_prepared(
185    segments: &[OwnedSegment],
186    mig: &MigSchema,
187    placements: &MigPlacements,
188    settings: SyntaxSettings,
189    accepted: &AcceptedCodes,
190) -> Vec<SyntaxFinding> {
191    let Some(unh_index) = segments.iter().position(|s| s.id == "UNH") else {
192        return Vec::new();
193    };
194    let unh = &segments[unh_index];
195    let unh_number = unh.segment_number;
196    let counter = |segment_number: u32| segment_number.saturating_sub(unh_number) + 1;
197    let unt_index = segments.iter().rposition(|s| s.id == "UNT");
198
199    // UCM: UNH and UNT. An error here stops the message.
200    let mut out = Vec::new();
201    for seg in [Some(unh_index), unt_index]
202        .into_iter()
203        .flatten()
204        .map(|i| &segments[i])
205    {
206        if let Some(slot) = mig.segments.iter().find(|s| s.id == seg.id) {
207            check_elements(
208                seg,
209                slot,
210                counter(seg.segment_number),
211                settings,
212                accepted,
213                &mut out,
214            );
215        }
216    }
217    match unt_index {
218        Some(i) => {
219            let unt = &segments[i];
220            let count = (i - unh_index + 1).to_string();
221            if !unt.get_element(0).is_empty() && unt.get_element(0) != count {
222                out.push(finding(
223                    SyntaxScope::Message,
224                    "29",
225                    unt,
226                    counter(unt.segment_number),
227                    Some((2, None)),
228                    format!(
229                        "UNT counts {} segments, the message has {count}",
230                        unt.get_element(0)
231                    ),
232                ));
233            }
234            if !unt.get_element(1).is_empty() && unt.get_element(1) != unh.get_element(0) {
235                out.push(finding(
236                    SyntaxScope::Message,
237                    "28",
238                    unt,
239                    counter(unt.segment_number),
240                    Some((3, None)),
241                    format!(
242                        "UNT names message '{}', UNH '{}'",
243                        unt.get_element(1),
244                        unh.get_element(0)
245                    ),
246                ));
247            }
248        }
249        None => out.push(SyntaxFinding {
250            scope: SyntaxScope::Message,
251            code: "13".into(),
252            segment_tag: "UNT".into(),
253            message_segment_number: counter(
254                segments.last().map_or(unh_number, |s| s.segment_number),
255            ),
256            element_position: None,
257            component_position: None,
258            detail: "UNT is missing".into(),
259        }),
260    }
261    for f in &mut out {
262        f.scope = SyntaxScope::Message;
263    }
264    if !out.is_empty() {
265        return out;
266    }
267
268    // Place the message on the MIG with every segment number kept. The
269    // assembler can merge a group's variants or keep them apart; each loses
270    // some messages (merged: the order within a variant, UTILTS SEQ+Z43 DTM
271    // RFF; apart: ORDERS' UNS after its LIN groups). A message fits the MIG
272    // when some placement fits it, so the one with fewer findings is taken.
273    let unmerged = &placements.unmerged;
274    let place = |assembly_mig: &MigSchema| {
275        let assembler = Assembler::with_config(
276            assembly_mig,
277            AssemblerConfig {
278                strict_code_matching: true,
279                skip_unknown_segments: true,
280                ..Default::default()
281            },
282        );
283        let (tree, mut diagnostics) = assembler.assemble_with_diagnostics(segments);
284        diagnostics.extend(structure_check::check_structure(&tree, unmerged));
285        (tree, diagnostics)
286    };
287    let merged = place(&placements.merged);
288    // Apart is taken only with fewer findings: none can beat a clean merge,
289    // which most messages are, so they are assembled once.
290    let (tree, diagnostics) = if merged.1.is_empty() {
291        merged
292    } else {
293        let apart = place(&placements.apart);
294        if apart.1.len() < merged.1.len() {
295            apart
296        } else {
297            merged
298        }
299    };
300
301    let first_segments = group_first_segments(mig);
302    let within =
303        |n: u32| n >= unh_number && unt_index.map_or(true, |i| n <= segments[i].segment_number);
304    let mut reported = HashSet::new();
305    for diag in diagnostics {
306        let n = u32::try_from(diag.position).unwrap_or(0);
307        if !within(n) {
308            continue;
309        }
310        let (code, tag) = match diag.kind {
311            StructureDiagnosticKind::MissingRequiredSegment => ("13", diag.segment_id.clone()),
312            StructureDiagnosticKind::MissingRequiredGroup => (
313                "13",
314                first_segments
315                    .get(diag.segment_id.as_str())
316                    .cloned()
317                    .unwrap_or(diag.segment_id.clone()),
318            ),
319            StructureDiagnosticKind::MaxRepetitionsExceeded => ("35", diag.segment_id.clone()),
320            StructureDiagnosticKind::GroupMaxRepetitionsExceeded => (
321                "36",
322                first_segments
323                    .get(diag.segment_id.as_str())
324                    .cloned()
325                    .unwrap_or(diag.segment_id.clone()),
326            ),
327            _ => ("15", diag.segment_id.clone()),
328        };
329        // The assembler and the structure check can both see one defect.
330        if !reported.insert((code, tag.clone(), n)) {
331            continue;
332        }
333        out.push(SyntaxFinding {
334            scope: SyntaxScope::Segment,
335            code: code.into(),
336            segment_tag: tag,
337            message_segment_number: counter(n),
338            element_position: None,
339            component_position: None,
340            detail: diag.message,
341        });
342    }
343
344    // UCD: every segment between UNH and UNT against its slot, each group
345    // repetition judged by its own MIG variant.
346    let mut root: Vec<&AssembledSegment> = tree.segments.iter().collect();
347    root.extend(tree.inter_group_segments.values().flatten());
348    let mut walk = ElementWalk {
349        settings,
350        accepted,
351        within: &within,
352        counter: &counter,
353        out: &mut out,
354    };
355    walk.segments(&root, &unmerged.segments);
356    walk.groups(&tree.groups, &unmerged.segment_groups);
357
358    // A segment whose only slot it fills with a wrong qualifier is reported
359    // once, as that value (UCD 12), not also as out of place (UCS 15).
360    let wrong_codes: HashSet<u32> = out
361        .iter()
362        .filter(|f| f.scope == SyntaxScope::Element && f.code == "12")
363        .map(|f| f.message_segment_number)
364        .collect();
365    out.retain(|f| {
366        !(f.scope == SyntaxScope::Segment
367            && f.code == "15"
368            && wrong_codes.contains(&f.message_segment_number))
369    });
370
371    out.sort_by_key(|f| {
372        (
373            f.message_segment_number,
374            f.element_position,
375            f.component_position,
376        )
377    });
378    out
379}
380
381fn finding(
382    scope: SyntaxScope,
383    code: &str,
384    seg: &OwnedSegment,
385    message_segment_number: u32,
386    at: Option<(u32, Option<u32>)>,
387    detail: String,
388) -> SyntaxFinding {
389    SyntaxFinding {
390        scope,
391        code: code.into(),
392        segment_tag: seg.id.clone(),
393        message_segment_number,
394        element_position: at.map(|(e, _)| e),
395        component_position: at.and_then(|(_, c)| c),
396        detail,
397    }
398}
399
400/// Every segment `Number` of `mig`: the filter then keeps all of it.
401fn all_numbers(mig: &MigSchema) -> HashSet<String> {
402    fn groups(list: &[MigSegmentGroup], out: &mut HashSet<String>) {
403        for g in list {
404            out.extend(g.segments.iter().filter_map(|s| s.number.clone()));
405            groups(&g.nested_groups, out);
406        }
407    }
408    let mut out: HashSet<String> = mig
409        .segments
410        .iter()
411        .filter_map(|s| s.number.clone())
412        .collect();
413    groups(&mig.segment_groups, &mut out);
414    out
415}
416
417/// Group id → the tag of its first segment (a missing group is reported as
418/// that segment).
419fn group_first_segments(mig: &MigSchema) -> HashMap<&str, String> {
420    fn walk<'m>(list: &'m [MigSegmentGroup], out: &mut HashMap<&'m str, String>) {
421        for g in list {
422            if let Some(first) = g.segments.first() {
423                out.entry(g.id.as_str()).or_insert_with(|| first.id.clone());
424            }
425            walk(&g.nested_groups, out);
426        }
427    }
428    let mut out = HashMap::new();
429    walk(&mig.segment_groups, &mut out);
430    out
431}
432
433/// The element check over the assembled tree, by MIG variant.
434struct ElementWalk<'a> {
435    settings: SyntaxSettings,
436    accepted: &'a AcceptedCodes,
437    within: &'a dyn Fn(u32) -> bool,
438    counter: &'a dyn Fn(u32) -> u32,
439    out: &'a mut Vec<SyntaxFinding>,
440}
441
442impl ElementWalk<'_> {
443    fn groups(&mut self, present: &[AssembledGroup], variants: &[MigSegmentGroup]) {
444        for group in present {
445            let same_id: Vec<&MigSegmentGroup> =
446                variants.iter().filter(|v| v.id == group.group_id).collect();
447            for rep in &group.repetitions {
448                // A repetition no variant claims is the structure check's
449                // finding (UCS 15); its elements have no slots to be judged by.
450                // Of several variants with its entry qualifier, the one it
451                // fits best.
452                let candidates: Vec<&MigSegmentGroup> = match same_id.as_slice() {
453                    [only] => vec![*only],
454                    _ => rep.segments.first().map_or(Vec::new(), |entry| {
455                        same_id
456                            .iter()
457                            .copied()
458                            .filter(|v| {
459                                v.entry_qualifier().is_some_and(|q| {
460                                    q.tag == entry.tag && q.matches(&entry.elements)
461                                })
462                            })
463                            .collect()
464                    }),
465                };
466                let mut best: Option<Vec<SyntaxFinding>> = None;
467                for variant in candidates {
468                    let mut found = Vec::new();
469                    let mut walk = ElementWalk {
470                        settings: self.settings,
471                        accepted: self.accepted,
472                        within: self.within,
473                        counter: self.counter,
474                        out: &mut found,
475                    };
476                    let own: Vec<&AssembledSegment> = rep.segments.iter().collect();
477                    walk.segments(&own, &variant.segments);
478                    walk.groups(&rep.child_groups, &variant.nested_groups);
479                    if best.as_ref().map_or(true, |b| found.len() < b.len()) {
480                        best = Some(found);
481                    }
482                }
483                self.out.extend(best.unwrap_or_default());
484            }
485        }
486    }
487
488    fn segments(&mut self, present: &[&AssembledSegment], slots: &[MigSegment]) {
489        for seg in present {
490            let Some(n) = seg.segment_number else {
491                continue;
492            };
493            if seg.tag == "UNH" || seg.tag == "UNT" || !(self.within)(n) {
494                continue;
495            }
496            let owned = OwnedSegment {
497                id: seg.tag.clone(),
498                elements: seg.elements.clone(),
499                segment_number: n,
500            };
501            // Of the slots `seg` may fill, the one it fits best: a group can
502            // hold several slots of one tag and qualifier (DTM+Z21 by day and
503            // by minute), and only one of them need take its values.
504            let mut best: Option<Vec<SyntaxFinding>> = None;
505            for slot in slots_for(seg, slots) {
506                let mut found = Vec::new();
507                check_elements(
508                    &owned,
509                    slot,
510                    (self.counter)(n),
511                    self.settings,
512                    self.accepted,
513                    &mut found,
514                );
515                if best.as_ref().map_or(true, |b| found.len() < b.len()) {
516                    best = Some(found);
517                }
518            }
519            self.out.extend(best.unwrap_or_default());
520        }
521    }
522}
523
524/// The slots of `slots` that `seg` may fill: those of its tag whose
525/// qualifier it carries, else those of its tag without a qualifier. None
526/// when every slot of its tag names qualifiers and `seg` carries none of
527/// them: the structure check reports that segment (UCS 15) — unless there is
528/// only one slot, whose qualifier is then judged as a value (UCD 12).
529fn slots_for<'m>(seg: &AssembledSegment, slots: &'m [MigSegment]) -> Vec<&'m MigSegment> {
530    let tagged: Vec<&MigSegment> = slots.iter().filter(|s| s.id == seg.tag).collect();
531    let carries = |slot: &MigSegment| {
532        qualifier(slot).map(|(e, c, d)| {
533            seg.elements
534                .get(e)
535                .and_then(|el| el.get(c))
536                .is_some_and(|v| lists_code(d, v))
537        })
538    };
539    let qualified: Vec<&MigSegment> = tagged
540        .iter()
541        .copied()
542        .filter(|slot| carries(slot) == Some(true))
543        .collect();
544    if !qualified.is_empty() {
545        return qualified;
546    }
547    let open: Vec<&MigSegment> = tagged
548        .iter()
549        .copied()
550        .filter(|slot| carries(slot).is_none())
551        .collect();
552    if !open.is_empty() {
553        return open;
554    }
555    match tagged.as_slice() {
556        [only] => vec![*only],
557        _ => Vec::new(),
558    }
559}
560
561/// A MIG example code (`XYZ`) stands for any value.
562fn is_placeholder(code: &CodeDefinition) -> bool {
563    code.name == "Beispielcode" || code.value == "XYZ"
564}
565
566/// The real codes of an element; empty when it takes any value.
567fn codes(d: &MigDataElement) -> Vec<&str> {
568    if d.codes.iter().any(is_placeholder) {
569        return Vec::new();
570    }
571    d.codes
572        .iter()
573        .map(|c| c.value.as_str())
574        .filter(|v| !v.is_empty())
575        .collect()
576}
577
578/// A slot's qualifier: its first coded element, as (element, component, codes).
579fn qualifier(slot: &MigSegment) -> Option<(usize, usize, &MigDataElement)> {
580    let simple = slot.data_elements.iter().map(|d| (d.position, 0, d));
581    let composite = slot.composites.iter().flat_map(|c| {
582        c.data_elements
583            .iter()
584            .map(move |d| (c.position, d.position, d))
585    });
586    simple
587        .chain(composite)
588        .filter(|(_, _, d)| has_codes(d))
589        .min_by_key(|(e, c, _)| (*e, *c))
590}
591
592/// Whether [`codes`] gives `d` a code list — without building it.
593fn has_codes(d: &MigDataElement) -> bool {
594    !d.codes.iter().any(is_placeholder) && d.codes.iter().any(|c| !c.value.is_empty())
595}
596
597/// Whether [`codes`] of `d` holds `value` — without building the list.
598fn lists_code(d: &MigDataElement, value: &str) -> bool {
599    has_codes(d) && !value.is_empty() && d.codes.iter().any(|c| c.value == value)
600}
601
602fn required(spec: Option<&str>, std: Option<&str>) -> bool {
603    matches!(spec.or(std).map(str::trim), Some("M" | "R"))
604}
605
606/// Check `seg`'s data elements against `slot`.
607fn check_elements(
608    seg: &OwnedSegment,
609    slot: &MigSegment,
610    counter: u32,
611    settings: SyntaxSettings,
612    accepted: &AcceptedCodes,
613    out: &mut Vec<SyntaxFinding>,
614) {
615    let number = slot.number.as_deref().unwrap_or("");
616    let also = |e: usize, c: usize| accepted.get(&(number.to_string(), e, c));
617    let defined = slot
618        .data_elements
619        .iter()
620        .map(|d| d.position)
621        .chain(slot.composites.iter().map(|c| c.position))
622        .max()
623        .map_or(0, |p| p + 1);
624    let last_filled = seg
625        .elements
626        .iter()
627        .rposition(|e| e.iter().any(|c| !c.is_empty()))
628        .map_or(0, |p| p + 1);
629    if last_filled > defined {
630        out.push(finding(
631            SyntaxScope::Segment,
632            "16",
633            seg,
634            counter,
635            None,
636            format!(
637                "{} has {last_filled} data elements, the MIG defines {defined}",
638                seg.id
639            ),
640        ));
641    }
642
643    for d in &slot.data_elements {
644        let element = seg.elements.get(d.position);
645        let at = d.position as u32 + 2;
646        let value = element.and_then(|e| e.first()).map_or("", String::as_str);
647        if element.is_some_and(|e| e.iter().skip(1).any(|c| !c.is_empty())) {
648            out.push(finding(
649                SyntaxScope::Element,
650                "16",
651                seg,
652                counter,
653                Some((at, None)),
654                format!(
655                    "{} {} is a simple data element but has components",
656                    seg.id, d.id
657                ),
658            ));
659        }
660        check_value(
661            seg,
662            d,
663            value,
664            counter,
665            (at, None),
666            settings,
667            &|| also(d.position, 0),
668            out,
669        );
670    }
671
672    for c in &slot.composites {
673        let at = c.position as u32 + 2;
674        let components: &[String] = seg.elements.get(c.position).map_or(&[], Vec::as_slice);
675        if components.iter().all(String::is_empty) {
676            if required(c.status_spec.as_deref(), c.status_std.as_deref()) {
677                out.push(finding(
678                    SyntaxScope::Element,
679                    "13",
680                    seg,
681                    counter,
682                    Some((at, None)),
683                    format!("{} {} ({}) is missing", seg.id, c.id, c.name),
684                ));
685            }
686            continue;
687        }
688        let defined = c
689            .data_elements
690            .iter()
691            .map(|d| d.position)
692            .max()
693            .map_or(0, |p| p + 1);
694        let last_filled = components
695            .iter()
696            .rposition(|v| !v.is_empty())
697            .map_or(0, |p| p + 1);
698        if last_filled > defined {
699            out.push(finding(
700                SyntaxScope::Element,
701                "16",
702                seg,
703                counter,
704                Some((at, None)),
705                format!(
706                    "{} {} has {last_filled} components, the MIG defines {defined}",
707                    seg.id, c.id
708                ),
709            ));
710        }
711        for d in &c.data_elements {
712            let value = components.get(d.position).map_or("", String::as_str);
713            check_value(
714                seg,
715                d,
716                value,
717                counter,
718                (at, Some(d.position as u32 + 1)),
719                settings,
720                &|| also(c.position, d.position),
721                out,
722            );
723        }
724    }
725}
726
727/// Check one simple or component data element's value.
728fn check_value<'a>(
729    seg: &OwnedSegment,
730    d: &MigDataElement,
731    value: &str,
732    counter: u32,
733    at: (u32, Option<u32>),
734    settings: SyntaxSettings,
735    // Looked up only for a code the MIG does not list: the key allocates.
736    also: &dyn Fn() -> Option<&'a Accepted>,
737    out: &mut Vec<SyntaxFinding>,
738) {
739    let mut push = |code: &str, detail: String| {
740        out.push(finding(
741            SyntaxScope::Element,
742            code,
743            seg,
744            counter,
745            Some(at),
746            detail,
747        ));
748    };
749    // Built only for a finding: this runs for every value of the message.
750    let name = || format!("{} {} ({})", seg.id, d.id, d.name);
751    if value.is_empty() {
752        if required(d.status_spec.as_deref(), d.status_std.as_deref()) {
753            push("13", format!("{} is missing", name()));
754        }
755        return;
756    }
757    if let Some(bad) = value.chars().find(|&c| !settings.charset.allows(c)) {
758        push(
759            "21",
760            format!(
761                "{} holds '{bad}', which its syntax level does not allow",
762                name()
763            ),
764        );
765        return;
766    }
767    if let Some(format) = d.format_spec.as_deref().or(d.format_std.as_deref()) {
768        if let Some((code, detail)) = check_format(value, format, settings.decimal_mark) {
769            push(code, format!("{} '{value}': {detail}", name()));
770            return;
771        }
772    }
773    if has_codes(d) && !lists_code(d, value) && !also().is_some_and(|a| a.accepts(value)) {
774        push(
775            "12",
776            format!(
777                "{} '{value}' is none of the MIG's codes {}",
778                name(),
779                codes(d).join(", ")
780            ),
781        );
782    }
783}
784
785/// A format `an..35`, `an3`, `n..15`, `n5`, `a3`: its kind, its length, and
786/// whether the length is a maximum.
787fn parse_format(format: &str) -> Option<(&str, usize, bool)> {
788    let format = format.trim();
789    let kind_end = format.find(|c: char| !c.is_ascii_alphabetic())?;
790    let (kind, rest) = format.split_at(kind_end);
791    let (upto, digits) = match rest.strip_prefix("..") {
792        Some(d) => (true, d),
793        None => (false, rest),
794    };
795    Some((kind, digits.parse().ok()?, upto))
796}
797
798/// The UCD code and reason when `value` breaks `format`.
799fn check_format(value: &str, format: &str, decimal_mark: char) -> Option<(&'static str, String)> {
800    let (kind, length, upto) = parse_format(format)?;
801    let counted = match kind {
802        "n" => {
803            let unsigned = value.strip_prefix('-').unwrap_or(value);
804            if let Some(c) = unsigned
805                .chars()
806                .find(|c| !c.is_ascii_digit() && *c != decimal_mark)
807            {
808                return Some(match c {
809                    '.' | ',' => (
810                        "19",
811                        format!("'{c}' is not the decimal mark '{decimal_mark}'"),
812                    ),
813                    _ => ("37", format!("'{c}' in a numeric data element ({format})")),
814                });
815            }
816            let marks = unsigned.matches(decimal_mark).count();
817            if marks > 1 || unsigned.ends_with(decimal_mark) {
818                return Some(("19", format!("not a decimal number ({format})")));
819            }
820            if unsigned.starts_with(decimal_mark) {
821                return Some(("38", "no digit before the decimal mark".to_string()));
822            }
823            unsigned.chars().filter(char::is_ascii_digit).count()
824        }
825        "a" => {
826            if let Some(c) = value.chars().find(char::is_ascii_digit) {
827                return Some((
828                    "37",
829                    format!("'{c}' in an alphabetic data element ({format})"),
830                ));
831            }
832            value.chars().count()
833        }
834        _ => value.chars().count(),
835    };
836    if counted > length {
837        return Some((
838            "39",
839            format!("{counted} characters, {format} allows {length}"),
840        ));
841    }
842    if !upto && counted < length {
843        return Some((
844            "40",
845            format!("{counted} characters, {format} requires {length}"),
846        ));
847    }
848    None
849}
850
851#[cfg(test)]
852mod tests {
853    use super::*;
854
855    #[test]
856    fn formats() {
857        assert_eq!(check_format("12345", "n5", '.'), None);
858        assert_eq!(check_format("1234", "n5", '.').map(|f| f.0), Some("40"));
859        assert_eq!(check_format("123456", "n..5", '.').map(|f| f.0), Some("39"));
860        assert_eq!(check_format("-12.5", "n..3", '.'), None);
861        assert_eq!(check_format(".5", "n..3", '.').map(|f| f.0), Some("38"));
862        assert_eq!(check_format("5.", "n..3", '.').map(|f| f.0), Some("19"));
863        assert_eq!(check_format("1,5", "n..3", '.').map(|f| f.0), Some("19"));
864        assert_eq!(check_format("1A", "n..3", '.').map(|f| f.0), Some("37"));
865        assert_eq!(check_format("Z1", "a..3", '.').map(|f| f.0), Some("37"));
866        assert_eq!(check_format("ÄBC", "an3", '.'), None);
867        assert_eq!(check_format("AB", "an3", '.').map(|f| f.0), Some("40"));
868    }
869
870    #[test]
871    fn charsets() {
872        assert!(Charset::Unoc.allows('ä'));
873        assert!(!Charset::Unoc.allows('€'));
874        assert!(!Charset::Unoc.allows('\n'));
875        assert!(!Charset::Unoa.allows('a'));
876        assert!(Charset::Unob.allows('a'));
877    }
878}