From 4769745bbed8fed2a73b0c5864395ef7e71588e8 Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Fri, 16 Jun 2023 08:08:58 +0200 Subject: [PATCH 1/7] Support alphanumeric visit order --- .../fabricmc/mappingio/tree/VisitOrder.java | 152 ++++++++++++++++-- 1 file changed, 135 insertions(+), 17 deletions(-) diff --git a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java index 4adceeae..966c529c 100644 --- a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java +++ b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java @@ -39,11 +39,15 @@ public static VisitOrder createByInputOrder() { return new VisitOrder(); } - public static VisitOrder createByName() { + /** + * Sorts classes by their source name, members by source name and descriptor, and locals by lv- and lvt-index. + * @param alphanumeric makes sure e.g. {@code mapping10} is ordered after {@code mapping2}. More performance intensive. + */ + public static VisitOrder createByName(boolean alphanumeric) { return new VisitOrder() - .classesBySrcName() - .fieldsBySrcNameDesc() - .methodsBySrcNameDesc() + .classesBySrcName(alphanumeric) + .fieldsBySrcNameDesc(alphanumeric) + .methodsBySrcNameDesc(alphanumeric) .methodArgsByLvIndex() .methodVarsByLvIndex(); } @@ -56,8 +60,8 @@ public VisitOrder classComparator(Comparator comparator) { return this; } - public VisitOrder classesBySrcName() { - return classComparator(compareBySrcName()); + public VisitOrder classesBySrcName(boolean alphanumeric) { + return classComparator(compareBySrcName(alphanumeric)); } public VisitOrder fieldComparator(Comparator comparator) { @@ -66,8 +70,8 @@ public VisitOrder fieldComparator(Comparator comparator) { return this; } - public VisitOrder fieldsBySrcNameDesc() { - return fieldComparator(compareBySrcNameDesc()); + public VisitOrder fieldsBySrcNameDesc(boolean alphanumeric) { + return fieldComparator(compareBySrcNameDesc(alphanumeric)); } public VisitOrder methodComparator(Comparator comparator) { @@ -76,8 +80,8 @@ public VisitOrder methodComparator(Comparator comparator) { return this; } - public VisitOrder methodsBySrcNameDesc() { - return methodComparator(compareBySrcNameDesc()); + public VisitOrder methodsBySrcNameDesc(boolean alphanumeric) { + return methodComparator(compareBySrcNameDesc(alphanumeric)); } public VisitOrder methodArgComparator(Comparator comparator) { @@ -138,15 +142,15 @@ public VisitOrder methodVarsFirst() { // customization helpers - public static Comparator compareBySrcName() { - return (a, b) -> compare(a.getSrcName(), b.getSrcName()); + public static Comparator compareBySrcName(boolean alphanumeric) { + return (a, b) -> compare(a.getSrcName(), b.getSrcName(), alphanumeric); } - public static Comparator compareBySrcNameDesc() { + public static Comparator compareBySrcNameDesc(boolean alphanumeric) { return (a, b) -> { - int cmp = compare(a.getSrcName(), b.getSrcName()); + int cmp = compare(a.getSrcName(), b.getSrcName(), alphanumeric); - return cmp != 0 ? cmp : compare(a.getSrcDesc(), b.getSrcDesc()); + return cmp != 0 ? cmp : compare(a.getSrcDesc(), b.getSrcDesc(), alphanumeric); }; } @@ -154,10 +158,10 @@ public static Comparator compareBySrcNameShortFirst() { return (a, b) -> compareShortFirst(a.getSrcName(), b.getSrcName()); } - public static int compare(String a, String b) { + public static int compare(String a, String b, boolean alphanumeric) { if (a == null || b == null) return compareNullLast(a, b); - return a.compareTo(b); + return alphanumeric ? alphanumComparator.compare(a, b) : a.compareTo(b); } public static int compareShortFirst(String a, String b) { @@ -264,6 +268,7 @@ public boolean isMethodVarsFirst() { return methodVarsFirst; } + private static final AlphanumComparator alphanumComparator = new AlphanumComparator(); private Comparator classComparator; private Comparator fieldComparator; private Comparator methodComparator; @@ -271,4 +276,117 @@ public boolean isMethodVarsFirst() { private Comparator methodVarComparator; private boolean methodsFirst; private boolean methodVarsFirst; + + /* + * The Alphanum Algorithm is an improved sorting algorithm for strings + * containing numbers. Instead of sorting 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. + + * This is an updated version with enhancements made by Daniel Migowski, + * Andre Bogus, and David Koelle. Updated by David Koelle in 2017. + * + * Copied from https://web.archive.org/web/20210506213829/http://www.davekoelle.com/files/AlphanumComparator.java, + * with some code style improvements applied. + */ + private static class AlphanumComparator implements Comparator { + private boolean isDigit(char ch) { + return ((ch >= 48) && (ch <= 57)); + } + + /** Length of string is passed in for improved efficiency (only need to calculate it once). **/ + private String getChunk(String s, int slength, int marker) { + StringBuilder chunk = new StringBuilder(); + char c = s.charAt(marker); + chunk.append(c); + marker++; + + if (isDigit(c)) { + while (marker < slength) { + c = s.charAt(marker); + if (!isDigit(c)) break; + chunk.append(c); + marker++; + } + } else { + while (marker < slength) { + c = s.charAt(marker); + if (isDigit(c)) break; + + chunk.append(c); + marker++; + } + } + + return chunk.toString(); + } + + public int compare(String s1, String s2) { + if ((s1 == null) || (s2 == null)) { + return 0; + } + + int thisMarker = 0; + int thatMarker = 0; + int s1Length = s1.length(); + int s2Length = s2.length(); + + while (thisMarker < s1Length && thatMarker < s2Length) { + String thisChunk = getChunk(s1, s1Length, thisMarker); + thisMarker += thisChunk.length(); + + String thatChunk = getChunk(s2, s2Length, thatMarker); + thatMarker += thatChunk.length(); + + // If both chunks contain numeric characters, sort them numerically + int result = 0; + + if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0))) { + // Simple chunk comparison by length. + int thisChunkLength = thisChunk.length(); + result = thisChunkLength - thatChunk.length(); + + // If equal, the first different number counts + if (result == 0) { + for (int 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; + } + } } From 6f03a7edece618700c6f170a05fcdcfa7f76dcbd Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Fri, 15 Sep 2023 16:53:36 +0200 Subject: [PATCH 2/7] Move Alphanum comparator into own package, add license file --- build.gradle | 1 + .../libs/alphanum/AlphanumComparator.java | 119 ++++++++++++++++++ .../fabricmc/mappingio/libs/alphanum/LICENSE | 21 ++++ .../fabricmc/mappingio/tree/VisitOrder.java | 114 +---------------- 4 files changed, 142 insertions(+), 113 deletions(-) create mode 100644 src/main/java/net/fabricmc/mappingio/libs/alphanum/AlphanumComparator.java create mode 100644 src/main/java/net/fabricmc/mappingio/libs/alphanum/LICENSE diff --git a/build.gradle b/build.gradle index d4d7e351..2dcb3f67 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,7 @@ dependencies { spotless { java { licenseHeaderFile(rootProject.file("HEADER")).yearSeparator(", ") + targetExclude 'src/main/java/net/fabricmc/mappingio/libs/**/*.java' } } diff --git a/src/main/java/net/fabricmc/mappingio/libs/alphanum/AlphanumComparator.java b/src/main/java/net/fabricmc/mappingio/libs/alphanum/AlphanumComparator.java new file mode 100644 index 00000000..0fc2e29b --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/libs/alphanum/AlphanumComparator.java @@ -0,0 +1,119 @@ +/* + * The Alphanum Algorithm is an improved sorting algorithm for strings + * containing numbers. Instead of sorting 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. + + * This is an updated version with enhancements made by Daniel Migowski, + * Andre Bogus, and David Koelle. Updated by David Koelle in 2017. + * + * Copied from https://web.archive.org/web/20210506213829/http://www.davekoelle.com/files/AlphanumComparator.java, + * with some code style improvements applied. + */ + +package net.fabricmc.mappingio.libs.alphanum; + +import java.util.Comparator; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public class AlphanumComparator implements Comparator { + private boolean isDigit(char ch) { + return ((ch >= 48) && (ch <= 57)); + } + + /** Length of string is passed in for improved efficiency (only need to calculate it once). **/ + private String getChunk(String s, int sLength, int marker) { + StringBuilder chunk = new StringBuilder(); + char c = s.charAt(marker); + chunk.append(c); + marker++; + + if (isDigit(c)) { + while (marker < sLength) { + c = s.charAt(marker); + if (!isDigit(c)) break; + chunk.append(c); + marker++; + } + } else { + while (marker < sLength) { + c = s.charAt(marker); + if (isDigit(c)) break; + + chunk.append(c); + marker++; + } + } + + return chunk.toString(); + } + + public int compare(String s1, String s2) { + if ((s1 == null) || (s2 == null)) { + return 0; + } + + int thisMarker = 0; + int thatMarker = 0; + int s1Length = s1.length(); + int s2Length = s2.length(); + + while (thisMarker < s1Length && thatMarker < s2Length) { + String thisChunk = getChunk(s1, s1Length, thisMarker); + thisMarker += thisChunk.length(); + + String thatChunk = getChunk(s2, s2Length, thatMarker); + thatMarker += thatChunk.length(); + + // If both chunks contain numeric characters, sort them numerically + int result = 0; + + if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0))) { + // Simple chunk comparison by length. + int thisChunkLength = thisChunk.length(); + result = thisChunkLength - thatChunk.length(); + + // If equal, the first different number counts + if (result == 0) { + for (int 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; + } +} diff --git a/src/main/java/net/fabricmc/mappingio/libs/alphanum/LICENSE b/src/main/java/net/fabricmc/mappingio/libs/alphanum/LICENSE new file mode 100644 index 00000000..16187301 --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/libs/alphanum/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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. diff --git a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java index 966c529c..d914384a 100644 --- a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java +++ b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java @@ -21,6 +21,7 @@ import java.util.Comparator; import java.util.List; +import net.fabricmc.mappingio.libs.alphanum.AlphanumComparator; import net.fabricmc.mappingio.tree.MappingTreeView.ClassMappingView; import net.fabricmc.mappingio.tree.MappingTreeView.ElementMappingView; import net.fabricmc.mappingio.tree.MappingTreeView.FieldMappingView; @@ -276,117 +277,4 @@ public boolean isMethodVarsFirst() { private Comparator methodVarComparator; private boolean methodsFirst; private boolean methodVarsFirst; - - /* - * The Alphanum Algorithm is an improved sorting algorithm for strings - * containing numbers. Instead of sorting 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. - - * This is an updated version with enhancements made by Daniel Migowski, - * Andre Bogus, and David Koelle. Updated by David Koelle in 2017. - * - * Copied from https://web.archive.org/web/20210506213829/http://www.davekoelle.com/files/AlphanumComparator.java, - * with some code style improvements applied. - */ - private static class AlphanumComparator implements Comparator { - private boolean isDigit(char ch) { - return ((ch >= 48) && (ch <= 57)); - } - - /** Length of string is passed in for improved efficiency (only need to calculate it once). **/ - private String getChunk(String s, int slength, int marker) { - StringBuilder chunk = new StringBuilder(); - char c = s.charAt(marker); - chunk.append(c); - marker++; - - if (isDigit(c)) { - while (marker < slength) { - c = s.charAt(marker); - if (!isDigit(c)) break; - chunk.append(c); - marker++; - } - } else { - while (marker < slength) { - c = s.charAt(marker); - if (isDigit(c)) break; - - chunk.append(c); - marker++; - } - } - - return chunk.toString(); - } - - public int compare(String s1, String s2) { - if ((s1 == null) || (s2 == null)) { - return 0; - } - - int thisMarker = 0; - int thatMarker = 0; - int s1Length = s1.length(); - int s2Length = s2.length(); - - while (thisMarker < s1Length && thatMarker < s2Length) { - String thisChunk = getChunk(s1, s1Length, thisMarker); - thisMarker += thisChunk.length(); - - String thatChunk = getChunk(s2, s2Length, thatMarker); - thatMarker += thatChunk.length(); - - // If both chunks contain numeric characters, sort them numerically - int result = 0; - - if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0))) { - // Simple chunk comparison by length. - int thisChunkLength = thisChunk.length(); - result = thisChunkLength - thatChunk.length(); - - // If equal, the first different number counts - if (result == 0) { - for (int 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; - } - } } From 5afaef7fd8fde84620d830205ad40be2159c160f Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Sun, 8 Sep 2024 10:00:08 +0200 Subject: [PATCH 3/7] Use nest-aware sort for classes and alphanumeric sort everywhere Also adds some missing customization helpers. --- .../fabricmc/mappingio/tree/VisitOrder.java | 111 ++++++++++++------ 1 file changed, 75 insertions(+), 36 deletions(-) diff --git a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java index 37c21acc..bfd00bf1 100644 --- a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java +++ b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java @@ -34,6 +34,9 @@ /** * Visitation order configuration for {@link MappingTreeView#accept(net.fabricmc.mappingio.MappingVisitor, VisitOrder)}. + * + * @apiNote The exposed comparison methods aim to produce the most human-friendly output, + * their sorting order is not guaranteed to be stable across library versions unless specified otherwise. */ public final class VisitOrder { private VisitOrder() { @@ -47,13 +50,14 @@ public static VisitOrder createByInputOrder() { /** * Sorts classes by their source name, members by source name and descriptor, and locals by lv- and lvt-index. - * @param alphanumeric makes sure e.g. {@code mapping10} is ordered after {@code mapping2}. More performance intensive. + * + * @apiNote The sorting order is not guaranteed to be stable across library versions. */ - public static VisitOrder createByName(boolean alphanumeric) { + public static VisitOrder createByName() { return new VisitOrder() - .classesBySrcName(alphanumeric) - .fieldsBySrcNameDesc(alphanumeric) - .methodsBySrcNameDesc(alphanumeric) + .classesBySrcName() + .fieldsBySrcNameDesc() + .methodsBySrcNameDesc() .methodArgsByLvIndex() .methodVarsByLvIndex(); } @@ -66,8 +70,12 @@ public VisitOrder classComparator(Comparator comparator) { return this; } - public VisitOrder classesBySrcName(boolean alphanumeric) { - return classComparator(compareBySrcName(alphanumeric)); + public VisitOrder classesBySrcName() { + return classComparator(compareBySrcName()); + } + + public VisitOrder classesBySrcNameShortFirst() { + return classComparator(compareBySrcNameShortFirst()); } public VisitOrder fieldComparator(Comparator comparator) { @@ -76,8 +84,8 @@ public VisitOrder fieldComparator(Comparator comparator) { return this; } - public VisitOrder fieldsBySrcNameDesc(boolean alphanumeric) { - return fieldComparator(compareBySrcNameDesc(alphanumeric)); + public VisitOrder fieldsBySrcNameDesc() { + return fieldComparator(compareBySrcNameDesc()); } public VisitOrder methodComparator(Comparator comparator) { @@ -86,8 +94,8 @@ public VisitOrder methodComparator(Comparator comparator) { return this; } - public VisitOrder methodsBySrcNameDesc(boolean alphanumeric) { - return methodComparator(compareBySrcNameDesc(alphanumeric)); + public VisitOrder methodsBySrcNameDesc() { + return methodComparator(compareBySrcNameDesc()); } public VisitOrder methodArgComparator(Comparator comparator) { @@ -111,11 +119,16 @@ public VisitOrder methodVarComparator(Comparator comparato } public VisitOrder methodVarsByLvtRowIndex() { - return methodVarComparator(Comparator.comparingInt(MethodVarMappingView::getLvIndex).thenComparingInt(MethodVarMappingView::getLvtRowIndex)); + return methodVarComparator(Comparator + .comparingInt(MethodVarMappingView::getLvIndex) + .thenComparingInt(MethodVarMappingView::getLvtRowIndex)); } public VisitOrder methodVarsByLvIndex() { - return methodVarComparator(Comparator.comparingInt(MethodVarMappingView::getLvIndex).thenComparingInt(MethodVarMappingView::getStartOpIdx)); + return methodVarComparator(Comparator + .comparingInt(MethodVarMappingView::getLvIndex) + .thenComparingInt(MethodVarMappingView::getStartOpIdx) + .thenComparingInt(MethodVarMappingView::getEndOpIdx)); } public VisitOrder methodsFirst(boolean methodsFirst) { @@ -146,28 +159,52 @@ public VisitOrder methodVarsFirst() { return methodVarsFirst(true); } - // customization helpers + // customization helpers (not guaranteed to be stable across versions) - public static Comparator compareBySrcName(boolean alphanumeric) { - return (a, b) -> compare(a.getSrcName(), b.getSrcName(), alphanumeric); + public static Comparator compareBySrcName() { + return (a, b) -> { + if (a instanceof ClassMappingView || b instanceof ClassMappingView) { + return compareNestaware(a.getSrcName(), b.getSrcName(), false); + } else { + return compare(a.getSrcName(), b.getSrcName()); + } + }; } - public static Comparator compareBySrcNameDesc(boolean alphanumeric) { + public static Comparator compareBySrcNameDesc() { return (a, b) -> { - int cmp = compare(a.getSrcName(), b.getSrcName(), alphanumeric); + int cmp = compare(a.getSrcName(), b.getSrcName()); - return cmp != 0 ? cmp : compare(a.getSrcDesc(), b.getSrcDesc(), alphanumeric); + return cmp != 0 ? cmp : compare(a.getSrcDesc(), b.getSrcDesc()); }; } public static Comparator compareBySrcNameShortFirst() { - return (a, b) -> compareShortFirst(a.getSrcName(), b.getSrcName()); + return (a, b) -> { + if (a instanceof ClassMappingView || b instanceof ClassMappingView) { + return compareNestaware(a.getSrcName(), b.getSrcName(), true); + } else { + return compareShortFirst(a.getSrcName(), b.getSrcName()); + } + }; } - public static int compare(@Nullable String a, @Nullable String b, boolean alphanumeric) { + public static Comparator compareBySrcNameDescShortFirst() { + return (a, b) -> { + int cmp = compareShortFirst(a.getSrcName(), b.getSrcName()); + + return cmp != 0 ? cmp : compare(a.getSrcDesc(), b.getSrcDesc()); + }; + } + + public static int compare(@Nullable String a, @Nullable String b) { if (a == null || b == null) return compareNullLast(a, b); - return alphanumeric ? alphanumComparator.compare(a, b) : a.compareTo(b); + return ALPHANUM.compare(a, b); + } + + public static int compare(String a, int startA, int endA, String b, int startB, int endB) { + return ALPHANUM.compare(a.substring(startA, endA), b.substring(startB, endB)); } public static int compareShortFirst(@Nullable String a, @Nullable String b) { @@ -175,7 +212,7 @@ public static int compareShortFirst(@Nullable String a, @Nullable String b) { int cmp = a.length() - b.length(); - return cmp != 0 ? cmp : a.compareTo(b); + return cmp != 0 ? cmp : ALPHANUM.compare(a, b); } public static int compareShortFirst(String a, int startA, int endA, String b, int startB, int endB) { @@ -183,19 +220,18 @@ public static int compareShortFirst(String a, int startA, int endA, String b, in int ret = Integer.compare(lenA, endB - startB); if (ret != 0) return ret; - for (int i = 0; i < lenA; i++) { - char ca = a.charAt(startA + i); - char cb = b.charAt(startB + i); - - if (ca != cb) { - return ca - cb; - } - } + return ALPHANUM.compare(a.substring(startA, endA), b.substring(startB, endB)); + } - return 0; + public static int compareNestaware(@Nullable String a, @Nullable String b) { + return compareNestaware(a, b, false); } public static int compareShortFirstNestaware(@Nullable String a, @Nullable String b) { + return compareNestaware(a, b, true); + } + + private static int compareNestaware(@Nullable String a, @Nullable String b, boolean shortFirst) { if (a == null || b == null) { return compareNullLast(a, b); } @@ -206,8 +242,11 @@ public static int compareShortFirstNestaware(@Nullable String a, @Nullable Strin int endA = a.indexOf('$', pos); int endB = b.indexOf('$', pos); - int ret = compareShortFirst(a, pos, endA >= 0 ? endA : a.length(), - b, pos, endB >= 0 ? endB : b.length()); + int ret = shortFirst + ? compareShortFirst(a, pos, endA >= 0 ? endA : a.length(), + b, pos, endB >= 0 ? endB : b.length()) + : compare(a, pos, endA >= 0 ? endA : a.length(), + b, pos, endB >= 0 ? endB : b.length()); if (ret != 0) { return ret; @@ -231,7 +270,7 @@ public static int compareNullLast(@Nullable String a, @Nullable String b) { } else if (b == null) { // only b null return -1; } else { // neither null - return a.compareTo(b); + return ALPHANUM.compare(a, b); } } @@ -274,7 +313,7 @@ public boolean isMethodVarsFirst() { return methodVarsFirst; } - private static final AlphanumComparator alphanumComparator = new AlphanumComparator(); + private static final AlphanumComparator ALPHANUM = new AlphanumComparator(); private Comparator classComparator; private Comparator fieldComparator; private Comparator methodComparator; From 2e3158355acd7a5b1f9ce6461027780c1116e30c Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Sun, 8 Sep 2024 10:02:34 +0200 Subject: [PATCH 4/7] Use Apache-2.0-licensed sorting algorithm --- .../lib/alphanum/AlphanumComparator.java | 119 -------------- .../lib/alphanum/AlphanumericComparator.java | 151 ++++++++++++++++++ .../fabricmc/mappingio/lib/alphanum/LICENSE | 21 --- .../fabricmc/mappingio/tree/VisitOrder.java | 4 +- 4 files changed, 153 insertions(+), 142 deletions(-) delete mode 100644 src/main/java/net/fabricmc/mappingio/lib/alphanum/AlphanumComparator.java create mode 100644 src/main/java/net/fabricmc/mappingio/lib/alphanum/AlphanumericComparator.java delete mode 100644 src/main/java/net/fabricmc/mappingio/lib/alphanum/LICENSE diff --git a/src/main/java/net/fabricmc/mappingio/lib/alphanum/AlphanumComparator.java b/src/main/java/net/fabricmc/mappingio/lib/alphanum/AlphanumComparator.java deleted file mode 100644 index 62c29fd6..00000000 --- a/src/main/java/net/fabricmc/mappingio/lib/alphanum/AlphanumComparator.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * The Alphanum Algorithm is an improved sorting algorithm for strings - * containing numbers. Instead of sorting 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. - - * This is an updated version with enhancements made by Daniel Migowski, - * Andre Bogus, and David Koelle. Updated by David Koelle in 2017. - * - * Copied from https://web.archive.org/web/20210506213829/http://www.davekoelle.com/files/AlphanumComparator.java, - * with some code style improvements applied. - */ - -package net.fabricmc.mappingio.lib.alphanum; - -import java.util.Comparator; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public class AlphanumComparator implements Comparator { - private boolean isDigit(char ch) { - return ((ch >= 48) && (ch <= 57)); - } - - /** Length of string is passed in for improved efficiency (only need to calculate it once). **/ - private String getChunk(String s, int sLength, int marker) { - StringBuilder chunk = new StringBuilder(); - char c = s.charAt(marker); - chunk.append(c); - marker++; - - if (isDigit(c)) { - while (marker < sLength) { - c = s.charAt(marker); - if (!isDigit(c)) break; - chunk.append(c); - marker++; - } - } else { - while (marker < sLength) { - c = s.charAt(marker); - if (isDigit(c)) break; - - chunk.append(c); - marker++; - } - } - - return chunk.toString(); - } - - public int compare(String s1, String s2) { - if ((s1 == null) || (s2 == null)) { - return 0; - } - - int thisMarker = 0; - int thatMarker = 0; - int s1Length = s1.length(); - int s2Length = s2.length(); - - while (thisMarker < s1Length && thatMarker < s2Length) { - String thisChunk = getChunk(s1, s1Length, thisMarker); - thisMarker += thisChunk.length(); - - String thatChunk = getChunk(s2, s2Length, thatMarker); - thatMarker += thatChunk.length(); - - // If both chunks contain numeric characters, sort them numerically - int result = 0; - - if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0))) { - // Simple chunk comparison by length. - int thisChunkLength = thisChunk.length(); - result = thisChunkLength - thatChunk.length(); - - // If equal, the first different number counts - if (result == 0) { - for (int 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; - } -} diff --git a/src/main/java/net/fabricmc/mappingio/lib/alphanum/AlphanumericComparator.java b/src/main/java/net/fabricmc/mappingio/lib/alphanum/AlphanumericComparator.java new file mode 100644 index 00000000..a53917fd --- /dev/null +++ b/src/main/java/net/fabricmc/mappingio/lib/alphanum/AlphanumericComparator.java @@ -0,0 +1,151 @@ +// Copied from https://github.com/sawano/alphanumeric-comparator/blob/5756d78617d411fbda4c51fe13d410c85392e737/src/main/java/se/sawano/java/text/AlphanumericComparator.java + +/* + * Copyright 2014 Daniel Sawano + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.mappingio.lib.alphanum; + +import static java.nio.CharBuffer.wrap; +import static java.util.Objects.requireNonNull; + +import java.nio.CharBuffer; +import java.text.Collator; +import java.util.Comparator; +import java.util.Locale; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public class AlphanumericComparator implements Comparator { + private final Collator collator; + + /** + * Creates a comparator that will use lexicographical sorting of the non-numerical parts of the compared strings. + */ + public AlphanumericComparator() { + collator = null; + } + + /** + * Creates a comparator that will use locale-sensitive sorting of the non-numerical parts of the compared strings. + * + * @param locale The locale to use. + */ + public AlphanumericComparator(Locale locale) { + this(Collator.getInstance(requireNonNull(locale))); + } + + /** + * Creates a comparator that will use the given collator to sort the non-numerical parts of the compared strings. + * + * @param collator The collator to use. + */ + public AlphanumericComparator(Collator collator) { + this.collator = requireNonNull(collator); + } + + @Override + public int compare(CharSequence s1, CharSequence s2) { + CharBuffer b1 = wrap(s1); + CharBuffer b2 = wrap(s2); + + while (b1.hasRemaining() && b2.hasRemaining()) { + moveWindow(b1); + moveWindow(b2); + int result = compare(b1, b2); + + if (result != 0) { + return result; + } + + prepareForNextIteration(b1); + prepareForNextIteration(b2); + } + + return s1.length() - s2.length(); + } + + private void moveWindow(CharBuffer buffer) { + int start = buffer.position(); + int end = buffer.position(); + boolean isNumerical = isDigit(buffer.get(start)); + + while (end < buffer.limit() && isNumerical == isDigit(buffer.get(end))) { + ++end; + + if (isNumerical && (start + 1 < buffer.limit()) && isZero(buffer.get(start)) && isDigit(buffer.get(end))) { + ++start; // trim leading zeros + } + } + + buffer.position(start).limit(end); + } + + private int compare(CharBuffer b1, CharBuffer b2) { + if (isNumerical(b1) && isNumerical(b2)) { + return compareNumerically(b1, b2); + } + + return compareAsStrings(b1, b2); + } + + private boolean isNumerical(CharBuffer buffer) { + return isDigit(buffer.charAt(0)); + } + + private boolean isDigit(char c) { + if (collator == null) { + int intValue = (int) c; + return intValue >= 48 && intValue <= 57; + } + + return Character.isDigit(c); + } + + private int compareNumerically(CharBuffer b1, CharBuffer b2) { + int diff = b1.length() - b2.length(); + + if (diff != 0) { + return diff; + } + + for (int i = 0; i < b1.remaining() && i < b2.remaining(); ++i) { + int result = Character.compare(b1.charAt(i), b2.charAt(i)); + + if (result != 0) { + return result; + } + } + + return 0; + } + + private void prepareForNextIteration(CharBuffer buffer) { + buffer.position(buffer.limit()).limit(buffer.capacity()); + } + + private int compareAsStrings(CharBuffer b1, CharBuffer b2) { + if (collator != null) { + return collator.compare(b1.toString(), b2.toString()); + } + + return b1.toString().compareTo(b2.toString()); + } + + private boolean isZero(char c) { + return c == '0'; + } +} diff --git a/src/main/java/net/fabricmc/mappingio/lib/alphanum/LICENSE b/src/main/java/net/fabricmc/mappingio/lib/alphanum/LICENSE deleted file mode 100644 index 16187301..00000000 --- a/src/main/java/net/fabricmc/mappingio/lib/alphanum/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -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. diff --git a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java index bfd00bf1..b62ffc55 100644 --- a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java +++ b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java @@ -23,7 +23,7 @@ import org.jetbrains.annotations.Nullable; -import net.fabricmc.mappingio.lib.alphanum.AlphanumComparator; +import net.fabricmc.mappingio.lib.alphanum.AlphanumericComparator; import net.fabricmc.mappingio.tree.MappingTreeView.ClassMappingView; import net.fabricmc.mappingio.tree.MappingTreeView.ElementMappingView; import net.fabricmc.mappingio.tree.MappingTreeView.FieldMappingView; @@ -313,7 +313,7 @@ public boolean isMethodVarsFirst() { return methodVarsFirst; } - private static final AlphanumComparator ALPHANUM = new AlphanumComparator(); + private static final AlphanumericComparator ALPHANUM = new AlphanumericComparator(); private Comparator classComparator; private Comparator fieldComparator; private Comparator methodComparator; From 4868b2935d854e7384d52eb7a5661d5789ffff12 Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Sun, 8 Sep 2024 14:23:06 +0200 Subject: [PATCH 5/7] Use `Locale.ROOT`'s collator to sort non-numerical parts --- src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java index b62ffc55..e44acf91 100644 --- a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java +++ b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Locale; import org.jetbrains.annotations.Nullable; @@ -313,7 +314,7 @@ public boolean isMethodVarsFirst() { return methodVarsFirst; } - private static final AlphanumericComparator ALPHANUM = new AlphanumericComparator(); + private static final AlphanumericComparator ALPHANUM = new AlphanumericComparator(Locale.ROOT); private Comparator classComparator; private Comparator fieldComparator; private Comparator methodComparator; From 33f233dd71fa8024391c381db60755eaf626ecd4 Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Sun, 8 Sep 2024 15:04:02 +0200 Subject: [PATCH 6/7] Add changes to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc08c27d..a23889ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `MappingFormat#features()` to allow for more fine-grained programmatic querying of format capabilities - Added tests to validate our writer outputs against 3rd-party readers - Overhauled the internal `ColumnFileReader` to behave more consistently +- Made `VisitOrder#createByName` use alphanumeric and nest-aware sorting - Made handling of the `NEEDS_MULTIPLE_PASSES` flag more consistent, reducing memory usage in a few cases - Made some internal methods in Enigma and TSRG readers actually private - Made all writers for formats which can't represent empty destination names skip such elements entirely, unless mapped child elements are present From ca04d792c98bd2d82775087c01e4da87622f67b9 Mon Sep 17 00:00:00 2001 From: NebelNidas Date: Mon, 9 Sep 2024 11:27:55 +0200 Subject: [PATCH 7/7] Make `AlphanumericComparator` package-private --- build.gradle | 3 ++- .../alphanum => tree}/AlphanumericComparator.java | 13 +++++-------- .../net/fabricmc/mappingio/tree/VisitOrder.java | 1 - 3 files changed, 7 insertions(+), 10 deletions(-) rename src/main/java/net/fabricmc/mappingio/{lib/alphanum => tree}/AlphanumericComparator.java (91%) diff --git a/build.gradle b/build.gradle index 93f954bd..cee72079 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,8 @@ allprojects { spotless { java { licenseHeaderFile(rootProject.file("HEADER")).yearSeparator(", ") - targetExclude 'src/**/java/net/fabricmc/mappingio/lib/**/*.java' + targetExclude 'src/test/java/net/fabricmc/mappingio/lib/**/*.java', + 'src/main/java/net/fabricmc/mappingio/tree/AlphanumericComparator.java' } } diff --git a/src/main/java/net/fabricmc/mappingio/lib/alphanum/AlphanumericComparator.java b/src/main/java/net/fabricmc/mappingio/tree/AlphanumericComparator.java similarity index 91% rename from src/main/java/net/fabricmc/mappingio/lib/alphanum/AlphanumericComparator.java rename to src/main/java/net/fabricmc/mappingio/tree/AlphanumericComparator.java index a53917fd..863551d0 100644 --- a/src/main/java/net/fabricmc/mappingio/lib/alphanum/AlphanumericComparator.java +++ b/src/main/java/net/fabricmc/mappingio/tree/AlphanumericComparator.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package net.fabricmc.mappingio.lib.alphanum; +package net.fabricmc.mappingio.tree; import static java.nio.CharBuffer.wrap; import static java.util.Objects.requireNonNull; @@ -26,16 +26,13 @@ import java.util.Comparator; import java.util.Locale; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public class AlphanumericComparator implements Comparator { +class AlphanumericComparator implements Comparator { private final Collator collator; /** * Creates a comparator that will use lexicographical sorting of the non-numerical parts of the compared strings. */ - public AlphanumericComparator() { + AlphanumericComparator() { collator = null; } @@ -44,7 +41,7 @@ public AlphanumericComparator() { * * @param locale The locale to use. */ - public AlphanumericComparator(Locale locale) { + AlphanumericComparator(Locale locale) { this(Collator.getInstance(requireNonNull(locale))); } @@ -53,7 +50,7 @@ public AlphanumericComparator(Locale locale) { * * @param collator The collator to use. */ - public AlphanumericComparator(Collator collator) { + AlphanumericComparator(Collator collator) { this.collator = requireNonNull(collator); } diff --git a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java index e44acf91..ca57a548 100644 --- a/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java +++ b/src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java @@ -24,7 +24,6 @@ import org.jetbrains.annotations.Nullable; -import net.fabricmc.mappingio.lib.alphanum.AlphanumericComparator; import net.fabricmc.mappingio.tree.MappingTreeView.ClassMappingView; import net.fabricmc.mappingio.tree.MappingTreeView.ElementMappingView; import net.fabricmc.mappingio.tree.MappingTreeView.FieldMappingView;