diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..101cb50 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + + - package-ecosystem: "nuget" + directory: "/src" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..542f35d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,20 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: Build + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..1f7b006 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,40 @@ +name: Publish package +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Verify commit exists in origin/main + run: | + git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + git branch --remote --contains | grep origin/main + + - name: Set VERSION variable from tag + run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV + + - name: Build + run: dotnet build --configuration Release /p:Version=${VERSION} + + - name: Test + run: dotnet test --configuration Release /p:Version=${VERSION} --no-build + + - name: Pack + run: dotnet pack --configuration Release /p:Version=${VERSION} --no-build --output . + + - name: Push + run: dotnet nuget push Anexia.Gregex.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_TOKEN} + env: + NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ccfe283 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: Test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85047e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.DotSettings.user +.idea +**\bin +**\obj \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 590267a..6d9771b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,17 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [x.y.z] - YYYY-MM-DD +## [0.0.1] - 2024-10-25 ### Added -- Lorem ipsum dolor sit amet - -### Deprecated -- Nothing. - -### Removed -- Nothing. - -### Fixed -- Nothing. +- Initial release with basic matchers. diff --git a/LICENSE b/LICENSE index cd0bfbf..9c5bbd7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018 ANEXIA Internetdienstleisungs GmbH +Copyright (c) 2024 ANEXIA Internetdienstleisungs GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 56478f5..c99b273 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,34 @@ -## PROJECT NAME +## Anexia.Gregex -A short description of what this project does. +Generalized regular expressions for sequences (IEnumerables, Lists, Arrays). ## Goals -It is a good idea to provide a mission statement for your project, enshrining -what the project wants to accomplish so that as more people join your project -everyone can work in alignment. +Provide an API to match lists similar to how you can match regexes against strings. -### Installation +### Usage -Instructions for how to download/install the code onto your machine. +The main entry point is the ```Gregex``` class it allows you to construct expressions. You can use the +Matcher class to match these expressions against instances of IEnumerable. Example: ``` -./install myProject --save -``` +using Anexia.Gregex; -### Usage +var testList = "FooBarFooBarFoo".ToCharArray(); -Usage instructions for your code. +var gregex = Gregex.Is('o').Times(2); -Example: +var matcher = new Matcher(); -``` -var myMod = require('mymodule'); +var matches = matcher.FindMatches(gregex, testList).ToArray(); -myMod.foo('hi'); +Console.WriteLine($"Found: {matches.Length} matches."); +Console.WriteLine(string.Join("\n", matches.AsEnumerable())); ``` +You can find more detailed examples in the [Examples Folder](examples). + ### Contributing Contributions are welcomed! Read the [Contributing Guide](CONTRIBUTING.md) for more information. diff --git a/dotnetcore-gregex.sln b/dotnetcore-gregex.sln new file mode 100644 index 0000000..18caa0e --- /dev/null +++ b/dotnetcore-gregex.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Anexia.Gregex", "src\Anexia.Gregex\Anexia.Gregex.csproj", "{07B62AC7-BCE4-4A20-8A0F-6EE1152F90E0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DB20A418-DCE6-457F-BF75-CC36CE1E8D25}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{471DF611-07A8-4581-9297-110580DBCC4F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Anexia.Gregex.Test", "test\Anexia.Gregex.Test\Anexia.Gregex.Test.csproj", "{905349B3-4F44-4EE9-AEFB-7E24967AF0B5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{9799FB17-155A-4CCC-A6A3-5FC365CEED3F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Anexia.Gregex.Examples", "examples\Anexia.Gregex.Examples\Anexia.Gregex.Examples.csproj", "{166B6342-8E5E-4C5F-90DF-F6DB456BA573}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {07B62AC7-BCE4-4A20-8A0F-6EE1152F90E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07B62AC7-BCE4-4A20-8A0F-6EE1152F90E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07B62AC7-BCE4-4A20-8A0F-6EE1152F90E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07B62AC7-BCE4-4A20-8A0F-6EE1152F90E0}.Release|Any CPU.Build.0 = Release|Any CPU + {905349B3-4F44-4EE9-AEFB-7E24967AF0B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {905349B3-4F44-4EE9-AEFB-7E24967AF0B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {905349B3-4F44-4EE9-AEFB-7E24967AF0B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {905349B3-4F44-4EE9-AEFB-7E24967AF0B5}.Release|Any CPU.Build.0 = Release|Any CPU + {166B6342-8E5E-4C5F-90DF-F6DB456BA573}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {166B6342-8E5E-4C5F-90DF-F6DB456BA573}.Debug|Any CPU.Build.0 = Debug|Any CPU + {166B6342-8E5E-4C5F-90DF-F6DB456BA573}.Release|Any CPU.ActiveCfg = Release|Any CPU + {166B6342-8E5E-4C5F-90DF-F6DB456BA573}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {07B62AC7-BCE4-4A20-8A0F-6EE1152F90E0} = {DB20A418-DCE6-457F-BF75-CC36CE1E8D25} + {905349B3-4F44-4EE9-AEFB-7E24967AF0B5} = {471DF611-07A8-4581-9297-110580DBCC4F} + {166B6342-8E5E-4C5F-90DF-F6DB456BA573} = {9799FB17-155A-4CCC-A6A3-5FC365CEED3F} + EndGlobalSection +EndGlobal diff --git a/examples/Anexia.Gregex.Examples/Anexia.Gregex.Examples.csproj b/examples/Anexia.Gregex.Examples/Anexia.Gregex.Examples.csproj new file mode 100644 index 0000000..8d7f80a --- /dev/null +++ b/examples/Anexia.Gregex.Examples/Anexia.Gregex.Examples.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/examples/Anexia.Gregex.Examples/AnyExample.cs b/examples/Anexia.Gregex.Examples/AnyExample.cs new file mode 100644 index 0000000..fd930dc --- /dev/null +++ b/examples/Anexia.Gregex.Examples/AnyExample.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + + +namespace Anexia.Gregex.Examples; + +/// +/// Example for how to use the method for matching any element. +/// +public static class AnyExample +{ + public static void Main() + { + var listOfWords = new List() + { + "Hello", + "World", + "This", + "Is", + "A", + "Test", + "For", + "Any", + "Regex" + }; + var anyString = Gregex.Any(); + + var matcher = new Matcher(); + + var matches = matcher.FindMatches(anyString, listOfWords); + + Console.WriteLine(string.Join(Environment.NewLine, matches)); + + } +} \ No newline at end of file diff --git a/examples/Anexia.Gregex.Examples/AtLeastOnceExample.cs b/examples/Anexia.Gregex.Examples/AtLeastOnceExample.cs new file mode 100644 index 0000000..505178f --- /dev/null +++ b/examples/Anexia.Gregex.Examples/AtLeastOnceExample.cs @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex.Examples; + +/// +/// Example of how to use the method to match an element at least once. +/// +public static class AtLeastOnceExample +{ + public static void Main() + { + var listOfStringWithOneStringRepeated = new List() + { + "Hello", "World", "Hello", "Hello", "Hello" + }; + + var atLeastOneHello = Gregex.Is("Hello").AtLeastOnce(); + + var matcher = new Matcher(); + + var matches = matcher.FindMatches(atLeastOneHello, listOfStringWithOneStringRepeated); + + Console.WriteLine(string.Join(Environment.NewLine, matches)); + } +} \ No newline at end of file diff --git a/examples/Anexia.Gregex.Examples/FollowedByExample.cs b/examples/Anexia.Gregex.Examples/FollowedByExample.cs new file mode 100644 index 0000000..eddcd10 --- /dev/null +++ b/examples/Anexia.Gregex.Examples/FollowedByExample.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex.Examples; + +/// +/// Example of how to use the method to match to consecutive elements. +/// +public static class FollowedByExample +{ + public static void Main() + { + var listOfWords = new List + { + "This", "is", "a", "test", "of", "Gregex", "followed", "by", "a", + "example", "one", "more", "time", "and", "then", + "we'll", "see", "if", "it's", "working", "This", "is", "not", "a", "test" + }; + + var aTest = Gregex.Is("a").FollowedBy(Gregex.Is("test")); + + var matcher = new Matcher(); + + var matches = matcher.FindMatches(aTest, listOfWords); + + Console.WriteLine(string.Join(Environment.NewLine, matches)); + } +} \ No newline at end of file diff --git a/examples/Anexia.Gregex.Examples/IncredientsExample.cs b/examples/Anexia.Gregex.Examples/IncredientsExample.cs new file mode 100644 index 0000000..832df1a --- /dev/null +++ b/examples/Anexia.Gregex.Examples/IncredientsExample.cs @@ -0,0 +1,60 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex.Examples; + +public static class IncredientsExample +{ + public static void Main() + { + var list = new List() + { + new Chocolate(200), + new Flour(50), + new Milk(150), + new Chocolate(120), + new Chocolate(100), + new Flour(200), + new Milk(150), + new Chocolate(70), + new Milk(100), + new Flour(250), + new Milk(100), + }; + + var grex = Gregex.TypeOf().FollowedBy(Gregex.Any().AtLeastOnce()) + .FollowedBy(Gregex.TypeOf()); + + var matcher = new Matcher(); + + var matches = matcher.FindMatches(grex, list); + + Console.WriteLine(string.Join(Environment.NewLine, matches)); + } + + interface IRecipeIngredient + { + public int Quantity { get; } + } + + private class Chocolate(int quantity) : IRecipeIngredient + { + public int Quantity { get; } = quantity; + public override string ToString() => $"{Quantity}g Chocolate"; + } + + private class Flour(int quantity) : IRecipeIngredient + { + public int Quantity { get; } = quantity; + public override string ToString() => $"{Quantity}g Flour"; + } + + private class Milk(int quantity) : IRecipeIngredient + { + public int Quantity { get; } = quantity; + public override string ToString() => $"{Quantity}g Milk"; + } +} \ No newline at end of file diff --git a/examples/Anexia.Gregex.Examples/IsExample.cs b/examples/Anexia.Gregex.Examples/IsExample.cs new file mode 100644 index 0000000..ca1c1c9 --- /dev/null +++ b/examples/Anexia.Gregex.Examples/IsExample.cs @@ -0,0 +1,43 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex.Examples; + +/// +/// Example for matching an element exactly using the method. +/// +public static class IsExample +{ + public static void Main() + { + var listOfWords = new List() + { + "Hello", + "World", + "This", + "Is", + "And", + "Test", + "Example", + "List", + "With", + "More", + "Words", + "Like", + "These", + "And", + "Those" + }; + + var gregex = Gregex.Is("And"); + + var matcher = new Matcher(); + + var matches = matcher.FindMatches(gregex, listOfWords); + + Console.WriteLine(string.Join(Environment.NewLine, matches)); + } +} \ No newline at end of file diff --git a/examples/Anexia.Gregex.Examples/Program.cs b/examples/Anexia.Gregex.Examples/Program.cs new file mode 100644 index 0000000..ab28a12 --- /dev/null +++ b/examples/Anexia.Gregex.Examples/Program.cs @@ -0,0 +1,8 @@ +using Anexia.Gregex.Examples; + +AnyExample.Main(); +IsExample.Main(); +TimesExample.Main(); +AtLeastOnceExample.Main(); +FollowedByExample.Main(); +IncredientsExample.Main(); \ No newline at end of file diff --git a/examples/Anexia.Gregex.Examples/TimesExample.cs b/examples/Anexia.Gregex.Examples/TimesExample.cs new file mode 100644 index 0000000..f37beba --- /dev/null +++ b/examples/Anexia.Gregex.Examples/TimesExample.cs @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex.Examples; + +/// +/// Example of how to use the method to match an element multiple times. +/// +public static class TimesExample +{ + public static void Main() + { + var listOfStringWithOneStringRepeated = new List() + { + "Hello", "World", "Hello", "Hello", "Hello" + }; + + var twoTimesHello = Gregex.Is("Hello").Times(2); + + var matcher = new Matcher(); + + var matches = matcher.FindMatches(twoTimesHello, listOfStringWithOneStringRepeated); + + Console.WriteLine(string.Join(Environment.NewLine, matches)); + } +} \ No newline at end of file diff --git a/src/Anexia.Gregex/Anexia.Gregex.csproj b/src/Anexia.Gregex/Anexia.Gregex.csproj new file mode 100644 index 0000000..be93566 --- /dev/null +++ b/src/Anexia.Gregex/Anexia.Gregex.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + + + Anexia + Mark Strempel + README.md + https://github.com/anexia/dotnetcore-gregex + MIT + + + + + <_Parameter1>$(MSBuildProjectName).Test + + + + + + + diff --git a/src/Anexia.Gregex/Any.cs b/src/Anexia.Gregex/Any.cs new file mode 100644 index 0000000..e0aade8 --- /dev/null +++ b/src/Anexia.Gregex/Any.cs @@ -0,0 +1,12 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex; + +internal record Any : IGregex +{ + public IMatch CreateMatch(T element) => new OneElementMatch(element); +} \ No newline at end of file diff --git a/src/Anexia.Gregex/Gregex.cs b/src/Anexia.Gregex/Gregex.cs new file mode 100644 index 0000000..fa50e4d --- /dev/null +++ b/src/Anexia.Gregex/Gregex.cs @@ -0,0 +1,86 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex; + +/// +/// Methods for creating gregex expressions. +/// +public static class Gregex +{ + /// + /// Creates an expression that matches if an element is equal to the given value. + /// + /// The value to match. + /// The type of elements that can be matched. + /// An expression that matches exactly . + public static IGregex Is(T expectedValue) => new Test(element => Equals(expectedValue, element)); + + /// + /// Creates an expression that matches all (sub-) sequences that match + /// consecutively for times. + /// + /// The expression to repeat. + /// How often the expression should match. + /// The type of elements that can be matched. + /// A new expression that checks for consecutive matches. + public static IGregex Times(this IGregex gregexToRepeat, int numberOfTimes) => + new Repeat(gregexToRepeat, numberOfTimes); + + /// + /// Creates an expression that matches if the given expression is repeated at least once. + /// + /// The expression that has to match at least once. + /// The type of elements that can be matched. + /// An expression that matches if matches at least once. + public static IGregex AtLeastOnce(this IGregex gregexToRepeat) => + new Repeat(gregexToRepeat, null); + + /// + /// Creates an expression that matches any element of the specified type. + /// + /// The type of elements that can be matched. + /// An expression that matches any element of type . + public static IGregex Any() => new Any(); + + /// + /// Creates an expression that matches when the first expression is immediately followed by the second expression. + /// + /// The first expression to be matched. + /// The second expression that follows the first expression. + /// The type of elements that can be matched. + /// An expression that matches sequences that contain before + /// . + public static IGregex FollowedBy(this IGregex first, IGregex second) => new Pair(first, second); + + /// + /// Creates an expression that matches a sequence of expressions in the specified order. + /// + /// The first expression in the sequence to be matched. + /// The subsequent expressions in the sequence to follow the first expression. + /// The type of elements that can be matched. + /// An expression that matches a sequence of expressions in the specified order, starting with + /// and followed by . + public static IGregex Pattern(IGregex first, params IGregex[] exps) + => exps.Aggregate(first, (lastExpression, nextExpression) => lastExpression.FollowedBy(nextExpression)); + + /// + /// Creates an expression that matches elements based on the provided predicate function. + /// + /// A function to determine if an element matches. + /// The type of elements that can be matched. + /// An expression that matches elements satisfying the provided . + public static IGregex Test(Func testFunction) => new Test(testFunction); + + /// + /// Creates an expression that matches if an element is of the specified subtype. + /// + /// The base type of elements that can be matched. + /// The specific subtype of the elements to match. + /// An expression that matches elements of type . + public static IGregex TypeOf() where TSubtype : TElements => + new Test(element => element is TSubtype); +} \ No newline at end of file diff --git a/src/Anexia.Gregex/IGregex.cs b/src/Anexia.Gregex/IGregex.cs new file mode 100644 index 0000000..7551435 --- /dev/null +++ b/src/Anexia.Gregex/IGregex.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex; + +/// +/// Base interface for expressions. +/// +/// Type of elements that can be matched. +public interface IGregex +{ + IMatch? CreateMatch(T element); +} \ No newline at end of file diff --git a/src/Anexia.Gregex/IMatch.cs b/src/Anexia.Gregex/IMatch.cs new file mode 100644 index 0000000..da59239 --- /dev/null +++ b/src/Anexia.Gregex/IMatch.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex; + +/// +/// Base interface for partial matches. +/// +/// +public interface IMatch +{ + /// + /// Indicates whether the current match is complete. + /// + /// + bool IsCompletable(); + + /// + /// Finishes the current partial match and creates a final match. + /// + /// The complete match. + Match Finish(); + + /// + /// Indicates whether the current match can be extended by . + /// + /// The next element of the sequencing. + /// True if the match can be extended. + bool IsExtendable(T nextElement); + + /// + /// Extends the current partial match and produces a new (partial) match that includes . + /// + /// The next element of the sequence. + /// The new partial match. + IEnumerable> Extend(T nextElement); +} \ No newline at end of file diff --git a/src/Anexia.Gregex/Match.cs b/src/Anexia.Gregex/Match.cs new file mode 100644 index 0000000..0f61af1 --- /dev/null +++ b/src/Anexia.Gregex/Match.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + + +namespace Anexia.Gregex; + +/// +/// Represents a match of an expression. +/// +/// The matched elements. +/// The type of the matched element. +public record Match(IEnumerable Elements) +{ + public virtual bool Equals(Match? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Elements.SequenceEqual(other.Elements); + } + + public override int GetHashCode() => Elements.GetHashCode(); + + public override string ToString() => $"{nameof(Elements)}: [{string.Join(", ", Elements)}]"; +} \ No newline at end of file diff --git a/src/Anexia.Gregex/Matcher.cs b/src/Anexia.Gregex/Matcher.cs new file mode 100644 index 0000000..7746164 --- /dev/null +++ b/src/Anexia.Gregex/Matcher.cs @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + + +using System.Collections.Immutable; + +namespace Anexia.Gregex; + +/// +/// Class that matches sequences and expressions. +/// +/// +/// +/// The type of the elements that can be matched. +public sealed class Matcher +{ + /// + /// Executes the given against the sequence . + /// + /// The expression to execute. + /// The elements to process. + /// The type of the elements in . + /// A sequence of all matches. + public IEnumerable> FindMatches(IGregex gregex, IEnumerable elements) where TInput : T + { + IImmutableList> partialMatches = ImmutableList>.Empty; + + foreach (var element in elements) + { + partialMatches = ProcessPartialMatches(partialMatches, element).ToImmutableArray(); + + var potentialMatch = gregex.CreateMatch(element); + + if (potentialMatch is not null) + { + partialMatches = partialMatches.Add(potentialMatch); + } + + foreach (var partialMatch in partialMatches) + { + if (partialMatch.IsCompletable()) + { + yield return partialMatch.Finish(); + } + } + } + } + + private static IEnumerable> ProcessPartialMatches(IReadOnlyCollection> partialMatches, + TInput element) where TInput : T => + partialMatches.Where(match => match.IsExtendable(element)).SelectMany(match => match.Extend(element)); +} \ No newline at end of file diff --git a/src/Anexia.Gregex/OneElementMatch.cs b/src/Anexia.Gregex/OneElementMatch.cs new file mode 100644 index 0000000..cb0e9eb --- /dev/null +++ b/src/Anexia.Gregex/OneElementMatch.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex; + +internal record OneElementMatch(T Element) : IMatch +{ + public bool IsCompletable() => true; + + public Match Finish() => new([Element]); + + public bool IsExtendable(T nextElement) => false; + + public IEnumerable> Extend(T nextElement) + { + throw new InvalidOperationException(); + } +} \ No newline at end of file diff --git a/src/Anexia.Gregex/Pair.cs b/src/Anexia.Gregex/Pair.cs new file mode 100644 index 0000000..44b2602 --- /dev/null +++ b/src/Anexia.Gregex/Pair.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex; + +internal record Pair(IGregex FirstExpression, IGregex SecondExpression) : IGregex +{ + public IMatch? CreateMatch(T element) + { + if (FirstExpression.CreateMatch(element) is { } initialMatch) + { + return new RepeatMatch(SecondExpression, initialMatch, 2); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Anexia.Gregex/Repeat.cs b/src/Anexia.Gregex/Repeat.cs new file mode 100644 index 0000000..a4a8d99 --- /dev/null +++ b/src/Anexia.Gregex/Repeat.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex; + +internal record Repeat(IGregex Gregex, int? Times): IGregex +{ + public IMatch? CreateMatch(T element) + { + if (Gregex.CreateMatch(element) is { } match) + { + return new RepeatMatch(Gregex, match, Times); + } + + return null; + } +} \ No newline at end of file diff --git a/src/Anexia.Gregex/RepeatMatch.cs b/src/Anexia.Gregex/RepeatMatch.cs new file mode 100644 index 0000000..823a61a --- /dev/null +++ b/src/Anexia.Gregex/RepeatMatch.cs @@ -0,0 +1,81 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +using System.Collections.Immutable; + +namespace Anexia.Gregex; + +internal record RepeatMatch( + IGregex SubExpression, + IMatch PartialMatch, + int? Times, + IImmutableList> PreviousMatches) : IMatch +{ + public RepeatMatch(IGregex SubExpression, IMatch PartialMatch, int? Times) : this(SubExpression, PartialMatch, + Times, ImmutableList>.Empty) + { + } + + private bool IsMaxCountReached() => Times != null && PreviousMatches.Count == Times - 1; + + public bool IsCompletable() => (Times == null && PartialMatch.IsCompletable()) + || (PartialMatch.IsCompletable() && IsMaxCountReached()); + + public Match Finish() + { + var finalMatch = PartialMatch.Finish(); + return new Match(PreviousMatches.Add(finalMatch).SelectMany(match => match.Elements)); + } + + public bool IsExtendable(T nextElement) => PartialMatch.IsExtendable(nextElement) || + (PartialMatch.IsCompletable() && + SubExpression.CreateMatch(nextElement) != null && + !IsMaxCountReached()); + + public IEnumerable> Extend(T nextElement) + { + if (PartialMatch.IsExtendable(nextElement)) + { + foreach (var match in PartialMatch.Extend(nextElement)) + { + yield return this with { PartialMatch = match }; + } + } + + if (PartialMatch.IsCompletable()) + { + var subExpressionMatch = SubExpression.CreateMatch(nextElement); + + if (subExpressionMatch is not null) + { + yield return this with + { + PartialMatch = subExpressionMatch, + PreviousMatches = PreviousMatches.Add(PartialMatch.Finish()) + }; + } + } + } + + public virtual bool Equals(RepeatMatch? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return SubExpression.Equals(other.SubExpression) && PartialMatch.Equals(other.PartialMatch) && + Times == other.Times && PreviousMatches.SequenceEqual(other.PreviousMatches); + } + + public override int GetHashCode() + { + return HashCode.Combine(SubExpression, PartialMatch, Times, PreviousMatches); + } + + public override string ToString() + { + return + $"{nameof(SubExpression)}: {SubExpression}, {nameof(PartialMatch)}: {PartialMatch}, {nameof(Times)}: {Times}, {nameof(PreviousMatches)}: [{string.Join(",", PreviousMatches)}]"; + } +} \ No newline at end of file diff --git a/src/Anexia.Gregex/Test.cs b/src/Anexia.Gregex/Test.cs new file mode 100644 index 0000000..3566232 --- /dev/null +++ b/src/Anexia.Gregex/Test.cs @@ -0,0 +1,12 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex; + +internal record Test(Func Predicate) : IGregex +{ + public IMatch? CreateMatch(T element) => Predicate(element) ? new OneElementMatch(element) : null; +} \ No newline at end of file diff --git a/test/Anexia.Gregex.Test/Anexia.Gregex.Test.csproj b/test/Anexia.Gregex.Test/Anexia.Gregex.Test.csproj new file mode 100644 index 0000000..507f76e --- /dev/null +++ b/test/Anexia.Gregex.Test/Anexia.Gregex.Test.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/test/Anexia.Gregex.Test/MatcherTest.cs b/test/Anexia.Gregex.Test/MatcherTest.cs new file mode 100644 index 0000000..5349c5f --- /dev/null +++ b/test/Anexia.Gregex.Test/MatcherTest.cs @@ -0,0 +1,22 @@ +using CsCheck; + +namespace Anexia.Gregex.Test; + +public class MatcherTest +{ + [Theory] + [MemberData(nameof(MatcherTestData.FindMatches), MemberType = typeof(MatcherTestData))] + public void FindMatches(Matcher matcher, IGregex exp, IEnumerable elements, IEnumerable> expectedMatches) + { + var actualMatches = matcher.FindMatches(exp, elements); + + Assert.Equal(expectedMatches, actualMatches); + } + + [Theory] + [MemberData(nameof(MatcherTestData.MatcherExecutesExpressionWithoutErrorsExamples), MemberType = typeof(MatcherTestData))] + public void MatcherExecutesExpressionWithoutErrors(Matcher matcher, Gen<(IGregex Expression, IEnumerable List)> expressionsAndLists) + { + expressionsAndLists.Sample(expAndList => _ = matcher.FindMatches(expAndList.Expression, expAndList.List).ToArray()); + } +} \ No newline at end of file diff --git a/test/Anexia.Gregex.Test/MatcherTestData.cs b/test/Anexia.Gregex.Test/MatcherTestData.cs new file mode 100644 index 0000000..5f08aee --- /dev/null +++ b/test/Anexia.Gregex.Test/MatcherTestData.cs @@ -0,0 +1,67 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +using CsCheck; + +namespace Anexia.Gregex.Test; + +public static class MatcherTestData +{ + public static TheoryData, IGregex, IEnumerable, IEnumerable>> FindMatches() + { + var matcher = new Matcher(); + + var elementValue = 5; + var expression = Gregex.Is(elementValue); + + return new TheoryData, IGregex, IEnumerable, IEnumerable>>() + { + { matcher, expression, [], [] }, + { matcher, expression, [1], [] }, + { matcher, expression, [elementValue], [new Match([elementValue])] }, + { matcher, expression, [1, elementValue], [new Match([elementValue])] }, + { matcher, expression, [elementValue, 1], [new Match([elementValue])] }, + { matcher, expression, [1, elementValue, 1], [new Match([elementValue])] }, + { matcher, expression, [elementValue, elementValue], [new Match([elementValue]), + new Match([elementValue])] }, + }; + } + + public static TheoryData, Gen<(IGregex, IEnumerable)>> MatcherExecutesExpressionWithoutErrorsExamples() + { + var maxDepth = 5; + var maxListLength = 20; + var maxRepetitions = 10u; + + var simpleExpressions = Gen.OneOf( + Gen.String.Select(Gregex.Is), + Gen.String[0, 5].Select(prefix => Gregex.Test(str => str.StartsWith(prefix)))); + + var higherOrderExpressions = Gen.Recursive>((depth, genGregex) => + { + if (depth == maxDepth) + { + return simpleExpressions; + } + var followedBy = genGregex.SelectMany(firstExpression => genGregex.Select(firstExpression.FollowedBy)); + var atLeastOnce = genGregex.Select(gregex => gregex.AtLeastOnce()); + var times = genGregex.SelectMany(exp => + Gen.UInt[0, maxRepetitions].Select(numberOfTimes => exp.Times((int)numberOfTimes))); + + return Gen.OneOf(followedBy, atLeastOnce, times, simpleExpressions); + }); + + var stringLists = Gen.String.Array[0, maxListLength]; + + var expressionsAndLists = + higherOrderExpressions.SelectMany(exp => stringLists.Select(strings => (exp, strings.AsEnumerable()))); + + return new TheoryData, Gen<(IGregex, IEnumerable)>>() + { + { new Matcher(), expressionsAndLists } + }; + } +} \ No newline at end of file diff --git a/test/Anexia.Gregex.Test/RepeatMatchTest.cs b/test/Anexia.Gregex.Test/RepeatMatchTest.cs new file mode 100644 index 0000000..e912a68 --- /dev/null +++ b/test/Anexia.Gregex.Test/RepeatMatchTest.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + + +namespace Anexia.Gregex.Test; + +public sealed class RepeatMatchTest +{ + [Theory] + [MemberData(nameof(RepeatMatchTestData.IsCompletableExamples), MemberType = typeof(RepeatMatchTestData))] + public void IsCompletable(IMatch match, bool isCompletable) + { + var actualIsCompletable = match.IsCompletable(); + + Assert.Equal(isCompletable, actualIsCompletable); + } + + [Theory] + [MemberData(nameof(RepeatMatchTestData.FinishExamples), MemberType = typeof(RepeatMatchTestData))] + public void Finish(IMatch match, Match expectedMatch) + { + var actualMatch = match.Finish(); + + Assert.Equal(expectedMatch, actualMatch); + } + + [Theory] + [MemberData(nameof(RepeatMatchTestData.IsExtendableExamples), MemberType = typeof(RepeatMatchTestData))] + public void IsExtendable(IMatch match, T element, bool expectedIsExtendable) + { + var actualIsExtendable = match.IsExtendable(element); + + Assert.Equal(expectedIsExtendable, actualIsExtendable); + } + + [Theory] + [MemberData(nameof(RepeatMatchTestData.ExtendExamples), MemberType = typeof(RepeatMatchTestData))] + public void Extend(IMatch match, T element, IEnumerable> expectedMatch) + { + var actualMatch = match.Extend(element); + + Assert.Equal(expectedMatch, actualMatch); + } +} \ No newline at end of file diff --git a/test/Anexia.Gregex.Test/RepeatMatchTestData.cs b/test/Anexia.Gregex.Test/RepeatMatchTestData.cs new file mode 100644 index 0000000..a246899 --- /dev/null +++ b/test/Anexia.Gregex.Test/RepeatMatchTestData.cs @@ -0,0 +1,145 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +using System.Collections.Immutable; +using Moq; + +namespace Anexia.Gregex.Test; + +public static class RepeatMatchTestData +{ + public static TheoryData, bool> IsCompletableExamples() + { + var subExpressionMock = new Mock>(); + var completablePartialMatchMock = new Mock>(); + completablePartialMatchMock.Setup(match => match.IsCompletable()).Returns(true); + var notCompletablePartialMatchMock = new Mock>(); + notCompletablePartialMatchMock.Setup(match => match.IsCompletable()).Returns(false); + + return new TheoryData, bool> + { + { new RepeatMatch(subExpressionMock.Object, completablePartialMatchMock.Object, 1) , true }, + { new RepeatMatch(subExpressionMock.Object, notCompletablePartialMatchMock.Object, 1) , false }, + { new RepeatMatch(subExpressionMock.Object, completablePartialMatchMock.Object, null) , true }, + { new RepeatMatch(subExpressionMock.Object, notCompletablePartialMatchMock.Object, null) , false }, + { new RepeatMatch(subExpressionMock.Object, completablePartialMatchMock.Object, 2) , false }, + { + new RepeatMatch(subExpressionMock.Object, completablePartialMatchMock.Object, 2, + ImmutableList.Create(new Match([true]))), + true + }, + { + new RepeatMatch(subExpressionMock.Object, completablePartialMatchMock.Object, 2, + ImmutableList.Create(new Match([true]), new Match([true]))), + false + }, + { new RepeatMatch(subExpressionMock.Object, completablePartialMatchMock.Object, null) , true }, + { + new RepeatMatch(subExpressionMock.Object, completablePartialMatchMock.Object, null, + ImmutableList.Create(new Match([true]), new Match([true]))), + true + }, + }; + } + + public static TheoryData, Match> FinishExamples() + { + var subExpressionMock = new Mock>(); + var finishablePartialMatchMock = new Mock>(); + finishablePartialMatchMock.Setup(match => match.Finish()).Returns(new Match([true])); + + return new TheoryData, Match>() + { + { + new RepeatMatch(subExpressionMock.Object, finishablePartialMatchMock.Object, 1), + new Match([true]) + }, + { + new RepeatMatch(subExpressionMock.Object, finishablePartialMatchMock.Object, 1, + ImmutableList.Create(new Match([false]))), + new Match([false, true]) + } + }; + } + + public static TheoryData, bool, bool> IsExtendableExamples() + { + var matchingTrueExpressionMock = new Mock>(); + matchingTrueExpressionMock.Setup(gregex => gregex.CreateMatch(true)).Returns(new Mock>().Object); + var onTrueExtendablePartialMatchMock = new Mock>(); + onTrueExtendablePartialMatchMock.Setup(match => match.IsExtendable(true)).Returns(true); + + var notExtendableCompletableMatchMock = new Mock>(); + notExtendableCompletableMatchMock.Setup(match => match.IsCompletable()).Returns(true); + + var notExtendableNotFinishableMatchMock = new Mock>(); + + return new TheoryData, bool, bool> + { + { + new RepeatMatch(matchingTrueExpressionMock.Object, onTrueExtendablePartialMatchMock.Object, 1), + true, true + }, + { + new RepeatMatch(matchingTrueExpressionMock.Object, notExtendableCompletableMatchMock.Object, 1), + true, false + }, + { + new RepeatMatch(matchingTrueExpressionMock.Object, notExtendableCompletableMatchMock.Object, 2), + true, true + }, + { + new RepeatMatch(matchingTrueExpressionMock.Object, notExtendableNotFinishableMatchMock.Object, 2), + true, false + }, + { + new RepeatMatch(matchingTrueExpressionMock.Object, notExtendableNotFinishableMatchMock.Object, 2), + false, false + }, + { + new RepeatMatch(matchingTrueExpressionMock.Object, onTrueExtendablePartialMatchMock.Object, 2), + false, false + } + }; + } + + public static TheoryData, bool, IEnumerable>> ExtendExamples() + { + var onTrueMatch = new Mock>(MockBehavior.Strict).Object; + + var matchingTrueExpressionMock = new Mock>(MockBehavior.Strict); + matchingTrueExpressionMock.Setup(gregex => gregex.CreateMatch(true)).Returns(onTrueMatch); + + var onTrueExtendablePartialMatchMock = new Mock>(MockBehavior.Strict); + onTrueExtendablePartialMatchMock.Setup(match => match.IsCompletable()).Returns(true); + onTrueExtendablePartialMatchMock.Setup(match => match.Finish()).Returns(new Match([true])); + onTrueExtendablePartialMatchMock.Setup(match => match.IsExtendable(true)).Returns(true); + onTrueExtendablePartialMatchMock.Setup(match => match.Extend(true)).Returns([onTrueMatch]); + + var notExtendableButFinishableMatchMock = new Mock>(); + notExtendableButFinishableMatchMock.Setup(match => match.IsCompletable()).Returns(true); + notExtendableButFinishableMatchMock.Setup(match => match.Finish()).Returns(new Match([true])); + + return new TheoryData, bool, IEnumerable>>() + { + { + new RepeatMatch(matchingTrueExpressionMock.Object, onTrueExtendablePartialMatchMock.Object, 2), + true, + [ + new RepeatMatch(matchingTrueExpressionMock.Object, onTrueMatch, 2), + new RepeatMatch(matchingTrueExpressionMock.Object, onTrueMatch, 2, + ImmutableList.Create(new Match([true]))) + ] + }, + { + new RepeatMatch(matchingTrueExpressionMock.Object, notExtendableButFinishableMatchMock.Object, 2), + true, + [new RepeatMatch(matchingTrueExpressionMock.Object, onTrueMatch, 2, + ImmutableList.Create(new Match([true])))] + } + }; + } +} \ No newline at end of file diff --git a/test/Anexia.Gregex.Test/RepeatTest.cs b/test/Anexia.Gregex.Test/RepeatTest.cs new file mode 100644 index 0000000..7a3cb46 --- /dev/null +++ b/test/Anexia.Gregex.Test/RepeatTest.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + + +namespace Anexia.Gregex.Test; + +public sealed class RepeatTest +{ + [Theory] + [MemberData(nameof(RepeatTestData.CreateMatchTestData), MemberType = typeof(RepeatTestData))] + internal void CreateMatch(IGregex repeat, T value, IMatch? expectedValue) + { + var actualMatch = repeat.CreateMatch(value); + + Assert.Equal(expectedValue, actualMatch); + } +} \ No newline at end of file diff --git a/test/Anexia.Gregex.Test/RepeatTestData.cs b/test/Anexia.Gregex.Test/RepeatTestData.cs new file mode 100644 index 0000000..e7534f6 --- /dev/null +++ b/test/Anexia.Gregex.Test/RepeatTestData.cs @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +using Moq; + +namespace Anexia.Gregex.Test; + +public static class RepeatTestData +{ + public static TheoryData, bool, IMatch?> CreateMatchTestData() + { + var mockSubExpression = new Mock>(); + var mockMatch = new Mock>(); + + var initialMatch = mockMatch.Object; + + mockSubExpression.Setup(gregex => gregex.CreateMatch(true)).Returns(initialMatch); + + var subExpression = mockSubExpression.Object; + + return new TheoryData, bool, IMatch?>() + { + { new Repeat(subExpression, 2), true, new RepeatMatch(subExpression, initialMatch, 2) }, + { new Repeat(subExpression, 2), false, null }, + }; + } +} \ No newline at end of file diff --git a/test/Anexia.Gregex.Test/TestTest.cs b/test/Anexia.Gregex.Test/TestTest.cs new file mode 100644 index 0000000..eed5965 --- /dev/null +++ b/test/Anexia.Gregex.Test/TestTest.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + + +namespace Anexia.Gregex.Test; + +public sealed class TestTest +{ + [Theory] + [MemberData(nameof(TestTestData.CreateMatchTestData), MemberType = typeof(TestTestData))] + public void CreateMatch(IGregex testGregex, T value, IMatch? expectedValue) + { + var actualMatch = testGregex.CreateMatch(value); + + Assert.Equal(expectedValue, actualMatch); + } +} \ No newline at end of file diff --git a/test/Anexia.Gregex.Test/TestTestData.cs b/test/Anexia.Gregex.Test/TestTestData.cs new file mode 100644 index 0000000..dd03e8e --- /dev/null +++ b/test/Anexia.Gregex.Test/TestTestData.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------ +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// ------------------------------------------------------------------------------------------ + +namespace Anexia.Gregex.Test; + +public static class TestTestData +{ + public static TheoryData, bool, IMatch?> CreateMatchTestData() + { + return new TheoryData, bool, IMatch?>() + { + { new Test(value => value), true, new OneElementMatch(true) }, + { new Test(value => value), false, null }, + { new Test(value => !value), true, null }, + { new Test(value => !value), false, new OneElementMatch(false) }, + }; + } +} \ No newline at end of file