1//! The 7-bit [ASCII](https://en.wikipedia.org/wiki/ASCII) character encoding standard.
2//!
3//! This is not to be confused with the 8-bit [extended ASCII](https://en.wikipedia.org/wiki/Extended_ASCII) character encoding.
4//!
5//! Even though this module concerns itself with 7-bit ASCII,
6//! functions use `u8` as the type instead of `u7` for convenience and compatibility.
7//! Characters outside of the 7-bit range are gracefully handled (e.g. by returning `false`).
8//!
9//! See also: https://en.wikipedia.org/wiki/ASCII#Character_set
10
11const std = @import("std");
12
13pub const lowercase = "abcdefghijklmnopqrstuvwxyz";
14pub const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
15pub const letters = lowercase ++ uppercase;
16
17/// The C0 control codes of the ASCII encoding.
18///
19/// See also: https://en.wikipedia.org/wiki/C0_and_C1_control_codes and `isControl`
20pub const control_code = struct {
21 /// Null.
22 pub const nul = 0x00;
23 /// Start of Heading.
24 pub const soh = 0x01;
25 /// Start of Text.
26 pub const stx = 0x02;
27 /// End of Text.
28 pub const etx = 0x03;
29 /// End of Transmission.
30 pub const eot = 0x04;
31 /// Enquiry.
32 pub const enq = 0x05;
33 /// Acknowledge.
34 pub const ack = 0x06;
35 /// Bell, Alert.
36 pub const bel = 0x07;
37 /// Backspace.
38 pub const bs = 0x08;
39 /// Horizontal Tab, Tab ('\t').
40 pub const ht = 0x09;
41 /// Line Feed, Newline ('\n').
42 pub const lf = 0x0A;
43 /// Vertical Tab.
44 pub const vt = 0x0B;
45 /// Form Feed.
46 pub const ff = 0x0C;
47 /// Carriage Return ('\r').
48 pub const cr = 0x0D;
49 /// Shift Out.
50 pub const so = 0x0E;
51 /// Shift In.
52 pub const si = 0x0F;
53 /// Data Link Escape.
54 pub const dle = 0x10;
55 /// Device Control One (XON).
56 pub const dc1 = 0x11;
57 /// Device Control Two.
58 pub const dc2 = 0x12;
59 /// Device Control Three (XOFF).
60 pub const dc3 = 0x13;
61 /// Device Control Four.
62 pub const dc4 = 0x14;
63 /// Negative Acknowledge.
64 pub const nak = 0x15;
65 /// Synchronous Idle.
66 pub const syn = 0x16;
67 /// End of Transmission Block
68 pub const etb = 0x17;
69 /// Cancel.
70 pub const can = 0x18;
71 /// End of Medium.
72 pub const em = 0x19;
73 /// Substitute.
74 pub const sub = 0x1A;
75 /// Escape.
76 pub const esc = 0x1B;
77 /// File Separator.
78 pub const fs = 0x1C;
79 /// Group Separator.
80 pub const gs = 0x1D;
81 /// Record Separator.
82 pub const rs = 0x1E;
83 /// Unit Separator.
84 pub const us = 0x1F;
85
86 /// Delete.
87 pub const del = 0x7F;
88
89 /// An alias to `dc1`.
90 pub const xon = dc1;
91 /// An alias to `dc3`.
92 pub const xoff = dc3;
93};
94
95/// Returns whether the character is alphanumeric: A-Z, a-z, or 0-9.
96pub fn isAlphanumeric(c: u8) bool {
97 return switch (c) {
98 '0'...'9', 'A'...'Z', 'a'...'z' => true,
99 else => false,
100 };
101}
102
103/// Returns whether the character is alphabetic: A-Z or a-z.
104pub fn isAlphabetic(c: u8) bool {
105 return switch (c) {
106 'A'...'Z', 'a'...'z' => true,
107 else => false,
108 };
109}
110
111/// Returns whether the character is a control character.
112///
113/// See also: `control_code`
114pub fn isControl(c: u8) bool {
115 return c <= control_code.us or c == control_code.del;
116}
117
118/// Returns whether the character is a digit.
119pub fn isDigit(c: u8) bool {
120 return switch (c) {
121 '0'...'9' => true,
122 else => false,
123 };
124}
125
126/// Returns whether the character is a lowercase letter.
127pub fn isLower(c: u8) bool {
128 return switch (c) {
129 'a'...'z' => true,
130 else => false,
131 };
132}
133
134/// Returns whether the character is printable and has some graphical representation,
135/// including the space character.
136pub fn isPrint(c: u8) bool {
137 return isAscii(c) and !isControl(c);
138}
139
140/// Returns whether the character has some graphical representation,
141pub fn isGraphical(c: u8) bool {
142 return isPrint(c) and c != ' ';
143}
144
145/// Returns whether the character is a punctuation character.
146pub fn isPunctuation(c: u8) bool {
147 return isGraphical(c) and !isAlphanumeric(c);
148}
149
150/// Returns whether this character is included in `whitespace`.
151pub fn isWhitespace(c: u8) bool {
152 return switch (c) {
153 ' ', '\t'...'\r' => true,
154 else => false,
155 };
156}
157
158/// Whitespace for general use.
159/// This may be used with e.g. `std.mem.trim` to trim whitespace.
160///
161/// See also: `isWhitespace`
162pub const whitespace = [_]u8{ ' ', '\t', '\n', '\r', control_code.vt, control_code.ff };
163
164test whitespace {
165 for (whitespace) |char| try std.testing.expect(isWhitespace(char));
166
167 var i: u8 = 0;
168 while (isAscii(i)) : (i += 1) {
169 if (isWhitespace(i)) try std.testing.expect(std.mem.findScalar(u8, &whitespace, i) != null);
170 }
171}
172
173/// Returns whether the character is an uppercase letter.
174pub fn isUpper(c: u8) bool {
175 return switch (c) {
176 'A'...'Z' => true,
177 else => false,
178 };
179}
180
181/// Returns whether the character is a hexadecimal digit: A-F, a-f, or 0-9.
182pub fn isHex(c: u8) bool {
183 return switch (c) {
184 '0'...'9', 'A'...'F', 'a'...'f' => true,
185 else => false,
186 };
187}
188
189/// Returns whether the character is a 7-bit ASCII character.
190pub fn isAscii(c: u8) bool {
191 return c < 128;
192}
193
194/// Uppercases the character and returns it as-is if already uppercase or not a letter.
195pub fn toUpper(c: u8) u8 {
196 const mask = @as(u8, @intFromBool(isLower(c))) << 5;
197 return c ^ mask;
198}
199
200/// Lowercases the character and returns it as-is if already lowercase or not a letter.
201pub fn toLower(c: u8) u8 {
202 const mask = @as(u8, @intFromBool(isUpper(c))) << 5;
203 return c | mask;
204}
205
206test "ASCII character classes" {
207 const testing = std.testing;
208
209 try testing.expect(!isControl('a'));
210 try testing.expect(!isControl('z'));
211 try testing.expect(!isControl(' '));
212 try testing.expect(isControl(control_code.nul));
213 try testing.expect(isControl(control_code.ff));
214 try testing.expect(isControl(control_code.us));
215 try testing.expect(isControl(control_code.del));
216 try testing.expect(!isControl(0x80));
217 try testing.expect(!isControl(0xff));
218
219 try testing.expect('C' == toUpper('c'));
220 try testing.expect(':' == toUpper(':'));
221 try testing.expect('\xab' == toUpper('\xab'));
222 try testing.expect(!isUpper('z'));
223 try testing.expect(!isUpper(0x80));
224 try testing.expect(!isUpper(0xff));
225
226 try testing.expect('c' == toLower('C'));
227 try testing.expect(':' == toLower(':'));
228 try testing.expect('\xab' == toLower('\xab'));
229 try testing.expect(!isLower('Z'));
230 try testing.expect(!isLower(0x80));
231 try testing.expect(!isLower(0xff));
232
233 try testing.expect(isAlphanumeric('Z'));
234 try testing.expect(isAlphanumeric('z'));
235 try testing.expect(isAlphanumeric('5'));
236 try testing.expect(isAlphanumeric('a'));
237 try testing.expect(!isAlphanumeric('!'));
238 try testing.expect(!isAlphanumeric(0x80));
239 try testing.expect(!isAlphanumeric(0xff));
240
241 try testing.expect(!isAlphabetic('5'));
242 try testing.expect(isAlphabetic('c'));
243 try testing.expect(!isAlphabetic('@'));
244 try testing.expect(isAlphabetic('Z'));
245 try testing.expect(!isAlphabetic(0x80));
246 try testing.expect(!isAlphabetic(0xff));
247
248 try testing.expect(isWhitespace(' '));
249 try testing.expect(isWhitespace('\t'));
250 try testing.expect(isWhitespace('\r'));
251 try testing.expect(isWhitespace('\n'));
252 try testing.expect(isWhitespace(control_code.ff));
253 try testing.expect(!isWhitespace('.'));
254 try testing.expect(!isWhitespace(control_code.us));
255 try testing.expect(!isWhitespace(0x80));
256 try testing.expect(!isWhitespace(0xff));
257
258 try testing.expect(!isHex('g'));
259 try testing.expect(isHex('b'));
260 try testing.expect(isHex('F'));
261 try testing.expect(isHex('9'));
262 try testing.expect(!isHex(0x80));
263 try testing.expect(!isHex(0xff));
264
265 try testing.expect(!isDigit('~'));
266 try testing.expect(isDigit('0'));
267 try testing.expect(isDigit('9'));
268 try testing.expect(!isDigit(0x80));
269 try testing.expect(!isDigit(0xff));
270
271 try testing.expect(isPrint(' '));
272 try testing.expect(isPrint('@'));
273 try testing.expect(isPrint('~'));
274 try testing.expect(!isPrint(control_code.esc));
275 try testing.expect(!isPrint(0x80));
276 try testing.expect(!isPrint(0xff));
277
278 try testing.expect(isGraphical('@'));
279 try testing.expect(isGraphical('!'));
280 try testing.expect(!isGraphical(' '));
281
282 try testing.expect(isPunctuation('@'));
283 try testing.expect(isPunctuation('!'));
284 try testing.expect(isPunctuation(';'));
285 try testing.expect(isPunctuation(','));
286 try testing.expect(!isPunctuation('A'));
287 try testing.expect(!isPunctuation('8'));
288}
289
290/// Writes a lower case copy of `ascii_string` to `output`.
291/// Asserts `output.len >= ascii_string.len`.
292pub fn lowerString(output: []u8, ascii_string: []const u8) []u8 {
293 std.debug.assert(output.len >= ascii_string.len);
294 for (ascii_string, 0..) |c, i| {
295 output[i] = toLower(c);
296 }
297 return output[0..ascii_string.len];
298}
299
300test lowerString {
301 var buf: [1024]u8 = undefined;
302 const result = lowerString(&buf, "aBcDeFgHiJkLmNOPqrst0234+💩!");
303 try std.testing.expectEqualStrings("abcdefghijklmnopqrst0234+💩!", result);
304}
305
306/// Allocates a lower case copy of `ascii_string`.
307/// Caller owns returned string and must free with `allocator`.
308pub fn allocLowerString(allocator: std.mem.Allocator, ascii_string: []const u8) ![]u8 {
309 const result = try allocator.alloc(u8, ascii_string.len);
310 return lowerString(result, ascii_string);
311}
312
313test allocLowerString {
314 const result = try allocLowerString(std.testing.allocator, "aBcDeFgHiJkLmNOPqrst0234+💩!");
315 defer std.testing.allocator.free(result);
316 try std.testing.expectEqualStrings("abcdefghijklmnopqrst0234+💩!", result);
317}
318
319/// Writes an upper case copy of `ascii_string` to `output`.
320/// Asserts `output.len >= ascii_string.len`.
321pub fn upperString(output: []u8, ascii_string: []const u8) []u8 {
322 std.debug.assert(output.len >= ascii_string.len);
323 for (ascii_string, 0..) |c, i| {
324 output[i] = toUpper(c);
325 }
326 return output[0..ascii_string.len];
327}
328
329test upperString {
330 var buf: [1024]u8 = undefined;
331 const result = upperString(&buf, "aBcDeFgHiJkLmNOPqrst0234+💩!");
332 try std.testing.expectEqualStrings("ABCDEFGHIJKLMNOPQRST0234+💩!", result);
333}
334
335/// Allocates an upper case copy of `ascii_string`.
336/// Caller owns returned string and must free with `allocator`.
337pub fn allocUpperString(allocator: std.mem.Allocator, ascii_string: []const u8) ![]u8 {
338 const result = try allocator.alloc(u8, ascii_string.len);
339 return upperString(result, ascii_string);
340}
341
342test allocUpperString {
343 const result = try allocUpperString(std.testing.allocator, "aBcDeFgHiJkLmNOPqrst0234+💩!");
344 defer std.testing.allocator.free(result);
345 try std.testing.expectEqualStrings("ABCDEFGHIJKLMNOPQRST0234+💩!", result);
346}
347
348/// Compares strings `a` and `b` case-insensitively and returns whether they are equal.
349pub fn eqlIgnoreCase(a: []const u8, b: []const u8) bool {
350 if (a.len != b.len) return false;
351 for (a, 0..) |a_c, i| {
352 if (toLower(a_c) != toLower(b[i])) return false;
353 }
354 return true;
355}
356
357test eqlIgnoreCase {
358 try std.testing.expect(eqlIgnoreCase("HEl💩Lo!", "hel💩lo!"));
359 try std.testing.expect(!eqlIgnoreCase("hElLo!", "hello! "));
360 try std.testing.expect(!eqlIgnoreCase("hElLo!", "helro!"));
361}
362
363pub fn startsWithIgnoreCase(haystack: []const u8, needle: []const u8) bool {
364 return if (needle.len > haystack.len) false else eqlIgnoreCase(haystack[0..needle.len], needle);
365}
366
367test startsWithIgnoreCase {
368 try std.testing.expect(startsWithIgnoreCase("boB", "Bo"));
369 try std.testing.expect(!startsWithIgnoreCase("Needle in hAyStAcK", "haystack"));
370}
371
372pub fn endsWithIgnoreCase(haystack: []const u8, needle: []const u8) bool {
373 return if (needle.len > haystack.len) false else eqlIgnoreCase(haystack[haystack.len - needle.len ..], needle);
374}
375
376test endsWithIgnoreCase {
377 try std.testing.expect(endsWithIgnoreCase("Needle in HaYsTaCk", "haystack"));
378 try std.testing.expect(!endsWithIgnoreCase("BoB", "Bo"));
379}
380
381/// Finds `needle` in `haystack`, ignoring case, starting at index 0.
382pub fn findIgnoreCase(haystack: []const u8, needle: []const u8) ?usize {
383 return findIgnoreCasePos(haystack, 0, needle);
384}
385
386/// Finds `needle` in `haystack`, ignoring case, starting at `start_index`.
387/// Uses Boyer-Moore-Horspool algorithm on large inputs; `findIgnoreCasePosLinear` on small inputs.
388pub fn findIgnoreCasePos(haystack: []const u8, start_index: usize, needle: []const u8) ?usize {
389 if (needle.len > haystack.len) return null;
390 if (needle.len == 0) return start_index;
391
392 if (haystack.len < 52 or needle.len <= 4)
393 return findIgnoreCasePosLinear(haystack, start_index, needle);
394
395 var skip_table: [256]usize = undefined;
396 boyerMooreHorspoolPreprocessIgnoreCase(needle, skip_table[0..]);
397
398 var i: usize = start_index;
399 while (i <= haystack.len - needle.len) {
400 if (eqlIgnoreCase(haystack[i .. i + needle.len], needle)) return i;
401 i += skip_table[toLower(haystack[i + needle.len - 1])];
402 }
403
404 return null;
405}
406
407/// Consider using `findIgnoreCasePos` instead of this, which will automatically use a
408/// more sophisticated algorithm on larger inputs.
409pub fn findIgnoreCasePosLinear(haystack: []const u8, start_index: usize, needle: []const u8) ?usize {
410 var i: usize = start_index;
411 const end = haystack.len - needle.len;
412 while (i <= end) : (i += 1) {
413 if (eqlIgnoreCase(haystack[i .. i + needle.len], needle)) return i;
414 }
415 return null;
416}
417
418fn boyerMooreHorspoolPreprocessIgnoreCase(pattern: []const u8, table: *[256]usize) void {
419 for (table) |*c| {
420 c.* = pattern.len;
421 }
422
423 var i: usize = 0;
424 // The last item is intentionally ignored and the skip size will be pattern.len.
425 // This is the standard way Boyer-Moore-Horspool is implemented.
426 while (i < pattern.len - 1) : (i += 1) {
427 table[toLower(pattern[i])] = pattern.len - 1 - i;
428 }
429}
430
431test findIgnoreCase {
432 try std.testing.expect(findIgnoreCase("one Two Three Four", "foUr").? == 14);
433 try std.testing.expect(findIgnoreCase("one two three FouR", "gOur") == null);
434 try std.testing.expect(findIgnoreCase("foO", "Foo").? == 0);
435 try std.testing.expect(findIgnoreCase("foo", "fool") == null);
436 try std.testing.expect(findIgnoreCase("FOO foo", "fOo").? == 0);
437
438 try std.testing.expect(findIgnoreCase("one two three four five six seven eight nine ten eleven", "ThReE fOUr").? == 8);
439 try std.testing.expect(findIgnoreCase("one two three four five six seven eight nine ten eleven", "Two tWo") == null);
440}
441
442/// Returns the lexicographical order of two slices. O(n).
443pub fn orderIgnoreCase(lhs: []const u8, rhs: []const u8) std.math.Order {
444 if (lhs.ptr != rhs.ptr) {
445 const n = @min(lhs.len, rhs.len);
446 var i: usize = 0;
447 while (i < n) : (i += 1) {
448 switch (std.math.order(toLower(lhs[i]), toLower(rhs[i]))) {
449 .eq => continue,
450 .lt => return .lt,
451 .gt => return .gt,
452 }
453 }
454 }
455 return std.math.order(lhs.len, rhs.len);
456}
457
458/// Returns the lexicographical order of two many-item pointers with NUL-termination. O(n).
459pub fn orderIgnoreCaseZ(lhs: [*:0]const u8, rhs: [*:0]const u8) std.math.Order {
460 return boundedOrderIgnoreCaseZ(lhs, rhs, std.math.maxInt(usize));
461}
462
463test orderIgnoreCaseZ {
464 try std.testing.expect(orderIgnoreCaseZ("aBcD", "Bee") == .lt);
465 try std.testing.expect(orderIgnoreCaseZ("AbC", "aBc") == .eq);
466 try std.testing.expect(orderIgnoreCaseZ("abC", "aBc0") == .lt);
467 try std.testing.expect(orderIgnoreCaseZ("", "") == .eq);
468 try std.testing.expect(orderIgnoreCaseZ("", "a") == .lt);
469
470 const s: [*:0]const u8 = "Abc";
471 try std.testing.expect(orderIgnoreCaseZ(s, s) == .eq);
472}
473
474/// Returns the lexicographical order of two many-item pointers with NUL-termination until some specified bound. O(n).
475pub fn boundedOrderIgnoreCaseZ(lhs: [*:0]const u8, rhs: [*:0]const u8, bound: usize) std.math.Order {
476 if (lhs == rhs) return .eq;
477 var i: usize = 0;
478 while (i < bound and toLower(lhs[i]) == toLower(rhs[i]) and lhs[i] != 0) : (i += 1) {}
479 return if (i < bound) std.math.order(toLower(lhs[i]), toLower(rhs[i])) else .eq;
480}
481
482/// Returns whether the lexicographical order of `lhs` is lower than `rhs`.
483pub fn lessThanIgnoreCase(lhs: []const u8, rhs: []const u8) bool {
484 return orderIgnoreCase(lhs, rhs) == .lt;
485}
486
487pub const HexEscape = struct {
488 bytes: []const u8,
489 charset: *const [16]u8,
490
491 pub const upper_charset = "0123456789ABCDEF";
492 pub const lower_charset = "0123456789abcdef";
493
494 pub fn format(se: HexEscape, w: *std.Io.Writer) std.Io.Writer.Error!void {
495 const charset = se.charset;
496
497 var buf: [4]u8 = undefined;
498 buf[0] = '\\';
499 buf[1] = 'x';
500
501 for (se.bytes) |c| {
502 if (std.ascii.isPrint(c)) {
503 try w.writeByte(c);
504 } else {
505 buf[2] = charset[c >> 4];
506 buf[3] = charset[c & 15];
507 try w.writeAll(&buf);
508 }
509 }
510 }
511};
512
513/// Replaces non-ASCII bytes with hex escapes.
514pub fn hexEscape(bytes: []const u8, case: std.fmt.Case) HexEscape {
515 return .{ .bytes = bytes, .charset = switch (case) {
516 .lower => HexEscape.lower_charset,
517 .upper => HexEscape.upper_charset,
518 } };
519}
520
521test hexEscape {
522 try std.testing.expectFmt("abc 123", "{f}", .{hexEscape("abc 123", .lower)});
523 try std.testing.expectFmt("ab\\xffc", "{f}", .{hexEscape("ab\xffc", .lower)});
524 try std.testing.expectFmt("abc 123", "{f}", .{hexEscape("abc 123", .upper)});
525 try std.testing.expectFmt("ab\\xFFc", "{f}", .{hexEscape("ab\xffc", .upper)});
526}