code: package org.rug.data.project;
  
  import java.util.*;
  import java.util.stream.IntStream;
  
  /**
   * Compares versions that match the following regex (\d\.)+\d\w*, namely dot-separated versions with
   * eventual word descriptors at the end.
   * The supported word descritors are, at the moment: "rc", "final" and every descriptor which lexicographical
   * order respects release order.
   */
  public class StringVersionComparator implements Comparator<String> {
  
      private final static Hashtable<String, String> orderStringVersions = new Hashtable<>();
      private final static String sep = "\\.";
      private final static String dot = ".";
  
      @Override
      public int compare(String o1, String o2) {
  
          var standardForm = standardize(o1, o2, "0");
  
          o1 = standardForm[0];
          o2 = standardForm[1];
  
          return o1.compareTo(o2);
      }
  
      /**
       * Convenience method for {@link #standardize(String, String, String)} where fillString = "0".
       * @param s1 A version to standardize
       * @param s2 A version to standardize.
       * @return Same as {@link #standardize(String, String, String)}, but as a List of strings.
       */
      public List<String> standardize(String s1, String s2){
          return Arrays.asList(standardize(s1, s2, "0"));
      }
  
      /**
       * Conforms the two strings to the same format by adding missing trailing versions and filling them
       * with {@code fillString}. Additionally, every split (version figures) are padded using
       * {@link #standardize(String)}, excluding the last split.
       * This method also takes care to add order prefixes to the last split.
       * @param s1 A dot-separated version.
       * @param s2 A dot-separated version.
       * @param fillString a string to use for padding, should be "0" to guarantee lexicographical order correctness.
       * @return an array where the first element is {@code s1} and the second element is {@code s2} standardized
       * as described above.
       */
      public String[] standardize(String s1, String s2, String fillString){
          var split1 = s1.toLowerCase().replace("-", "").split(sep);
          var split2 = s2.toLowerCase().replace("-", "").split(sep);
  
          var splitsToAdd = split1.length - split2.length;
          if (splitsToAdd != 0) {
              var shorterSplit = new ArrayList<>(Arrays.asList(split1.length < split2.length ? split1 : split2));
              IntStream.range(0, Math.abs(splitsToAdd)).forEach(i -> shorterSplit.add(fillString));
              if (splitsToAdd < 0)
                  split1 = shorterSplit.toArray(new String[0]);
              else
                  split2 = shorterSplit.toArray(new String[0]);
          }
  
          var regex = ".*[a-zA-Z]+.*";
          var split1Matches = split1[split1.length - 1].matches(regex);
          var split2Matches = split2[split2.length - 1].matches(regex);
  
          if (split1Matches && !split2Matches)
              split2[split1.length - 1] = split2[split2.length - 1] + "z";
          else if (!split1Matches && split2Matches)
              split1[split1.length - 1] = split1[split1.length - 1] + "z";
          else if (split1Matches && split2Matches) {
              split1[split1.length - 1] = addOrderPrefixes(split1[split1.length - 1]);
              split2[split2.length - 1] = addOrderPrefixes(split2[split2.length - 1]);
          }else{
              split1[split1.length - 1] = String.format("%" + 5 + "s", split1[split1.length - 1]).replace(' ', '0');
              split2[split2.length - 1] = String.format("%" + 5 + "s", split2[split2.length - 1]).replace(' ', '0');
          }
  
          return new String[]{standardize(String.join(dot, split1)),
                              standardize(String.join(dot, split2))};
      }
  
      /**
       * Convenience method for {@link #standardize(String[], int, char)} with {@code padLength = 5}
       * and {@code padCharacter = '0'}.
       * @param version A version to pad.
       * @return the padded dot-separated version.
       */
      public String standardize(String version){
          return standardize(version.split(sep), 5, '0');
      }
  
      /**
       * Pads the given version.
       * @param splits the version in the form of splits.
       * @param padLength the length of the padding.
       * @param padCharacter the character to use for padding.
       * @return a dot-separated version padded.
       */
      public String standardize(String[] splits, int padLength, char padCharacter){
          for (int i = 0; i < splits.length - 1; i++){
              splits[i] = String.format("%" + padLength + "s", splits[i]).replace(' ', padCharacter);
          }
  
          return String.join(".", splits);
      }
  
      /**
       * Replaces all occurrences of descriptors that are non lexicographically-ordered with a lexicographically-ordered
       * version of them.
       * @param split the split of the version that contains the character to replace.
       * @return {@code split} with ordered descriptors.
       */
      private String addOrderPrefixes(String split){
          var prefixedVersionBuilder = new StringBuilder();
          orderStringVersions.forEach((k, v) -> {
              prefixedVersionBuilder.append(split.replace(k, v));
          });
          return prefixedVersionBuilder.toString();
      }
  
      static {
          orderStringVersions.put("rc", "arc");
          orderStringVersions.put("final", "zfinal");
      }
  
  }
  