/**
* Copyright (c) 2009 - 2012 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package org.candlepin.common.util;
import java.io.Serializable;
import java.util.Comparator;
/**
* Implement the rpmvercmp function provided by librpm
* in Java. The comparator operates on two strings that
* represent an RPM version or release.
*
* <p> This comparator is not perfectly antysymmetric for unequal versions,
* but close enough to warrant being a comparator. For examples of asymmetry,
* check the test.
*/
public class RpmVersionComparator implements Comparator<String>, Serializable {
private static final long serialVersionUID = 4036652892664704660L;
/**
* {@inheritDoc}
*/
public int compare(String str1, String str2) {
if (str1 == null && str2 == null) {
return 0;
}
// relies on the check above
if (str1 == null || str2 == null) {
return 1;
}
// This method tries to mimic rpmvercmp.c as
// closely as possible; it is deliberately doing things
// in a more C-like manner
if (str1 != null && str1.equals(str2)) {
return 0;
}
int b1 = 0;
int b2 = 0;
/* loop through each version segment of str1 and str2 and compare them */
while (b1 < str1.length() && b2 < str2.length()) {
b1 = skipNonAlnum(str1, b1);
b2 = skipNonAlnum(str2, b2);
/* grab first completely alpha or completely numeric segment */
/* str1.substring(b1, e1) and str2.substring(b2, e2) will */
/* contain the segments */
int e1, e2;
boolean isnum;
if (xisdigit(xchar(str1, b1))) {
e1 = skipDigits(str1, b1);
e2 = skipDigits(str2, b2);
isnum = true;
}
else {
e1 = skipAlpha(str1, b1);
e2 = skipAlpha(str2, b2);
isnum = false;
}
/* take care of the case where the two version segments are */
/* different types: one numeric, the other alpha (i.e. empty) */
if (b1 == e1) {
return -1; /* arbitrary */
}
if (b2 == e2) {
return (isnum ? 1 : -1);
}
if (isnum) {
b1 = skipZeros(str1, b1, e1);
b2 = skipZeros(str2, b2, e2);
/* whichever number has more digits wins */
if (e1 - b1 > e2 - b2) {
return 1;
}
if (e2 - b2 > e1 - b1) {
return -1;
}
}
/* compareTo will return which one is greater - even if the two */
/* segments are alpha or if they are numeric. don't return */
/* if they are equal because there might be more segments to */
/* compare */
String seg1 = str1.substring(b1, e1);
String seg2 = str2.substring(b2, e2);
int rc = seg1.compareTo(seg2);
if (rc != 0) {
return (rc < 0) ? -1 : 1;
}
// Reinitialize
b1 = e1;
b2 = e2;
}
/* this catches the case where all numeric and alpha segments have */
/* compared identically but the segment separating characters were */
/* different */
if (b1 == str1.length() && b2 == str2.length()) {
return 0;
}
/* whichever version still has characters left over wins */
if (b1 == str1.length()) {
return -1;
}
else {
return 1;
}
}
private int skipZeros(String s, int b, int e) {
/* throw away any leading zeros - it's a number, right? */
while (xchar(s, b) == '0' && b < e) {
b++;
}
return b;
}
private int skipDigits(String s, int i) {
while (i < s.length() && xisdigit(xchar(s, i))) {
i++;
}
return i;
}
private int skipAlpha(String s, int i) {
while (i < s.length() && xisalpha(xchar(s, i))) {
i++;
}
return i;
}
private int skipNonAlnum(String s, int i) {
while (i < s.length() && !xisalnum(xchar(s, i))) {
i++;
}
return i;
}
private boolean xisalnum(char c) {
return xisdigit(c) || xisalpha(c);
}
private boolean xisdigit(char c) {
return Character.isDigit(c);
}
private boolean xisalpha(char c) {
return Character.isLetter(c);
}
private char xchar(String s, int i) {
return (i < s.length() ? s.charAt(i) : '\0');
}
}