const std = @import("std"); const expectEqualStrings = std.testing.expectEqualStrings; pub fn fromPascalCase(allocator: std.mem.Allocator, name: []const u8) ![]u8 { const rc = try allocator.alloc(u8, name.len * 2); // This is overkill, but is > the maximum length possibly needed errdefer allocator.free(rc); var utf8_name = (std.unicode.Utf8View.init(name) catch unreachable).iterator(); var target_inx: u64 = 0; var curr_char = (try isAscii(utf8_name.nextCodepoint())).?; target_inx = setNext(lowercase(curr_char), rc, target_inx); var prev_char = curr_char; if (try isAscii(utf8_name.nextCodepoint())) |ch| { curr_char = ch; } else { // Single character only - we're done here _ = setNext(0, rc, target_inx); return rc[0..target_inx]; } while (try isAscii(utf8_name.nextCodepoint())) |next_char| { if (next_char == ' ') { // a space shouldn't be happening. But if it does, it clues us // in pretty well: // // MyStuff Is Awesome // |^ // |next_char // ^ // prev_codepoint/ascii_prev_char (and target_inx) target_inx = setNext(lowercase(curr_char), rc, target_inx); target_inx = setNext('_', rc, target_inx); curr_char = (try isAscii(utf8_name.nextCodepoint())).?; target_inx = setNext(lowercase(curr_char), rc, target_inx); prev_char = curr_char; curr_char = (try isAscii(utf8_name.nextCodepoint())).?; continue; } if (between(curr_char, 'A', 'Z')) { if (isAcronym(curr_char, next_char)) { // We could be in an acronym at the start of a word. This // is the only case where we actually need to look back at the // previous character, and if that's the case, throw in an // underscore // "SAMLMySAMLAcronymThing"); if (between(prev_char, 'a', 'z')) target_inx = setNext('_', rc, target_inx); //we are in an acronym - don't snake, just lower target_inx = setNext(lowercase(curr_char), rc, target_inx); } else { target_inx = setNext('_', rc, target_inx); target_inx = setNext(lowercase(curr_char), rc, target_inx); } } else { target_inx = setNext(curr_char, rc, target_inx); } prev_char = curr_char; curr_char = next_char; } // work in the last codepoint - force lowercase target_inx = setNext(lowercase(curr_char), rc, target_inx); rc[target_inx] = 0; return rc[0..target_inx]; } fn isAcronym(char1: u8, char2: u8) bool { return isAcronymChar(char1) and isAcronymChar(char2); } fn isAcronymChar(char: u8) bool { return between(char, 'A', 'Z') or between(char, '0', '9'); } fn isAscii(codepoint: ?u21) !?u8 { if (codepoint) |cp| { if (cp > 0xff) return error.UnicodeNotSupported; return @as(u8, @truncate(cp)); } return null; } fn setNext(ascii: u8, slice: []u8, inx: u64) u64 { slice[inx] = ascii; return inx + 1; } fn lowercase(ascii: u8) u8 { var lowercase_char = ascii; if (between(ascii, 'A', 'Z')) lowercase_char = ascii + ('a' - 'A'); return lowercase_char; } fn between(char: u8, from: u8, to: u8) bool { return char >= from and char <= to; } test "converts from PascalCase to snake_case" { const allocator = std.testing.allocator; const snake_case = try fromPascalCase(allocator, "MyPascalCaseThing"); defer allocator.free(snake_case); try expectEqualStrings("my_pascal_case_thing", snake_case); } test "handles from PascalCase acronyms to snake_case" { const allocator = std.testing.allocator; const snake_case = try fromPascalCase(allocator, "SAMLMySAMLAcronymThing"); defer allocator.free(snake_case); try expectEqualStrings("saml_my_saml_acronym_thing", snake_case); } test "spaces in the name" { const allocator = std.testing.allocator; const snake_case = try fromPascalCase(allocator, "API Gateway"); defer allocator.free(snake_case); try expectEqualStrings("api_gateway", snake_case); } test "S3" { const allocator = std.testing.allocator; const snake_case = try fromPascalCase(allocator, "S3"); defer allocator.free(snake_case); try expectEqualStrings("s3", snake_case); } test "ec2" { const allocator = std.testing.allocator; const snake_case = try fromPascalCase(allocator, "EC2"); defer allocator.free(snake_case); try expectEqualStrings("ec2", snake_case); } test "IoT 1Click Devices Service" { const allocator = std.testing.allocator; const snake_case = try fromPascalCase(allocator, "IoT 1Click Devices Service"); defer allocator.free(snake_case); // NOTE: There is some debate amoung humans about what this should // turn into. Should it be iot_1click_... or iot_1_click...? try expectEqualStrings("iot_1_click_devices_service", snake_case); }