Wednesday, 18 June 2008

String, GString type and equality

This might be very schoolboy blog for some of you - I still hope that it might be useful to avoid some bugs.

Some weeks back my pair and I were fixing a bug that finally boiled down to the fact that the contains() method on a List returned false if the list contained the GString counterpart of the same literal.

Having played a bit I learned a couple of things about GStrings. Please find some bullet points and the code snippet bellow.

1. def var1 = "this is a String instead of a GString even if it is surrounded by double quotes - can has no dollar sign"
2. def var2 = "this is a GString because it contains a dollar sign - $var1"
3. String var3 = "this is a string because it was defined as a String"
4. String var4 = "$var3 - this is still a string because it was defined as a String"

1. a String and a GString can be ==
2. a String and a GString are NEVER equals()
3. as a consequence a list containing a literal will not return true if the type (GString/String) does not match

class WhereIsYourDummyTests extends GroovyTestCase {
def e = "y"
def dummy = "dumm$e"

void assertTypeIsStringForAFixGString() {
assert e instanceof java.lang.String
assert dummy instanceof groovy.lang.GString

void testEqualsNotEquals() {
assert "dummy" == dummy
assert dummy == "dummy"


void testContainsDoesntContain() {
assert ['dummy'].contains("dummy")
assert ["dummy"].contains('dummy')
assert [dummy].contains(dummy)

assert ![dummy].contains("dummy")
assert ![dummy].contains('dummy')
assert !["dummy"].contains(dummy)
assert !['dummy'].contains(dummy)

assert [dummy.toString()].contains("dummy")
assert [dummy.toString()].contains('dummy')

void testContainsIfVariablesAreStringInsteadOfDef() {
String e = "y" // String instead of def
String dummy = "dumm$e" // String instead of def

assert ['dummy'].contains("dummy")
assert ["dummy"].contains('dummy')
assert [dummy].contains(dummy)

assert [dummy].contains("dummy")
assert [dummy].contains('dummy')
assert ["dummy"].contains(dummy)
assert ['dummy'].contains(dummy)

assert [dummy.toString()].contains("dummy")
assert [dummy.toString()].contains('dummy')


Dread Pirate Rob said...

GString's equals implementation actually violates the contract of equals which states that x.equals(y) must return the same value as y.equals(x).

The Groovy == operator 'cheats' its way round this by actually using "x.compareTo(y) == 0" if the objects on either side of the operator implement java.util.Comparable which both String and GString do.

SteveC said...

You also see this problem when comparing maps, e.g.

String first = 'Fred'
String last = 'Bloggs'
def data = [fullName: "${first} ${last}]
assertEquals([fullName: 'Fred Bloggs', data])

The workarounds are

1. Test each element of your map explicitly
2. Use "${first} ${last}".toString() in your code
3. Build strings as you would in java using +, concat, sprintf etc

All Yuck