将动态大小文件中的字节(BCD、HEX、BIN)解析为C#实体对象,反之亦然



背景: 我即将用一台已经存在了20多年的设备交换数据,唯一支持的交换数据的方式是导入和导出包含不同类型记录的文件。我必须支持大约15-20种不同类型的文件记录,所以我真的希望找到一个好的解决方案来解析这些文件中的数据

文件记录包含许多不同的数据。文件说明如下:

产品文件:

类型BCDBCD4位BCD,HH:MM//tr>
字段 大小
ProductID 4字节
记录大小 2字节 十六进制
状态A 2字节 BIN
价格 4字节 BCD
标签1 1字节 十六进制
标签2 1字节 十六进制
状态B 2字节 BIN
第二价格 4字节
按日期销售 2字节 3位BCD,-编号。天数
按时间出售 2字节

首先我必须为我的英语道歉。我已经实现了一个有效的解决方案,它不像你期望的那样通用或好(使用属性和这种thins),但比手工完成所有事情要好得多。

我首先制作了一个解析器,它可以读取和转换任何类型的字段:

public class FileParser
{
public string Content { get; set; }
public int Position { get; set; }
public string Read(int len)
{
var ret = Content.Substring(Position, len * 2);
Position += len * 2;
return ret;
}
public int ParseIntBCD(int len)
{
return int.Parse(Read(len));
}
public int ParseHEX (int len)
{
return int.Parse(Read(len), System.Globalization.NumberStyles.HexNumber);
}
}

然后我创建了一个抽象类,它将成为您想要处理的每种记录类型的基类:

public abstract class BaseRecord
{
public static T ReadRecord<T>(FileParser parser) where T: BaseRecord
{
var instance = Activator.CreateInstance<T>();
instance.FillFields(parser);
return instance;
}
public int ProductId { get; set; }
public int Length { get; set; }
public int StatusA { get; set; }
public void FillFields(FileParser parser)
{
ProductId = parser.ParseIntBCD(4);
Length = parser.ParseHEX(2);
StatusA = parser.ParseBIN(2);
InternalFillFields(parser);
}
public abstract void InternalFillFields(FileParser parser);
}

它使用模板方法模式来允许每个子类定义其解析记录的实现。

后来,当我试图解决Product类时,我注意到对于产品,有些字段并不总是被读取的,所以在解析中,我为每个现有的解析方法实现了一个Conditional parse方法:

public int? ConditionalParseIntBCD(int? status, int flag, int len)
{
return status.HasValue && (status.Value & flag) != 0 ? ParseIntBCD(len) : null;
}
public int? ConditionalParseHEX(int? status, int flag, int len)
{
return status.HasValue && (status.Value & flag) != 0 ? ParseHEX(len) : null;
}

然后我可以实现产品类如下:

public class Product : BaseRecord
{
public decimal? Price { get; set; }
public int? Label1 { get; set; }
public int? Label2 { get; set; }
public int? StatusB { get; set; }
public decimal? SecondPrice { get; set; }
public int? SellByDate { get; set; }
public TimeSpan? SellByTime { get; set; }
public override void InternalFillFields(FileParser parser)
{
Price = parser.ConditionalParseDecimalBCD(StatusA, 0x8000, 4, 2);
Label1 = parser.ConditionalParseHEX(StatusA, 0x4000, 1);
Label2 = parser.ConditionalParseHEX(StatusA, 0x2000, 1);
StatusB = parser.ConditionalParseBIN(StatusA, 0x0001, 2);
SecondPrice = parser.ConditionalParseDecimalBCD(StatusB, 0x8000, 4, 2);
SellByDate = parser.ConditionalParseIntBCD(StatusB, 0x4000, 2);
SellByTime = parser.ConditionalParseTimeSpan(StatusB, 0x2000);
}
}

作为最后一步,我已经将这个方法添加到文件解析器中

public bool EOF { get { return Position >= Content.Length; } }
public IList<T> Decode<T>(string file) where T : BaseRecord
{
Content = file;
Position = 0;
var l = new List<T>();
while (!EOF)
l.Add(BaseRecord.ReadRecord<T>(this));
return l;
}

正如您所看到的,解决方案并不像使用属性和通用解决方案那样优雅,但一旦定义了所需的解析函数,每个Record定义就非常简单明了。

我已经实现了所需的代码来测试您成功制作的示例,并将其上传到以下存储库中的GitHub:示例存储库

这个例子只是为了解释我的解决方案,它没有任何错误处理,如果文件格式不正确,它就会失败,但我知道,如果你喜欢我的解决方法,开始工作会很有用。

请让我知道它是否有效!


编辑

为了实现相反的方向,您需要实现一些方法来对字段进行编码,例如:

public string EncodeIntBcd(int len, int? value)
{
return value.HasValue
? value.Value.ToString().PadLeft(len * 2, '0')
: "";
}

然后为每个类制作相应的编码方法:

public override string InternalEncodeFields(FileParser parser)
{
var ret = "";
ret += parser.EncodeBin(flag1 = Price.HasValue, flag2 = Label1.HasValue, flag3 = Label2.HasValue, flag16 = StatusB.HasValue);
ret += parser.EncodeDecimalBcd(4, 2, Price);
ret += parser.EncodeHex(0x8000, 1, Label1);
ret += parser.EncodeHex(0x8000, 1, Label2);
ret += parser.EncodeBin(flag1 = SecondPrice.HasValue, flag2 = SellByDate.HasValue, flag3 = SellByTime.HasValue);
ret += parser.EncodeDecimalBcd(4, 2, SecondPrice);
ret += parser.EncodeIntBcd(2, SellByDate);
ret += parser.EncodeTimeSpan(SellByTime);
return ret;
}

(*)请注意,对于StatusA和StatusB,我没有使用其当前值,我是在生成记录时计算的。这样,您就不需要手动为它们赋值。(**)还要注意,StatusA是在Product中实现的,而不是在BaseRecord中实现的。这是因为它的值取决于Prodyct Class 的具体特性

然后,在BaseRecord类中,您会得到这样的东西:

public string EncodeFields(FileParser parser)
{
var r = InternalEncodeFields(parser);
var len = (r.Length / 2) + 6; //4 of ProductId + 2 of Length
r = parser.EncodeIntBcd(4, ProductId) + parser.EncodeHex(2, len) + r;
return r;
}
public abstract string InternalEncodeFields(FileParser parser);

在FileParser中,实现将看起来像:

public string Encode<T>(IList<T> records) where T:BaseRecord
{
string file = "";
foreach (var r in records)
file += r.EncodeFields(this);
return file;
}

我还没有实现答案的第二部分,但我知道,如果你理解第一部分,那么做第二部分会很容易。

最新更新