diff --git a/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs b/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs index aa35e42..49496da 100644 --- a/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs +++ b/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs @@ -360,6 +360,24 @@ private static void ParseSubfieldLabels( repeatingGroupStartIndex = subfieldIndex; rawName = rawName.Substring(1); } + else + { + // Handle mid-string '*' — concatenated array leader/repeating separator. + // ISO 8211 §6.4.3.3: leader labels appear before '*', cell labels after. + int starIndex = rawName.IndexOf('*'); + if (starIndex > 0) + { + string leaderName = rawName.Substring(0, starIndex); + if (!string.IsNullOrEmpty(leaderName)) + { + names.Add(leaderName); + subfieldIndex++; + } + + repeatingGroupStartIndex = subfieldIndex; + rawName = rawName.Substring(starIndex + 1); + } + } if (!string.IsNullOrEmpty(rawName)) { diff --git a/src/EncDotNet.Iso8211/Iso8211FieldReader.cs b/src/EncDotNet.Iso8211/Iso8211FieldReader.cs index 47f1eb7..e702064 100644 --- a/src/EncDotNet.Iso8211/Iso8211FieldReader.cs +++ b/src/EncDotNet.Iso8211/Iso8211FieldReader.cs @@ -432,8 +432,10 @@ public IEnumerable GetSubfieldGroups() } else if (!inRepeatingGroup && subfieldIndex == _fieldDefinition.RepeatingSubfieldStartIndex && _fieldDefinition.HasRepeatingGroup) { - // Just entered the repeating group for the first time - groupCount = 1; + // Just entered the repeating group for the first time. + // Set to 0; the wrap-around at line ~424 will increment to 1 + // after the first complete group is parsed. + groupCount = 0; inRepeatingGroup = true; } } diff --git a/tests/EndDotNet.UnitTests/Iso8211DataDescriptiveRecordReaderTests.cs b/tests/EndDotNet.UnitTests/Iso8211DataDescriptiveRecordReaderTests.cs index 9657936..3027cdf 100644 --- a/tests/EndDotNet.UnitTests/Iso8211DataDescriptiveRecordReaderTests.cs +++ b/tests/EndDotNet.UnitTests/Iso8211DataDescriptiveRecordReaderTests.cs @@ -1426,5 +1426,71 @@ public void Parse_TwoUtFormat_ConcatenatedArrayWithSubfieldLabels_ParsesAsSubfie Assert.Equal(0, sg3d.RepeatingSubfieldStartIndex); } + [Fact] + public void Parse_TwoUtFormat_ConcatenatedArrayWithLeaderAndRepeating_ParsesCorrectly() + { + // Arrange — ConcatenatedArray with mid-string '*' separating leader from repeating subfields. + // This is the S-101 C3IL pattern: VCID is a leader subfield, YCOO/XCOO/ZCOO repeat. + var record = CreateDdrRecordTwoUt( + tag: "C3IL", + dataStructureCode: '3', + dataTypeCode: '5', + fieldName: "3-D Coordinate with Leader", + subfieldLabels: "VCID*YCOO!XCOO!ZCOO", + formatControls: "(b11,3b24)"); + + // Act + var ddr = Iso8211DataDescriptiveRecordReader.Read(record); + var c3il = ddr.FieldDefinitions[0]; + + // Assert — 4 subfield definitions + Assert.Equal(4, c3il.SubfieldDefinitions.Count); + + Assert.Equal("VCID", c3il.SubfieldDefinitions[0].Name); + Assert.Equal(Iso8211SubfieldFormatType.UnsignedInteger, c3il.SubfieldDefinitions[0].Format.FormatType); + Assert.Equal(1, c3il.SubfieldDefinitions[0].Format.Width); + + Assert.Equal("YCOO", c3il.SubfieldDefinitions[1].Name); + Assert.Equal(Iso8211SubfieldFormatType.SignedInteger, c3il.SubfieldDefinitions[1].Format.FormatType); + Assert.Equal(4, c3il.SubfieldDefinitions[1].Format.Width); + + Assert.Equal("XCOO", c3il.SubfieldDefinitions[2].Name); + Assert.Equal("ZCOO", c3il.SubfieldDefinitions[3].Name); + + // Repeating group starts at index 1 (after VCID leader) + Assert.True(c3il.HasRepeatingGroup); + Assert.Equal(1, c3il.RepeatingSubfieldStartIndex); + + // VCID is non-repeating leader, YCOO/XCOO/ZCOO are repeating + Assert.False(c3il.SubfieldDefinitions[0].IsRepeating); + Assert.True(c3il.SubfieldDefinitions[1].IsRepeating); + Assert.True(c3il.SubfieldDefinitions[2].IsRepeating); + Assert.True(c3il.SubfieldDefinitions[3].IsRepeating); + } + + [Fact] + public void Parse_TwoUtFormat_VectorStyleStarAtStart_StillWorks() + { + // Arrange — regression guard: '*' at the start of labels (Vector-style) + var record = CreateDdrRecordTwoUt( + tag: "SG2D", + dataStructureCode: '1', + dataTypeCode: '6', + fieldName: "2-D Coordinate Field", + subfieldLabels: "*YCOO!XCOO", + formatControls: "(2b24)"); + + // Act + var ddr = Iso8211DataDescriptiveRecordReader.Read(record); + var sg2d = ddr.FieldDefinitions[0]; + + // Assert + Assert.Equal(2, sg2d.SubfieldDefinitions.Count); + Assert.Equal("YCOO", sg2d.SubfieldDefinitions[0].Name); + Assert.Equal("XCOO", sg2d.SubfieldDefinitions[1].Name); + Assert.Equal(0, sg2d.RepeatingSubfieldStartIndex); + Assert.True(sg2d.HasRepeatingGroup); + } + #endregion } diff --git a/tests/EndDotNet.UnitTests/Iso8211FieldReaderTests.cs b/tests/EndDotNet.UnitTests/Iso8211FieldReaderTests.cs index ef01b0e..0177206 100644 --- a/tests/EndDotNet.UnitTests/Iso8211FieldReaderTests.cs +++ b/tests/EndDotNet.UnitTests/Iso8211FieldReaderTests.cs @@ -1191,5 +1191,85 @@ public void GetSubfieldGroups_Ucs2NatfFieldMultipleGroups_ParsesCorrectly() Assert.Equal("BOB", groups[1].GetSubfield("ATVL")); } + [Fact] + public void ReadConcatenatedArrayWithLeaderAndRepeatingGroups() + { + // Arrange — C3IL-style field: 1 leader subfield (VCID) + 3 repeating subfields (YCOO, XCOO, ZCOO) + var fieldDef = new Iso8211FieldDefinition + { + Tag = "C3IL", + DataStructureCode = Iso8211DataStructureCode.ConcatenatedArray, + DataTypeCode = Iso8211DataTypeCode.MixedDataTypes, + FieldName = "C3IL", + FormatControls = string.Empty, + SubfieldDefinitions = ImmutableArray.Create( + new Iso8211SubfieldDefinition + { + Name = "VCID", + Format = new Iso8211SubfieldFormat { FormatType = Iso8211SubfieldFormatType.UnsignedInteger, Width = 1 }, + Index = 0, + IsRepeating = false + }, + new Iso8211SubfieldDefinition + { + Name = "YCOO", + Format = new Iso8211SubfieldFormat { FormatType = Iso8211SubfieldFormatType.SignedInteger, Width = 4 }, + Index = 1, + IsRepeating = true + }, + new Iso8211SubfieldDefinition + { + Name = "XCOO", + Format = new Iso8211SubfieldFormat { FormatType = Iso8211SubfieldFormatType.SignedInteger, Width = 4 }, + Index = 2, + IsRepeating = true + }, + new Iso8211SubfieldDefinition + { + Name = "ZCOO", + Format = new Iso8211SubfieldFormat { FormatType = Iso8211SubfieldFormatType.SignedInteger, Width = 4 }, + Index = 3, + IsRepeating = true + }), + RepeatingSubfieldStartIndex = 1 + }; + + // 1 leader byte (VCID=7) + 3 sounding points × 12 bytes each = 37 bytes + var data = ConcatFieldData( + new byte[] { 7 }, // VCID = 7 + Int32LE(407128000), // YCOO[0] + Int32LE(-740060000), // XCOO[0] + Int32LE(150), // ZCOO[0] — depth + Int32LE(407130000), // YCOO[1] + Int32LE(-740062000), // XCOO[1] + Int32LE(200), // ZCOO[1] + Int32LE(407125000), // YCOO[2] + Int32LE(-740058000), // XCOO[2] + Int32LE(100) // ZCOO[2] + ); + + var reader = new Iso8211FieldReader(fieldDef, data); + + // Assert — leader subfield + Assert.Equal((byte)7, reader.GetFixedSubfield("VCID")); + + // Assert — repeating groups + Assert.Equal(3, reader.GroupCount); + var groups = reader.GetSubfieldGroups().ToArray(); + Assert.Equal(3, groups.Length); + + Assert.Equal(407128000, groups[0].GetSubfield("YCOO")); + Assert.Equal(-740060000, groups[0].GetSubfield("XCOO")); + Assert.Equal(150, groups[0].GetSubfield("ZCOO")); + + Assert.Equal(407130000, groups[1].GetSubfield("YCOO")); + Assert.Equal(-740062000, groups[1].GetSubfield("XCOO")); + Assert.Equal(200, groups[1].GetSubfield("ZCOO")); + + Assert.Equal(407125000, groups[2].GetSubfield("YCOO")); + Assert.Equal(-740058000, groups[2].GetSubfield("XCOO")); + Assert.Equal(100, groups[2].GetSubfield("ZCOO")); + } + #endregion }