| 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 | |
| 11 | const std = @import("std"); |
| 12 | |
| 13 | pub const lowercase = "abcdefghijklmnopqrstuvwxyz"; |
| 14 | pub const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; |
| 15 | pub 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` |
| 20 | pub 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. |
| 96 | pub 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. |
| 104 | pub 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` |
| 114 | pub 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. |
| 119 | pub 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. |
| 127 | pub 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. |
| 136 | pub fn isPrint(c: u8) bool { |
| 137 | return isAscii(c) and !isControl(c); |
| 138 | } |
| 139 | |
| 140 | /// Returns whether the character has some graphical representation, |
| 141 | pub fn isGraphical(c: u8) bool { |
| 142 | return isPrint(c) and c != ' '; |
| 143 | } |
| 144 | |
| 145 | /// Returns whether the character is a punctuation character. |
| 146 | pub fn isPunctuation(c: u8) bool { |
| 147 | return isGraphical(c) and !isAlphanumeric(c); |
| 148 | } |
| 149 | |
| 150 | /// Returns whether this character is included in `whitespace`. |
| 151 | pub 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` |
| 162 | pub const whitespace = [_]u8{ ' ', '\t', '\n', '\r', control_code.vt, control_code.ff }; |
| 163 | |
| 164 | test 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. |
| 174 | pub 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. |
| 182 | pub 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. |
| 190 | pub 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. |
| 195 | pub 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. |
| 201 | pub fn toLower(c: u8) u8 { |
| 202 | const mask = @as(u8, @intFromBool(isUpper(c))) << 5; |
| 203 | return c | mask; |
| 204 | } |
| 205 | |
| 206 | test "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`. |
| 292 | pub 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 | |
| 300 | test 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`. |
| 308 | pub 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 | |
| 313 | test 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`. |
| 321 | pub 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 | |
| 329 | test 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`. |
| 337 | pub 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 | |
| 342 | test 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. |
| 349 | pub 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 | |
| 357 | test 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 | |
| 363 | pub 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 | |
| 367 | test startsWithIgnoreCase { |
| 368 | try std.testing.expect(startsWithIgnoreCase("boB", "Bo")); |
| 369 | try std.testing.expect(!startsWithIgnoreCase("Needle in hAyStAcK", "haystack")); |
| 370 | } |
| 371 | |
| 372 | pub 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 | |
| 376 | test 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. |
| 382 | pub 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. |
| 388 | pub 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. |
| 409 | pub 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 | |
| 418 | fn 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 | |
| 431 | test 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). |
| 443 | pub 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). |
| 459 | pub fn orderIgnoreCaseZ(lhs: [*:0]const u8, rhs: [*:0]const u8) std.math.Order { |
| 460 | return boundedOrderIgnoreCaseZ(lhs, rhs, std.math.maxInt(usize)); |
| 461 | } |
| 462 | |
| 463 | test 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). |
| 475 | pub 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`. |
| 483 | pub fn lessThanIgnoreCase(lhs: []const u8, rhs: []const u8) bool { |
| 484 | return orderIgnoreCase(lhs, rhs) == .lt; |
| 485 | } |
| 486 | |
| 487 | pub 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. |
| 514 | pub 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 | |
| 521 | test 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 | } |