How to pluralize words with a GSP tag

Let’s say you need to output the number of results for a query. For example:

	2 results found for your query.

Your GSP looks probably something like that:

// ...
	${count} results found for your query.
// ...

All is good until there is only one result:

	1 results found for your query.

Oops, that looks bad 🙂

Here is one way to fix it:

// ...
	${count} <%= count > 1 ? "results" : "result" %> found for your query.
// ...

Wouldn’t it be nice it we could embed this behavior in some view helper function that we could reuse?

In Grails, you can do that with a custom GSP tag.

So let’s code our custom pluralize tag using TDD.

Here is a first test:

// file: test/unit/pluralize/ViewHelperTagLibSpec.groovy
package pluralize

import grails.test.mixin.TestFor
import spock.lang.Specification
import spock.lang.Unroll

@TestFor(ViewHelperTagLib)
class ViewHelperTagLibSpec extends Specification {

    @Unroll
	def "Should pluralize by adding 's' to the singular form"() {
		given:
		    def template = '<my:pluralize count="${count}" singular="${singular}" />'
		expect:
		    renderedContent == applyTemplate(template, [count:count, singular:singular])
		where:
			renderedContent || count | singular
			'1 result'      || 1     | 'result'
			'2 results'     || 2     | 'result'
	}
	
}

If you run this test with the command grails test-app unit:spock ViewHelperTagLib, you should get an error since there is no ViewHelperTagLib. So let’s create it:

// file: grails-app/taglib/pluralize/ViewHelperTagLib.groovy
package pluralize

class ViewHelperTagLib {
    
    static namespace = "my"

    def pluralize = { attrs, body ->
        out << body()
    }
    
}

If you run the test again, you should see no more compilation errors but the tests are still failing.

Now, let’s code the pluralize behaviour:

// file: grails-app/taglib/pluralize/ViewHelperTagLib.groovy
package pluralize

class ViewHelperTagLib {
    
    static namespace = "my"

    def pluralize = { attrs, body ->
        out << attrs['count'] + " " + ( attrs['count'] > 1 ? attrs['singular'] + "s" : attrs['singular'] )
    }
    
}

This time, the tests pass and you can use the tag like this:

// ...
	<my:pluralize count="${count}" singular="result" /> found for your query.
// ...

Unfortunately not all noun gets pluralized with ‘s’. For example:

	1 person, 2 people
	1 tooth, 2 teeth
	1 mouse, 2 mice

Let’s add a second test to describe this behavior:

// file: test/unit/pluralize/ViewHelperTagLibSpec.groovy

    

    @Unroll
	def "Should appropriately pluralize exceptions"() {
		given:
		    def template = '<my:pluralize count="${count}" singular="${singular}" plural="${plural}" />'
		expect:
		    renderedContent == applyTemplate(template, [count:count, singular:singular, plural:plural])
		where:
			renderedContent || count | singular | plural
			'1 person'      || 1     | 'person' | 'people'
			'2 people'      || 2     | 'person' | 'people'
	}

And here is our updated custom tag:

// file: grails-app/taglib/pluralize/ViewHelperTagLib.groovy
package pluralize

class ViewHelperTagLib {
    
    static namespace = "my"

    def pluralize = { attrs, body ->
        def plural = attrs['plural'] ?: attrs['singular'] + "s"
        out << attrs['count'] + " " + ( attrs['count'] > 1 ? plural : attrs['singular'] )
    }
    
}

Back to our GSP view:

// ...
	<my:pluralize count="${count}" singular="person" plural="people" /> found for your query.
// ...

And the HTML output:

	1 person found for your query.
	2 people found for your query.

That's it for today.

Advertisements

Refactoring with TDD

As everybody knows, good software engineering recommends to respect separation of concerns. For example, controller’s main responsibilities are to process incoming HTTP requests, to invoke some business logic and to dispatch the response.

You can see that the following controller doesn’t respect this recommendation since the searching logic is implemented directly into the controller method.

package todo
 
class TaskController {
 
    static scaffold = true
 
    def list() {
        params.max = Math.min(params.max ? params.int('max') : 5, 100)
 
        def taskList = Task.createCriteria().list (params) {
            if ( params.query ) {
                ilike("description", "%${params.query}%")
            }
        }
 
        [taskInstanceList: taskList, taskInstanceTotal: taskList.totalCount]
    }
 
}

Obviously, it’s a very simple example and most people would say that it’s not a big deal but let me show you how to refactor this code to respect separation of concerns.

The most common places to implement business logic in Grails are services and domain classes. In our case, a service would be unnecessary at this point so let’s move business logic to the Task domain class.

This time, I’ll use Test Driven Development so I’m writing the test first:

// file: test/unit/todo/TaskSpec.groovy
package todo

import grails.test.mixin.TestFor

import spock.lang.Specification
import spock.lang.Unroll

@TestFor(Task)
class TaskSpec extends Specification {

	@Unroll
	def "search() should return #count results for query '#query'"() {
	    given:
            [
                new Task(description: "Buy bread"),
                new Task(description: "Buy milk"),
	        ]*.save()
	    expect:
            count == Task.search(query).count()
	    where:
    	    count   | query
    	    2       | null
    	    2       | "Buy"
    	    1       | "Bread"
    	    0       | "iPod"
	}

}

This is a Spock unit test:

  • it first creates and saves 2 tasks.
  • it then passes a query string to the Task domain class
  • it then asserts the number of matching results for various queries

If you run this test with the command grails test-app unit:spock TaskSpec, you should see the following:

| Running 1 spock test... 1 of 1
| Failure:  search() should return '2' results for query 'null'(todo.TaskSpec)
|  groovy.lang.MissingMethodException: No signature of method: todo.Task.search() is applicable for argument types: (null) values: [null]
[some output omitted]
| Completed 1 spock test, 4 failed in 362ms
| Tests FAILED  - view reports in target/test-reports

All 4 test cases fail as Grails complains that it can’t find any search method in the Task class. Let’s create one with a named query:

package todo

class Task {

    String description

    static namedQueries = {
        search { query ->
        }
    }

}

Here is what Grails outputs if we run the same command again:

| Running 1 spock test... 3 of 1
| Failure:  search() should return '1' results for query 'Bread'(todo.TaskSpec)
|  Condition not satisfied:

count == Task.search(query).count()
|     |       |      |      |
1     false   |      Bread  2
              org.grails.datastore.gorm.query.NamedCriteriaProxy@5d5ef3e7

	at todo.TaskSpec.search() should return '#count' results for query '#query'(TaskSpec.groovy:19)
| Running 1 spock test... 4 of 1
| Failure:  search() should return '0' results for query 'iPod'(todo.TaskSpec)
|  Condition not satisfied:

count == Task.search(query).count()
|     |       |      |      |
0     false   |      iPod   2
              org.grails.datastore.gorm.query.NamedCriteriaProxy@74ef95c6

	at todo.TaskSpec.search() should return '#count' results for query '#query'(TaskSpec.groovy:19)
| Completed 1 spock test, 2 failed in 762ms
| Tests FAILED  - view reports in target/test-reports

OK, no more groovy.lang.MissingMethodException but some test cases still fail. So let’s implement the search named query. All we have to do is to move the business logic from the controller method to the domain class:

package todo

class Task {

    String description

    static namedQueries = {
        search { query ->
            if ( query ) {
                ilike("description", "%${query}%")
            }
        }
    }

}

And now, all test cases pass:

| Completed 1 spock test, 0 failed in 329ms
| Tests PASSED - view reports in target/test-reports

Since we moved the search logic to the domain class, we can now remove it from the controller:

package todo
 
class TaskController {
 
    static scaffold = true
 
    def list() {
        params.max = Math.min(params.max ? params.int('max') : 5, 100)
 
        def taskList = Task.search(params.query).list(params)
 
        [taskInstanceList: taskList, taskInstanceTotal: taskList.totalCount]
    }
 
}

We successfully refactored our controller method to move business logic where it belongs.