diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b04047a..981203b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - smalltalk: [ Pharo64-8.0, Pharo64-7.0, Pharo32-7.0, Pharo32-6.1 ] + smalltalk: [ Pharo64-8.0, Pharo64-7.0, Pharo32-7.0 ] name: ${{ matrix.smalltalk }} steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 320444a..9d8199e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ [![GitHub release](https://img.shields.io/github/release/ba-st/Buoy.svg)](https://github.com/ba-st/Buoy/releases/latest) [![Build Status](https://github.com/ba-st/Buoy/workflows/Build/badge.svg?branch=release-candidate)](https://github.com/ba-st/Buoy/actions?query=workflow%3ABuild) [![Coverage Status](https://codecov.io/github/ba-st/Buoy/coverage.svg?branch=release-candidate)](https://codecov.io/gh/ba-st/Buoy/branch/release-candidate) -[![Pharo 6.1](https://img.shields.io/badge/Pharo-6.1-informational)](https://pharo.org) [![Pharo 7.0](https://img.shields.io/badge/Pharo-7.0-informational)](https://pharo.org) [![Pharo 8.0](https://img.shields.io/badge/Pharo-8.0-informational)](https://pharo.org) @@ -57,12 +56,6 @@ Provides [extensions to the SUnit framework](docs/SUnit.md). - The code is licensed under [MIT](LICENSE). - The documentation is licensed under [CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/). -## Quick Start - -- Download the latest [Pharo 32](https://get.pharo.org/) or [64 bits VM](https://get.pharo.org/64/). -- Download a ready to use image from the [release page](https://github.com/ba-st/Buoy/releases/latest) -- Explore the [documentation](docs/) - ## Installation To load the project in a Pharo image, or declare it as a dependency of your own project follow this [instructions](docs/Installation.md). diff --git a/docs/Comparison.md b/docs/Comparison.md index e650f49..87aace0 100644 --- a/docs/Comparison.md +++ b/docs/Comparison.md @@ -23,36 +23,45 @@ hash ^ self equalityHashCombinator combineHashesOfAll: { alpha. beta. gamma } ``` -## `StandardComparator` -It eases the implementation of comparison for equality of objects. Instances can be built in different ways: +## Equality Checkers +Equality checkers help to implement the equality method for objects. Any object can send to itself the message `equalityChecker`, configure it and then use it to check against the target object of the comparison. -- `StandardComparator differentiatingType`: compares for identity (`==`) or if an object `isKindOf` anotherObject. -- `StandardComparator differentiatingSending: aSelectorsCollection`: compares for identity (`==`), or if an object `isKindOf` anotherObject and all selectors are equal for both objects. -- `StandardComparator differentiatingThrough: aBlock`: compares for identity (`==`), or if an object `isKindOf` anotherObject and the block returns true when applied to both objects. +Equality checkers always performs a `==` comparison first and proceeds with the rest of the rules only if the objects are not identical. -Some examples +By default `equalityChecker` is an instance of `PropertyBasedEqualityChecker` and it alredy knowns the receiving instance. It can be configured with: +- `compare: selector` will add a rule to the checker that sends the provided message on the receiver and target object and compare the results by `=` +- `compare: block` will add a rule to the checker that evaluates the provided block on the receiver and target object and compare the results by `=` +- `compareAll:` it's like `compare:` but receives a collection of selectors or blocks. +- `compareWith: block` receives a two argument block and will add a rule to the checker that evaluates that block with the receiver and target objects. It expects that `block` evaluates to a `Boolean`. + +The property based equality checker has always an implicit rule checking first if the target object is of the same type of the receiver. You check all the configured rules by sending `checkAgainst:` to the checker with the target object. + +Buoy also offers a `SequenceableCollectionEqualityChecker` that can be used to compare two sequenceable collections by sending to it the message `check:against:` with both collections. It will check that both collections are sequenceable and contains the same elements in the same order. + +### Examples + +This examples assumes that equalityChecker is not reimplemented. ```smalltalk -|comparator| -comparator := StandardComparator differentiatingType. -comparator check: (Set with: 11) against: (Set with: 22) >>> true. -comparator check: (Set with: 11) against: (OrderedCollection with: 11) >>> false. +"Just type checking" +|checker| +checker := (Set with: 11) equalityChecker. +(checker checkAgainst: (Set with: 22)) >>> true. +(checker checkAgainst: #(11)) >>> false. ``` ```smalltalk -| comparator | -comparator := StandardComparator differentiatingSending: #(abs). -comparator check: 1 against: -1 >>> true. -comparator check: 1 against: 2 >>> false. +| checker | +checker := 1 equalityChecker. +checker compare: #abs. +(checker checkAgainst: -1) >>> true. +(checker checkAgainst: 2) >>> false ``` ```smalltalk -| comparator | - -comparator := -StandardComparator differentiatingThrough: [:oneObject :anotherObject | -oneObject asArray = anotherObject asArray]. - -comparator check: (Set with: 34) against: (Set with: 34) >>> true. -comparator check: (Set with: 34) against: (Set with: 33) >>> false. +| checker | +checker := (Set with: 34) equalityChecker. +checker compareWith: [:a :b | a asArray = b asArray]. +(checker checkAgainst: (Set with: 34)) >>> true. +(checker checkAgainst: (Set with: 33)) >>> false. ``` diff --git a/source/Buoy-Comparison-Tests/BuoyComparisonObjectExtensionsTest.class.st b/source/Buoy-Comparison-Tests/BuoyComparisonObjectExtensionsTest.class.st index c93599c..30f9f72 100644 --- a/source/Buoy-Comparison-Tests/BuoyComparisonObjectExtensionsTest.class.st +++ b/source/Buoy-Comparison-Tests/BuoyComparisonObjectExtensionsTest.class.st @@ -4,8 +4,19 @@ Class { #category : #'Buoy-Comparison-Tests' } +{ #category : #tests } +BuoyComparisonObjectExtensionsTest >> testEqual [ + + self + assert: ( ObjectUsingComparisonAffordances with: 1 and: 2 ) + equals: ( ObjectUsingComparisonAffordances with: 1 and: 2 ); + deny: ( ObjectUsingComparisonAffordances with: 1 and: 2 ) + equals: ( ObjectUsingComparisonAffordances with: 2 and: 1 ); + deny: ( ObjectUsingComparisonAffordances with: 1 and: 2 ) equals: self +] + { #category : #tests } BuoyComparisonObjectExtensionsTest >> testHash [ - self assert: ( ObjectUsingEqualityHashCombinator with: 1 and: 2 ) hash equals: ( 1 bitXor: 2 ) + self assert: ( ObjectUsingComparisonAffordances with: 1 and: 2 ) hash equals: ( 1 bitXor: 2 ) ] diff --git a/source/Buoy-Comparison-Tests/ObjectUsingComparisonAffordances.class.st b/source/Buoy-Comparison-Tests/ObjectUsingComparisonAffordances.class.st new file mode 100644 index 0000000..cc67e56 --- /dev/null +++ b/source/Buoy-Comparison-Tests/ObjectUsingComparisonAffordances.class.st @@ -0,0 +1,49 @@ +" +I'm an example used for testing purposes +" +Class { + #name : #ObjectUsingComparisonAffordances, + #superclass : #Object, + #instVars : [ + 'first', + 'second' + ], + #category : #'Buoy-Comparison-Tests' +} + +{ #category : #'instance creation' } +ObjectUsingComparisonAffordances class >> with: anInteger and: anInteger2 [ + + ^ self new initializeWith: anInteger and: anInteger2 +] + +{ #category : #comparing } +ObjectUsingComparisonAffordances >> = anObject [ + + ^ self equalityChecker + compareAll: #(#first #second); + checkAgainst: anObject +] + +{ #category : #accessing } +ObjectUsingComparisonAffordances >> first [ + ^ first +] + +{ #category : #comparing } +ObjectUsingComparisonAffordances >> hash [ + + ^ self equalityHashCombinator combineHashOf: first with: second +] + +{ #category : #initialization } +ObjectUsingComparisonAffordances >> initializeWith: anInteger and: anInteger2 [ + + first := anInteger. + second := anInteger2 +] + +{ #category : #accessing } +ObjectUsingComparisonAffordances >> second [ + ^ second +] diff --git a/source/Buoy-Comparison-Tests/ObjectUsingEqualityHashCombinator.class.st b/source/Buoy-Comparison-Tests/ObjectUsingEqualityHashCombinator.class.st deleted file mode 100644 index 44fb078..0000000 --- a/source/Buoy-Comparison-Tests/ObjectUsingEqualityHashCombinator.class.st +++ /dev/null @@ -1,28 +0,0 @@ -Class { - #name : #ObjectUsingEqualityHashCombinator, - #superclass : #Object, - #instVars : [ - 'first', - 'second' - ], - #category : #'Buoy-Comparison-Tests' -} - -{ #category : #'instance creation' } -ObjectUsingEqualityHashCombinator class >> with: anInteger and: anInteger2 [ - - ^ self new initializeWith: anInteger and: anInteger2 -] - -{ #category : #comparing } -ObjectUsingEqualityHashCombinator >> hash [ - - ^ self equalityHashCombinator combineHashOf: first with: second -] - -{ #category : #initialization } -ObjectUsingEqualityHashCombinator >> initializeWith: anInteger and: anInteger2 [ - - first := anInteger. - second := anInteger2 -] diff --git a/source/Buoy-Comparison-Tests/PropertyBasedEqualityCheckerTest.class.st b/source/Buoy-Comparison-Tests/PropertyBasedEqualityCheckerTest.class.st new file mode 100644 index 0000000..be659b9 --- /dev/null +++ b/source/Buoy-Comparison-Tests/PropertyBasedEqualityCheckerTest.class.st @@ -0,0 +1,82 @@ +" +An #PropertyBasedEqualityCheckerTest is a test class for testing the behavior of #PropertyBasedEqualityChecker +" +Class { + #name : #PropertyBasedEqualityCheckerTest, + #superclass : #TestCase, + #category : #'Buoy-Comparison-Tests' +} + +{ #category : #tests } +PropertyBasedEqualityCheckerTest >> testCheckingIdenticalObjects [ + + | checker | + + checker := self equalityChecker. + checker compareWith: [ :a :b | self fail ]. + + self assert: ( checker checkAgainst: self ) +] + +{ #category : #tests } +PropertyBasedEqualityCheckerTest >> testPropertyBlockComparison [ + + | checker | + + checker := #(1 2 3 4) equalityChecker. + checker compare: [ :collection | collection last even ]. + + self + assert: ( checker checkAgainst: #(2) ); + assert: ( checker checkAgainst: #(1 2 3 4) ); + deny: ( checker checkAgainst: #(3) ); + deny: ( checker checkAgainst: #(1 2 3 3) ). + + checker := #(1 2 3 3) equalityChecker. + checker compare: [ :collection | collection last even ]. + + self + assert: ( checker checkAgainst: #(1) ); + assert: ( checker checkAgainst: #(1 2 3 3) ); + deny: ( checker checkAgainst: #(2) ); + deny: ( checker checkAgainst: #(1 2 3 4) ) +] + +{ #category : #tests } +PropertyBasedEqualityCheckerTest >> testPropertyComparison [ + + | checker | + + checker := #(1 2 3 4) equalityChecker. + checker compare: #first. + + self + assert: ( checker checkAgainst: #(1 1 1 1) ); + deny: ( checker checkAgainst: #(2 2 3 4) ) +] + +{ #category : #tests } +PropertyBasedEqualityCheckerTest >> testSeveralPropertiesComparison [ + + | checker | + + checker := #(1 2 3 4) equalityChecker. + checker compareAll: #(#first #second). + + self + assert: ( checker checkAgainst: #(1 2 1 2) ); + deny: ( checker checkAgainst: #(1 1 3 4) ); + deny: ( checker checkAgainst: #(2 2 3 4) ) +] + +{ #category : #tests } +PropertyBasedEqualityCheckerTest >> testTypeComparison [ + + | checker | + + checker := self equalityChecker. + + self + assert: ( checker checkAgainst: self class new ); + deny: ( checker checkAgainst: self class superclass new ) +] diff --git a/source/Buoy-Comparison-Tests/SequenceableCollectionEqualityCheckerTest.class.st b/source/Buoy-Comparison-Tests/SequenceableCollectionEqualityCheckerTest.class.st new file mode 100644 index 0000000..f51e1ab --- /dev/null +++ b/source/Buoy-Comparison-Tests/SequenceableCollectionEqualityCheckerTest.class.st @@ -0,0 +1,26 @@ +" +A SequenceableCollectionEqualityCheckerTest is a test class for testing the behavior of SequenceableCollectionEqualityChecker +" +Class { + #name : #SequenceableCollectionEqualityCheckerTest, + #superclass : #TestCase, + #category : #'Buoy-Comparison-Tests' +} + +{ #category : #tests } +SequenceableCollectionEqualityCheckerTest >> testCheckAgainst [ + + | checker base | + + base := #(1 2 3 4). + checker := SequenceableCollectionEqualityChecker new. + + self + assert: ( checker check: base against: #(1 2 3 4) ); + assert: ( checker check: base against: #(1 2 3 4) asOrderedCollection ); + deny: ( checker check: base against: #(1 2 3) ); + deny: ( checker check: base against: #(0 2 3 4) ); + deny: ( checker check: base against: #(1 0 3 4) ); + deny: ( checker check: base against: #(1 2 0 4) ); + deny: ( checker check: base against: #(1 2 3 0) ) +] diff --git a/source/Buoy-Comparison-Tests/StandardComparatorTest.class.st b/source/Buoy-Comparison-Tests/StandardComparatorTest.class.st deleted file mode 100644 index 5e9cb74..0000000 --- a/source/Buoy-Comparison-Tests/StandardComparatorTest.class.st +++ /dev/null @@ -1,79 +0,0 @@ -Class { - #name : #StandardComparatorTest, - #superclass : #TestCase, - #category : #'Buoy-Comparison-Tests' -} - -{ #category : #tests } -StandardComparatorTest >> testCheckingDifferentObjectsOfTheSameType [ - - | comparator | - - comparator := - StandardComparator differentiatingThrough: [:oneObject :anotherObject | - oneObject asArray = anotherObject asArray]. - - self deny: (comparator check: (Set with: 11) against: (Set with: 22)). - - comparator := StandardComparator differentiatingSending: #(asArray). - - self deny: (comparator check: (Set with: 11) against: (Set with: 22)) -] - -{ #category : #tests } -StandardComparatorTest >> testCheckingEquivalentObjects [ - - | comparator | - - comparator := - StandardComparator differentiatingThrough: [:oneObject :anotherObject | - oneObject asArray = anotherObject asArray]. - - self assert: (comparator check: (Set with: 34) against: (Set with: 34)). - - comparator := StandardComparator differentiatingSending: #(asArray). - - self assert: (comparator check: (Set with: 34) against: (Set with: 34)) -] - -{ #category : #tests } -StandardComparatorTest >> testCheckingObjectsOfDifferentTypes [ - - | comparator | - - comparator := - StandardComparator differentiatingThrough: [:oneObject :anotherObject | - oneObject asArray = anotherObject asArray]. - - self deny: (comparator check: (Set with: 34) against: (OrderedCollection with: 34)). - - comparator := StandardComparator differentiatingSending: #(asArray). - - self deny: (comparator check: (Set with: 34) against: (OrderedCollection with: 34)) -] - -{ #category : #tests } -StandardComparatorTest >> testCheckingOnlyType [ - - | comparator | - - comparator := StandardComparator differentiatingType. - - self assert: (comparator check: (Set with: 11) against: (Set with: 22)). - self deny: (comparator check: Set new against: OrderedCollection new). - - comparator := StandardComparator differentiatingSending: #(). - - self assert: (comparator check: (Set with: 11) against: (Set with: 22)). - self deny: (comparator check: Set new against: OrderedCollection new) -] - -{ #category : #tests } -StandardComparatorTest >> testCheckingSameObject [ - - | comparator | - - comparator := StandardComparator differentiatingThrough: [ :oneObject :anotherObject | self fail ]. - - self assert: ( comparator check: self against: self ) -] diff --git a/source/Buoy-Comparison/EqualityChecker.class.st b/source/Buoy-Comparison/EqualityChecker.class.st new file mode 100644 index 0000000..bfde995 --- /dev/null +++ b/source/Buoy-Comparison/EqualityChecker.class.st @@ -0,0 +1,21 @@ +" +I'm an abstract class defining the interface for equality checking. +My intent is to provide a way of compare two objects. +" +Class { + #name : #EqualityChecker, + #superclass : #Object, + #category : #'Buoy-Comparison' +} + +{ #category : #testing } +EqualityChecker >> check: aBaseObject against: aTargetObject [ + + ^ self subclassResponsibility +] + +{ #category : #testing } +EqualityChecker >> is: aBaseObject identicalTo: aTargetObject [ + + ^ aBaseObject == aTargetObject +] diff --git a/source/Buoy-Comparison/Object.extension.st b/source/Buoy-Comparison/Object.extension.st index 5d3515a..41d7a5a 100644 --- a/source/Buoy-Comparison/Object.extension.st +++ b/source/Buoy-Comparison/Object.extension.st @@ -1,5 +1,11 @@ Extension { #name : #Object } +{ #category : #'*Buoy-Comparison' } +Object >> equalityChecker [ + + ^ PropertyBasedEqualityChecker on: self +] + { #category : #'*Buoy-Comparison' } Object >> equalityHashCombinator [ diff --git a/source/Buoy-Comparison/PropertyBasedEqualityChecker.class.st b/source/Buoy-Comparison/PropertyBasedEqualityChecker.class.st new file mode 100644 index 0000000..7dc98a7 --- /dev/null +++ b/source/Buoy-Comparison/PropertyBasedEqualityChecker.class.st @@ -0,0 +1,67 @@ +" +I'm a checker that can be configured to check for certain properties to determine object equality. +I'm the default checker available to any object by sending equalityChecker to itself. +" +Class { + #name : #PropertyBasedEqualityChecker, + #superclass : #EqualityChecker, + #instVars : [ + 'base', + 'comparisonRules' + ], + #category : #'Buoy-Comparison' +} + +{ #category : #'instance creation' } +PropertyBasedEqualityChecker class >> on: aBaseObject [ + + ^ self new initializeOn: aBaseObject +] + +{ #category : #testing } +PropertyBasedEqualityChecker >> check: aBaseObject against: aTargetObject [ + + ^ ( self is: aBaseObject identicalTo: aTargetObject ) + or: [ ( aTargetObject isA: aBaseObject class ) + and: [ comparisonRules allSatisfy: [ :rule | rule value: aBaseObject value: aTargetObject ] ] + ] +] + +{ #category : #testing } +PropertyBasedEqualityChecker >> checkAgainst: aTargetObject [ + + ^ self check: base against: aTargetObject +] + +{ #category : #configuring } +PropertyBasedEqualityChecker >> compare: aPropertyOrMonadycBlock [ + + self + compareWith: + [ :first :second | ( aPropertyOrMonadycBlock value: first ) = ( aPropertyOrMonadycBlock value: second ) ] +] + +{ #category : #configuring } +PropertyBasedEqualityChecker >> compareAll: aPropertyCollection [ + + aPropertyCollection do: [ :property | self compare: property ] +] + +{ #category : #configuring } +PropertyBasedEqualityChecker >> compareWith: aDyadicBlock [ + + comparisonRules add: aDyadicBlock +] + +{ #category : #initalize } +PropertyBasedEqualityChecker >> initialize [ + + super initialize. + comparisonRules := OrderedCollection new +] + +{ #category : #initalize } +PropertyBasedEqualityChecker >> initializeOn: aBaseObject [ + + base := aBaseObject +] diff --git a/source/Buoy-Comparison/SequenceableCollectionEqualityChecker.class.st b/source/Buoy-Comparison/SequenceableCollectionEqualityChecker.class.st new file mode 100644 index 0000000..b27f087 --- /dev/null +++ b/source/Buoy-Comparison/SequenceableCollectionEqualityChecker.class.st @@ -0,0 +1,31 @@ +" +I'm a checker used to compare sequenceable collections. +" +Class { + #name : #SequenceableCollectionEqualityChecker, + #superclass : #EqualityChecker, + #category : #'Buoy-Comparison' +} + +{ #category : #testing } +SequenceableCollectionEqualityChecker >> check: base against: target [ + + ^ ( self is: base identicalTo: target ) + or: [ base isSequenceable + and: [ target isSequenceable and: [ self has: base theSameElementsThan: target ] ] + ] +] + +{ #category : #private } +SequenceableCollectionEqualityChecker >> has: base theSameElementsThan: target [ + + ^ base size = target size + and: [ base + with: target + do: [ :first :second | + first = second + ifFalse: [ ^ false ] + ]. + true + ] +] diff --git a/source/Buoy-Comparison/StandardComparator.class.st b/source/Buoy-Comparison/StandardComparator.class.st deleted file mode 100644 index edeb1de..0000000 --- a/source/Buoy-Comparison/StandardComparator.class.st +++ /dev/null @@ -1,47 +0,0 @@ -Class { - #name : #StandardComparator, - #superclass : #Object, - #instVars : [ - 'differentiationBlock' - ], - #category : #'Buoy-Comparison' -} - -{ #category : #'instance creation' } -StandardComparator class >> differentiatingSending: aSelectorsCollection [ - - ^aSelectorsCollection isEmpty - ifTrue: [self differentiatingType] - ifFalse: [ - self differentiatingThrough: [:oneObject :anotherObject | - aSelectorsCollection allSatisfy: [:selector | - (oneObject perform: selector) = (anotherObject perform: selector)]]] -] - -{ #category : #'instance creation' } -StandardComparator class >> differentiatingThrough: aBlock [ - - ^self new initializeDifferentiatingThrough: aBlock -] - -{ #category : #'instance creation' } -StandardComparator class >> differentiatingType [ - - ^self differentiatingThrough: [:anObject :anotherObject | true] - - -] - -{ #category : #Testing } -StandardComparator >> check: anObject against: anotherObject [ - - ^anObject == anotherObject or: [ - (anotherObject isA: anObject class) - and: [differentiationBlock value: anObject value: anotherObject]] -] - -{ #category : #initialization } -StandardComparator >> initializeDifferentiatingThrough: aBlock [ - - differentiationBlock := aBlock -] diff --git a/source/Buoy-Deprecated/StandardComparator.class.st b/source/Buoy-Deprecated/StandardComparator.class.st new file mode 100644 index 0000000..6827b02 --- /dev/null +++ b/source/Buoy-Deprecated/StandardComparator.class.st @@ -0,0 +1,50 @@ +Class { + #name : #StandardComparator, + #superclass : #Object, + #instVars : [ + 'differentiationBlock' + ], + #category : #'Buoy-Deprecated' +} + +{ #category : #'instance creation' } +StandardComparator class >> differentiatingSending: aSelectorsCollection [ + + self + deprecated: 'Use equalityChecker' + transformWith: '`@object differentiatingSending: `@selectors' -> 'self equalityChecker compare: `@selectors'. + + ^ self +] + +{ #category : #'instance creation' } +StandardComparator class >> differentiatingThrough: aBlock [ + + self + deprecated: 'Use equalityChecker' + transformWith: '`@object differentiatingThrough: `@block' -> 'self equalityChecker compareWith: `@block'. + ^ self +] + +{ #category : #'instance creation' } +StandardComparator class >> differentiatingType [ + + self + deprecated: 'Use equalityChecker' + transformWith: '`@object differentiatingType' -> 'self equalityChecker'. + ^ self +] + +{ #category : #testing } +StandardComparator class >> isDeprecated [ + + ^ true +] + +{ #category : #Testing } +StandardComparator >> check: anObject against: anotherObject [ + + self + deprecated: 'Use equalityChecker affordances' + transformWith: '`@checker check: `@anObject against: `@anotherObject' -> '`@checker checkAgainst: `@anotherObject' +]