ExtremeAndy.CombinatoryFilters 4.1.0

dotnet add package ExtremeAndy.CombinatoryFilters --version 4.1.0                
NuGet\Install-Package ExtremeAndy.CombinatoryFilters -Version 4.1.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ExtremeAndy.CombinatoryFilters" Version="4.1.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add ExtremeAndy.CombinatoryFilters --version 4.1.0                
#r "nuget: ExtremeAndy.CombinatoryFilters, 4.1.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install ExtremeAndy.CombinatoryFilters as a Cake Addin
#addin nuget:?package=ExtremeAndy.CombinatoryFilters&version=4.1.0

// Install ExtremeAndy.CombinatoryFilters as a Cake Tool
#tool nuget:?package=ExtremeAndy.CombinatoryFilters&version=4.1.0                

CombinatoryFilters

Functional filter abstraction for creating, applying, mapping, and reducing combinatory filter structures

Installation

dotnet add package ExtremeAndy.CombinatoryFilters

Usage

  1. Define your filter interface(s) and/or class(es). Here's an example of a simple filter which checks whether an integer is between UpperBound and LowerBound.
public class NumericRangeFilter : Filter<int>
{
	public NumericRangeFilter(int lowerBound, int upperBound)
	{
		LowerBound = lowerBound;
		UpperBound = upperBound;
	}

	public int LowerBound { get; }

	public int UpperBound { get; }

	public override bool IsMatch(int item) => LowerBound <= item && item <= UpperBound;
}
  1. Optionally implement IEquatable<TFilter> on your filter class. If this is not done, then calling .Equals() on an IFilterNode in your filter tree will default to value/reference equality when comparing your leaf filters.

    <details> <summary>Example code</summary>

    public class NumericRangeFilter : Filter<int>, IEquatable<NumericRangeFilter>
    {
        public NumericRangeFilter(int lowerBound, int upperBound)
        {
            LowerBound = lowerBound;
            UpperBound = upperBound;
        }
    
        public int LowerBound { get; }
    
        public int UpperBound { get; }
    
        public override bool IsMatch(int item) => LowerBound <= item && item <= UpperBound;
    
        public bool Equals(NumericRangeFilter other)
        {
            if (other is null)
            {
                return false;
            }
    
            return LowerBound == other.LowerBound
                && UpperBound == other.UpperBound;
        }
    
        public override bool Equals(object obj)
            => obj is NumericRangeFilter other && Equals(other);
    
        public override int GetHashCode()
        {
            unchecked
            {
                return (LowerBound.GetHashCode() * 397) ^ UpperBound.GetHashCode();
            }
        }
    }
    

    </details>

  2. Optionally implement IComparable<TFilter> on your filter class. This will allow the Sort() method to be used without passing an explicit IComparer<TFilter>.

  3. Create an instance of your filter and apply it to some values

var filter = new NumericRangeFilter(5, 10);
var filterNode = filter.ToLeafFilterNode();
var values = new[] { 1, 3, 5, 9, 11 };
var expectedFilteredValues = new[] { 5, 9 };

var filterPredicate = filterNode.GetPredicate<NumericRangeFilter, int>();
var filteredValues = values.Where(filterPredicate);

Assert.Equal(expectedFilteredValues, filteredValues);

Complex filters

You can assemble arbitrarily complex filters as follows:

var filter5To10 = new NumericRangeFilter(5, 10);
var filter8To15 = new NumericRangeFilter(8, 15);
var filter5To10Or8To15 = new CombinationFilterNode<NumericRangeFilter>(new[] { filter5To10, filter8To15 }, CombinationOperator.Any);
var filter9To12 = new NumericRangeFilter(9, 12);
var filter = new CombinationFilterNode<NumericRangeFilter>(new IFilterNode<NumericRangeFilter>[] { filter5To10Or8To15, filter9To12.ToLeafFilterNode() }, CombinationOperator.All);
Inversion

Any filter can be inverted using .Invert().

Testing a single value

You can test a single value as follows:

var filter5To10 = new NumericRangeFilter(5, 10);
var filter8To15 = new NumericRangeFilter(8, 15);
var combinationFilter = new CombinationFilterNode<NumericRangeFilter>(new[] { filter5To10, filter8To15 });
var isMatch = combinationFilter.IsMatch(7);

However, IsMatch causes an allocation and is not recommended for testing many items. Instead, use filter.GetPredicate:

var filter5To10 = new NumericRangeFilter(5, 10);
var filter8To15 = new NumericRangeFilter(8, 15);
var combinationFilter = new CombinationFilterNode<NumericRangeFilter>(new[] { filter5To10, filter8To15 });
var filterPredicate = combinationFilter.GetPredicate<NumericRangeFilter, int>();
var lotsOfIntegers = Enumerable.Range(0, 1000000);
var matches = lotsOfIntegers.Where(filterPredicate);

Preserving ordering of filters

CombinationFilterNode stores Nodes in the same order they are passed in. Operations such as Collapse should still preserve the order of Nodes, but this is not well tested.

Advanced usage

IFilterNode<> supports Map, Match and Aggregate for mapping and reducing filters.

Map usage

In this example, we reduce the range of the leaf node filters by increasing the lower bound by 1 and decreasing the upper bound by 1. The structure of all the All, Any and Invert operations remains unchanged.

var shortenedFilters = filter.Map(f =>
{
    var newLowerBound = f.LowerBound + 1;
    var newUpperBound = f.UpperBound - 1;
    return new NumericRangeFilter(newLowerBound, newUpperBound);
});

Aggregate usage

In this example, we want to compute the length of the longest filter interval, or infinity if any filter is inverted.

var longestIntervalLength = filter.Aggregate<double>(
    (lengths, _) => lengths.Max(),
    length => double.PositiveInfinity,
    f => f.Filter.UpperBound - f.Filter.LowerBound);

GetPartial usage

GetPartial provides a way to compute a partial filter, which is a kind of subset of a filter. When applied, a partial filter is guaranteed to return a superset of the result that the original filter would have returned when applied. This is a special case of the Relax operation, where leaf nodes are maximally relaxed (i.e. replaced with True) if the predicate is satisfied.

This is useful for performing pre-filtering on an incomplete dataset that doesn't (yet) contain all the information required to apply the final filter.

This is normally quite a trivial problem, but when there are InvertedFilters and CombinationFilters in the mix, computing the minimal partial filter is not intuitive or easy to demonstrate.

Here is a contrived example (note: this doesn't do anything useful, just demonstrates usage):

// All the numbers from -5 to 10, excluding numbers from 2 to 6
var filter = new CombinationFilterNode<NumericRangeFilter>(new IFilterNode<NumericRangeFilter>[]
{
    new NumericRangeFilter(-5, 10).ToLeafFilterNode(),
    new NumericRangeFilter(2, 6).ToLeafFilterNode().Invert()
}, CombinationOperator.All);

// Exclude filters with negative values
var partialFilter = filter.GetPartial(f => f.LowerBound >= 0);

// Initially we only have positive numbers
var positiveValues = new[] { 1, 3, 5, 7, 12 };
var prefilteredValues = positiveValues.Where(partialFilter.GetPredicate<NumericRangeFilter, int>()).ToList();
Assert.Equal(new[] { 1, 7, 12 }, prefilteredValues);

// Now we include some additional values
var additionalValues = new[] { -7, -4, 11 };
var combinedValues = prefilteredValues.Concat(additionalValues);

// Finally we apply our 'full' filter
var finalValues = combinedValues.Where(filter.GetPredicate<NumericRangeFilter, int>());
Assert.Equal(new[] { 1, 7, -4 }, finalValues);

Relax usage

Relax provides a way to relax a filter by relaxing its leaf nodes.

Example TBD.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 is compatible.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETFramework 4.7.2

    • No dependencies.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
4.1.0 218,311 9/7/2021
4.0.0 915 8/28/2021
3.0.3 839 7/28/2021
3.0.2 467 7/27/2021
3.0.1 421 7/25/2021
3.0.0 1,082 4/22/2021
2.0.4 401 2/24/2021
2.0.3 4,040 2/24/2021
2.0.2 385 2/24/2021
2.0.1 400 2/18/2021
2.0.0 3,206 6/18/2020
1.3.0 1,509 4/24/2020
1.2.0 2,797 12/10/2019
1.1.8 1,389 8/4/2019
1.1.7 588 8/4/2019
1.1.6 568 8/4/2019
1.1.5 596 8/2/2019
1.1.4 596 8/2/2019
1.1.3 565 8/2/2019
1.1.2 546 8/2/2019
1.1.1 569 8/2/2019
1.1.0 583 8/2/2019
1.0.4 601 7/22/2019
1.0.3 555 7/21/2019
1.0.2 556 7/21/2019
1.0.1 612 7/20/2019
1.0.0 523 7/20/2019