Skip to content

Commit d6b7a22

Browse files
authored
Use alphanumeric and nest-aware sorting for VisitOrder#createByName (#36)
1 parent 983c427 commit d6b7a22

File tree

4 files changed

+215
-20
lines changed

4 files changed

+215
-20
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
- Added `MappingFormat#features()` to allow for more fine-grained programmatic querying of format capabilities
99
- Added tests to validate our writer outputs against 3rd-party readers
1010
- Overhauled the internal `ColumnFileReader` to behave more consistently
11+
- Made `VisitOrder#createByName` use alphanumeric and nest-aware sorting
1112
- Made handling of the `NEEDS_MULTIPLE_PASSES` flag more consistent, reducing memory usage in a few cases
1213
- Made some internal methods in Enigma and TSRG readers actually private
1314
- Made all writers for formats which can't represent empty destination names skip such elements entirely, unless mapped child elements are present

build.gradle

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ allprojects {
4444
spotless {
4545
java {
4646
licenseHeaderFile(rootProject.file("HEADER")).yearSeparator(", ")
47-
targetExclude 'src/test/java/net/fabricmc/mappingio/lib/**/*.java'
47+
targetExclude 'src/test/java/net/fabricmc/mappingio/lib/**/*.java',
48+
'src/main/java/net/fabricmc/mappingio/tree/AlphanumericComparator.java'
4849
}
4950
}
5051

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copied from https://github.com/sawano/alphanumeric-comparator/blob/5756d78617d411fbda4c51fe13d410c85392e737/src/main/java/se/sawano/java/text/AlphanumericComparator.java
2+
3+
/*
4+
* Copyright 2014 Daniel Sawano
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package net.fabricmc.mappingio.tree;
20+
21+
import static java.nio.CharBuffer.wrap;
22+
import static java.util.Objects.requireNonNull;
23+
24+
import java.nio.CharBuffer;
25+
import java.text.Collator;
26+
import java.util.Comparator;
27+
import java.util.Locale;
28+
29+
class AlphanumericComparator implements Comparator<CharSequence> {
30+
private final Collator collator;
31+
32+
/**
33+
* Creates a comparator that will use lexicographical sorting of the non-numerical parts of the compared strings.
34+
*/
35+
AlphanumericComparator() {
36+
collator = null;
37+
}
38+
39+
/**
40+
* Creates a comparator that will use locale-sensitive sorting of the non-numerical parts of the compared strings.
41+
*
42+
* @param locale The locale to use.
43+
*/
44+
AlphanumericComparator(Locale locale) {
45+
this(Collator.getInstance(requireNonNull(locale)));
46+
}
47+
48+
/**
49+
* Creates a comparator that will use the given collator to sort the non-numerical parts of the compared strings.
50+
*
51+
* @param collator The collator to use.
52+
*/
53+
AlphanumericComparator(Collator collator) {
54+
this.collator = requireNonNull(collator);
55+
}
56+
57+
@Override
58+
public int compare(CharSequence s1, CharSequence s2) {
59+
CharBuffer b1 = wrap(s1);
60+
CharBuffer b2 = wrap(s2);
61+
62+
while (b1.hasRemaining() && b2.hasRemaining()) {
63+
moveWindow(b1);
64+
moveWindow(b2);
65+
int result = compare(b1, b2);
66+
67+
if (result != 0) {
68+
return result;
69+
}
70+
71+
prepareForNextIteration(b1);
72+
prepareForNextIteration(b2);
73+
}
74+
75+
return s1.length() - s2.length();
76+
}
77+
78+
private void moveWindow(CharBuffer buffer) {
79+
int start = buffer.position();
80+
int end = buffer.position();
81+
boolean isNumerical = isDigit(buffer.get(start));
82+
83+
while (end < buffer.limit() && isNumerical == isDigit(buffer.get(end))) {
84+
++end;
85+
86+
if (isNumerical && (start + 1 < buffer.limit()) && isZero(buffer.get(start)) && isDigit(buffer.get(end))) {
87+
++start; // trim leading zeros
88+
}
89+
}
90+
91+
buffer.position(start).limit(end);
92+
}
93+
94+
private int compare(CharBuffer b1, CharBuffer b2) {
95+
if (isNumerical(b1) && isNumerical(b2)) {
96+
return compareNumerically(b1, b2);
97+
}
98+
99+
return compareAsStrings(b1, b2);
100+
}
101+
102+
private boolean isNumerical(CharBuffer buffer) {
103+
return isDigit(buffer.charAt(0));
104+
}
105+
106+
private boolean isDigit(char c) {
107+
if (collator == null) {
108+
int intValue = (int) c;
109+
return intValue >= 48 && intValue <= 57;
110+
}
111+
112+
return Character.isDigit(c);
113+
}
114+
115+
private int compareNumerically(CharBuffer b1, CharBuffer b2) {
116+
int diff = b1.length() - b2.length();
117+
118+
if (diff != 0) {
119+
return diff;
120+
}
121+
122+
for (int i = 0; i < b1.remaining() && i < b2.remaining(); ++i) {
123+
int result = Character.compare(b1.charAt(i), b2.charAt(i));
124+
125+
if (result != 0) {
126+
return result;
127+
}
128+
}
129+
130+
return 0;
131+
}
132+
133+
private void prepareForNextIteration(CharBuffer buffer) {
134+
buffer.position(buffer.limit()).limit(buffer.capacity());
135+
}
136+
137+
private int compareAsStrings(CharBuffer b1, CharBuffer b2) {
138+
if (collator != null) {
139+
return collator.compare(b1.toString(), b2.toString());
140+
}
141+
142+
return b1.toString().compareTo(b2.toString());
143+
}
144+
145+
private boolean isZero(char c) {
146+
return c == '0';
147+
}
148+
}

src/main/java/net/fabricmc/mappingio/tree/VisitOrder.java

+64-19
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Collection;
2121
import java.util.Comparator;
2222
import java.util.List;
23+
import java.util.Locale;
2324

2425
import org.jetbrains.annotations.Nullable;
2526

@@ -33,6 +34,9 @@
3334

3435
/**
3536
* Visitation order configuration for {@link MappingTreeView#accept(net.fabricmc.mappingio.MappingVisitor, VisitOrder)}.
37+
*
38+
* @apiNote The exposed comparison methods aim to produce the most human-friendly output,
39+
* their sorting order is not guaranteed to be stable across library versions unless specified otherwise.
3640
*/
3741
public final class VisitOrder {
3842
private VisitOrder() {
@@ -44,6 +48,11 @@ public static VisitOrder createByInputOrder() {
4448
return new VisitOrder();
4549
}
4650

51+
/**
52+
* Sorts classes by their source name, members by source name and descriptor, and locals by lv- and lvt-index.
53+
*
54+
* @apiNote The sorting order is not guaranteed to be stable across library versions.
55+
*/
4756
public static VisitOrder createByName() {
4857
return new VisitOrder()
4958
.classesBySrcName()
@@ -65,6 +74,10 @@ public VisitOrder classesBySrcName() {
6574
return classComparator(compareBySrcName());
6675
}
6776

77+
public VisitOrder classesBySrcNameShortFirst() {
78+
return classComparator(compareBySrcNameShortFirst());
79+
}
80+
6881
public VisitOrder fieldComparator(Comparator<FieldMappingView> comparator) {
6982
this.fieldComparator = comparator;
7083

@@ -106,11 +119,16 @@ public VisitOrder methodVarComparator(Comparator<MethodVarMappingView> comparato
106119
}
107120

108121
public VisitOrder methodVarsByLvtRowIndex() {
109-
return methodVarComparator(Comparator.comparingInt(MethodVarMappingView::getLvIndex).thenComparingInt(MethodVarMappingView::getLvtRowIndex));
122+
return methodVarComparator(Comparator
123+
.comparingInt(MethodVarMappingView::getLvIndex)
124+
.thenComparingInt(MethodVarMappingView::getLvtRowIndex));
110125
}
111126

112127
public VisitOrder methodVarsByLvIndex() {
113-
return methodVarComparator(Comparator.comparingInt(MethodVarMappingView::getLvIndex).thenComparingInt(MethodVarMappingView::getStartOpIdx));
128+
return methodVarComparator(Comparator
129+
.comparingInt(MethodVarMappingView::getLvIndex)
130+
.thenComparingInt(MethodVarMappingView::getStartOpIdx)
131+
.thenComparingInt(MethodVarMappingView::getEndOpIdx));
114132
}
115133

116134
public VisitOrder methodsFirst(boolean methodsFirst) {
@@ -141,10 +159,16 @@ public VisitOrder methodVarsFirst() {
141159
return methodVarsFirst(true);
142160
}
143161

144-
// customization helpers
162+
// customization helpers (not guaranteed to be stable across versions)
145163

146164
public static <T extends ElementMappingView> Comparator<T> compareBySrcName() {
147-
return (a, b) -> compare(a.getSrcName(), b.getSrcName());
165+
return (a, b) -> {
166+
if (a instanceof ClassMappingView || b instanceof ClassMappingView) {
167+
return compareNestaware(a.getSrcName(), b.getSrcName(), false);
168+
} else {
169+
return compare(a.getSrcName(), b.getSrcName());
170+
}
171+
};
148172
}
149173

150174
public static <T extends MemberMappingView> Comparator<T> compareBySrcNameDesc() {
@@ -156,41 +180,58 @@ public static <T extends MemberMappingView> Comparator<T> compareBySrcNameDesc()
156180
}
157181

158182
public static <T extends ElementMappingView> Comparator<T> compareBySrcNameShortFirst() {
159-
return (a, b) -> compareShortFirst(a.getSrcName(), b.getSrcName());
183+
return (a, b) -> {
184+
if (a instanceof ClassMappingView || b instanceof ClassMappingView) {
185+
return compareNestaware(a.getSrcName(), b.getSrcName(), true);
186+
} else {
187+
return compareShortFirst(a.getSrcName(), b.getSrcName());
188+
}
189+
};
190+
}
191+
192+
public static <T extends MemberMappingView> Comparator<T> compareBySrcNameDescShortFirst() {
193+
return (a, b) -> {
194+
int cmp = compareShortFirst(a.getSrcName(), b.getSrcName());
195+
196+
return cmp != 0 ? cmp : compare(a.getSrcDesc(), b.getSrcDesc());
197+
};
160198
}
161199

162200
public static int compare(@Nullable String a, @Nullable String b) {
163201
if (a == null || b == null) return compareNullLast(a, b);
164202

165-
return a.compareTo(b);
203+
return ALPHANUM.compare(a, b);
204+
}
205+
206+
public static int compare(String a, int startA, int endA, String b, int startB, int endB) {
207+
return ALPHANUM.compare(a.substring(startA, endA), b.substring(startB, endB));
166208
}
167209

168210
public static int compareShortFirst(@Nullable String a, @Nullable String b) {
169211
if (a == null || b == null) return compareNullLast(a, b);
170212

171213
int cmp = a.length() - b.length();
172214

173-
return cmp != 0 ? cmp : a.compareTo(b);
215+
return cmp != 0 ? cmp : ALPHANUM.compare(a, b);
174216
}
175217

176218
public static int compareShortFirst(String a, int startA, int endA, String b, int startB, int endB) {
177219
int lenA = endA - startA;
178220
int ret = Integer.compare(lenA, endB - startB);
179221
if (ret != 0) return ret;
180222

181-
for (int i = 0; i < lenA; i++) {
182-
char ca = a.charAt(startA + i);
183-
char cb = b.charAt(startB + i);
184-
185-
if (ca != cb) {
186-
return ca - cb;
187-
}
188-
}
223+
return ALPHANUM.compare(a.substring(startA, endA), b.substring(startB, endB));
224+
}
189225

190-
return 0;
226+
public static int compareNestaware(@Nullable String a, @Nullable String b) {
227+
return compareNestaware(a, b, false);
191228
}
192229

193230
public static int compareShortFirstNestaware(@Nullable String a, @Nullable String b) {
231+
return compareNestaware(a, b, true);
232+
}
233+
234+
private static int compareNestaware(@Nullable String a, @Nullable String b, boolean shortFirst) {
194235
if (a == null || b == null) {
195236
return compareNullLast(a, b);
196237
}
@@ -201,8 +242,11 @@ public static int compareShortFirstNestaware(@Nullable String a, @Nullable Strin
201242
int endA = a.indexOf('$', pos);
202243
int endB = b.indexOf('$', pos);
203244

204-
int ret = compareShortFirst(a, pos, endA >= 0 ? endA : a.length(),
205-
b, pos, endB >= 0 ? endB : b.length());
245+
int ret = shortFirst
246+
? compareShortFirst(a, pos, endA >= 0 ? endA : a.length(),
247+
b, pos, endB >= 0 ? endB : b.length())
248+
: compare(a, pos, endA >= 0 ? endA : a.length(),
249+
b, pos, endB >= 0 ? endB : b.length());
206250

207251
if (ret != 0) {
208252
return ret;
@@ -226,7 +270,7 @@ public static int compareNullLast(@Nullable String a, @Nullable String b) {
226270
} else if (b == null) { // only b null
227271
return -1;
228272
} else { // neither null
229-
return a.compareTo(b);
273+
return ALPHANUM.compare(a, b);
230274
}
231275
}
232276

@@ -269,6 +313,7 @@ public boolean isMethodVarsFirst() {
269313
return methodVarsFirst;
270314
}
271315

316+
private static final AlphanumericComparator ALPHANUM = new AlphanumericComparator(Locale.ROOT);
272317
private Comparator<ClassMappingView> classComparator;
273318
private Comparator<FieldMappingView> fieldComparator;
274319
private Comparator<MethodMappingView> methodComparator;

0 commit comments

Comments
 (0)