C#字符串约定解析

3

我是一名新手,对C#不是很熟悉。我有一个用户输入的字符串(希望是有效的)。

这个字符串将组成多个信息部分和子部分。

例如:

1-7 will //represent values 1 2 3 4 5 6 7

3:.25:7 //will be the numbers contained between 3 and 7 in increments of .25 
        // (3 3.25 3.5 3.75 4 ... 6.75 7)
1,4,5  //will represent values 1 4 5

我希望能够迭代遍历一个类似这样的字符串,并获取与分号(;)分隔的数据一样多的数组。
主要目标是解析像这样的内容:
1-7;3:.25:10;1,5,9;4-7

由于上述有4个“数据集”,因此我应该创建4个包含值的新数组。如果我有n个“数据集”,我应该创建n个新数组。

稍后,我想以嵌套方式迭代所有组合的数组。

如果可能(不是必需的),还可以进行以下表示形式的混合:

1-7,9,16:2:20;

我希望我的例子能够清晰地表达我的意思。

谢谢!

8个回答

3

嗯,在这里可能会有一个聪明的正则表达式答案,但我会尝试使用我最喜欢的string.Split()函数。

作为第一步,您可以在“;”上拆分输入字符串。

string[] datasets = inputString.Split(';');

就你最后提到的那一点而言,逗号“,”似乎做的更多或更少是一样的,你可以将其与Split(';', ',')合并或保持分开。

string[] parts = datasets[i].Split(',');

一个部分可以是以下三种情况之一:单个数字、范围或步进范围。
您可以使用 string.IndexOf() 进行检测。
string[] rangeParts = parts[j].Split('-');
string[] steppedParts = parts[j].Split(':');

结果应分别具有长度2和3。

然后应使用TryParse()检查生成的字符串,并由于使用了标点符号,最好修复Culture:

bool valid = double.TryParse(parts[k], 
  System.Globalization.NumberStyles.AllowDecimalPoint, 
  System.Globalization.CultureInfo.InvariantCulture, out value);

这些是零件,需要进行一些组装。

2

我建议使用正则表达式。首先,我会使用以下表达式将其分成几个部分。

^((?<section>[^;]+)(;|$))+

然后将每个部分分成子部分。
^((?<subsection>[^,]+)(,|$))+

现在匹配三种可能的子段类型。
(?<value>^[0-9]+$)|
(?<range>^[0-9]+-[0-9]+$)|
(?<rangewithstep>^[0-9]+:\.[0-9]+:[0-9]+$)

最后,您必须分析范围类型的子段。
^(?<start>[0-9]+)-(?<end>[0-9]+)$

^(?<start>[0-9]+):(?<step>\.[0-9]+):(?<end>[0-9]+)$

现在的问题是将提取的字符串解析成数字并将它们添加到数组中。我把所有东西都放在一个小型控制台应用程序中,它可以完成这个任务。虽然远非完美 - 没有错误处理,什么都没有,只是解析演示输入。我合并了之前提到的一些表达式,使代码更紧凑,可能也更好。
using System;
using System.Text.RegularExpressions;
using System.Globalization;

namespace RangeParser
{
    class Program
    {
        static void Main(string[] args)
        {
            String input = "1-7,9,16:2:20;1-7; 3:.75 : 10;1,5,9;4-7";

            Match sections = (new Regex(@"^((?<section>[^;]+)(;|$))+")).Match(input.Replace(" ", ""));

            foreach (Capture section in sections.Groups["section"].Captures)
            {
                Console.Write("Section ");

                Match subsections = (new Regex(@"^((?<subsection>[^,]+)(,|$))+")).Match(section.Value);

                foreach (Capture subsection in subsections.Groups["subsection"].Captures)
                {
                    Match subsectionparts = (new Regex(@"^(?<start>[0-9]*\.?[0-9]+)(((:(?<step>[0-9]*\.?[0-9]+):)|-)(?<end>[0-9]*\.?[0-9]+))?$")).Match(subsection.Value);

                    if (subsectionparts.Groups["start"].Length > 0)
                    {
                        Decimal start = Decimal.Parse(subsectionparts.Groups["start"].Value, CultureInfo.InvariantCulture);
                        Decimal end = start;
                        Decimal step = 1;

                        if (subsectionparts.Groups["end"].Length > 0)
                        {
                            end = Decimal.Parse(subsectionparts.Groups["end"].Value, CultureInfo.InvariantCulture);

                            if (subsectionparts.Groups["step"].Length > 0)
                            {
                                step = Decimal.Parse(subsectionparts.Groups["step"].Value, CultureInfo.InvariantCulture);
                            }
                        }

                        Decimal current = start;

                        while (current <= end)
                        {
                            Console.Write(String.Format("{0} ", current));

                            current += step;
                        }
                    }
                }

                Console.WriteLine();
            }

            Console.ReadLine();
        }
    }
}

更新

修改以允许类似于“1.5:0.2:3.6”的内容。

更新

为什么要使用十进制而不是单精度或双精度?

输入中的数字是十进制数,不能被单精度或双精度精确表示,因为它们使用基于2的表示。例如,0.1由单精度值0.100000001490116119384765625表示。

Single x = 0.0F;

for (int i = 0; i < 8; i++)
{
   x += 0.1F;
}

Console.WriteLine(x);

这个程序只需8次迭代即可输出0.8000001。1000次迭代后,误差增长到0.00095,显示为99.99905而不是100.0,在一百万次迭代后,结果为100,958.3而不是100,000。

对于十进制数没有这样的错误,因为十进制使用基数10表示,并且能够精确表示类似于0.1的十进制数字。


这不是一个正则表达式有点过于丑陋的情况吗?我认为直接的代码解决方案会更简单和易懂。 - C. Ross
也许吧。我选择使用正则表达式,因为你可以覆盖所有情况(如果你的正则表达式是正确的)。如果你使用String.Split()和相关方法来处理,当输入有效时,你会得到一个更简单的解决方案。但是,使用字符串方法捕获所有无效输入可能真的会变成一场恐怖。 - Daniel Brückner
当我开始将数字解析为十进制时,我已经知道它是一个有效的数字,不会失败。或者想想像“1-2-3;,-1:.:”这样的输入 - 你会将它们分割但很可能会在后面崩溃或返回无意义的结果。 - Daniel Brückner
在David的代码中,访问part[2]将超出输入'1:2'的范围,解析可能会失败,并且肯定还有一些未捕获的错误。捕获所有这些错误可能会使代码比正则表达式代码更难读懂。 - Daniel Brückner

2
这里有一些C#代码,可以实现您想要的功能:
    var results = ParseExpression("1-7;3:.25:10;1,5,9;4-7");

    private static List<List<float>> ParseExpression(string expression)
    {
        // "x-y" is the same as "x:1:y" so simplify the expression...
        expression = expression.Replace("-", ":1:");

        var results = new List<List<float>>();
        foreach (var part in expression.Split(';'))
            results.Add(ParseSubExpression(part));

        return results;
    }

    private static List<float> ParseSubExpression(string part)
    {
        var results = new List<float>();

        // If this is a set of numbers...
        if (part.IndexOf(',') != -1)
            // Then add each member of the set...
            foreach (string a in part.Split(','))
                results.AddRange(ParseSubExpression(a));
        // If this is a range that needs to be computed...
        else if (part.IndexOf(":") != -1)
        {
            // Parse out the range parameters...
            var parts = part.Split(':');
            var start = float.Parse(parts[0]);
            var increment = float.Parse(parts[1]);
            var end = float.Parse(parts[2]);

            // Evaluate the range...
            for (var i = start; i <= end; i += increment)
                results.Add(i);
        }
        else
            results.Add(float.Parse(part));

        return results;
    }

我建议不要使用浮点数,因为 i += increment 会在每次迭代中引入增长的数字误差。 - Daniel Brückner

2
以下是对我的正则表达式解决方案的评论,激发了我进行分析。
“这不是一个用正则表达式有点过于丑陋的情况吗?我认为直接编写代码会更简单易懂。- C. Ross”
我的回答如下。
“也许吧。我选择使用正则表达式是因为你可以涵盖所有情况(如果你的正则表达式是正确的)。如果你使用String.Split()等方法来做,如果输入有效,则可以得到更简单的解决方案。但是使用字符串方法捕获所有无效输入可能真的会变成一场恐怖。当我开始将数字解析为十进制数时,我已经知道它是一个有效的数字,不会失败。或者想想像'1-2-3;,-1:.:'这样的输入 - 你会将它们拆分,但很可能在后面崩溃或返回一个无意义的结果。在David的代码中,访问part[2]将超出输入'1:2'的范围,解析可能会失败,而且还有几个未被捕获的错误。捕获它们所有可能会使代码比正则表达式代码更难读。- danbruc”
所以,我决定使用微软强大的工具PEX分析我的正则表达式方法和David的字符串操作方法。我没有修改David的代码,而是用构建结果语句代替了我的解决方案中的控制台输出,这些语句与David一样生成List<List<Decimal>>
为了使全面的分析成为可能,我限制PEX只生成长度小于45个字符的输入,并且仅使用以下9种不同的字符。
019.;,-:!

没有必要使用所有数字,因为它们应该都表现相同。我包括9是为了方便发现溢出,但0和1也足够了——PEX可能会找到1000而不是999。我包括0和1是为了发现非常小的数字(例如0.000[...]001)的错误,但没有出现任何问题。我认为非常小的数字被默默地四舍五入为零,但我没有进一步调查。或者44个字符(由于28到29位小数的精度加上其他字符的空间)只是生成足够小的数字的长度不够。其他字符包括在内,因为它们是输入中的其他有效字符。最后,我包括感叹号作为无效字符的替代。
分析结果证明了我的正确性。PEX在我的代码中发现了两个错误。我没有检查空输入(我有意跳过了这一部分,以便专注于重要部分),导致了众所周知的NullReferenceException,而PEX发现输入“999999999999999999999999999999”会导致Decimal.Parse()失败并出现OverflowException异常。
PEX还报告了一些错误的负面结果。例如,“!;9,;.0;990:!!:,900:09”被报告为导致FormatException的输入。重新运行生成的测试不会引发任何异常。事实证明,“.0”在探索期间导致测试失败。查看其他失败的测试显示,在探索期间以小数点开头的所有输入都会导致Decimal.Parse()失败。但它们是有效的数字,在正常执行期间不会失败。我无法解释这些错误的正面结果。
以下是对字符串操作解决方案运行一次PEX的结果。两个实现都缺少空值检查和溢出异常处理。但简单的字符串操作解决方案无法处理许多格式不正确的输入。它们几乎都导致FormatException异常,但PEX还发现了我预测到的IndexOutOfRangeException异常。
FormatException:           "!,"
FormatException:           ","
FormatException:           "1,"
FormatException:           "!"
FormatException:           ";9"
FormatException:           "::"
FormatException:           "!.999009"
FormatException:           "!.0!99!9"
FormatException:           "0,9.90:!!,,,!,,,,,,!,,,0!!!9,!"
FormatException:           ""
FormatException:           "-99,9"
FormatException:           "1,9,,,!,,,,,,9,,,9,1,!9,,,,!,!"
FormatException:           "!:,"
FormatException:           "!9!:.!!,!!!."
FormatException:           "!:"
IndexOutOfRangeException:  "1:9"
FormatException:           "09..::!"
FormatException:           "9,0..:!.!,,,!,,,,,,!,,,!!-,!,!"
OverflowException:         "99999999999999999999999999999999999999999999"
FormatException:           "!."
FormatException:           "999909!!"
FormatException:           "-"
FormatException:           "9,9:9:999,,,9,,,,,,!,,,!9!!!,!"
FormatException:           "!9,"
FormatException:           "!.09!!0!"
FormatException:           "9-;"
FormatException:           ":"
FormatException:           "!.!9!9!!"
NullReferenceException:    null
FormatException:           ":,"
FormatException:           "!!"
FormatException:           "9;"

问题现在是,处理所有这些情况会有多难。简单的解决方案是使用try/catch语句保护解析指令。我不确定这是否足以保证对输入的格式良好部分进行正确操作。但也许这并不需要,格式不良的输入应该导致空结果,这将使修复解决方案变得容易。
最后,这里是达到的代码覆盖率结果。请注意,我使用十进制和单一进行了正则表达式解决方案的分析,因为PEX无法检测Decimal.Parse()内部使用的一个方法。
ParseExpression(string)            100,00%  10/10 blocks
ParseSubExpression(string)          96,15%  25/26 blocks

ParseExpressionRegex(string)        95,06%  77/81 blocks
ParseExpressionRegexSingle(string)  94,87%  74/78 blocks

对我来说的结论是 - 正则表达式解决方案确实应该被优先选择。它们在设计和理解上可能有些困难,但它们比基于简单字符串操作的实现更能处理格式不正确的输入。还有要记住的一点是 - 我并没有检查返回的结果是否正确,这是另一个问题。

1

C# 中没有解析范围的约定,因此您可以自由地进行任何最合理的操作。

但是,您可能希望从数学中的区间表示法派生出您的符号。

[2,4] - numbers between 2 and 4
(0,7] - numbers between 0 and 7, but not including 0

0

我不确定我完全理解你的问题,但听起来你正在寻找String.Split()


0

好的,当我使用Hank的前两个部分(假设数据集中有一个逗号)拆分字符串时

然后我可以用剩余的信息填充数组。

对于用“-”分隔的部分,我会取“-”之前的值,并从那里开始进行循环,直到“-”之后的值。

对于用“:”分隔的部分,我做的几乎是一样的事情,除了在for循环更新时不是i++增量,而是i+=(中间值)。

要解析出“-”或“:”字符之前和之后的值,我只需再次拆分并知道数组中哪些索引对应什么。

谢谢,

我明天会更新我的最终解决方案。

如果Henk Holterman想要根据我上面说的内容(解析其他部分的描述)更新他的解决方案,我会在我的主页上投票支持。由于某种原因,他们在这里阻止了OpenID。

顺便说一句:我不明白为什么他们不让我作为访客接受解决方案,即使我提供了正确的电子邮件地址,我也应该能够接受解决方案,对吧?


0

首先,通过分号将字符串拆分为单独的集合。然后,通过逗号将每个集合拆分为集合中的单独数字或范围。

现在您拥有的字符串可以是以下之一:

  • 一个单独的数字,例如42

  • 一系列数字,例如1-7

  • 步进范围,例如1:.5:7

您可以通过检查字符串是否包含连字符或冒号来识别第二个和第三个。然后,您将拆分这些字符串并进行一些循环以将数字添加到集合中。

通过像这样在同一级别处理数字和范围,它们可以按照您想要的方式混合。

一些提示:

使用double.TryParse解析数字。使用CultureInfo.InvariantCulture作为格式提供程序,它使用句点作为小数分隔符。

您可以使用List<double>来保存每个集合的数字。最终结果可以是列表数组,或者如果您想要数组数组,则可以使用ToArray方法创建数组。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接