Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/reading-iso8211.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ foreach (Iso8211Record record in document.DataRecords.Take(3))
of coordinates, `GetSubfieldValues<T>` returns every value of one subfield and
`GetSubfieldGroups` returns each group.

To read a long repeating group, such as the thousands of coordinates in an
edge, look up each subfield's ordinal once with `GetOrdinal`, then read each
repetition by group index and ordinal. This avoids a name lookup and a group
object for every repetition:

```csharp
var reader = new Iso8211FieldReader(ddr.GetFieldDefinition("SG2D")!, field.Data);
int y = reader.GetOrdinal("YCOO");
int x = reader.GetOrdinal("XCOO");

var points = new List<(int Y, int X)>(reader.GroupCount);
for (int group = 0; group < reader.GroupCount; group++)
{
points.Add((reader.GetSubfield<int>(group, y), reader.GetSubfield<int>(group, x)));
}
```

`GroupCount` counts complete repetitions only. If the field data ends partway
through one, `GetSubfieldGroups` still returns that partial group, and
`TryGetSubfield<T>(group, ordinal, out value)` returns `false` for the
subfields it lacks.

Character data is read as UTF-8 by default. Pass `lexicalLevel: 2` to the
`Iso8211FieldReader` constructor for fields encoded as UCS-2 (UTF-16LE).

Expand Down
15 changes: 13 additions & 2 deletions src/EncDotNet.Iso8211/Iso8211DataDescriptiveRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ public sealed record Iso8211DataDescriptiveRecord
/// </summary>
/// <param name="tag">The field tag to search for.</param>
/// <returns>The field definition with the specified tag, or <c>null</c> if not found.</returns>
public Iso8211FieldDefinition? GetFieldDefinition(string tag) =>
FieldDefinitions.FirstOrDefault(f => f.Tag == tag);
public Iso8211FieldDefinition? GetFieldDefinition(string tag)
{
var definitions = FieldDefinitions;
for (var i = 0; i < definitions.Count; i++)
{
if (definitions[i].Tag == tag)
{
return definitions[i];
}
}

return null;
}
}
15 changes: 13 additions & 2 deletions src/EncDotNet.Iso8211/Iso8211FieldDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ public sealed record Iso8211FieldDefinition
/// </summary>
/// <param name="name">The subfield name to search for.</param>
/// <returns>The subfield definition with the specified name, or <c>null</c> if not found.</returns>
public Iso8211SubfieldDefinition? GetSubfieldDefinition(string name) =>
SubfieldDefinitions.FirstOrDefault(s => s.Name == name);
public Iso8211SubfieldDefinition? GetSubfieldDefinition(string name)
{
var definitions = SubfieldDefinitions;
for (var i = 0; i < definitions.Count; i++)
{
if (definitions[i].Name == name)
{
return definitions[i];
}
}

return null;
}
}
137 changes: 137 additions & 0 deletions src/EncDotNet.Iso8211/Iso8211FieldLayout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using System.Runtime.CompilerServices;

namespace EncDotNet.Iso8211;

/// <summary>
/// The structure of an <see cref="Iso8211FieldDefinition"/>, worked out once and shared by
/// every <see cref="Iso8211FieldReader"/> that reads a field with that definition.
/// </summary>
/// <remarks>
/// <para>
/// Layouts are cached per definition instance in a <see cref="ConditionalWeakTable{TKey, TValue}"/>
/// rather than stored on the definition, because <see cref="Iso8211FieldDefinition"/> is a record:
/// a cached member would take part in its generated equality, and a <c>with</c> expression would
/// copy a stale layout onto a definition whose subfields had changed. Definitions are treated as
/// immutable once a reader has used them.
/// </para>
/// <para>
/// When every subfield has a fixed width, as in coordinate and pointer fields such as SG2D, SG3D,
/// FSPT and VRPT, the byte offset of any subfield instance follows from its position alone, so
/// readers don't need a per-instance offset table.
/// </para>
/// </remarks>
internal sealed class Iso8211FieldLayout
{
private static readonly ConditionalWeakTable<Iso8211FieldDefinition, Iso8211FieldLayout> Cache = new();

private readonly string[] _names;

private Iso8211FieldLayout(Iso8211FieldDefinition definition)
{
var subfields = definition.SubfieldDefinitions;
Count = subfields.Count;
_names = new string[Count];
Formats = new Iso8211SubfieldFormat[Count];
Offsets = new int[Count];

for (var i = 0; i < Count; i++)
{
_names[i] = subfields[i].Name;
Formats[i] = subfields[i].Format;
}

RepeatStart = definition.RepeatingSubfieldStartIndex;
HasRepeatingGroup = RepeatStart >= 0;
PrefixCount = HasRepeatingGroup ? RepeatStart : Count;
RepeatCount = HasRepeatingGroup ? Count - RepeatStart : 0;

// A repeating group that starts past the last subfield is malformed; leave it to the
// general path, which reports it as it always has.
IsFixedWidth = Count > 0 && (!HasRepeatingGroup || RepeatCount > 0);
for (var i = 0; i < Count && IsFixedWidth; i++)
{
IsFixedWidth = Formats[i].IsFixedWidth;
}

if (IsFixedWidth)
{
// Offsets of the leading subfields from the start of the field, and of the
// repeating subfields from the start of their group.
var offset = 0;
for (var i = 0; i < PrefixCount; i++)
{
Offsets[i] = offset;
offset += Formats[i].Width;
}

PrefixBytes = offset;
offset = 0;
for (var i = PrefixCount; i < Count; i++)
{
Offsets[i] = offset;
offset += Formats[i].Width;
}

Stride = offset;
}
}

/// <summary>Gets the number of subfield definitions.</summary>
public int Count { get; }

/// <summary>Gets each subfield's format, by definition position.</summary>
public Iso8211SubfieldFormat[] Formats { get; }

/// <summary>Gets whether the field has a repeating subfield group.</summary>
public bool HasRepeatingGroup { get; }

/// <summary>Gets the position of the first repeating subfield, or <c>-1</c>.</summary>
public int RepeatStart { get; }

/// <summary>Gets the number of subfields before the repeating group (all of them if none repeat).</summary>
public int PrefixCount { get; }

/// <summary>Gets the number of subfields in each repetition of the group.</summary>
public int RepeatCount { get; }

/// <summary>Gets whether every subfield has a fixed width.</summary>
public bool IsFixedWidth { get; }

/// <summary>
/// Gets each subfield's byte offset when <see cref="IsFixedWidth"/>: from the start of the
/// field for leading subfields, and from the start of the group for repeating ones.
/// </summary>
public int[] Offsets { get; }

/// <summary>Gets the total width of the leading subfields when <see cref="IsFixedWidth"/>.</summary>
public int PrefixBytes { get; }

/// <summary>Gets the width of one repetition of the group when <see cref="IsFixedWidth"/>.</summary>
public int Stride { get; }

/// <summary>
/// Gets the layout for a field definition, building it on first use.
/// </summary>
public static Iso8211FieldLayout For(Iso8211FieldDefinition definition) =>
Cache.GetValue(definition, static d => new Iso8211FieldLayout(d));

/// <summary>
/// Gets the position of the first subfield definition with the given name, or <c>-1</c>.
/// </summary>
/// <remarks>
/// A field has a handful of four-character names, so a loop is as fast as a dictionary and
/// allocates nothing.
/// </remarks>
public int IndexOf(string name)
{
for (var i = 0; i < _names.Length; i++)
{
if (string.Equals(_names[i], name, StringComparison.Ordinal))
{
return i;
}
}

return -1;
}
}
Loading
Loading