Monday, June 5, 2023

Horror Code: The Uptime Comparator of Death

This article was originally posted on JRoller on December 23, 2011.

We have an application that monitors other applications, something like the Windows Task Manager. The developers in charge of this application were given the task to add an uptime column to the table. We advised them to use a tool from the Linux machine on which the application was running which gives back the uptime in seconds. All they had to do was format this information nicely and print it in the cell.

Not agreeing, they felt it would be easier to use another small program existing on the Linux machine, that was already presenting the information nicely. The column looked actually OK, except when someone wanted to sort the table according to the uptime... The String comparison did not yield the expected results. But our developers, instead of backing up to the first proposed solution, felt that they could simply work around the problem with the use of a regular expression. Maybe, but there are regulars expressions and "wait, let me find out how this thingy works" regulars expressions. Here is their uptime comparator:
/**
*
* UptimeComparator : A comparator to compare strings containing
* numbers in the natural order instead of the  ASCII order.
*/
class UptimeComparator implements Comparator {    
  private static final String NUMBER_PATTERN = "(\\d+)";    
 
  @Override    
  /**    
   * Expecting the uptime string  in the below formats only.    
   * Otherwise this method may not work according to expectation !!!    
   * 1) 3 min    
   * 2) 4:48 hour    
   * 3) 10 days 4:48 hours    
   * 4) 10 days 3 min    
   */    
  public int compare(String arg0, String arg1) {        
    // To avoid comparison issues, due to white spaces, remove excess spaces        
    arg0 = arg0.replaceAll("[\\s]+", " ");        
    arg1 = arg1.replaceAll("[\\s]+", " ");        
    /**        
     * Time mentioned in minute is always smaller than        
     * that mentioned in days or hours        
     */        
    if ((arg1.matches("[\\d]+ min")
        && arg0.matches("[\\d]+ days [\\d]+:[\\d]+ hour"))                
      || (arg1.matches("[\\d]+ min")                    
        && arg0.matches("[\\d]+ days [\\d]+ min"))                
      || (arg1.matches("[\\d]+ min")                    
        && arg0.matches("[\\d]+:[\\d]+ hour"))) {            
          return 1;        
    }        
    if ((arg0.matches("[\\d]+ min")                    
        && arg1.matches("[\\d]+ days [\\d]+:[\\d]+ hour"))                
      || (arg0.matches("[\\d]+ min")                    
        && arg1.matches("[\\d]+ days [\\d]+ min"))                
      || (arg0.matches("[\\d]+ min")                    
        && arg1.matches("[\\d]+:[\\d]+ hour"))) {            
          return -1;        
    }        
    /**        
     * Time mentioned in hours is always smaller than        
     * that mentioned in days.        
     */        
    if ((arg1.matches("[\\d]+:[\\d]+ hour")                    
        && arg0.matches("[\\d]+ days [\\d]+:[\\d]+ hour"))                
      || (arg1.matches("[\\d]+:[\\d]+ hour")                    
        && arg0.matches("[\\d]+ days [\\d]+ min"))) {            
          return 1;        
    }        
    if ((arg0.matches("[\\d]+:[\\d]+ hour")                    
        && arg1.matches("[\\d]+ days [\\d]+:[\\d]+ hour"))                
      || (arg0.matches("[\\d]+:[\\d]+ hour")                    
        && arg1.matches("[\\d]+ days [\\d]+ min"))) {            
          return -1;        
    }        
    /**        
     * If both days are not equal, compare only the days.        
     */        
    if((arg0.matches("[\\d]+ days ([\\w]|[\\W])*"))                
        && (arg1.matches("[\\d]+ days ([\\w]|[\\W])*"))){            
          String day1 = arg0.split("[\\s]+")[0];            
          String day2 = arg1.split("[\\s]+")[0];            
          if (!day1.equals(day2) && day1.matches(NUMBER_PATTERN)                    
            && day2.matches(NUMBER_PATTERN)) {                
              return (int)(Double.parseDouble(day1) - Double.parseDouble(day2));            
          }        
    }        
    /**        
     * If both days are equal....        
     * Eg.  "X days Y min" is always less than "X days P:Q hour"        
     */        
    if ((arg1.matches("[\\d]+ days [\\d]+ min")                
      && arg0.matches("[\\d]+ days [\\d]+:[\\d]+ hour"))) {            
        return 1;        
    }        
    if ((arg0.matches("[\\d]+ days [\\d]+ min")                
      && arg1.matches("[\\d]+ days [\\d]+:[\\d]+ hour"))) {            
        return -1;        
    }        
    /**        
     * If both string are identical, compare based on the integer in them.        
     */        
    return compareStringWithNumber(arg0, arg1);    
  }    
 
  /**    
   * Compare two strings with numbers in the natural order.    
   * compareStringWithNumber.    
   *    
   * @param str1    
   * @param str2    
   * @return    
   */    
  public int compareStringWithNumber(String str1, String str2) {        
    if (str1 == null || str2 == null) {            
      return 0;        
    }        
    List split1 = split(str1);        
    List split2 = split(str2);        
    int diff = 0;        
    for (int i = 0; diff == 0 && i < split1.size() && i < split2.size(); i++) {
      String token1 = split1.get(i);
      String token2 = split2.get(i);
      if (token1.matches(NUMBER_PATTERN)                    
        && token2.matches(NUMBER_PATTERN)) {                
          diff = (int) Math.signum(Double.parseDouble(token1)                          
            - Double.parseDouble(token2));            
      } else {                
        diff = token1.compareToIgnoreCase(token2);
      }        
    }        
    if (diff != 0) {            
      return diff;        
    }        
    return split1.size() - split2.size();    
  }    
 
  /**    
   * Splits a string into strings and number tokens.    
   */    
  private List split(String s) {        
    List list = new ArrayList();        
    Scanner scanner = new Scanner(s);        
    int index = 0;        
    String num = null;        
    while ((num = scanner.findInLine(NUMBER_PATTERN)) != null) {            
      int indexOfNumber = s.indexOf(num, index);            
      if (indexOfNumber > index) {                
        list.add(s.substring(index, indexOfNumber).trim());            
      }            
      list.add(num);            
      index = indexOfNumber + num.length();        
    }        
    if (index < s.length()) {
        list.add(s.substring(index));
    }
    return list;
}

Before you utter the WTF words, let me explain how this works. In fact it is pretty simple.

First, we notice that there are altogether four possibilities, as you can see in the comments. So we make a nice hierarchy of the possibilities: if one uptime has only minutes while the other has not, then we know that the one that has only minutes is the smallest. Clever!

Repeat this thinking process with hours. Great!

Now let's take care of another case: the two uptime Strings contain days, and we are able to quickly compare the number of days. Youpee!

Same number of days? We can check that if we have one uptime containing only minutes and the other one only hours, then we have a winner. Woohoo!

Still no decisions? It is time to use the hammer, the "compareStringWithNumber" method.

This method uses another helper method called "split". What it does is that it creates a list of all the numbers and other characters in the uptime String. So we use a regular expression to find all the numbers, and try to look for it again in the String (because those damn Scanners can not give us this information). This whole method is of course buggy, I let you find the cases when it does not work (hint: 11:11 hour will not for instance).

Now let's go back to the hammer. We have our two lists of numbers and things, with probably the same numbers of element (or not?). We check again that we are really dealing with numbers, because we should have the same format, but we are not sure. So just in case, we insert a String comparison. We might have days before minutes, but we should not. Or should we? No, I'm pretty confident all this works.

I told you, all very simple indeed.

Unfortunately, because of the way the article was stored (HTML does not like comparison characters), part of the code was lost. But I find that even like this, you can still enjoy its cleverness.

UPDATE: I found the complete version of this article on the Wayback Machine! So now you get the code in its complete splendor.

No comments:

Post a Comment