LINQ 秘籍:将固定宽度行拆分为字符串数组






2.57/5 (3投票s)
需要处理固定宽度文件?LINQ 让它变得容易!
引言
使用 LINQ,我们可以轻松地将固定宽度平面文件中的行拆分为字符串数组。此代码非常容易改编以返回强类型结果,但在此示例中,我们只返回一个字符串数组。
背景
我的一个项目导入各种格式的文件。分隔记录很容易,但有些供应商仍然喜欢他们的平面文件。
Using the Code
我们将编写一个非常简单的类,其中包含一个方法和用于测试此方法的单元测试。我们将假设您对 C# 和单元测试有一定程度的了解,并且我们将使用 Microsoft Team Test 测试框架。
首先,让我们创建一个名为 LINQToFixedWidth 的新 C# 类库项目。它将包含一个名为 Class1
的类。将其重命名为 FixedWidth
,然后添加我们的方法,该方法有两个参数(我们要解析的字符串和每个字段的宽度)
public string[] SplitByWidth(string s, int[] widths)
{
}
当然,这还不能构建。让我们给它一些返回值,以便我们可以构建并创建我们的第一个单元测试。
public string[] SplitByWidth(string s, int[] widths)
{
return new string[0];
}
现在我们的方法返回一个空字符串数组,我们的项目可以编译。在我们编写方法之前,让我们确保我们知道我们在寻找什么并创建我们的测试。在 VS2008 Team System 中,只需右键单击该方法,然后单击“创建单元测试…”创建一个名为 LINQToFixedWidth_Test 的新 C# 测试项目。它将创建您的新项目和测试类,以及一个模板方法。让我们更改该方法,以便我们可以将我们想要的行为实际编码。
我们将从一个易于目视解析的字符串开始,以便在调试器中快速发现问题。五个字段,每个字段两个字符
string s = "1122334455";
int[] widths = { 2, 2, 2, 2, 2 };
我们期望返回的数组是
string[] expected = { "11", "22", "33", "44", "55" };
我们将编写一个简单的循环,以便我们可以轻松地确定哪个元素不正确,而不是一次比较整个数组。完成的测试方法如下所示
/// <summary>
///A test for SplitByWidth
///</summary>
[TestMethod()]
public void SplitByWidthTest()
{
FixedWidth target = new FixedWidth();
string s = "1122334455";
int[] widths = { 2, 2, 2, 2, 2 };
string[] expected = { "11", "22",
"33", "44", "55" };
string[] actual;
actual = target.SplitByWidth(s, widths);
Assert.AreEqual(expected.Length, actual.Length);
for (int i = 0; i < expected.Length; i++)
{
Assert.AreEqual(expected[i], actual[i], expected[i]);
}
}
现在我们有了一个合理的测试用例:将此 10 个字符的字符串正确拆分为 5 个 2 个字符的字段。我们运行它,当然,它会失败。我们预期的数组长度是 5,而实际返回的数组是空的。既然我们知道我们想要什么,并且不会意外得到它,让我们开始编写一些代码!
我们不再需要空数组——我们只需要它来构建类。我们将从要返回的字段数组开始。让我们将其设置为与 widths
数组参数中指定的字段数相同的大小。
public string[] SplitByWidth(string s, int[] widths)
{
string[] ret = new string[widths.Length];
return ret;
}
如果我们再次运行我们的测试,现在我们会发现它在不同的行失败了!数组长度相同。元素不匹配,但我们取得了进展。现在,让我们来填充该数组。
public string[] SplitByWidth(string s, int[] widths)
{
string[] ret = new string[widths.Length];
char[] c = s.ToCharArray();
int startPos = 0;
for (int i = 0; i < widths.Length; i++)
{
int width = widths[i];
ret[i] = new string(c.Skip(startPos).Take(width).ToArray<char>());
startPos += width;
}
return ret;
}
我们将字符串转换为字符数组。这就是魔力发生的地方。LINQ 为我们提供了用于处理数组的神奇、迷人、奇妙的功能。我们关注两个 LINQ 方法:Skip()
和 Take()
。Skip()
的作用正如其名:它会跳过数组中的元素。我们的第一个循环说明我们应该跳过 0 个位置,然后从数组中获取字段宽度的元素。第一个字段从 0 开始,长度为 2 个字符。
一旦我们有了字符,我们将指定它们作为数组返回(ToArray<char>()
调用为我们提供了一个字符数组),并创建一个包含该结果的新字符串。第一个字段已填充。我们所要做的就是将新的起始位置设置为字段结束的位置,然后循环。
现在我们运行测试,它通过了!太棒了!让我们为更复杂的行编写一个测试,并确保我们的逻辑有效。
/// <summary>
///A test for SplitByWidth
///</summary>
[TestMethod()]
public void SplitByWidthTest2()
{
FixedWidth target = new FixedWidth();
string s = "111222222222344444444444445555";
int[] widths = { 3, 9, 1, 13, 4 };
string[] expected = { "111", "222222222",
"3", "4444444444444", "5555" };
string[] actual;
actual = target.SplitByWidth(s, widths);
Assert.AreEqual(expected.Length, actual.Length);
for (int i = 0; i < expected.Length; i++)
{
Assert.AreEqual(expected[i], actual[i], expected[i]);
}
}
运行它,我们看到它也通过了!干得好。
我们的测试用例有效。现在,让我们让我们的方法更健壮一些,并添加一些负面测试用例。如果我们传入一个 null 值会怎么样?或者一个空字符串?或者一个长度不足以支持所有指定字段宽度的字符串?让我们添加一些测试来找出答案。
[TestMethod(), ExpectedException(typeof(ArgumentException), "No field sizes specified.")]
public void SplitByWidthNoFieldsTest()
{
FixedWidth target = new FixedWidth();
string s = null;
int[] widths = { };
string[] expected = { };
string[] actual;
actual = target.SplitByWidth(s, widths);
}
我们的测试方法期望一个 ArgumentException
。运行它。您会看到它失败了。现在,让我们添加异常处理程序。
if (widths.Length==0)
throw new ArgumentException("No field sizes specified.");
我们的测试通过了!让我们再添加一个检查。
[TestMethod(), ExpectedException(typeof(ArgumentException),
"String does not contain enough characters for this format.")]
public void SplitByWidthsTooLongTest()
{
FixedWidth target = new FixedWidth();
string s = "1";
int[] widths = { 5 };
string[] actual;
actual = target.SplitByWidth(s, widths);
}
我们又失败了,正如我们所料!让我们添加代码来使测试通过。
if (s.Length < widths.Sum())
throw new ArgumentException("String does not contain enough " +
"characters for this format.");
我们可以使用另一个 LINQ 函数!无需任何循环,我们就可以使用 Sum()
方法获取数组中所有元素的总和!如果我们的字符串长度不够,我们就抛出异常。现在测试通过了。下一个条件,null 或空字符串。现在,如果我们传入一个空字符串和一个空的宽度数组,我们就会抛出另一个参数异常。
[TestMethod(), ExpectedException(typeof(ArgumentException), "No data provided.")]
public void SplitByWidthsTooLongNullTest()
{
FixedWidth target = new FixedWidth();
string s = null;
int[] widths = { 5 };
string[] actual;
actual = target.SplitByWidth(s, widths);
Assert.AreEqual(0, actual.Length);
}
我们也对此进行了编码
if (string.IsNullOrEmpty(s))
throw new ArgumentException("No data provided.");
总结一下我们所做的,我们的测试用例是
/// <summary>
///A test for SplitByWidth
///</summary>
[TestMethod()]
public void SplitByWidthTest()
{
FixedWidth target = new FixedWidth();
string s = "1122334455";
int[] widths = { 2, 2, 2, 2, 2 };
string[] expected = { "11", "22",
"33", "44", "55" };
string[] actual;
actual = target.SplitByWidth(s, widths);
Assert.AreEqual(expected.Length, actual.Length);
for (int i = 0; i < expected.Length; i++)
{
Assert.AreEqual(expected[i], actual[i], expected[i]);
}
}
/// <summary>
///A test for SplitByWidth
///</summary>
[TestMethod()]
public void SplitByWidthTest2()
{
FixedWidth target = new FixedWidth();
string s = "111222222222344444444444445555";
int[] widths = { 3, 9, 1, 13, 4 };
string[] expected = { "111", "222222222",
"3", "4444444444444", "5555" };
string[] actual;
actual = target.SplitByWidth(s, widths);
Assert.AreEqual(expected.Length, actual.Length);
for (int i = 0; i < expected.Length; i++)
{
Assert.AreEqual(expected[i], actual[i], expected[i]);
}
}
/// <summary>
///A test for SplitByWidth
///</summary>
[TestMethod()]
public void SplitByWidthTwoLinesTest()
{
FixedWidth target = new FixedWidth();
string s = "1122334455\r\n5544332211";
int[] widths = { 2, 2, 2, 2, 2 };
string[] expected1 = { "11", "22", "33", "44", "55" };
string[] expected2 = { "55", "44", "33", "22", "11" };
string[] lines = Regex.Split(s, "\r\n");
string[] actual1 = target.SplitByWidth(lines[0], widths);
string[] actual2 = target.SplitByWidth(lines[1], widths);
Assert.AreEqual(expected1.Length, actual1.Length);
for (int i = 0; i < expected1.Length; i++)
{
Assert.AreEqual(expected1[i], actual1[i], expected1[i]);
}
Assert.AreEqual(expected2.Length, actual2.Length);
for (int i = 0; i < expected2.Length; i++)
{
Assert.AreEqual(expected2[i], actual2[i], expected2[i]);
}
}
/// <summary>
///A test for SplitByWidth
///</summary>
[TestMethod(), ExpectedException(typeof(ArgumentException),
"No field sizes specified.")]
public void SplitByWidthNoFieldsTest()
{
FixedWidth target = new FixedWidth();
string s = null;
int[] widths = { };
string[] expected = { };
string[] actual;
actual = target.SplitByWidth(s, widths);
}
/// <summary>
///A test for SplitByWidth
///</summary>
[TestMethod(), ExpectedException(typeof(ArgumentException),
"String does not contain enough characters for this format.")]
public void SplitByWidthsTooLongTest()
{
FixedWidth target = new FixedWidth();
string s = "1";
int[] widths = { 5 };
string[] actual;
actual = target.SplitByWidth(s, widths);
}
/// <summary>
///A test for SplitByWidth
///</summary>
[TestMethod(), ExpectedException(typeof(ArgumentException),
"No field sizes specified.")]
public void SplitByWidthsNullTest()
{
FixedWidth target = new FixedWidth();
string s = null;
int[] widths = { };
string[] actual;
actual = target.SplitByWidth(s, widths);
Assert.AreEqual(0, actual.Length);
}
/// <summary>
///A test for SplitByWidth
///</summary>
[TestMethod(), ExpectedException(typeof(ArgumentException), "No data provided.")]
public void SplitByWidthsTooLongNullTest()
{
FixedWidth target = new FixedWidth();
string s = null;
int[] widths = { 5 };
string[] actual;
actual = target.SplitByWidth(s, widths);
Assert.AreEqual(0, actual.Length);
}
以及我们的方法
public string[] SplitByWidth(string s, int[] widths)
{
if (widths.Length == 0)
throw new ArgumentException("No field sizes specified.");
if (string.IsNullOrEmpty(s))
throw new ArgumentException("No data provided.");
if (s.Length < widths.Sum())
throw new ArgumentException("String does not contain " +
"enough characters for this format.");
string[] ret = new string[widths.Length];
char[] c = s.ToCharArray();
int startPos = 0;
for (int i = 0; i < widths.Length; i++)
{
int width = widths[i];
ret[i] = new string(c.Skip(startPos).Take(width).ToArray<char>());
startPos += width;
}
return ret;
}
我们使用 LINQ 解析了一个固定宽度的平面文件行,并且我们获得了完整的代码覆盖率。我认为我们的方法相当可靠。恭喜!
关注点
当然,我们几代人以来都能处理固定宽度记录。金字塔中有象形文字详细记录了古代人处理固定宽度文件的解决方案。这个小小的菜谱向我们展示了如何使用几个简单的 LINQ 函数来轻松快速地实现我们想要的结果。
历史
- 提交于 2009 年 10 月 15 日。