Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
/*
 * The Alphanum Algorithm is an improved sorting algorithm for strings
 * containing numbers. Rather than sort numbers in ASCII order like
 * a standard sort, this algorithm sorts numbers in numeric order.
 *
 * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com
 *
 * Released under the MIT License - https://opensource.org/licenses/MIT
 *
 * Copyright 2007-2017 David Koelle
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
 * USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.keenwrite.util;

import java.io.Serializable;
import java.util.Comparator;

import static java.lang.Character.isDigit;

/**
 * Responsible for sorting lists that may contain numeric values. Usage:
 * <pre>
 *   Collections.sort(list, new AlphanumComparator());
 * </pre>
 * <p>
 * Where "list" is the list to sort alphanumerically, not lexicographically.
 * </p>
 */
public final class AlphanumComparator<T> implements
  Comparator<T>, Serializable {

  /**
   * Returns a chunk of text that is continuous with respect to digits or
   * non-digits.
   *
   * @param s      The string to compare.
   * @param length The string length, for improved efficiency.
   * @param marker The current index into a subset of the given string.
   * @return The substring {@code s} that is a continuous text chunk of the
   * same character type.
   */
  private StringBuilder chunk( final String s, final int length, int marker ) {
    assert s != null;
    assert length >= 0;
    assert marker < length;

    // Prevent any possible memory re-allocations by using the length.
    final var chunk = new StringBuilder( length );
    var c = s.charAt( marker );
    final var chunkType = isDigit( c );

    // While the character at the current position is the same type (numeric or
    // alphabetic), append the character to the current chunk.
    while( marker < length &&
      isDigit( c = s.charAt( marker++ ) ) == chunkType ) {
      chunk.append( c );
    }

    return chunk;
  }

  /**
   * Performs an alphanumeric comparison of two strings, sorting numerically
   * first when numbers are found within the string. If either argument is
   * {@code null}, this will return zero.
   *
   * @param o1 The object to compare against {@code s2}, converted to string.
   * @param o2 The object to compare against {@code s1}, converted to string.
   * @return a negative integer, zero, or a positive integer if the first
   * argument is less than, equal to, or greater than the second, respectively.
   */
  @Override
  public int compare( final T o1, final T o2 ) {
    if( o1 == null || o2 == null ) {
      return 0;
    }

    final var s1 = o1.toString();
    final var s2 = o2.toString();
    final var s1Length = s1.length();
    final var s2Length = s2.length();

    var thisMarker = 0;
    var thatMarker = 0;

    while( thisMarker < s1Length && thatMarker < s2Length ) {
      final var thisChunk = chunk( s1, s1Length, thisMarker );
      final var thisChunkLength = thisChunk.length();
      thisMarker += thisChunkLength;
      final var thatChunk = chunk( s2, s2Length, thatMarker );
      final var thatChunkLength = thatChunk.length();
      thatMarker += thatChunkLength;

      // If both chunks contain numeric characters, sort them numerically
      int result;

      if( isDigit( thisChunk.charAt( 0 ) ) &&
        isDigit( thatChunk.charAt( 0 ) ) ) {
        // If equal, the first different number counts
        if( (result = thisChunkLength - thatChunkLength) == 0 ) {
          for( var i = 0; i < thisChunkLength; i++ ) {
            result = thisChunk.charAt( i ) - thatChunk.charAt( i );

            if( result != 0 ) {
              return result;
            }
          }
        }
      }
      else {
        result = thisChunk.compareTo( thatChunk );
      }

      if( result != 0 ) {
        return result;
      }
    }

    return s1Length - s2Length;
  }
}