001package org.unix4j.util.sort;
002
003import org.unix4j.util.StringUtil;
004
005import java.util.Comparator;
006import java.util.Locale;
007import java.util.Objects;
008
009/**
010 * A comparator for decimal strings sorted numerically, first by numeric sign; then by SI suffix
011 * (either empty, or 'k' or 'K', or one of 'MGTPEZY', in that order); and finally by numeric value.
012 * For example, '1023M' sorts before '1G' because 'M' (mega) precedes 'G' (giga) as an SI suffix.
013 * <p>
014 * The syntax for numbers is the same as for {@link DecimalNumberStringComparator}; the SI suffix must
015 * immediately follow the number.
016 */
017public class UnitsNumberStringComparator implements Comparator<CharSequence> {
018
019        private static final String SI_UNITS = "KMGTPEZY";
020
021        /**
022         * The instance for the default locale returned by {@link #getInstance()}.
023         */
024        private static final UnitsNumberStringComparator DEFAULT_INSTANCE = new UnitsNumberStringComparator();
025
026        /**
027         * Returns the instance for the default locale.
028         *
029         * @see Locale#getDefault()
030         */
031        public static UnitsNumberStringComparator getInstance() {
032                return DEFAULT_INSTANCE;
033        }
034
035        /**
036         * Returns an instance for the specified locale.
037         */
038        public static UnitsNumberStringComparator getInstance(Locale locale) {
039                return new UnitsNumberStringComparator(locale);
040        }
041
042        private final DecimalNumberStringComparator numberComparator;
043
044        /**
045         * Private constructor used to create the {@link #DEFAULT_INSTANCE}.
046         */
047        private UnitsNumberStringComparator() {
048                this(DecimalNumberStringComparator.getInstance());
049        }
050
051        /**
052         * Private constructor used by {@link #getInstance(Locale)}.
053         */
054        private UnitsNumberStringComparator(Locale locale) {
055                this(DecimalNumberStringComparator.getInstance(locale));
056        }
057
058        /**
059         * Constructor with decimal number comparator.
060         *
061         * @param numberComparator
062         *            the decimal number comparator
063         */
064        public UnitsNumberStringComparator(DecimalNumberStringComparator numberComparator) {
065                this.numberComparator = Objects.requireNonNull(numberComparator);
066        }
067
068        @Override
069        public int compare(CharSequence s1, CharSequence s2) {
070                final int start1 = StringUtil.findStartTrimWhitespace(s1);
071                final int end1 = StringUtil.findEndTrimWhitespace(s1);
072                final int start2 = StringUtil.findStartTrimWhitespace(s2);
073                final int end2 = StringUtil.findEndTrimWhitespace(s2);
074                final boolean isNeg1 = start1 < end1 && s1.charAt(start1) == '-';
075                final boolean isNeg2 = start2 < end2 && s2.charAt(start2) == '-';
076                final int si1 = siUnit(s1, start1, end1);
077                final int si2 = siUnit(s2, start2, end2);
078                final int e1 = si1 < 0 ? end1 : end1 - 1;
079                final int e2 = si2 < 0 ? end2 : end2 - 1;
080                final int cmp = numberComparator.compare(s1, start1, e1, s2, start2, e2);
081                if (isNeg1 != isNeg2) {
082                        return cmp;
083                }
084                if (si1 < si2) {
085                        return isNeg1 ? 1 : -1;
086                }
087                if (si1 > si2) {
088                        return isNeg1 ? -1 : 1;
089                }
090                return cmp;
091        }
092
093        private static final int siUnit(CharSequence s, int start, int end) {
094                if (start < end) {
095                        final int ch = s.charAt(end - 1);
096                        final int si = SI_UNITS.indexOf(ch);
097                        if (si >= 0) {
098                                return si;
099                        }
100                        if (ch == 'k') return 0;//lower case only for k
101                }
102                return -1;
103        }
104}