001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright (c) 2005-2017 The ConQAT Project                               |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016|                                                                          |
017+-------------------------------------------------------------------------*/
018package org.conqat.lib.commons.string;
019
020import java.math.BigInteger;
021import java.util.Comparator;
022
023/**
024 *
025 * Compares two strings lexicographically, except at locations in the strings
026 * where numbers should be compared. In this case, the value of the number is
027 * compared. Useful for cases such as comparing "foo 5" to "foo 20", where the
028 * digit 2 is lexicographically before the digit 5, leading to an undesired
029 * sorting result.
030 */
031public class NumbersAwareStringComparator implements Comparator<String> {
032
033        /** Singleton instance. */
034        public static final NumbersAwareStringComparator INSTANCE = new NumbersAwareStringComparator();
035
036        /** {@inheritDoc} */
037        @Override
038        public int compare(String s1, String s2) {
039                if (StringUtils.isEmpty(s1) || StringUtils.isEmpty(s2)) {
040                        return StringUtils.compare(s1, s2);
041                }
042                String prefix1 = getPrefix(s1);
043                String prefix2 = getPrefix(s2);
044                if (prefix1.equals(prefix2)) {
045                        return compare(StringUtils.stripPrefix(s1, prefix1), StringUtils.stripPrefix(s2, prefix2));
046                }
047                if (startsWithDigit(prefix1) && startsWithDigit(prefix2)) {
048                        int comparisonResult = new BigInteger(prefix1).compareTo(new BigInteger(prefix2));
049                        if (comparisonResult == 0) {
050                                return compare(StringUtils.stripPrefix(s1, prefix1), StringUtils.stripPrefix(s2, prefix2));
051                        }
052                        return comparisonResult;
053                }
054                return prefix1.compareTo(prefix2);
055        }
056
057        /** Tests if the first character is digit. */
058        private static boolean startsWithDigit(String s) {
059                if (StringUtils.isEmpty(s)) {
060                        return false;
061                }
062                char first = s.charAt(0);
063                return Character.isDigit(first);
064        }
065
066        /**
067         * Returns the prefix, which can be leading digit characters or leading
068         * non-digit characters.
069         */
070        private static String getPrefix(String s) {
071                if (startsWithDigit(s)) {
072                        return s.replaceAll("[\\D]+.*", "");
073                }
074                return s.replaceAll("[\\d]+.*", "");
075        }
076
077}