Is There a Groovier Way To Add Dashes to a String?

You could first use the [] string operator to get the substrings instead of substring and drop the intermediate variables. For example in the case for length == 10:

"${isbn[0]}-${isbn[1..6]}-${isbn[7..8]}-${isbn[9]}"

Now, there is a bit of repetition there. You can get instead first get all the isbn segments and then .join them with '-':

[isbn[0], isbn[1..6], isbn[7..8], isbn[9]].join('-')

And, even further, instead of referencing isbn every time, you can make a list of the ranges you want to get and then get them all the same time using collect:

[0, 1..6, 7..8, 9].collect { isbn[it] }.join('-')

If you're going for code golfing, you can also do:

('-'+isbn)[1, 0, 2..7, 0, 8..9, 0, 10]

I'll leave it to you to figure out how that works, but i guess it's probably not a good idea to leave that on production code, unless you want to surprise future maintainers hehe.


Also, notice that the format when length == 13 is the same as for length == 10 but with a different prefix, you can then reuse the same function in that case. The whole function (with a couple of tests) would be:

/**
 * 10 digit - #-######-##-#
 * 13 digit - ###-#-######-##-#
 **/
def formatIsbn(isbn) {
    switch (isbn?.length()) {
        case 10: return [0, 1..6, 7..8, 9].collect { isbn[it] }.join('-')
        case 13: return isbn.take(3) + '-' + formatIsbn(isbn.drop(3))
        default: return isbn
    }
}

assert formatIsbn('abcdefghij') == 'a-bcdefg-hi-j'
assert formatIsbn('abcdefghijklm') == 'abc-d-efghij-kl-m'

Now, i think there are some bad smells in that code. Can isbn be null? At least to me, this doesn't look like a function that needs to bother about the nullity of its argument, or at least that's not clear by reading its name (it should be called something like formatIsbnOrNull instead if both ISBN strings and null values are accepted). If null values are not valid, then let it blow up with a NullPointerException when accessing isbn.length() so the caller know they have passed a wrong argument, instead of silently returning the same null.

The same goes for the return ISBN at the end. Is it expected for that function to receive a string that's neither 10 nor 13 characters long? If not, better throw new IllegalArgumentException() and let the caller know they have called it wrongly.


Finally, i'm not sure if this is the most "readable" solution. Another possible solution is having a string for the format, like '###-#-######-##-#' and then replace the #s by the isbn characters. I think it might be more self-documenting:

def formatIsbn(isbn) {
    def format = [
        10: '#-######-##-#',
        13: '###-#-######-##-#'
    ][isbn.length()]
    def n = 0
    format.replaceAll(/#/) { isbn[n++] }
}

Consider adding the method to the String class, as shown here. Note that this answer is a spin on a clever suggestion in epidemian's answer (re: collect).

Note:

This code augments String with asIsbn().

The range [0..2] does not need the call to asIsbn(), but the symmetry of using collect twice is irresistable.

Groovy returns the last expression in if/else, so 'return' is not necessary

/**
 * 10 digit - #-######-##-#
 * 13 digit - ###-#-######-##-#
 **/
String.metaClass.asIsbn = { ->
    if (delegate.length() == 10) {
        [0, 1..6, 7..8, 9].collect { delegate[it] }.join('-')
    } else if (delegate.length() == 13) {
        [0..2, 3..12].collect { delegate[it].asIsbn() }.join('-')
    } else {
        delegate
    }
}

assert "abcdefghij".asIsbn() == 'a-bcdefg-hi-j'
assert "abcdefghijklm".asIsbn() == 'abc-d-efghij-kl-m'
assert "def".asIsbn() == "def"
String s = null 
assert s?.asIsbn() == null

Dunno if I like this any better. I'd make the position map a static final, too.

private isbnify(String isbn) {
  def dashesAt = [ 10: [[0,1], [1,7], [7,9],  [9,10]],
                   13: [[0,3], [3,4], [4,10], [10,12], [12,13]]]
  def dashes = dashesAt[isbn?.length()]
  (dashes == null) ? isbn 
                   : dashes.collect { isbn.substring(*it) }.join('-')
}

Ranges make for a bit less clutter, IMO:

private isbnify3(String isbn) {
  def dashesAt = [ 10: [0, 1..6, 7..8, 9],
                   13: [0..2, 3, 4..9, 10..11, 12]]
  def dashes = dashesAt[isbn?.length()]
  dashes == null ? isbn : dashes.collect { isbn[it] }.join("-")
}

With an inject-with-two-accumulators it should be easy to do a list-of-dash-positions version, too.


I would try using Regex... I think it's pretty much readable if you know how to use regex, and it's javascript inspired syntax in groovy is pretty cool also.

One more thing: it's pretty clear, looking at the capture groups, what your string looks like for the desired formatting.

private formatISBN(String isbn) {
    if (isbn?.length() == 10) {
        m = isbn =~ /(\d{1})(\d{6})(\d{2})(\d{1})/
        return "${m[0][1]}-${m[0][2]}-${m[0][3]}-${m[0][4]}"
    } else if (isbn?.length() == 13) {
        m = isbn =~ /(\d{3})(\d{1})(\d{6})(\d{2})(\d{1})/
        return "${m[0][1]}-${m[0][2]}-${m[0][3]}-${m[0][4]}-${m[0][5]}"        
    } else {
        return isbn
    }
}

Btw, @epidemian suggestion using backreferences is great! I think the code would look like:

private formatISBN(String isbn) {
    if (isbn?.length() == 10) {
        return isbn.replaceAll(/(\d{1})(\d{6})(\d{2})(\d{1})/, '$1-$2-$3-$4')
    } else if (isbn?.length() == 13) {
        return isbn.replaceAll(/(\d{3})(\d{1})(\d{6})(\d{2})(\d{1})/, '$1-$2-$3-$4-$5')
    } else {
        return isbn
    }
}

Tags:

String

Groovy