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    let apart = place(&placements.apart);
289    let (tree, diagnostics) = if apart.1.len() < merged.1.len() {
290        apart
291    } else {
292        merged
293    };
294
295    let first_segments = group_first_segments(mig);
296    let within =
297        |n: u32| n >= unh_number && unt_index.map_or(true, |i| n <= segments[i].segment_number);
298    let mut reported = HashSet::new();
299    for diag in diagnostics {
300        let n = u32::try_from(diag.position).unwrap_or(0);
301        if !within(n) {
302            continue;
303        }
304        let (code, tag) = match diag.kind {
305            StructureDiagnosticKind::MissingRequiredSegment => ("13", diag.segment_id.clone()),
306            StructureDiagnosticKind::MissingRequiredGroup => (
307                "13",
308                first_segments
309                    .get(diag.segment_id.as_str())
310                    .cloned()
311                    .unwrap_or(diag.segment_id.clone()),
312            ),
313            StructureDiagnosticKind::MaxRepetitionsExceeded => ("35", diag.segment_id.clone()),
314            StructureDiagnosticKind::GroupMaxRepetitionsExceeded => (
315                "36",
316                first_segments
317                    .get(diag.segment_id.as_str())
318                    .cloned()
319                    .unwrap_or(diag.segment_id.clone()),
320            ),
321            _ => ("15", diag.segment_id.clone()),
322        };
323        // The assembler and the structure check can both see one defect.
324        if !reported.insert((code, tag.clone(), n)) {
325            continue;
326        }
327        out.push(SyntaxFinding {
328            scope: SyntaxScope::Segment,
329            code: code.into(),
330            segment_tag: tag,
331            message_segment_number: counter(n),
332            element_position: None,
333            component_position: None,
334            detail: diag.message,
335        });
336    }
337
338    // UCD: every segment between UNH and UNT against its slot, each group
339    // repetition judged by its own MIG variant.
340    let mut root: Vec<&AssembledSegment> = tree.segments.iter().collect();
341    root.extend(tree.inter_group_segments.values().flatten());
342    let mut walk = ElementWalk {
343        settings,
344        accepted,
345        within: &within,
346        counter: &counter,
347        out: &mut out,
348    };
349    walk.segments(&root, &unmerged.segments);
350    walk.groups(&tree.groups, &unmerged.segment_groups);
351
352    // A segment whose only slot it fills with a wrong qualifier is reported
353    // once, as that value (UCD 12), not also as out of place (UCS 15).
354    let wrong_codes: HashSet<u32> = out
355        .iter()
356        .filter(|f| f.scope == SyntaxScope::Element && f.code == "12")
357        .map(|f| f.message_segment_number)
358        .collect();
359    out.retain(|f| {
360        !(f.scope == SyntaxScope::Segment
361            && f.code == "15"
362            && wrong_codes.contains(&f.message_segment_number))
363    });
364
365    out.sort_by_key(|f| {
366        (
367            f.message_segment_number,
368            f.element_position,
369            f.component_position,
370        )
371    });
372    out
373}
374
375fn finding(
376    scope: SyntaxScope,
377    code: &str,
378    seg: &OwnedSegment,
379    message_segment_number: u32,
380    at: Option<(u32, Option<u32>)>,
381    detail: String,
382) -> SyntaxFinding {
383    SyntaxFinding {
384        scope,
385        code: code.into(),
386        segment_tag: seg.id.clone(),
387        message_segment_number,
388        element_position: at.map(|(e, _)| e),
389        component_position: at.and_then(|(_, c)| c),
390        detail,
391    }
392}
393
394/// Every segment `Number` of `mig`: the filter then keeps all of it.
395fn all_numbers(mig: &MigSchema) -> HashSet<String> {
396    fn groups(list: &[MigSegmentGroup], out: &mut HashSet<String>) {
397        for g in list {
398            out.extend(g.segments.iter().filter_map(|s| s.number.clone()));
399            groups(&g.nested_groups, out);
400        }
401    }
402    let mut out: HashSet<String> = mig
403        .segments
404        .iter()
405        .filter_map(|s| s.number.clone())
406        .collect();
407    groups(&mig.segment_groups, &mut out);
408    out
409}
410
411/// Group id → the tag of its first segment (a missing group is reported as
412/// that segment).
413fn group_first_segments(mig: &MigSchema) -> HashMap<&str, String> {
414    fn walk<'m>(list: &'m [MigSegmentGroup], out: &mut HashMap<&'m str, String>) {
415        for g in list {
416            if let Some(first) = g.segments.first() {
417                out.entry(g.id.as_str()).or_insert_with(|| first.id.clone());
418            }
419            walk(&g.nested_groups, out);
420        }
421    }
422    let mut out = HashMap::new();
423    walk(&mig.segment_groups, &mut out);
424    out
425}
426
427/// The element check over the assembled tree, by MIG variant.
428struct ElementWalk<'a> {
429    settings: SyntaxSettings,
430    accepted: &'a AcceptedCodes,
431    within: &'a dyn Fn(u32) -> bool,
432    counter: &'a dyn Fn(u32) -> u32,
433    out: &'a mut Vec<SyntaxFinding>,
434}
435
436impl ElementWalk<'_> {
437    fn groups(&mut self, present: &[AssembledGroup], variants: &[MigSegmentGroup]) {
438        for group in present {
439            let same_id: Vec<&MigSegmentGroup> =
440                variants.iter().filter(|v| v.id == group.group_id).collect();
441            for rep in &group.repetitions {
442                // A repetition no variant claims is the structure check's
443                // finding (UCS 15); its elements have no slots to be judged by.
444                // Of several variants with its entry qualifier, the one it
445                // fits best.
446                let candidates: Vec<&MigSegmentGroup> = match same_id.as_slice() {
447                    [only] => vec![*only],
448                    _ => rep.segments.first().map_or(Vec::new(), |entry| {
449                        same_id
450                            .iter()
451                            .copied()
452                            .filter(|v| {
453                                v.entry_qualifier().is_some_and(|q| {
454                                    q.tag == entry.tag && q.matches(&entry.elements)
455                                })
456                            })
457                            .collect()
458                    }),
459                };
460                let mut best: Option<Vec<SyntaxFinding>> = None;
461                for variant in candidates {
462                    let mut found = Vec::new();
463                    let mut walk = ElementWalk {
464                        settings: self.settings,
465                        accepted: self.accepted,
466                        within: self.within,
467                        counter: self.counter,
468                        out: &mut found,
469                    };
470                    let own: Vec<&AssembledSegment> = rep.segments.iter().collect();
471                    walk.segments(&own, &variant.segments);
472                    walk.groups(&rep.child_groups, &variant.nested_groups);
473                    if best.as_ref().map_or(true, |b| found.len() < b.len()) {
474                        best = Some(found);
475                    }
476                }
477                self.out.extend(best.unwrap_or_default());
478            }
479        }
480    }
481
482    fn segments(&mut self, present: &[&AssembledSegment], slots: &[MigSegment]) {
483        for seg in present {
484            let Some(n) = seg.segment_number else {
485                continue;
486            };
487            if seg.tag == "UNH" || seg.tag == "UNT" || !(self.within)(n) {
488                continue;
489            }
490            let owned = OwnedSegment {
491                id: seg.tag.clone(),
492                elements: seg.elements.clone(),
493                segment_number: n,
494            };
495            // Of the slots `seg` may fill, the one it fits best: a group can
496            // hold several slots of one tag and qualifier (DTM+Z21 by day and
497            // by minute), and only one of them need take its values.
498            let mut best: Option<Vec<SyntaxFinding>> = None;
499            for slot in slots_for(seg, slots) {
500                let mut found = Vec::new();
501                check_elements(
502                    &owned,
503                    slot,
504                    (self.counter)(n),
505                    self.settings,
506                    self.accepted,
507                    &mut found,
508                );
509                if best.as_ref().map_or(true, |b| found.len() < b.len()) {
510                    best = Some(found);
511                }
512            }
513            self.out.extend(best.unwrap_or_default());
514        }
515    }
516}
517
518/// The slots of `slots` that `seg` may fill: those of its tag whose
519/// qualifier it carries, else those of its tag without a qualifier. None
520/// when every slot of its tag names qualifiers and `seg` carries none of
521/// them: the structure check reports that segment (UCS 15) — unless there is
522/// only one slot, whose qualifier is then judged as a value (UCD 12).
523fn slots_for<'m>(seg: &AssembledSegment, slots: &'m [MigSegment]) -> Vec<&'m MigSegment> {
524    let tagged: Vec<&MigSegment> = slots.iter().filter(|s| s.id == seg.tag).collect();
525    let carries = |slot: &MigSegment| {
526        qualifier(slot).map(|(e, c, codes)| {
527            seg.elements
528                .get(e)
529                .and_then(|el| el.get(c))
530                .is_some_and(|v| codes.contains(v))
531        })
532    };
533    let qualified: Vec<&MigSegment> = tagged
534        .iter()
535        .copied()
536        .filter(|slot| carries(slot) == Some(true))
537        .collect();
538    if !qualified.is_empty() {
539        return qualified;
540    }
541    let open: Vec<&MigSegment> = tagged
542        .iter()
543        .copied()
544        .filter(|slot| carries(slot).is_none())
545        .collect();
546    if !open.is_empty() {
547        return open;
548    }
549    match tagged.as_slice() {
550        [only] => vec![*only],
551        _ => Vec::new(),
552    }
553}
554
555/// A MIG example code (`XYZ`) stands for any value.
556fn is_placeholder(code: &CodeDefinition) -> bool {
557    code.name == "Beispielcode" || code.value == "XYZ"
558}
559
560/// The real codes of an element; empty when it takes any value.
561fn codes(d: &MigDataElement) -> Vec<&str> {
562    if d.codes.iter().any(is_placeholder) {
563        return Vec::new();
564    }
565    d.codes
566        .iter()
567        .map(|c| c.value.as_str())
568        .filter(|v| !v.is_empty())
569        .collect()
570}
571
572/// A slot's qualifier: its first coded element, as (element, component, codes).
573fn qualifier(slot: &MigSegment) -> Option<(usize, usize, Vec<String>)> {
574    let simple = slot.data_elements.iter().map(|d| (d.position, 0, d));
575    let composite = slot.composites.iter().flat_map(|c| {
576        c.data_elements
577            .iter()
578            .map(move |d| (c.position, d.position, d))
579    });
580    simple
581        .chain(composite)
582        .filter(|(_, _, d)| !codes(d).is_empty())
583        .min_by_key(|(e, c, _)| (*e, *c))
584        .map(|(e, c, d)| (e, c, codes(d).into_iter().map(String::from).collect()))
585}
586
587fn required(spec: Option<&str>, std: Option<&str>) -> bool {
588    matches!(spec.or(std).map(str::trim), Some("M" | "R"))
589}
590
591/// Check `seg`'s data elements against `slot`.
592fn check_elements(
593    seg: &OwnedSegment,
594    slot: &MigSegment,
595    counter: u32,
596    settings: SyntaxSettings,
597    accepted: &AcceptedCodes,
598    out: &mut Vec<SyntaxFinding>,
599) {
600    let number = slot.number.as_deref().unwrap_or("");
601    let also = |e: usize, c: usize| accepted.get(&(number.to_string(), e, c));
602    let defined = slot
603        .data_elements
604        .iter()
605        .map(|d| d.position)
606        .chain(slot.composites.iter().map(|c| c.position))
607        .max()
608        .map_or(0, |p| p + 1);
609    let last_filled = seg
610        .elements
611        .iter()
612        .rposition(|e| e.iter().any(|c| !c.is_empty()))
613        .map_or(0, |p| p + 1);
614    if last_filled > defined {
615        out.push(finding(
616            SyntaxScope::Segment,
617            "16",
618            seg,
619            counter,
620            None,
621            format!(
622                "{} has {last_filled} data elements, the MIG defines {defined}",
623                seg.id
624            ),
625        ));
626    }
627
628    for d in &slot.data_elements {
629        let element = seg.elements.get(d.position);
630        let at = d.position as u32 + 2;
631        let value = element.and_then(|e| e.first()).map_or("", String::as_str);
632        if element.is_some_and(|e| e.iter().skip(1).any(|c| !c.is_empty())) {
633            out.push(finding(
634                SyntaxScope::Element,
635                "16",
636                seg,
637                counter,
638                Some((at, None)),
639                format!(
640                    "{} {} is a simple data element but has components",
641                    seg.id, d.id
642                ),
643            ));
644        }
645        check_value(
646            seg,
647            d,
648            value,
649            counter,
650            (at, None),
651            settings,
652            also(d.position, 0),
653            out,
654        );
655    }
656
657    for c in &slot.composites {
658        let at = c.position as u32 + 2;
659        let components: &[String] = seg.elements.get(c.position).map_or(&[], Vec::as_slice);
660        if components.iter().all(String::is_empty) {
661            if required(c.status_spec.as_deref(), c.status_std.as_deref()) {
662                out.push(finding(
663                    SyntaxScope::Element,
664                    "13",
665                    seg,
666                    counter,
667                    Some((at, None)),
668                    format!("{} {} ({}) is missing", seg.id, c.id, c.name),
669                ));
670            }
671            continue;
672        }
673        let defined = c
674            .data_elements
675            .iter()
676            .map(|d| d.position)
677            .max()
678            .map_or(0, |p| p + 1);
679        let last_filled = components
680            .iter()
681            .rposition(|v| !v.is_empty())
682            .map_or(0, |p| p + 1);
683        if last_filled > defined {
684            out.push(finding(
685                SyntaxScope::Element,
686                "16",
687                seg,
688                counter,
689                Some((at, None)),
690                format!(
691                    "{} {} has {last_filled} components, the MIG defines {defined}",
692                    seg.id, c.id
693                ),
694            ));
695        }
696        for d in &c.data_elements {
697            let value = components.get(d.position).map_or("", String::as_str);
698            check_value(
699                seg,
700                d,
701                value,
702                counter,
703                (at, Some(d.position as u32 + 1)),
704                settings,
705                also(c.position, d.position),
706                out,
707            );
708        }
709    }
710}
711
712/// Check one simple or component data element's value.
713fn check_value(
714    seg: &OwnedSegment,
715    d: &MigDataElement,
716    value: &str,
717    counter: u32,
718    at: (u32, Option<u32>),
719    settings: SyntaxSettings,
720    also: Option<&Accepted>,
721    out: &mut Vec<SyntaxFinding>,
722) {
723    let mut push = |code: &str, detail: String| {
724        out.push(finding(
725            SyntaxScope::Element,
726            code,
727            seg,
728            counter,
729            Some(at),
730            detail,
731        ));
732    };
733    let name = format!("{} {} ({})", seg.id, d.id, d.name);
734    if value.is_empty() {
735        if required(d.status_spec.as_deref(), d.status_std.as_deref()) {
736            push("13", format!("{name} is missing"));
737        }
738        return;
739    }
740    if let Some(bad) = value.chars().find(|&c| !settings.charset.allows(c)) {
741        push(
742            "21",
743            format!("{name} holds '{bad}', which its syntax level does not allow"),
744        );
745        return;
746    }
747    if let Some(format) = d.format_spec.as_deref().or(d.format_std.as_deref()) {
748        if let Some((code, detail)) = check_format(value, format, settings.decimal_mark) {
749            push(code, format!("{name} '{value}': {detail}"));
750            return;
751        }
752    }
753    let allowed = codes(d);
754    if !allowed.is_empty() && !allowed.contains(&value) && !also.is_some_and(|a| a.accepts(value)) {
755        push(
756            "12",
757            format!(
758                "{name} '{value}' is none of the MIG's codes {}",
759                allowed.join(", ")
760            ),
761        );
762    }
763}
764
765/// A format `an..35`, `an3`, `n..15`, `n5`, `a3`: its kind, its length, and
766/// whether the length is a maximum.
767fn parse_format(format: &str) -> Option<(&str, usize, bool)> {
768    let format = format.trim();
769    let kind_end = format.find(|c: char| !c.is_ascii_alphabetic())?;
770    let (kind, rest) = format.split_at(kind_end);
771    let (upto, digits) = match rest.strip_prefix("..") {
772        Some(d) => (true, d),
773        None => (false, rest),
774    };
775    Some((kind, digits.parse().ok()?, upto))
776}
777
778/// The UCD code and reason when `value` breaks `format`.
779fn check_format(value: &str, format: &str, decimal_mark: char) -> Option<(&'static str, String)> {
780    let (kind, length, upto) = parse_format(format)?;
781    let counted = match kind {
782        "n" => {
783            let unsigned = value.strip_prefix('-').unwrap_or(value);
784            if let Some(c) = unsigned
785                .chars()
786                .find(|c| !c.is_ascii_digit() && *c != decimal_mark)
787            {
788                return Some(match c {
789                    '.' | ',' => (
790                        "19",
791                        format!("'{c}' is not the decimal mark '{decimal_mark}'"),
792                    ),
793                    _ => ("37", format!("'{c}' in a numeric data element ({format})")),
794                });
795            }
796            let marks = unsigned.matches(decimal_mark).count();
797            if marks > 1 || unsigned.ends_with(decimal_mark) {
798                return Some(("19", format!("not a decimal number ({format})")));
799            }
800            if unsigned.starts_with(decimal_mark) {
801                return Some(("38", "no digit before the decimal mark".to_string()));
802            }
803            unsigned.chars().filter(char::is_ascii_digit).count()
804        }
805        "a" => {
806            if let Some(c) = value.chars().find(char::is_ascii_digit) {
807                return Some((
808                    "37",
809                    format!("'{c}' in an alphabetic data element ({format})"),
810                ));
811            }
812            value.chars().count()
813        }
814        _ => value.chars().count(),
815    };
816    if counted > length {
817        return Some((
818            "39",
819            format!("{counted} characters, {format} allows {length}"),
820        ));
821    }
822    if !upto && counted < length {
823        return Some((
824            "40",
825            format!("{counted} characters, {format} requires {length}"),
826        ));
827    }
828    None
829}
830
831#[cfg(test)]
832mod tests {
833    use super::*;
834
835    #[test]
836    fn formats() {
837        assert_eq!(check_format("12345", "n5", '.'), None);
838        assert_eq!(check_format("1234", "n5", '.').map(|f| f.0), Some("40"));
839        assert_eq!(check_format("123456", "n..5", '.').map(|f| f.0), Some("39"));
840        assert_eq!(check_format("-12.5", "n..3", '.'), None);
841        assert_eq!(check_format(".5", "n..3", '.').map(|f| f.0), Some("38"));
842        assert_eq!(check_format("5.", "n..3", '.').map(|f| f.0), Some("19"));
843        assert_eq!(check_format("1,5", "n..3", '.').map(|f| f.0), Some("19"));
844        assert_eq!(check_format("1A", "n..3", '.').map(|f| f.0), Some("37"));
845        assert_eq!(check_format("Z1", "a..3", '.').map(|f| f.0), Some("37"));
846        assert_eq!(check_format("ÄBC", "an3", '.'), None);
847        assert_eq!(check_format("AB", "an3", '.').map(|f| f.0), Some("40"));
848    }
849
850    #[test]
851    fn charsets() {
852        assert!(Charset::Unoc.allows('ä'));
853        assert!(!Charset::Unoc.allows('€'));
854        assert!(!Charset::Unoc.allows('\n'));
855        assert!(!Charset::Unoa.allows('a'));
856        assert!(Charset::Unob.allows('a'));
857    }
858}