package com.onaro.sanscreen.client.view.tabular.value; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; /** * Value class that attempts to parse text values as integers, for comparison (sorting) * purposes. If the value cannot be interpreted as an integer, the value will fall back on * using {@link java.lang.String#compareToIgnoreCase(String)} comparison behavior. IntegerTextValue * instances are normally created using the {@link #parse(String)} method. The parsing algorithm * allows for hexadecimal character values (0-9a-fA-f) and only considers a limited number of * characters when determining whether it's a number for efficiency sake. The parsing algorithm * also uses the {@link #SEPARATOR_CHARACTERS} to allow the string to represent several numbers. * When more than one number is included in the string, the comparison compares each number index * in order. * * @author jmyers * */ public class IntegerTextValue implements TableValue { public static final int HEX_RADIX = 16; /** * The default limit (6) of the number of characters to consider when * parsing a text value into a number (this limit does not include * separator characters) */ public static final int DEFAULT_MAX_NUMBER_CHARACTERS = 6; /** * Characters that are used to split the string into separate numbers when parsing the * text value. */ public static final char[] SEPARATOR_CHARACTERS = new char[] {':', '.', '-'}; private final Object display; private final Integer[] numberValues; public IntegerTextValue(Integer numberValue) { this(numberValue == null ? null : numberValue.toString(), new Integer[] { numberValue }); } private IntegerTextValue(Object display, Integer[] numberValues) { this.display = display; this.numberValues = numberValues; } /** * Create a NumericTextValue from a given string, using the default maximum number * of numeric characters. * @param textValue the text value to interpret * @return the IntegerTextValue for the textValue */ public static IntegerTextValue parse(final String textValue) { return parse(textValue, DEFAULT_MAX_NUMBER_CHARACTERS); } /** * Create a NumericTextValue from a given string, using the default maximum number * of numeric characters. * @param textValue the text value to interpret * @param maxNumberCharacters the maximum number of characters to consider in a numeric value * @return the IntegerTextValue for the textValue */ public static IntegerTextValue parse(final String textValue, final int maxNumberCharacters) { StringBuilder numberString = new StringBuilder(maxNumberCharacters); boolean isNumeric = true; List numbers = new ArrayList(2); // iterate over the characters in the text value, while it's considered numeric for (int i = 0; isNumeric && i < textValue.length(); i++) { char testChar = textValue.charAt(i); // If we're hitting a separator character if (ArrayUtils.contains(SEPARATOR_CHARACTERS, testChar)) { // If we've already got some number text if (numberString.length() > 0) { // Parse the text and add the number to the list try { Integer number = Integer.valueOf(numberString.toString(), HEX_RADIX); numbers.add(number); // Start a new number numberString = new StringBuilder(maxNumberCharacters); } catch (NumberFormatException e) { // Should not happen but fail gracefully anyways isNumeric = false; } } else { isNumeric = false; } continue; } // Check if the character is a hex digit (0-9a-fA-F) if (Character.digit(testChar, HEX_RADIX) != -1) { numberString.append(testChar); } else { // Otherwise the value is not numeric isNumeric = false; } // If we're now past the maximum length (excluding separator characters) // toggle the numeric flag off. if (numberString.length() > maxNumberCharacters) { isNumeric = false; } } // If the value is numeric, and there's another number to parse if (isNumeric && numberString.length() > 0) { // Parse the number try { Integer number = Integer.valueOf(numberString.toString(), HEX_RADIX); numbers.add(number); // And create the value return new IntegerTextValue(textValue, numbers.toArray(new Integer[numbers.size()])); } catch (NumberFormatException e) { // Should not happen but fail gracefully anyways } } // If the value is not numeric, simply display the text and use compareToIgnoreCase sorting return new IntegerTextValue(textValue, null); } public Object getDisplay() { return display != null ? display : StringUtils.EMPTY; } /** * Since this method is used by he grouping-table, it returns the display value. * @return Return's the same value as {@link TableValue#getDisplay} does */ public String toString() { return getDisplay().toString(); } @Override public int compareTo (TableValue o) { if (this == o) { return 0; } if (o == null) { throw new NullPointerException("null cannot be compared to an IntegerTextValue"); //$NON-NLS-1$ } if (! (o instanceof IntegerTextValue)) { throw new ClassCastException("'" + o + "' cannot be compared to an IntegerTextValue"); //$NON-NLS-1$ //$NON-NLS-2$ } IntegerTextValue other = (IntegerTextValue)o; // If either values don't have number values defined, use compareToIgnoreCase // on the display string // Always put numeric values before non-numeric values, for consistent ordering. (IBG-6223) if (numberValues == null) { if (other.numberValues == null) { return toString().compareToIgnoreCase(other.toString()); } else { return 1; } } else if (other.numberValues == null) { return -1; } // Otherwise compare using the numbers for (int i = 0; i < Math.min(numberValues.length, other.numberValues.length); i++) { long cmp = numberValues[i].intValue() - other.numberValues[i].intValue(); if (cmp != 0) return cmp > 0 ? 1 : -1; } // If the numbers have matched up to this point, compare based on array length return numberValues.length - other.numberValues.length; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((display == null) ? 0 : display.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; IntegerTextValue other = (IntegerTextValue) obj; if (display == null) { if (other.display != null) return false; } else if (!display.equals(other.display)) return false; return true; } }