Use Jenkins Hash (#40)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index deb6d55..eb48144 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 1.0.0
+
+- Update hashCode implementation to use `Jenkins Hash` ([#39](https://github.com/felangel/equatable/issues/39)).
+- Documentation Updates
+
# 0.6.1
- Minor documentation updates
diff --git a/lib/src/equatable_utils.dart b/lib/src/equatable_utils.dart
index e5c8176..778ab4f 100644
--- a/lib/src/equatable_utils.dart
+++ b/lib/src/equatable_utils.dart
@@ -1,27 +1,7 @@
import 'package:collection/collection.dart';
-int mapPropsToHashCode(dynamic props) {
- int hashCode = 0;
-
- if (props is Map) {
- props.forEach((key, value) {
- final propHashCode = mapPropsToHashCode(key) ^ mapPropsToHashCode(value);
- hashCode = hashCode ^ propHashCode;
- });
- } else if (props is List || props is Iterable || props is Set) {
- props.forEach((prop) {
- final propHashCode =
- (prop is List || prop is Iterable || prop is Set || prop is Map)
- ? mapPropsToHashCode(prop)
- : prop.hashCode;
- hashCode = hashCode ^ propHashCode;
- });
- } else {
- hashCode = hashCode ^ props.hashCode;
- }
-
- return hashCode;
-}
+int mapPropsToHashCode(Iterable props) =>
+ _finish(props.fold(0, (hash, object) => _combine(hash, object)));
const DeepCollectionEquality _equality = DeepCollectionEquality();
@@ -45,3 +25,26 @@
}
return true;
}
+
+/// Jenkins Hash Functions
+/// https://en.wikipedia.org/wiki/Jenkins_hash_function
+int _combine(int hash, dynamic object) {
+ if (object is Map) {
+ object.forEach((key, value) {
+ hash = _combine(hash, [key, value]);
+ });
+ return hash;
+ }
+ if (object is Iterable) {
+ return mapPropsToHashCode(object);
+ }
+ hash = 0x1fffffff & (hash + object.hashCode);
+ hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
+ return hash ^ (hash >> 6);
+}
+
+int _finish(int hash) {
+ hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
+ hash = hash ^ (hash >> 11);
+ return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index dcd9f3a..f17a414 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: equatable
description: An abstract class that helps to implement equality without needing to explicitly override == and hashCode.
-version: 0.6.1
+version: 1.0.0
author: Felix Angelov <felangelov@gmail.com>
homepage: https://github.com/felangel/equatable
diff --git a/test/equatable_mixin_test.dart b/test/equatable_mixin_test.dart
index b1da0ff..7defdbf 100644
--- a/test/equatable_mixin_test.dart
+++ b/test/equatable_mixin_test.dart
@@ -1,5 +1,6 @@
import 'dart:convert';
+import 'package:equatable/src/equatable_utils.dart';
import 'package:test/test.dart';
import 'package:equatable/equatable.dart';
@@ -102,7 +103,10 @@
test('should return correct hashCode', () {
final instance = EmptyEquatable();
- expect(instance.hashCode, instance.runtimeType.hashCode);
+ expect(
+ instance.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
+ );
});
test('should return true when instances are different', () {
@@ -134,7 +138,7 @@
final instance = SimpleEquatable('simple');
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^ instance.data.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
@@ -179,7 +183,7 @@
final instance = SimpleEquatable(0);
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^ instance.data.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
@@ -218,7 +222,7 @@
final instance = SimpleEquatable(true);
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^ instance.data.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
@@ -265,7 +269,7 @@
));
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^ instance.data.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
@@ -309,6 +313,7 @@
final instance = MultipartEquatable("s1", "s2");
expect(instance.toString(), 'MultipartEquatable<String>');
});
+
test('should return true when instance is the same', () {
final instance = MultipartEquatable("s1", "s2");
expect(instance == instance, true);
@@ -318,12 +323,17 @@
final instance = MultipartEquatable("s1", "s2");
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^
- instance.d1.hashCode ^
- instance.d2.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
+ test('should return different hashCodes when property order has changed',
+ () {
+ final instance1 = MultipartEquatable("s1", "s2");
+ final instance2 = MultipartEquatable("s2", "s1");
+ expect(instance1.hashCode == instance2.hashCode, isFalse);
+ });
+
test('should return true when instances are different', () {
final instanceA = MultipartEquatable("s1", "s2");
final instanceB = MultipartEquatable("s1", "s2");
@@ -377,11 +387,7 @@
);
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^
- instance.name.hashCode ^
- instance.age.hashCode ^
- instance.hairColor.hashCode ^
- instance.children[0].hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
@@ -466,9 +472,7 @@
) as Map<String, dynamic>);
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^
- instance.username.hashCode ^
- instance.password.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
diff --git a/test/equatable_test.dart b/test/equatable_test.dart
index d7f1ce6..a35b302 100644
--- a/test/equatable_test.dart
+++ b/test/equatable_test.dart
@@ -1,5 +1,6 @@
import 'dart:convert';
+import 'package:equatable/src/equatable_utils.dart';
import 'package:test/test.dart';
import 'package:equatable/equatable.dart';
@@ -105,7 +106,10 @@
test('should return correct hashCode', () {
final instance = EmptyEquatable();
- expect(instance.hashCode, instance.runtimeType.hashCode);
+ expect(
+ instance.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
+ );
});
test('should return true when instances are different', () {
@@ -137,7 +141,7 @@
final instance = SimpleEquatable('simple');
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^ instance.data.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
@@ -187,7 +191,7 @@
final instance = SimpleEquatable(0);
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^ instance.data.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
@@ -226,7 +230,7 @@
final instance = SimpleEquatable(true);
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^ instance.data.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
@@ -273,7 +277,7 @@
));
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^ instance.data.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
@@ -317,6 +321,7 @@
final instance = MultipartEquatable("s1", "s2");
expect(instance.toString(), 'MultipartEquatable<String>');
});
+
test('should return true when instance is the same', () {
final instance = MultipartEquatable("s1", "s2");
expect(instance == instance, true);
@@ -326,12 +331,17 @@
final instance = MultipartEquatable("s1", "s2");
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^
- instance.d1.hashCode ^
- instance.d2.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
+ test('should return different hashCodes when property order has changed',
+ () {
+ final instance1 = MultipartEquatable("s1", "s2");
+ final instance2 = MultipartEquatable("s2", "s1");
+ expect(instance1.hashCode == instance2.hashCode, isFalse);
+ });
+
test('should return true when instances are different', () {
final instanceA = MultipartEquatable("s1", "s2");
final instanceB = MultipartEquatable("s1", "s2");
@@ -385,11 +395,7 @@
);
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^
- instance.name.hashCode ^
- instance.age.hashCode ^
- instance.hairColor.hashCode ^
- instance.children[0].hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});
@@ -474,9 +480,7 @@
) as Map<String, dynamic>);
expect(
instance.hashCode,
- instance.runtimeType.hashCode ^
- instance.username.hashCode ^
- instance.password.hashCode,
+ instance.runtimeType.hashCode ^ mapPropsToHashCode(instance.props),
);
});