/*
Copyright (C) 2016 maik.jablonski@jease.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package jfix.util;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
/**
* Provides Comparators for Natural String Order
*
* The implementation was taken from:
* http://weblogs.java.net/blog/176/2006/01/13/natural-string-order
*
* Minor modifications done by Maik Jablonski.
*
*
* Copyright (c) 2006, Stephen Kelvin Friedrich, All rights reserved.
*
* This a BSD license. If you use or enhance the code, I'd be pleased if you
* sent a mail to s.friedrich@eekboom.com
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the "Stephen Kelvin Friedrich" nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
public class Natural<E> {
private Natural() {
}
/**
* Returns new instance of natural comparator which uses the smart
* comparision of objects (see compare(o1,o2)).
*/
public static <E> Comparator<E> newComparator() {
return new Comparator<E>() {
public int compare(E o1, E o2) {
return Natural.compare(o1, o2);
}
};
}
/**
* Smart compare of two objects: numbers and dates will compared by value,
* for all other objects a natural comparision of string-values will be
* used.
*/
public static int compare(Object o1, Object o2) {
if (o1 instanceof Number && o2 instanceof Number) {
return Double.compare(((Number) o1).doubleValue(),
((Number) o2).doubleValue());
}
if (o1 instanceof Date && o2 instanceof Date) {
return ((Date) o1).compareTo(((Date) o2));
}
return compareObjects(String.valueOf(o1), String.valueOf(o2),
Collator.getInstance());
}
/**
* Returns a natural sorted list of given collection.
*/
public static <E> List<E> sort(Collection<E> collection) {
List<E> list = new ArrayList<E>(collection);
list.sort(Natural.newComparator());
return list;
}
/**
* Returns a natural sorted array of given array.
*/
public static <E> E[] sort(E[] array) {
E[] arrayCopy = array.clone();
Arrays.sort(arrayCopy, Natural.newComparator());
return arrayCopy;
}
/**
* @param s
* first string
* @param t
* second string
* @param collator
* used to compare subwords that aren't numbers - if null,
* characters will be compared individually based on their
* Unicode value
* @return zero if <code>s</code> and <code>t</code> are equal, a value less
* than zero if <code>s</code> lexicographically precedes
* <code>t</code> and a value larger than zero if <code>s</code>
* lexicographically follows <code>t</code>
*/
private static int compareObjects(String s, String t, Collator collator) {
int sIndex = 0;
int tIndex = 0;
int sLength = s.length();
int tLength = t.length();
while (true) {
// both character indices are after a subword (or at zero)
// Check if one string is at end
if (sIndex == sLength && tIndex == tLength) {
return 0;
}
if (sIndex == sLength) {
return -1;
}
if (tIndex == tLength) {
return 1;
}
// Compare sub word
char sChar = s.charAt(sIndex);
char tChar = t.charAt(tIndex);
boolean sCharIsDigit = Character.isDigit(sChar);
boolean tCharIsDigit = Character.isDigit(tChar);
if (sCharIsDigit && tCharIsDigit) {
// Compare numbers
// skip leading 0s
int sLeadingZeroCount = 0;
while (sChar == '0') {
++sLeadingZeroCount;
++sIndex;
if (sIndex == sLength) {
break;
}
sChar = s.charAt(sIndex);
}
int tLeadingZeroCount = 0;
while (tChar == '0') {
++tLeadingZeroCount;
++tIndex;
if (tIndex == tLength) {
break;
}
tChar = t.charAt(tIndex);
}
boolean sAllZero = sIndex == sLength
|| !Character.isDigit(sChar);
boolean tAllZero = tIndex == tLength
|| !Character.isDigit(tChar);
if (sAllZero && tAllZero) {
continue;
}
if (sAllZero && !tAllZero) {
return -1;
}
if (tAllZero) {
return 1;
}
int diff = 0;
do {
if (diff == 0) {
diff = sChar - tChar;
}
++sIndex;
++tIndex;
if (sIndex == sLength && tIndex == tLength) {
return diff != 0 ? diff : sLeadingZeroCount
- tLeadingZeroCount;
}
if (sIndex == sLength) {
if (diff == 0) {
return -1;
}
return Character.isDigit(t.charAt(tIndex)) ? -1 : diff;
}
if (tIndex == tLength) {
if (diff == 0) {
return 1;
}
return Character.isDigit(s.charAt(sIndex)) ? 1 : diff;
}
sChar = s.charAt(sIndex);
tChar = t.charAt(tIndex);
sCharIsDigit = Character.isDigit(sChar);
tCharIsDigit = Character.isDigit(tChar);
if (!sCharIsDigit && !tCharIsDigit) {
// both number sub words have the same length
if (diff != 0) {
return diff;
}
break;
}
if (!sCharIsDigit) {
return -1;
}
if (!tCharIsDigit) {
return 1;
}
} while (true);
} else {
// To use the collator the whole subwords have to be
// compared - character-by-character comparision
// is not possible. So find the two subwords first
int aw = sIndex;
int bw = tIndex;
do {
++sIndex;
} while (sIndex < sLength
&& !Character.isDigit(s.charAt(sIndex)));
do {
++tIndex;
} while (tIndex < tLength
&& !Character.isDigit(t.charAt(tIndex)));
String as = s.substring(aw, sIndex);
String bs = t.substring(bw, tIndex);
int subwordResult = collator.compare(as, bs);
if (subwordResult != 0) {
return subwordResult;
}
}
}
}
}