GitHub Repository
Previously: Countable and uncountable sets in .NET (clean version).
Thesis
There is a full featured support of countable sets in .NET: IEnumerable<T>
. What about uncountable sets; sets defined by predicate? How can they be manipulated and interact with IEnumerable<T>
?
Solution
Let;s introduce two library classes: Universe
and Set
, where Universe
is a factory of Sets
and Sets
are defined by predicate, condition like Func<T, bool>
.
Example:
using static Universe;
Set<int> integers = Set<int>();
Set<int> zero = Set<int>(i => i == 0);
Set<int> positive = Set<int>(i => i > 0);
We define some basic calculus on sets:
Set<int> nonPositive = !positive;
Set<int> negative = !positive – zero;
Set<int> nonZero = positive | negative;
Intersection:
Set<int> liquidFreshWaterC = Universe.Set<int>(t => t > 0 && t < 100);
Set<int> liquidSaltWaterC = Universe.Set<int>(t => t > -21.1 && t < 102);
Set<int> liquidWaterC = liquidFreshWaterC & liquidSaltWaterC; // = 0 … 100
Now, how to test the set (it is just a combined condition underneath, nothing else) – let’s use intersection operator, as scalar value is just a set of one element:
bool isLiquidWater = liquidWaterC & 25; // = true
Union operator provides us with an another set:
Set<int> temperatures = liquidWaterC | 200; // 0 … 100, 200
The most useful feature is an integration with IEnumerable<T>
. Let’s have:
Int[] tempC = new[] {-100, -10, 0, 10, 100, 200};
We can inersect them:
Enumerable<int> t = temperatures & tempC; // = 0, 10, 100, 200
Please note that non empty Enumerable<T>
is truthy; an empty one is falsy:
bool nonEmpty = t; // true
We can join them, so result will be another Set<T>
:
Set<T> joined = temperatures & tempC; // -100, -10, 0 … 100, 200
We can even exclude set from enumeration getting an enumeration, or exclude enumeration from set – getting set as a result.
Demo
Let's define Customer
, Order
, Invoice
to calculate discounts (full solution is available online to play with):
class Customer
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
public List<Invoice> Invoices { get; set; }
}
class Order
{
public decimal Total { get; set; }
}
class Invoice
{
public decimal Total { get; set; }
}
Now helpers:
static class Balances
{
public static decimal Invoiced(this Customer customer) =>
customer.Invoices.Sum(i => i.Total);
public static decimal Ordered(this Customer customer) =>
customer.Orders.Sum(o => o.Total);
public static decimal Balance(this Customer customer) =>
customer.Ordered() - customer.Invoiced();
}
Our discount rules are going to be:
var loyal = Set<Customer>(с => с.Invoiced() > 10000);
var debtors = Set<Customer>(c => c.Balance() > 0);
var creditable = Set<Customer>(c => c.Balance() < 5000) & loyal;
var bulk = Set<Customer>(c => c.Orders.Any(o => o.Total > 2000));
// I think this syntax makes total sense
var tenOff = bulk & loyal & !debtors;
var fiveOff = bulk & loyal & creditable - tenOff;
var noOff = !tenOff & !fiveOff;
Let’s test the sets:
foreach (var c in Repository.Customers & fiveOff)
WriteLine($"-5%: {c.Name}");
foreach (var c in Repository.Customers & tenOff)
WriteLine($"-10%: {c.Name}");
foreach (var c in Repository.Customers & noOff)
WriteLine($"0%: {c.Name}");
Library code:
static class Universe
{
public static Set<T> Set<T>() => Set<T>(i => true);
public static Set<T> Set<T>(Predicate<T> predicate) => new Set<T>(predicate);
}
class Set<T>
{
public Set(Predicate<T> predicate)
{
Predicate = predicate;
}
public static Enumerable<T> operator &(Set<T> left, T right) =>
left.Predicate(right) ? new Enumerable<T>(right) : Enumerable<T>.Empty;
public static Enumerable<T> operator &(T left, Set<T> right) =>
right.Predicate(left) ? new Enumerable<T>(left) : Enumerable<T>.Empty;
public static Set<T> operator &(Set<T> left, Set<T> right) =>
new Set<T>(i => left.Predicate(i) && right.Predicate(i));
public static Enumerable<T> operator &(Set<T> left, IEnumerable<T> right) =>
new Enumerable<T>(right.Where(i => left.Predicate(i)));
public static Enumerable<T> operator &(IEnumerable<T> left, Set<T> right) =>
new Enumerable<T>(left.Where(i => right.Predicate(i)));
public static Set<T> operator |(Set<T> left, T right) =>
new Set<T>(i => left.Predicate(i) || right.Equals(i));
public static Set<T> operator |(T left, Set<T> right) =>
new Set<T>(i => left.Equals(i) || right.Predicate(i));
public static Set<T> operator |(Set<T> left, Set<T> right) =>
new Set<T>(i => left.Predicate(i) || right.Predicate(i));
public static Set<T> operator |(Set<T> left, IEnumerable<T> right) =>
new Set<T>(i => left.Predicate(i) || right.Contains(i));
public static Set<T> operator |(IEnumerable<T> left, Set<T> right) =>
new Set<T>(i => left.Contains(i) || right.Predicate(i));
public static Set<T> operator -(Set<T> left, T right) =>
new Set<T>(i => left.Predicate(i) && !right.Equals(i));
public static Set<T> operator -(T left, Set<T> right) =>
new Set<T>(i => left.Equals(i) && !right.Predicate(i));
public static Set<T> operator -(Set<T> left, Set<T> right) =>
new Set<T>(i => left.Predicate(i) && !right.Predicate(i));
public static Set<T> operator -(Set<T> left, IEnumerable<T> right) =>
new Set<T>(i => left.Predicate(i) && !right.Contains(i));
public static Enumerable<T> operator -(IEnumerable<T> left, Set<T> right) =>
new Enumerable<T>(left.Where(i => !right.Predicate(i)));
public static Set<T> operator !(Set<T> set) =>
new Set<T>(i => !set.Predicate(i));
Predicate <T> Predicate { get; }
}
class Enumerable<T> : IEnumerable<T>
{
public static readonly Enumerable<T> Empty = new Enumerable<T>();
public static implicit operator bool(Enumerable<T> intersection) => intersection.Any();
public Enumerable(params T[] items)
{
Items = items;
}
public Enumerable(IEnumerable<T> items)
{
Items = items;
}
public IEnumerator<T> GetEnumerator() => Items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
IEnumerable<T> Items { get; }
}
temperatures & tempC;
results in an object of typEnumerable<int>
first, and than in an object of typeSet<T>
\$\endgroup\$Enumerable<T>
... \$\endgroup\$