Search code examples
c#linq

Is double grouping possible with Linq?


Given Row type

public record Row(bool? Left, string OperatorName, bool? Right, bool? Result);

and a list of Rows.

This list is generated by a list of boolean operators (2 examples shown below) that I can omit the details for the sake of simplicity.

bool? xor(bool? x, bool? y) => x ^ y;
bool? xor_(bool? x, bool? y) => (!x & y) | (x & !y);
IEnumerable<Row> rows =
[
    new Row(null, "_or_", null, null),
    new Row(null, "_or_", true, true),
    new Row(null, "_or_", false, null),
    new Row(true, "_or_", true, true),
    new Row(true, "_or_", false, true),
    new Row(false, "_or_", false, false),

    new Row(null, "and", null, null),
    new Row(null, "and", true, null),
    new Row(null, "and", false, false),
    new Row(true, "and", true, true),
    new Row(true, "and", false, false),
    new Row(false, "and", false, false),

    new Row(null, "or", null, null),
    new Row(null, "or", true, true),
    new Row(null, "or", false, null),
    new Row(true, "or", true, true),
    new Row(true, "or", false, true),
    new Row(false, "or", false, false),

    new Row(null, "or_", null, null),
    new Row(null, "or_", true, true),
    new Row(null, "or_", false, null),
    new Row(true, "or_", true, true),
    new Row(true, "or_", false, true),
    new Row(false, "or_", false, false),

    new Row(null, "xor", null, null),
    new Row(null, "xor", true, null),
    new Row(null, "xor", false, null),
    new Row(true, "xor", true, false),
    new Row(true, "xor", false, true),
    new Row(false, "xor", false, false),

    new Row(null, "xor_", null, null),
    new Row(null, "xor_", true, null),
    new Row(null, "xor_", false, null),
    new Row(true, "xor_", true, false),
    new Row(true, "xor_", false, true),
    new Row(false, "xor_", false, false)
];

Objective

How to use Linq and rows to produce the following output on the console?

Group 1:
xor
xor_
Group 2:
and
Group 3:
_or_
or
or_

My idea

First group by OperatorName, so we have 6 groups

[
    new Row(null, "_or_", null, null),
    new Row(null, "_or_", true, true),
    new Row(null, "_or_", false, null),
    new Row(true, "_or_", true, true),
    new Row(true, "_or_", false, true),
    new Row(false, "_or_", false, false),
]
[
    new Row(null, "and", null, null),
    new Row(null, "and", true, null),
    new Row(null, "and", false, false),
    new Row(true, "and", true, true),
    new Row(true, "and", false, false),
    new Row(false, "and", false, false)   
]
[
    new Row(null, "or", null, null),
    new Row(null, "or", true, true),
    new Row(null, "or", false, null),
    new Row(true, "or", true, true),
    new Row(true, "or", false, true),
    new Row(false, "or", false, false)
]
[
    new Row(null, "or_", null, null),
    new Row(null, "or_", true, true),
    new Row(null, "or_", false, null),
    new Row(true, "or_", true, true),
    new Row(true, "or_", false, true),
    new Row(false, "or_", false, false),
]
[
    new Row(null, "xor", null, null),
    new Row(null, "xor", true, null),
    new Row(null, "xor", false, null),
    new Row(true, "xor", true, false),
    new Row(true, "xor", false, true),
    new Row(false, "xor", false, false)
]
[
    new Row(null, "xor_", null, null),
    new Row(null, "xor_", true, null),
    new Row(null, "xor_", false, null),
    new Row(true, "xor_", true, false),
    new Row(true, "xor_", false, true),
    new Row(false, "xor_", false, false)
]

Because xor_ and xor are identical truth tables (because (!x & y) | (x & !y) and x ^ y are tautologies), then they must be in the same group. The second grouping is my problem. How to get the following output on the console?

Group 1:
xor
xor_
Group 2:
and
Group 3:
_or_
or
or_

Last edit

For those who are confused in understanding the problem, now I illustrate with a much simpler one.

I have a list of functions and

(string name, Func<int,int> func) [] functions = new []
{
   ("a", x => 3*x + 1),
   ("b", x => 2*x + x + 1),
   ("c", x => x*x - x*x + 3*x +1),
   ("d", x => x*x),
   ("e", x => x*x + 1 -1),
   ("f", x => 2*(x+1) + x -1)
};

a list of inputs

int[] inputs = [0,1,2];

A list rows is generated by applying inputs to each function in functions. For simulation purposes, rows is hard coded as follows.

(int x, string funcName, int y)[] rows =
[
   (0, "a", 1),
   (1, "a", 4),
   (2, "a", 7),

   (0, "b", 1),
   (1, "b", 4),
   (2, "b", 7),

   (0, "c", 1),
   (1, "c", 4),
   (2, "c", 7),

   (0, "d", 0),
   (1, "d", 1),
   (2, "d", 4),

   (0, "e", 0),
   (1, "e", 1),
   (2, "e", 4),

   (0, "f", 1),
   (1, "f", 4),
   (2, "f", 7)
];

The expected output:

Group 1:
a
b
c
f

Group 2:
d
e

Why? because functions a, b, c, f can be simplified to 3*x+1 and functions d, e can be simplified to x*x.


Solution

  • With a little setup, the actual operations are pretty easy. So, setup first.

    For ease of hashing (and performance in general), we can encode your truth tables as simple integers where true/false/null are two bits each, and the operands and results can be concatenated to one integer:

    record Row(bool? Left, string OperatorName, bool? Right, bool? Result)
    {
        public ulong CodedRepresentation =>
            Left switch { true => 1UL, false => 2UL, null => 3UL }
            | (Right switch { true => 1UL, false => 2UL, null => 3UL } << 2)
            | (Result switch { true => 1UL, false => 2UL, null => 3UL } << 4);
    }
    

    Next we need an enumerable comparer, which is something .Net never provided for some reason:

    class EnumerableComparer<T> : IEqualityComparer<IEnumerable<T>>
    {
        public bool Equals(IEnumerable<T>? x, IEnumerable<T>? y) => 
            x is not null && y is not null && !x.Except(y).Any() && !y.Except(x).Any();
        public int GetHashCode([DisallowNull] IEnumerable<T> obj)
        {
            var hash = new HashCode();
            foreach (var item in obj.Order())
                hash.Add(item);
            return hash.ToHashCode();
        }
    }
    

    And lastly, the actual logic you're looking for:

        foreach (var result in rows.GroupBy(r => r.OperatorName)
            .Select(r => (name: r.Key, operations: r.Select(w => w.CodedRepresentation)))
            .GroupBy(w => w.operations, new EnumerableComparer<ulong>()))
        {
            Console.WriteLine(string.Join(" = ", result.Select(w => w.name)));
        }
    

    All together, the result we get is what you'd expect:

    and
    or
    xor = xor_