65.9K
CodeProject 正在变化。 阅读更多。
Home

CollectionRegex -- 集合的正则表达式

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2012 年 4 月 16 日

CPOL

4分钟阅读

viewsIcon

27171

downloadIcon

212

使用熟悉的语言在对象序列中查找模式。

先决条件

阅读本文的读者应了解正则表达式的基础知识。本文不解释正则表达式。 Regular Expression .info 网站是正则表达式的一个很好的资源,既可以作为入门点,也可以作为参考。

引言

本文介绍了一个简单、强类型的类库,它提供了在对象序列中查找模式的方法。模式用正则表达式描述,序列是实现 IEnumerable<T> 泛型接口的任何类的实例 [msdn]。这包括众所周知的 .NET Framework 类,如 List<T>HashSet<T>Dictionary<K,V>,以及 LINQ 查询返回的对象。

Using the Code

可以通过调用 CollectionRegex.Match 方法来访问库的功能。此方法提供实例和静态版本。为了使其可用,需要以下信息:

  1. 一个由正则表达式表示的模式,
  2. 模式中的字符与集合项之间的关系,
  3. (可选)要传递给 .NET 正则表达式引擎选项

当所有上述参数都已知后,就可以调用 Match 方法并获取结果。Match 方法返回所有匹配项的枚举,即 IEnumerable<CollectionMatch<T>>。实现使用了 yield return [msdn] 构造,因此操作是按需执行的。该库支持命名和未命名捕获组。

示例 1

以下代码分析数字序列,以查找至少三个连续数字超出指定范围的位置。每个数字被归类为三个类别之一:“太低”(a)、“正常”(b)或“太高”(c)。将使用的正则表达式是:

[^b]{3,}

该表达式查找三个或更多非 b 字符的子序列。就集合而言,它匹配三个或更多连续数字,这些数字**不**属于“正常”类别。由于定义的类别是 a、b 和 c,因此该表达式等同于 [ac]{3,}。所需的 .NET 代码非常直接。当然,模式中的字符与类别之间的关系需要定义。在此示例中,它通过 lambda 表达式完成。它们是匿名方法,接受集合中的每个数字并返回一个布尔值,当数字属于特定类别时,该布尔值为 true。
// create a regex object
var reg = new CollectionRegex<double>(@"[^b]{3,}");
double low = 3,
        high = 7;
// define predicates for "low", "ok" and "high"
// with lambda expressions
reg.AddPredicate(s => s <= low, 'a');
reg.AddPredicate(s => s > low && s < high, 'b');
reg.AddPredicate(s => s >= high, 'c');
测试代码
// create a test sequence
var collection = new List<double>
{
4, 5, 9, 6,
7, 8, 9, 8,
6, 4, 3, 2,
4, 2, 2, 3, 3, 5, 5, 5,
3, 2, 7, 5
};
// run the regex
var actual = reg.Match(collection).ToList();
// check if results are as expected
Assert.AreEqual(3, actual.Count());
Assert.AreEqual(4, actual[0].Index);
Assert.AreEqual(4, actual[0].Count);
Assert.AreEqual(13, actual[1].Index);
Assert.AreEqual(4, actual[1].Count);
Assert.AreEqual(20, actual[2].Index);
Assert.AreEqual(3, actual[2].Count);

测试集合在内部由字符串“bbcb cccc bbaa baaa abbb aacb”(不带空格)表示。正则表达式匹配三个子序列:{7, 8, 9, 8}(cccc)、{2, 2, 3, 3}(aaaa)和 {3, 2, 7}(aac)。

LINQ 替代方案

对于那些以 C# 思考的人来说,通过检查不使用 CollectionRegex 库的实现来理解这个想法可能会更容易。
List<IEnumerable<double>> results = new List<IEnumerable<double>>();
for (int i = 0; i < collection.Count; i++)
{
    IEnumerable<double> enu = collection.Skip(i);
    enu = enu.TakeWhile(x => x <= low || x >= high);
    if (enu.Count() >= 3)
    {
        results.Add(enu);
        i += enu.Count() - 1;
    }
}

示例 2

以下代码查找生产过程中未成功完成的请求。集合包含 ProductionEvent 类型的对象,这些对象表示一个项目(r)、一个成功报告(s)或一个失败报告(f)的请求。如果项目未能生产,则将失败报告放入集合中。一个项目即使失败了几次也可以成功生产。正则表达式旨在查找根本没有生产出来的项目。

(?<item>r)f+(?=r|$)

  1. (?<item>r) 匹配一个项目请求,并定义一个名为 item 的命名组。
  2. f+ 匹配一个或多个失败报告。
  3. 最后,最后一部分 (?=r|$) 进行一个 正向先行断言,查找另一个请求(r)或集合的结尾($),以确保在分析的请求之后没有成功的报告。

例如,在序列 rs rfff rffffs rff 中,它会找到第二个和第四个请求(rfffrff)。但是,它不会匹配第三个(rffffs),因为它最终成功完成了,尽管过程中有四次失败。源代码假定存在一个名为 ProductionEvent 的类,该类公开两个属性:TypeItemName

var reg = new CollectionRegex<ProductionEvent>(@"(?<item>r)f+(?=r|$)");
reg.AddPredicate(s => s.Type == ProductionEventTypes.Success, 's');
reg.AddPredicate(s => s.Type == ProductionEventTypes.ItemRequest, 'r');
reg.AddPredicate(s => s.Type == ProductionEventTypes.Failure, 'f');
以及测试代码
var collection = new List<ProductionEvent>
{
    new ProductionEvent { Type = ProductionEventTypes.ItemRequest, ItemName="chocolade" },
    new ProductionEvent { Type= ProductionEventTypes.Success },
    new ProductionEvent { Type = ProductionEventTypes.ItemRequest, ItemName="impossible1" },
    new ProductionEvent { Type= ProductionEventTypes.Failure },
    new ProductionEvent { Type= ProductionEventTypes.Failure },
    new ProductionEvent { Type= ProductionEventTypes.Failure },
    new ProductionEvent { Type = ProductionEventTypes.ItemRequest, ItemName="problematic" },
    new ProductionEvent { Type= ProductionEventTypes.Failure },
    new ProductionEvent { Type= ProductionEventTypes.Failure },
    new ProductionEvent { Type= ProductionEventTypes.Failure },
    new ProductionEvent { Type= ProductionEventTypes.Failure },
    new ProductionEvent { Type= ProductionEventTypes.Success },
    new ProductionEvent { Type = ProductionEventTypes.ItemRequest, ItemName="impossible2" },
    new ProductionEvent { Type= ProductionEventTypes.Failure },
};
var actual = reg.Match(collection).ToList();
foreach (CollectionMatch<ProductionEvent> e in actual)
{
    // access a named group "item"
    Assert.IsTrue(e.Groups["item"].Single().ItemName.StartsWith("impossible"));
}

LINQ 替代方案

在下面的代码片段中,requests 列表模拟了一个捕获组“item”。

List<IEnumerable<ProductionEvent>> results = new List<IEnumerable<ProductionEvent>>();
List<ProductionEvent> requests = new List<ProductionEvent>();
for (int i = 0; i < collection.Count; i++)
{
    IEnumerable<ProductionEvent> begin = collection.Skip(i);
    IEnumerable<ProductionEvent> enu = begin;
    if (enu.Count() >= 2)
    {
        var request = enu.First();
        if (request.Type == ProductionEventTypes.ItemRequest)
        {
            enu = enu.Skip(1);
            var x = enu.First();
            if (x.Type == ProductionEventTypes.Failure)
            {
                enu = enu.SkipWhile(p => p.Type == ProductionEventTypes.Failure);
                if (enu.Count() == 0)
                {
                    results.Add(begin);
                    requests.Add(request);
                    i += begin.Count();
                }
                else if (enu.First().Type == ProductionEventTypes.ItemRequest)
                {
                    // (dirty)
                    var result = begin.TakeWhile(p => !object.ReferenceEquals(p, enu.First()));
                    results.Add(result);
                    requests.Add(request);
                    i += result.Count();
                }
            }
        }
    }
}

备注

谓词必须是互斥的。如果任何项匹配多个谓词,则 Match 方法将抛出 ArgumentException。

谓词是继承自抽象类 CollectionRegexPredicate 的类的实例。如果你想提供一种自定义机制来检查一个对象是否属于某个类别,你所要做的就是实现 IsMatch(T obj) 方法。

未被任何谓词匹配的项在正则表达式中用逗号(',')表示。因此,像 [^ab] 这样的否定组将匹配这些项。在经过长时间的思考后选择了逗号,在 ASCII 表中没有其他字符可以准确地代表这些项。(第一个可打印、无问题且易于人类计数的字符)。

除了常规的类库,我还为 IEnumerable 类编写了一个扩展方法,它允许使用如下构造:

var predicates = new CollectionRegexDelegatePredicate<double>[] { 
            new CollectionRegexDelegatePredicate<double>(s => s <= low, 'a'),
            new CollectionRegexDelegatePredicate<double>(s => s > low && s < high, 'b'),
            new CollectionRegexDelegatePredicate<double>(s => s >= high, 'c'),
        };
var actual = collection.MatchRegex(predicates, @"[^b]{3,}")s;

请使用文章下方的消息板报告错误或提交功能请求。

历史

  • 2012-04-14 -- 文章和 API 更新
  • 2012-04-11 -- 发布了原始版本
© . All rights reserved.