views:

394

answers:

12

I've got a few Comparators -- one for Dates, one for decimals, one for percentages, etc.

At first my decimal comparator looked like this:

class NumericComparator implements Comparator<String> {

  @Override
  public int compare(String s1, String s2) {
    final Double i1 = Double.parseDouble(s1);
    final Double i2 = Double.parseDouble(s2);
    return i1.compareTo(i2);
  }

}

Life was simple. Of course, this doesn't handle the case where the strings aren't parseable. So I improved compare():

class NumericComparator implements Comparator<String> {

  @Override
  public int compare(String s1, String s2) {
    final Double i1;
    final Double i2;

    try {
      i1 = Double.parseDouble(s1);
    } catch (NumberFormatException e) {
      try {
        i2 = Double.parseDouble(s2);
      } catch (NumberFormatException e2) {
        return 0;
      }
      return -1;
    }
    try {
      i2 = Double.parseDouble(s2);
    } catch (NumberFormatException e) {
      return 1;
    }

    return i1.compareTo(i2);
  }
}

Life was better. Tests felt more solid. However, my code reviewer pointed out, "What about nulls?"

Great, so now I have to repeat the above with NullPointerException or prepend the method body with:

if (s1 == null) {
  if (s2 == null) {
    return 0;
  } else {
    return -1;
  }
} else if (s2 == null) {
  return 1;
}

This method is huge. The worst part is, I need to repeat this pattern with three other classes which compare different types of strings and could raise three other exceptions while parsing.

I'm not a Java expert. Is there a cleaner, neater solution than -- gasp -- copying and pasting? Should I trade correctness for lack of complexity so as long as it is documented?


Update: Some have suggested that it's not the Comparator's job to handle null values. Since the sort results are displayed to users I indeed want nulls to be sorted consistently.

+1  A: 

You could create a utility method that handles parsing and returns a certain value in the case of nulls or parse exceptions.

Matt Moriarity
What would those certain values be?
a paid nerd
Depends how you want the comparison to work out. You could return something like Double.MIN_VALUE or something if you want the comparison to still work, or if you just want to only check one case, return null and check for it and that will signal any error.
Matt Moriarity
+2  A: 

There are a lot of subjective answers to this question. Here's my own $.02.

First, the trouble you're describing is the canonical symptom of a language that lacks first-class functions, which would enable you to succinctly describe these patterns.

Second, in my opinion, it should be an error to compare two Strings as Doubles if one of them cannot be considered a representation of a double. (The same is true for nulls, etc.) Therefore, you should permit the exceptions to propagate! This will be a contentious opinion, I expect.

Jonathan Feinberg
I'm confused as to why you would think that "first-class functions" would help.
Tom Hawtin - tackline
i agree re exceptions, possibly nulls too depending on your scenario/usage
pstanton
If first class functions could help here, could you demonstrate that using single method interfaces and anonymous inner types? Cause I'd like to know how they would be used to fix that :-)
Grundlefleck
A: 

If you are able to change the signature I would suggest you write the method so that it can accept any supported Object.

  public int compare(Object o1, Object o2) throws ClassNotFoundException {
      String[] supportedClasses = {"String", "Double", "Integer"};
      String j = "java.lang.";
      for(String s : supportedClasses){
          if(Class.forName(j+s).isInstance(o1) && Class.forName(j+s).isInstance(o1)){
              // compare apples to apples
              return ((Comparable)o1).compareTo((Comparable)o2);
          }
      }
      throw new ClassNotFoundException("Not a supported Class");
  }

You might even define it recursively where you cast your Strings to Doubles and then return the result of calling itself with those objects.

Off Rhoden
I'm afraid you're out of context, his problem is comparing 2 strings but expecting they're representing Doubles.
zim2001
A: 

IMHO you should first create a method that returns a Double from a String, embedding the null and parsing failure cases (but you must define what to do in such cases : throw an exception ? return a default value ??).

Then your comparator just have to compare obtained Double instances.

In other words, refactoring...

But I still wonder why you need to compare strings though expecting they represent doubles. I mean, what prevents you from manipulating doubles in the code that would actually use this comparator ?

zim2001
+4  A: 

You are implementing a Comparator<String>. String's methods, including compareTo throw a NullPointerException if a null is handed in to them, so you should too. Similarly, Comparator throws a ClassCastException if the arguments' types prevent them from being compared. I would recommend you implement these inherited behaviors.

class NumericComparator implements Comparator<String> {

  public int compare(String s1, String s2) {
    final Double i1;
    final Double i2;
    if(s1 == null)
    {
      throw new NullPointerException("s1 is null"); // String behavior
    }
    try {
      i1 = Double.parseDouble(s1)
    } catch (NumberFormatException e) {
      throw new ClassCastException("s1 incorrect format"); // Comparator  behavior
    }

    if(s2 == null)
    {
      throw new NullPointerException("s2 is null"); // String behavior
    }
    try {
      i2 = Double.parseDouble(s1)
    } catch (NumberFormatException e) {
      throw new ClassCastException("s2 incorrect format"); // Comparator  behavior
    }
    return i1.compareTo(i2);
  }
}

You can almost regain the original elegance by extracting a method to do the type checking and conversion.

class NumericComparator implements Comparator<String> {

  public int compare(String s1, String s2) {
    final Double i1;
    final Double i2;

    i1 = parseStringAsDouble(s1, "s1");
    i2 = parseStringAsDouble(s2, "s2");
    return i1.compareTo(i2);
  }

  private double parseStringAsDouble(String s, String name) {

    Double i;
    if(s == null) {
      throw new NullPointerException(name + " is null"); // String behavior
    }
    try {
      i = Double.parseDouble(s1)
    } catch (NumberFormatException e) {
      throw new ClassCastException(name + " incorrect format"); // Comparator  behavior
    }
    return i;
  }
}

If you are not particular about the Exception messages, you can lose the "name" parameter. I'm sure you can lose an extra line here or word there by applying little tricks.

You say you need to repeat this pattern with three other classes which compare different types of strings and could raise three other exceptions. It's difficult to offer specifics there without seeing the situation, but you may be able to use "Pull Up Method" on a version of my parseStringAsDouble into a common ancestor of NumericComparator that itself implements java's Comparator.

Ewan Todd
I liked the suggestion of throwing a `ClassCastException`. However, doing so bubbles the error up through the `Collections.sort()` I'm performing and the entire sort fails., and this isn't desired behavior for displaying "sorted" results to a user. (I should have mentioned the latter in the question.)
a paid nerd
You can be permissive at any of a number of points. It looks like this particular part of the code is in need of complexity control, so perhaps the permissiveness can go somewhere else. Perhaps you could implement a `santize(Collection c)` to remove failing items from the collection. If you expect most inputs to be ill-formed (as defined by the comparator implementation) then you could run sanitize on every collection before the sort. Otherwise, if you expect ill-formed collections to be rare you can `try` the sort and run `sanitize` and `sort` if you get an exception.
Ewan Todd
+1  A: 

Take a step back. Where does those Strings come from? For what is this Comparator to be used? Do you have a Collection of Strings which you would like to sort or so?

BalusC
A: 

So I improved compare()...

sure you did.

first, the Comparator interface doesn't specify what happens with nulls. if your null checking if statement works for your use case, that's great, but the general solution is throwing an npe.

as to cleaner... why final? why all the catch/throws? why use compareTo for a primitive wrapper?

class NumericComparator implements Comparator<String> {
 public int compare(String s1, String s2) throws NullPointerException, NumberFormatException {

  double test = Double.parseDouble(s1) - Double.parseDouble(s2);

  int retVal = 0;
  if (test < 0) retVal = -1;
  else if (test > 0) retVal = 1;

  return retVal;  
 }
}

seems you might find it clearer renaming test to t1 and retVal to q.

as to repeating the pattern... eh. you might be able to use generics with reflection to invoke appropriate parseX methods. seems like that'd not be worth it though.

colin
Why not use the compareTo of Double? Your test value might overflow, which is not necessary with comparisons instead of a subtraction.
rsp
what's the case where that occurs?isn't worst case for that test Double.MAX_VALUE - Double.MIN_VALUE? that resolves to Double.POSITIVE_INFINITY, which is > 0, so you get the right answer. granted, it doesn't handle Double.NaN. but i don't know how you'd generate Double.NaN given that we're throwing NumberFormatExceptions.
colin
+1  A: 

tl;dr: Take guidance from the JDK. The Double comparator is not defined for either non-numbers or nulls. Make people give you useful data (Doubles, Dates, Dinosaurs, whatever) and write your comparators for that.

As near as I can tell, this is a case of user input validation. For example, if you are taking input from a dialog box, the correct place to ensure that you have a parseable String that is a Double, Date or whatever is in the input handler. Make sure it's good before the user can tab away, hit "Okay" or equivalent.

Here's why I think this:

First question: if the Strings aren't parseable as numbers, I think you're trying to solve the problem in the wrong place. Say, for instance, I try to compare "1.0" to "Two". The second is clearly not parseable as a Double but is it less than the first? Or is it greater. I would argue that the users should have to turn their Strings into Doubles before they ask your which is greater (which you can easily answer with Double.compareTo, for instance).

Second question: if the Strings are "1.0" and null, which is greater? The JDK source doesn't handle NullPointerExceptions in the Comparator: if you give it a null, autoboxing will fail.

The worst part is, I need to repeat this pattern with three other classes which compare different types of strings and could raise three other exceptions while parsing.

Exactly why I would argue that the parsing should happen outside your Comparator with exception-handling dealt with before it arrives at your code.

Bob Cross
Regarding the first, no, I'm not trying to compare `1.0` with `Two` -- the format of the strings is guaranteed to be the same. Regarding the second, it doesn't matter which one is greater so as long as the result is consistent.
a paid nerd
@a paid nerd, I understand that that's not something that you're trying to solve. I'm pointing out that that's what your code is essentially doing. Perhaps it would be more obvious if I used "1.0" and "Eggplant" in my example. I'll add my summary comment to the top of my answer.
Bob Cross
+1  A: 

Try this:

import com.google.common.base.Function;
import com.google.common.collect.Ordering;

Ordering.nullsFirst().onResultOf(
    new Function<String, Double>() {
      public Double apply(String s) {
      try {
        return Double.parseDouble(s);
      } catch (NumberFormatException e) {
        return null;
      }
    })

The only problem, if it you consider it that, is that null Strings and other non-parseable Strings will all be intermingled. That's probably not a big deal, considering the benefits -- this gives you a comparator that is guaranteed to be correct, whereas with a hand-coded comparator, even relatively simple ones, it's amazing how easy it is to commit a subtle error that breaks transitivity or, umm, antisymmetricity.

http://google-collections.googlecode.com

Kevin Bourrillion
+1  A: 

Here's how I'd improve the comparator:

First, exctract a method for converting the value. It's being repeated, multiple try...catches are always ugly -> better to have as few of them as possible.

private Double getDouble(String number) {
 try {
  return Double.parseDouble(number);
 } catch(NumberFormatException e) {
  return null;
 }
}

Next, write down simple rules to show how you want the flow of the comparator to be.

if i1==null && i2!=null return -1
if i1==null && i2==null return 0
if i1!=null && i2==null return 1
if i1!=null && i2!=null return comparison

Finally do horrible obfuscation to the actual comparator to raise a few WTF:s in code review (or like others like to say it, "Implement the Comparator"):

class NumericComparator implements Comparator<String> {

     public int compare(String s1, String s2) {
      final Double i1 = getDouble(s1);
      final Double i2 = getDouble(s2);

      return (i1 == null) ? (i2 == null) ? 0 : -1 : (i2 == null) ? 1 : i1.compareTo(i2);
     }
     private Double getDouble(String number) {
          try {
               return Double.parseDouble(number);
          } catch(NumberFormatException e) {
               return null;
          }
     }
}

...yes, that's a branching nested ternary. If anyone complains about it, say what others here have been saying: Handling nulls isn't Comparator's job.

Esko
This seems to fit my scenario the best.
a paid nerd
A: 

It seems that there are two concerns being mixed here and maybe should be broken up into separate components. Consider the following:

public class ParsingComparator implements Comparator<String> {
  private Parser parser;

  public int compare(String s1, String s2) {
    Object c1 = parser.parse(s1);
    Object c2 = parser.parse(s2);
    new CompareToBuilder().append(c1, c2).toComparison();
  }
}

The Parser interface would have implementations for numbers, dates, etc. You could potentially use the java.text.Format class for your Parser interface. If you don't want to use commons-lang, you could replace the use of CompareToBuilder with some logic to handle nulls and use Comparable instead of Object for c1 and c2.

David
+1  A: 

according to your needs and Ewan's post, I think there's a way to extract the structure that you can reuse:

class NumericComparator implements Comparator<String> {
    private SafeAdaptor<Double> doubleAdaptor = new SafeAdaptor<Double>(){
     public Double parse(String s) {
      return Double.parseDouble(s);
     }
    };
    public int compare(String s1, String s2) {
     final Double i1 =doubleAdaptor.getValue(s1, "s1");
     final Double i2 = doubleAdaptor.getValue(s2, "s2");
     return i1.compareTo(i2);
    }
}

abstract class SafeAdaptor<T>{
    public abstract T parse(String s);
    public T getValue(String str, String name) {
     T i;
     if (str == null) {
      throw new NullPointerException(name + " is null"); // String
     }
     try {
      i = parse(str);
     } catch (NumberFormatException e) {
      throw new ClassCastException(name + " incorrect format"); // Comparator
     }
     return i;
    }

}

I extract the method as an abstract class which can be reuse in other cases(although the class name is suck).

cheers.

Zanyking