forked from
rockorager.dev/lsr
this repo has no description
1//! This file is a port of C implementaion that can be found here
2//! https://github.com/sourcefrog/natsort.
3const std = @import("std");
4const isSpace = std.ascii.isWhitespace;
5const isDigit = std.ascii.isDigit;
6const Order = std.math.Order;
7const testing = std.testing;
8
9pub fn order(a: []const u8, b: []const u8) Order {
10 return natOrder(a, b, false);
11}
12
13pub fn orderIgnoreCase(a: []const u8, b: []const u8) Order {
14 return natOrder(a, b, true);
15}
16
17fn natOrder(a: []const u8, b: []const u8, comptime fold_case: bool) Order {
18 var ai: usize = 0;
19 var bi: usize = 0;
20
21 while (true) : ({
22 ai += 1;
23 bi += 1;
24 }) {
25 var ca = if (ai == a.len) 0 else a[ai];
26 var cb = if (bi == b.len) 0 else b[bi];
27
28 while (isSpace(ca)) {
29 ai += 1;
30 ca = if (ai == a.len) 0 else a[ai];
31 }
32
33 while (isSpace(cb)) {
34 bi += 1;
35 cb = if (bi == b.len) 0 else b[bi];
36 }
37
38 if (isDigit(ca) and isDigit(cb)) {
39 const fractional = ca == '0' or cb == '0';
40
41 if (fractional) {
42 const result = compareLeft(a[ai..], b[bi..]);
43 if (result != .eq) return result;
44 } else {
45 const result = compareRight(a[ai..], b[bi..]);
46 if (result != .eq) return result;
47 }
48 }
49
50 if (ca == 0 and cb == 0) {
51 return .eq;
52 }
53
54 if (fold_case) {
55 ca = std.ascii.toUpper(ca);
56 cb = std.ascii.toUpper(cb);
57 }
58
59 if (ca < cb) {
60 return .lt;
61 }
62
63 if (ca > cb) {
64 return .gt;
65 }
66 }
67}
68
69fn compareLeft(a: []const u8, b: []const u8) Order {
70 var i: usize = 0;
71 while (true) : (i += 1) {
72 const ca = if (i == a.len) 0 else a[i];
73 const cb = if (i == b.len) 0 else b[i];
74
75 if (!isDigit(ca) and !isDigit(cb)) {
76 return .eq;
77 }
78 if (!isDigit(ca)) {
79 return .lt;
80 }
81 if (!isDigit(cb)) {
82 return .gt;
83 }
84 if (ca < cb) {
85 return .lt;
86 }
87 if (ca > cb) {
88 return .gt;
89 }
90 }
91
92 return .eq;
93}
94
95fn compareRight(a: []const u8, b: []const u8) Order {
96 var bias = Order.eq;
97
98 var i: usize = 0;
99 while (true) : (i += 1) {
100 const ca = if (i == a.len) 0 else a[i];
101 const cb = if (i == b.len) 0 else b[i];
102
103 if (!isDigit(ca) and !isDigit(cb)) {
104 return bias;
105 }
106 if (!isDigit(ca)) {
107 return .lt;
108 }
109 if (!isDigit(cb)) {
110 return .gt;
111 }
112
113 if (ca < cb) {
114 if (bias != .eq) {
115 bias = .lt;
116 }
117 } else if (ca > cb) {
118 if (bias != .eq) {
119 bias = .gt;
120 }
121 } else if (ca == 0 and cb == 0) {
122 return bias;
123 }
124 }
125
126 return .eq;
127}
128
129const SortContext = struct {
130 ignore_case: bool = false,
131 reverse: bool = false,
132
133 fn compare(self: @This(), a: []const u8, b: []const u8) bool {
134 const ord: std.math.Order = if (self.reverse) .gt else .lt;
135 if (self.ignore_case) {
136 return orderIgnoreCase(a, b) == ord;
137 } else {
138 return order(a, b) == ord;
139 }
140 }
141};
142
143test "lt" {
144 try testing.expectEqual(Order.lt, order("a_1", "a_10"));
145}
146
147test "eq" {
148 try testing.expectEqual(Order.eq, order("a_1", "a_1"));
149}
150
151test "gt" {
152 try testing.expectEqual(Order.gt, order("a_10", "a_1"));
153}
154
155fn sortAndAssert(context: SortContext, input: [][]const u8, want: []const []const u8) !void {
156 std.sort.pdq([]const u8, input, context, SortContext.compare);
157
158 for (input, want) |actual, expected| {
159 try testing.expectEqualStrings(expected, actual);
160 }
161}
162
163test "sorting" {
164 const context = SortContext{};
165 var items = [_][]const u8{
166 "item100",
167 "item10",
168 "item1",
169 };
170 const want = [_][]const u8{
171 "item1",
172 "item10",
173 "item100",
174 };
175
176 try sortAndAssert(context, &items, &want);
177}
178
179test "sorting 2" {
180 const context = SortContext{};
181 var items = [_][]const u8{
182 "item_30",
183 "item_15",
184 "item_3",
185 "item_2",
186 "item_10",
187 };
188 const want = [_][]const u8{
189 "item_2",
190 "item_3",
191 "item_10",
192 "item_15",
193 "item_30",
194 };
195
196 try sortAndAssert(context, &items, &want);
197}
198
199test "leading zeros" {
200 const context = SortContext{};
201 var items = [_][]const u8{
202 "item100",
203 "item999",
204 "item001",
205 "item010",
206 "item000",
207 };
208 const want = [_][]const u8{
209 "item000",
210 "item001",
211 "item010",
212 "item100",
213 "item999",
214 };
215
216 try sortAndAssert(context, &items, &want);
217}
218
219test "dates" {
220 const context = SortContext{};
221 var items = [_][]const u8{
222 "2000-1-10",
223 "2000-1-2",
224 "1999-12-25",
225 "2000-3-23",
226 "1999-3-3",
227 };
228 const want = [_][]const u8{
229 "1999-3-3",
230 "1999-12-25",
231 "2000-1-2",
232 "2000-1-10",
233 "2000-3-23",
234 };
235
236 try sortAndAssert(context, &items, &want);
237}
238
239test "fractions" {
240 const context = SortContext{};
241 var items = [_][]const u8{
242 "Fractional release numbers",
243 "1.011.02",
244 "1.010.12",
245 "1.009.02",
246 "1.009.20",
247 "1.009.10",
248 "1.002.08",
249 "1.002.03",
250 "1.002.01",
251 };
252 const want = [_][]const u8{
253 "1.002.01",
254 "1.002.03",
255 "1.002.08",
256 "1.009.02",
257 "1.009.10",
258 "1.009.20",
259 "1.010.12",
260 "1.011.02",
261 "Fractional release numbers",
262 };
263
264 try sortAndAssert(context, &items, &want);
265}
266
267test "words" {
268 const context = SortContext{};
269 var items = [_][]const u8{
270 "fred",
271 "pic2",
272 "pic100a",
273 "pic120",
274 "pic121",
275 "jane",
276 "tom",
277 "pic02a",
278 "pic3",
279 "pic4",
280 "1-20",
281 "pic100",
282 "pic02000",
283 "10-20",
284 "1-02",
285 "1-2",
286 "x2-y7",
287 "x8-y8",
288 "x2-y08",
289 "x2-g8",
290 "pic01",
291 "pic02",
292 "pic 6",
293 "pic 7",
294 "pic 5",
295 "pic05",
296 "pic 5 ",
297 "pic 5 something",
298 "pic 4 else",
299 };
300 const want = [_][]const u8{
301 "1-02",
302 "1-2",
303 "1-20",
304 "10-20",
305 "fred",
306 "jane",
307 "pic01",
308 "pic02",
309 "pic02a",
310 "pic02000",
311 "pic05",
312 "pic2",
313 "pic3",
314 "pic4",
315 "pic 4 else",
316 "pic 5",
317 "pic 5 ",
318 "pic 5 something",
319 "pic 6",
320 "pic 7",
321 "pic100",
322 "pic100a",
323 "pic120",
324 "pic121",
325 "tom",
326 "x2-g8",
327 "x2-y08",
328 "x2-y7",
329 "x8-y8",
330 };
331
332 try sortAndAssert(context, &items, &want);
333}
334
335test "fuzz" {
336 const Context = struct {
337 fn testOne(context: @This(), input: []const u8) anyerror!void {
338 _ = context;
339
340 const a = input[0..(input.len / 2)];
341 const b = input[(input.len / 2)..];
342 _ = order(a, b);
343 }
344 };
345
346 try std.testing.fuzz(Context{}, Context.testOne, .{});
347}