diff --git a/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs b/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs index 64ee021..78966f6 100644 --- a/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs +++ b/src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecordReader.cs @@ -697,8 +697,15 @@ private static Iso8211SubfieldFormat ParseBitStringFormat(string format, int pos } /// - /// Parses a binary format like b11, b12, b14, b21, b22, b24. + /// Parses a binary format like b11, b12, b14, b21, b22, b24, + /// or b48. /// + /// + /// The binary format is b followed by two digits: the first digit is the data type + /// (1 unsigned integer, 2 signed integer, 4/5 floating-point real) and + /// the second digit is the width in bytes. The width is honored as written so that, for example, + /// the 8-byte b48 control is read as 8 bytes rather than being truncated or dropped. + /// private static Iso8211SubfieldFormat? ParseBinaryFormat(string format, int pos) { pos++; // Skip 'b' @@ -708,8 +715,8 @@ private static Iso8211SubfieldFormat ParseBitStringFormat(string format, int pos return null; } - // Parse sign indicator: 1 = unsigned, 2 = signed - var signIndicator = format[pos] - '0'; + // Parse data type indicator: 1 = unsigned, 2 = signed, 4/5 = floating-point. + var typeIndicator = format[pos] - '0'; pos++; if (pos >= format.Length) @@ -717,7 +724,7 @@ private static Iso8211SubfieldFormat ParseBitStringFormat(string format, int pos return null; } - // Parse byte width + // Parse byte width (the remaining digit(s), e.g. the '8' in 'b48'). int width = 0; while (pos < format.Length && char.IsDigit(format[pos])) { @@ -726,7 +733,7 @@ private static Iso8211SubfieldFormat ParseBitStringFormat(string format, int pos } Iso8211SubfieldFormatType formatType; - switch (signIndicator) + switch (typeIndicator) { case 1: formatType = Iso8211SubfieldFormatType.UnsignedInteger; @@ -734,6 +741,10 @@ private static Iso8211SubfieldFormat ParseBitStringFormat(string format, int pos case 2: formatType = Iso8211SubfieldFormatType.SignedInteger; break; + case 4: + case 5: + formatType = Iso8211SubfieldFormatType.FloatingPoint; + break; default: return null; } diff --git a/src/EncDotNet.Iso8211/Iso8211FieldReader.cs b/src/EncDotNet.Iso8211/Iso8211FieldReader.cs index ce8c92b..496bf7a 100644 --- a/src/EncDotNet.Iso8211/Iso8211FieldReader.cs +++ b/src/EncDotNet.Iso8211/Iso8211FieldReader.cs @@ -521,6 +521,7 @@ private T ConvertValue(ParsedSubfield parsed, Iso8211SubfieldDefinition subfi Iso8211SubfieldFormatType.Real => ConvertAsciiReal(span), Iso8211SubfieldFormatType.UnsignedInteger => ConvertUnsignedBinary(span, format.Width), Iso8211SubfieldFormatType.SignedInteger => ConvertSignedBinary(span, format.Width), + Iso8211SubfieldFormatType.FloatingPoint => ConvertFloatingBinary(span, format.Width), Iso8211SubfieldFormatType.BitString => ConvertBitString(span), _ => throw new InvalidOperationException($"Unknown format type: {format.FormatType}") }; @@ -810,6 +811,49 @@ private static object ConvertSignedBinary(ReadOnlySpan data, int width) throw new InvalidOperationException($"Cannot convert signed binary to type {typeof(T).Name}."); } + /// + /// Converts floating-point binary data (ISO 8211 b4x/b5x, IEEE 754) to the requested type. + /// + private static object ConvertFloatingBinary(ReadOnlySpan data, int width) + { + int actualWidth = Math.Min(width, data.Length); + + double value = actualWidth switch + { + 4 => BinaryPrimitives.ReadSingleLittleEndian(data), + 8 => BinaryPrimitives.ReadDoubleLittleEndian(data), + _ => throw new InvalidOperationException( + $"Unsupported floating-point binary width: {actualWidth} byte(s).") + }; + + if (typeof(T) == typeof(double)) + { + return value; + } + if (typeof(T) == typeof(float)) + { + return (float)value; + } + if (typeof(T) == typeof(decimal)) + { + return (decimal)value; + } + if (typeof(T) == typeof(int)) + { + return (int)value; + } + if (typeof(T) == typeof(long)) + { + return (long)value; + } + if (typeof(T) == typeof(string)) + { + return value.ToString(CultureInfo.InvariantCulture); + } + + throw new InvalidOperationException($"Cannot convert floating-point binary to type {typeof(T).Name}."); + } + /// /// Converts bit string data (ISO 8211 B(n) format) to the requested type. /// diff --git a/src/EncDotNet.Iso8211/Iso8211SubfieldFormat.cs b/src/EncDotNet.Iso8211/Iso8211SubfieldFormat.cs index 1f872ae..8149cdd 100644 --- a/src/EncDotNet.Iso8211/Iso8211SubfieldFormat.cs +++ b/src/EncDotNet.Iso8211/Iso8211SubfieldFormat.cs @@ -66,6 +66,7 @@ public readonly record struct Iso8211SubfieldFormat { Iso8211SubfieldFormatType.UnsignedInteger => Width, Iso8211SubfieldFormatType.SignedInteger => Width, + Iso8211SubfieldFormatType.FloatingPoint => Width, Iso8211SubfieldFormatType.BitString => Width, _ => Width }; @@ -78,6 +79,7 @@ public readonly record struct Iso8211SubfieldFormat Iso8211SubfieldFormatType.Real => Width > 0 ? $"R({Width})" : "R", Iso8211SubfieldFormatType.UnsignedInteger => $"b1{Width}", Iso8211SubfieldFormatType.SignedInteger => $"b2{Width}", + Iso8211SubfieldFormatType.FloatingPoint => $"b4{Width}", Iso8211SubfieldFormatType.BitString => $"B({Width * 8})", _ => $"?({Width})" }; diff --git a/src/EncDotNet.Iso8211/Iso8211SubfieldFormatType.cs b/src/EncDotNet.Iso8211/Iso8211SubfieldFormatType.cs index a220df0..d187b07 100644 --- a/src/EncDotNet.Iso8211/Iso8211SubfieldFormatType.cs +++ b/src/EncDotNet.Iso8211/Iso8211SubfieldFormatType.cs @@ -34,5 +34,12 @@ public enum Iso8211SubfieldFormatType : byte /// Bit string data. Corresponds to format code B(n) where n is the width in bits. /// The is stored in bytes (n / 8). /// - BitString = 5 + BitString = 5, + + /// + /// Floating-point binary (IEEE 754) data. Corresponds to format codes b4x and + /// b5x where x is the byte width (4 for single precision, 8 for double precision). + /// Used, for example, by the S-100 DSSI origin-shift subfields (b48). + /// + FloatingPoint = 6 } diff --git a/tests/EndDotNet.UnitTests/Iso8211FieldReaderTests.cs b/tests/EndDotNet.UnitTests/Iso8211FieldReaderTests.cs index 014d16f..e4e7eb6 100644 --- a/tests/EndDotNet.UnitTests/Iso8211FieldReaderTests.cs +++ b/tests/EndDotNet.UnitTests/Iso8211FieldReaderTests.cs @@ -1320,4 +1320,84 @@ public void ReadConcatenatedArrayWithLeaderAndRepeatingGroups() } #endregion + + #region Regression Tests - 8-byte Binary Format Controls (b48) + + /// + /// Regression test for issue #9: mis-parsed binary + /// format controls of the form bXY where the width digit is 8 (e.g. b48). + /// + /// + /// The S-100 / S-101 DSSI field declares (3b48,10b14): three 8-byte floating-point + /// origin shifts (DCOX/DCOY/DCOZ) followed by ten 4-byte unsigned integers. Previously + /// the leading b48 subfields were dropped (or read as 4 bytes), shifting every later + /// subfield by 12 bytes and decoding CMFX/CMFY/CMFZ as 0. This test parses the raw + /// 64-byte DSSI blob from the issue and asserts the correctly aligned values. + /// + [Fact] + public void GetSubfield_DssiFieldWith8ByteBinaryControls_DecodesCorrectly() + { + // Arrange — the real DSSI format control read back from a cell's DDR. + var formats = Iso8211DataDescriptiveRecordReader.ParseFormatControls("(3b48,10b14)"); + + var names = new[] + { + "DCOX", "DCOY", "DCOZ", + "CMFX", "CMFY", "CMFZ", + "NOIR", "NOPN", "NOMN", "NOCN", "NOXN", "NOSN", "NOFR" + }; + + // The three leading subfields must be 8-byte floating-point; the rest 4-byte unsigned. + Assert.Equal(13, formats.Length); + for (int i = 0; i < 3; i++) + { + Assert.Equal(Iso8211SubfieldFormatType.FloatingPoint, formats[i].FormatType); + Assert.Equal(8, formats[i].Width); + } + for (int i = 3; i < 13; i++) + { + Assert.Equal(Iso8211SubfieldFormatType.UnsignedInteger, formats[i].FormatType); + Assert.Equal(4, formats[i].Width); + } + + var definitions = ImmutableArray.CreateBuilder(); + for (int i = 0; i < names.Length; i++) + { + definitions.Add(new Iso8211SubfieldDefinition + { + Name = names[i], + Format = formats[i], + Index = i, + IsRepeating = false + }); + } + + var fieldDef = new Iso8211FieldDefinition + { + Tag = "DSSI", + DataStructureCode = Iso8211DataStructureCode.Vector, + DataTypeCode = Iso8211DataTypeCode.MixedDataTypes, + FieldName = "DSSI", + FormatControls = "(3b48,10b14)", + SubfieldDefinitions = definitions.ToImmutable(), + RepeatingSubfieldStartIndex = -1 + }; + + var data = Convert.FromHexString( + "00000000000000000000000000000000000000000000000080969800809698006400000055000000C012000005000000451500002B0500007A0400000E0C0000"); + + Assert.Equal(64, data.Length); + + var reader = new Iso8211FieldReader(fieldDef, data); + + // Act & Assert — the multiplication factors must align past the three 8-byte shifts. + Assert.Equal(10000000u, reader.GetSubfield("CMFX")); + Assert.Equal(10000000u, reader.GetSubfield("CMFY")); + Assert.Equal(100u, reader.GetSubfield("CMFZ")); + Assert.Equal(85u, reader.GetSubfield("NOIR")); + Assert.Equal(4800u, reader.GetSubfield("NOPN")); + Assert.Equal(5u, reader.GetSubfield("NOMN")); + } + + #endregion }