From 1157950813c03c9eacf24a8e45c8f1063b6d84cb Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 18 May 2026 18:13:08 -0700 Subject: [PATCH] Fix ParseFormatControls to handle nested parenthesized groups ParseFormatControls now recursively expands nested parenthesized sub-groups in ISO 8211 format control strings. For example, (b11,(3b24)) correctly produces [b11, b24, b24, b24] instead of only [b11]. When a format part is itself parenthesized (e.g. (3b24) or 3(b24)), the method strips the outer parens, parses the leading repeat count, and recursively calls ParseFormatControls on the inner content. This fixes parsing of S-101 fields like C3IL whose format controls contain nested groups such as (b11,(3b24)). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Iso8211DataDescriptiveRecordReader.cs | 44 +++++++++++++- ...Iso8211DataDescriptiveRecordReaderTests.cs | 60 +++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs b/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs index 49496da..64ee021 100644 --- a/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs +++ b/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs @@ -491,12 +491,50 @@ internal static ImmutableArray ParseFormatControls(string foreach (var part in parts) { - var parsed = ParseSingleFormat(part.Trim(), out int repeatCount); + var trimmedPart = part.Trim(); + + // Check for parenthesized sub-groups, e.g. "(3b24)" or "2(b14,I(10))" + int parenStart = trimmedPart.IndexOf('('); + if (parenStart >= 0 && trimmedPart.EndsWith(')')) + { + // Parse any leading repeat count before the opening paren + int outerRepeat = 0; + for (int i = 0; i < parenStart; i++) + { + if (char.IsDigit(trimmedPart[i])) + { + outerRepeat = outerRepeat * 10 + (trimmedPart[i] - '0'); + } + else + { + // Not a digit before '(' — not a sub-group, fall through to ParseSingleFormat + outerRepeat = -1; + break; + } + } + + if (outerRepeat >= 0) + { + int count = outerRepeat > 0 ? outerRepeat : 1; + // Recursively parse the parenthesized content + var innerContent = trimmedPart.Substring(parenStart + 1, trimmedPart.Length - parenStart - 2); + var innerFormats = ParseFormatControls(innerContent); + + for (int r = 0; r < count; r++) + { + formats.AddRange(innerFormats); + } + + continue; + } + } + + var parsed = ParseSingleFormat(trimmedPart, out int repeatCount); if (parsed.HasValue) { // Expand repeat counts: e.g., "2b24" produces two b24 entries - int count = repeatCount > 0 ? repeatCount : 1; - for (int r = 0; r < count; r++) + int repeatN = repeatCount > 0 ? repeatCount : 1; + for (int r = 0; r < repeatN; r++) { formats.Add(parsed.Value); } diff --git a/tests/EndDotNet.UnitTests/Iso8211DataDescriptiveRecordReaderTests.cs b/tests/EndDotNet.UnitTests/Iso8211DataDescriptiveRecordReaderTests.cs index 3027cdf..53ffed6 100644 --- a/tests/EndDotNet.UnitTests/Iso8211DataDescriptiveRecordReaderTests.cs +++ b/tests/EndDotNet.UnitTests/Iso8211DataDescriptiveRecordReaderTests.cs @@ -921,6 +921,66 @@ public void ParseFormatControls_MultipleRepeatCounts_ExpandAll() Assert.Equal(4, formats[4].Width); } + [Fact] + public void ParseFormatControls_NestedParentheses_InnerGroupExpanded() + { + // Act — "(b11,(3b24))" should produce: b11, b24, b24, b24 + var formats = Iso8211DataDescriptiveRecordReader.ParseFormatControls("(b11,(3b24))"); + + // Assert + Assert.Equal(4, formats.Length); + + Assert.Equal(Iso8211SubfieldFormatType.UnsignedInteger, formats[0].FormatType); + Assert.Equal(1, formats[0].Width); + + for (int i = 1; i < 4; i++) + { + Assert.Equal(Iso8211SubfieldFormatType.SignedInteger, formats[i].FormatType); + Assert.Equal(4, formats[i].Width); + } + } + + [Fact] + public void ParseFormatControls_NestedParentheses_RepeatBeforeParen() + { + // Act — "(b11,3(b24))" should produce: b11, b24, b24, b24 + var formats = Iso8211DataDescriptiveRecordReader.ParseFormatControls("(b11,3(b24))"); + + // Assert + Assert.Equal(4, formats.Length); + + Assert.Equal(Iso8211SubfieldFormatType.UnsignedInteger, formats[0].FormatType); + Assert.Equal(1, formats[0].Width); + + for (int i = 1; i < 4; i++) + { + Assert.Equal(Iso8211SubfieldFormatType.SignedInteger, formats[i].FormatType); + Assert.Equal(4, formats[i].Width); + } + } + + [Fact] + public void ParseFormatControls_NestedParentheses_MixedGroup() + { + // Act — "(A,2(I(10),b14))" should produce: A, I(10), b14, I(10), b14 + var formats = Iso8211DataDescriptiveRecordReader.ParseFormatControls("(A,2(I(10),b14))"); + + // Assert + Assert.Equal(5, formats.Length); + + Assert.Equal(Iso8211SubfieldFormatType.CharacterData, formats[0].FormatType); + + Assert.Equal(Iso8211SubfieldFormatType.Integer, formats[1].FormatType); + Assert.Equal(10, formats[1].Width); + Assert.Equal(Iso8211SubfieldFormatType.UnsignedInteger, formats[2].FormatType); + Assert.Equal(4, formats[2].Width); + + Assert.Equal(Iso8211SubfieldFormatType.Integer, formats[3].FormatType); + Assert.Equal(10, formats[3].Width); + Assert.Equal(Iso8211SubfieldFormatType.UnsignedInteger, formats[4].FormatType); + Assert.Equal(4, formats[4].Width); + } + #endregion #region Repeating Group Tests