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
}