Home > database >  Compare Two Jagged Arrays in C# for "except"
Compare Two Jagged Arrays in C# for "except"

Time:10-23

I'm trying to find the members of two arrays where they appear in one but not the other. I've read this article and I think while I understand it I'm doing something incorrectly.

Assume the following simple code:

using System;
using System.Linq;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        int[][] currentLocation = new int[][]
                {
                    new int[] { 11, 10 },
                    new int[] { 11, 11 },
                    new int[] { 11, 12 },
                    new int[] { 11, 13 },
                };
        int[][] proposedLocation = new int[][]
                {
                    new int[] { 11, 12 },
                    new int[] { 11, 13 },
                    new int[] { 11, 14 },
                    new int[] { 11, 15 },
                };
        
        IEnumerable<int[]> onlyInFirstSet = proposedLocation.Except(currentLocation);
        foreach (int[] number in onlyInFirstSet)
            Console.WriteLine(number[0].ToString()   "," number[1].ToString());
    }
}

Now what I'd hope/think/expect to see would be:

11,14
11,15

...because in the proposedLocation these are the only ones that don't appear in currentLocation However I'm actually getting:

11,12
11,13
11,14
11,15

(i.e. the whole of proposedLocation)

Question: how do I extract values that only appear in proposedLocation? I'd be beneficial if I could somehow retain the int[][] rather than an odd bit of string while at it but that's a bonus.

CodePudding user response:

Agreed with -MySkullCaveIsADarkPlace. However, if you want to achieve this with a workaround, refer to the below.

Instead of an jagged array, use an object.

var currentLocation = new object[]
                        {
                        new  { a = 11, b = 10 },
                        new  { a = 11, b = 11 },
                        new  { a = 11, b = 12 },
                        new  { a = 11, b = 13 }
                        };
    
                var proposedLocation = new object[]
                        {
                        new  { a = 11, b = 12 },
                        new  { a = 11, b = 13 },
                        new  { a = 11, b = 14 },
                        new  { a = 11, b = 15 }
                        };
    
                var a = proposedLocation.Except(currentLocation);

CodePudding user response:

Using an int[] { 11, 10} for a location isn't sufficient for a few reasons.

  • First, all arrays have reference semantics, which means an equality check does not check values but for reference equality (if they are the same object in memory). This means the following check produces false.

    int[] a = new[] { 1,2 };
    int[] b = new[] { 1,2 };
    if( a == b || a.Equals(b) ) 
    {
        // this will never happen
    }
    
  • Second, there is enforcement of two coordinates for each location. Each row in a jagged array can have different number of columns. This makes for some awkward coding to be thorough in your code.

  • Third, the values of an array can be changed at any time, without you having a way to prevent this. Consider the code

    int[] a = new[] { 1, 2};
    a[0] = 10;
    

    This can cause bugs as locations you think have specific values, due to a programming error, can easily change locations.

  • Lastly, if you want to display the values of a location, you can't just call Console.WriteLine(array) because it won't display the values, and thus you need a loop, or use string.Join() to create a displayable array.

Location as a struct (X,Y)

All of the above, combined, call for a different approach. I suggest using a structure that can store two values (just as the array), only two values, no more or less, can check for equality with other locations, and finally has a built-in way to convert to a string for display.

public readonly struct Location : IEquatable<Location>
{
    public Location(int x, int y) : this()
    {
        X = x;
        Y = y;
    }
    public int X { get; }
    public int Y { get; }

    public override string ToString() => $"({X},{Y})";

    #region IEquatable Members

    /// <summary>
    /// Equality overrides from <see cref="System.Object"/>
    /// </summary>
    /// <param name="obj">The object to compare this with</param>
    /// <returns>False if object is a different type, otherwise it calls <code>Equals(Location)</code></returns>
    public override bool Equals(object obj)
    {
        if (obj is Location item)
        {
            return Equals(item);
        }
        return false;
    }

    /// <summary>
    /// Checks for equality among <see cref="Location"/> classes
    /// </summary>
    /// <returns>True if equal</returns>
    public bool Equals(Location other)
    {
        return this.X == other.X && this.Y == other.Y;
    }
    /// <summary>
    /// Calculates the hash code for the <see cref="Location"/>
    /// </summary>
    /// <returns>The int hash value</returns>
    public override int GetHashCode()
    {
            unchecked
            {
                int hc = -1817952719;
                hc = (-1521134295) * hc   this.X.GetHashCode();
                hc = (-1521134295) * hc   this.Y.GetHashCode();
                return hc;
            }                
    }
    public static bool operator ==(Location target, Location other) { return target.Equals(other); }
    public static bool operator !=(Location target, Location other) { return !target.Equals(other); }

    #endregion
}

Some key features of this structure are

  • X, Y properties hold the values and are unchanging. A location is defined by its (x,y) values and they go in pairs together. The values are set at the constructor and are readonly.

  • Equals(Location other) check for value equality (both x and y).

  • ToString() automatically converts the structure to a string when needed. This allows calls like Console.WriteLine(location) to produce the results (11,14) for example.

  • LINQ operations automatically take advantage of Equals() function as we forward the generic Equals(object) to the specific Equals(Location) function.

  • The GetHashCode() function you can ignore for now. It is there because it is required by certain collections, and it guarantees that if two locations are different, that they will have different hash-codes.

Program

Now to test for the use case described in the question

class Program
{
    static void Main(string[] args)
    {
        Location[] currentLocation = new Location[]
        {
            new Location( 11, 10 ),
            new Location( 11, 11 ),
            new Location( 11, 12 ),
            new Location( 11, 13 ),
        };
        Location[] proposedLocation = new Location[]
        {
            new Location( 11, 12 ),
            new Location( 11, 13 ),
            new Location( 11, 14 ),
            new Location( 11, 15 ),
        };

        var list = Enumerable.Except(proposedLocation, currentLocation).ToArray();
        foreach (var item in list)
        {
            Console.WriteLine(item);
        }
    }
}

with output as expected:

(11,14)
(11,15)

Note that due to the operation of Except() and the definition provided, you need to subtract the fist list from the second list, and that is way I am calling .Except(proposedLocation, currentLocation) and not the other way around.

Location as a tuple (int,int)

You can achieve the same result, by using a Tuple of two int values. Actually a ValueTuple<int,int> would have the exact same behavior as our custom structure Location above.

The code below has the exact same result as before, does not require a custom structure, and keeps type safety, as opposed to a solution that casts locations as object.

class Program
{
    static void Main(string[] args)
    {
        var currentLocation = new List<ValueTuple<int, int>>()            
        {
            ( 11, 10 ),
            ( 11, 11 ),
            ( 11, 12 ),
            ( 11, 13 ),
        };
        var proposedLocation = new List<ValueTuple<int,int>>()
        {
            ( 11, 12 ),
            ( 11, 13 ),
            ( 11, 14 ),
            ( 11, 15 ),
        };

        var list = Enumerable.Except(proposedLocation, currentLocation).ToArray();
        foreach (var item in list)
        {
            Console.WriteLine(item);
        }
        // Output:
        // (11,14)
        // (11,15)
    }
}
  • Related