mstefarov / fNbt

A C# library for reading and writing Named Binary Tag (NBT) files and streams. A continuation of libnbt project, originally by Erik Davidson (aphistic).
116 stars 31 forks source link

Comparing #17

Open tarik02 opened 8 years ago

tarik02 commented 8 years ago

Can you add method to compare two compounds?

mstefarov commented 8 years ago

There should probably be a way to easily check if two compounds have the same contents. I will add overrides of GetHashCode and Equals methods on NbtTag classes in next release. For now, you can use this implementation of IEqualityComparer<NbtTag>:

/// <summary> Compares NbtTag for equality by comparing their types, names, and values. Considers compound tags to be equal
///     if they contain equal sets of tags. Considered list tags to be equal if their tags are equal and in the same order. </summary>
public class NbtComparer : IEqualityComparer<NbtTag> {
    public static readonly NbtComparer Instance = new NbtComparer();

    public bool Equals(NbtTag x, NbtTag y) {
        if (x == y) return true;
        if (x == null || y == null) return false;
        return (x.TagType == y.TagType)
                && string.Equals(x.Name, y.Name) // null names are permitted
                && DeepEquals(x, y);
    }

    public int GetHashCode(NbtTag tag) {
        var hash = tag.TagType.GetHashCode();
        hash = hash*23 ^ (tag.Name ?? "").GetHashCode();

        var rawValue = GetRawValue(tag);
        if (rawValue != null) {
            hash = hash*23 ^ rawValue.GetHashCode();
        } else if (tag.TagType == NbtTagType.List) {
            var list = (NbtList)tag;
            hash = hash*23 ^ list.ListType.GetHashCode();
            hash = hash*23 ^ list.Count.GetHashCode();
        } else if (tag.TagType == NbtTagType.Compound) {
            var comp = (NbtCompound)tag;
            hash = hash*23 ^ comp.Count.GetHashCode();
        } else {
            // END and unknown
            throw new ArgumentException("Cannot compare tags of type " + tag.TagType);
        }
        return hash;
    }

    // Compare detailed attributes of two given tags
    private bool DeepEquals(NbtTag x, NbtTag y) {
        // Assume that tags have same type and are non-null
        var rawValue1 = GetRawValue(x);
        if (rawValue1 != null) {
            var rawValue2 = GetRawValue(y);
            // Regular tags are equal if their values are equal
            return Equals(rawValue1, rawValue2);
        } else if (x.TagType == NbtTagType.Compound) {
            var xComp = (NbtCompound)x;
            var yComp = (NbtCompound)y;
            // Compounds are equal if their child-count and contents are equal
            return (xComp.Count == yComp.Count) &&
                    new HashSet<NbtTag>(xComp, this).SetEquals(yComp);
        } else if (x.TagType == NbtTagType.List) {
            var xList = (NbtList)x;
            var yList = (NbtList)y;
            // Lists are considered equal if their type, count, and contents are equal
            return (xList.ListType == yList.ListType) &&
                    (xList.Count == yList.Count) &&
                    xList.SequenceEqual(yList, this);
        } else {
            // END and unknown
            throw new ArgumentException("Cannot compare tags of type " + x.TagType);
        }
    }

    private object GetRawValue(NbtTag tag) {
        switch (tag.TagType) {
            case NbtTagType.Byte:
                return tag.ByteValue;
            case NbtTagType.ByteArray:
                return tag.ByteArrayValue;
            case NbtTagType.Double:
                return tag.DoubleValue;
            case NbtTagType.Float:
                return tag.FloatValue;
            case NbtTagType.Int:
                return tag.IntValue;
            case NbtTagType.IntArray:
                return tag.IntArrayValue;
            case NbtTagType.Long:
                return tag.LongValue;
            case NbtTagType.Short:
                return tag.ShortValue;
            case NbtTagType.String:
                return tag.StringValue;
            default:
                // returns null for List, Compound, and unknown tag types
                return null;
        }
    }
}

Here is how you'd use it, for example:

private static void Main(string[] args) {
    var comp1 = new NbtCompound("Foo") {
        new NbtInt("IntField", 1),
        new NbtString("StringVal", "blah"),
        new NbtIntArray("ByteArrayVal", new[] { 1, 2, 3 }),
        new NbtList("ListVal")
    };
    var comp2 = new NbtCompound("Bar") {
        new NbtInt("IntField", 2),
        new NbtString("StringVal", "blah"),
        new NbtIntArray("ByteArrayVal", new[] { 1, 2, 3 }),
        new NbtDouble("DoubleVal", 1.23)
    };

    PrintDifference(comp1, comp2, NbtComparer.Instance);
    Console.ReadLine();
}

private static void PrintDifference(NbtCompound x, NbtCompound y, IEqualityComparer<NbtTag> comparer) {
    if (comparer.Equals(x, y)) {
        Console.WriteLine("They're equal!");
    } else {
        Console.WriteLine("They're NOT equal!");
        if (!string.Equals(x.Name, y.Name)) {
            Console.WriteLine("Names are different: {0} vs {1}", x.Name, y.Name);
        }

        // Compare named tags that are present in both compounds
        foreach (var sharedName in x.Names.Intersect(y.Names)) {
            if (comparer.Equals(x[sharedName], y[sharedName])) {
                Console.WriteLine("  \"{0}\" is same ({1})", sharedName, x[sharedName]);
            } else {
                Console.WriteLine("  \"{0}\" is different ({1} vs {2})", sharedName, x[sharedName],
                                    y[sharedName]);
            }
        }

        // Print named tags that are unique to one of the compounds
        foreach (var newName in x.Names.Except(y.Names)) {
            Console.WriteLine("  \"{0}\" is only in {1} ({2})", newName, x.Name, x[newName]);
        }
        foreach (var newName in y.Names.Except(x.Names)) {
            Console.WriteLine("  \"{0}\" is only in {1} ({2})", newName, y.Name, y[newName]);
        }
    }
}
mstefarov commented 8 years ago

Now that I think about it, tags with numeric values (byte, int, long, float, double) can also implement IComparable. Perhaps string tags too.

tarik02 commented 8 years ago

@fragmer, when is next release?

mstefarov commented 8 years ago

Well, I can do a minor release (0.6.4) with just these additions in a couple days.

Timeline of the next major release (either 0.7 or 1.0) is uncertain. The big new feature (automated serialization/deserialization of objects) has been stalled for a while. It needs well over 100 hours of additional work, and I don't have much spare time these days.

mstefarov commented 8 years ago

I added #18 and #19 to track progress on this feature.