├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── zig-string-tests.zig └── zig-string.zig /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Download zig 18 | run: wget https://ziglang.org/builds/zig-linux-x86_64-0.15.0-dev.75+03123916e.tar.xz 19 | 20 | - name: Extract 21 | run: tar -xf zig-linux-x86_64-0.15.0-dev.75+03123916e.tar.xz 22 | 23 | - name: Alias 24 | run: alias zig=$PWD/zig-linux-x86_64-0.15.0-dev.75+03123916e/zig 25 | 26 | - name: Version 27 | run: $PWD/zig-linux-x86_64-0.15.0-dev.75+03123916e/zig version 28 | 29 | - name: Test 30 | run: $PWD/zig-linux-x86_64-0.15.0-dev.75+03123916e/zig build test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /zig-cache/ 2 | /.zig-cache/ 3 | /zig-out/ 4 | /NUL 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jakub Szarkowicz (JakubSzark) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zig String (A UTF-8 String Library) 2 | 3 | [![CI](https://github.com/JakubSzark/zig-string/actions/workflows/main.yml/badge.svg)](https://github.com/JakubSzark/zig-string/actions/workflows/main.yml) ![Github Repo Issues](https://img.shields.io/github/issues/JakubSzark/zig-string?style=flat) ![GitHub Repo stars](https://img.shields.io/github/stars/JakubSzark/zig-string?style=social) 4 | 5 | This library is a UTF-8 compatible **string** library for the **Zig** programming language. 6 | I made this for the sole purpose to further my experience and understanding of zig. 7 | Also it may be useful for some people who need it (including myself), with future projects. Project is also open for people to add to and improve. Please check the **issues** to view requested features. 8 | 9 | # Basic Usage 10 | 11 | ```zig 12 | const std = @import("std"); 13 | const String = @import("./zig-string.zig").String; 14 | // ... 15 | 16 | // Use your favorite allocator 17 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 18 | defer arena.deinit(); 19 | 20 | // Create your String 21 | var myString = String.init(arena.allocator()); 22 | defer myString.deinit(); 23 | 24 | // Use functions provided 25 | try myString.concat("🔥 Hello!"); 26 | _ = myString.pop(); 27 | try myString.concat(", World 🔥"); 28 | 29 | // Success! 30 | std.debug.assert(myString.cmp("🔥 Hello, World 🔥")); 31 | 32 | ``` 33 | 34 | # Installation 35 | 36 | Add this to your build.zig.zon 37 | 38 | ```zig 39 | .dependencies = .{ 40 | .string = .{ 41 | .url = "https://github.com/JakubSzark/zig-string/archive/refs/heads/master.tar.gz", 42 | //the correct hash will be suggested by zig 43 | } 44 | } 45 | 46 | ``` 47 | 48 | And add this to you build.zig 49 | 50 | ```zig 51 | const string = b.dependency("string", .{ 52 | .target = target, 53 | .optimize = optimize, 54 | }); 55 | exe.root_module.addImport("string", string.module("string")); 56 | 57 | ``` 58 | 59 | You can then import the library into your code like this 60 | 61 | ```zig 62 | const String = @import("string").String; 63 | ``` 64 | 65 | # How to Contribute 66 | 67 | 1. Fork 68 | 2. Clone 69 | 3. Add Features (Use Zig FMT) 70 | 4. Make a Test 71 | 5. Pull Request 72 | 6. Success! 73 | 74 | # Working Features 75 | 76 | If there are any issues with complexity please open an issue 77 | (I'm no expert when it comes to complexity) 78 | 79 | | Function | Description | 80 | | ------------------ | ------------------------------------------------------------------------ | 81 | | allocate | Sets the internal buffer size | 82 | | capacity | Returns the capacity of the String | 83 | | charAt | Returns character at index | 84 | | clear | Clears the contents of the String | 85 | | clone | Copies this string to a new one | 86 | | cmp | Compares to string literal | 87 | | concat | Appends a string literal to the end | 88 | | deinit | De-allocates the String | 89 | | find | Finds first string literal appearance | 90 | | rfind | Finds last string literal appearance | 91 | | includesLiteral | Whether or not the provided literal is in the String | 92 | | includesString | Whether or not the provided String is within the String | 93 | | init | Creates a String with an Allocator | 94 | | init_with_contents | Creates a String with specified contents | 95 | | insert | Inserts a character at an index | 96 | | isEmpty | Checks if length is zero | 97 | | iterator | Returns a StringIterator over the String | 98 | | len | Returns count of characters stored | 99 | | pop | Removes the last character | 100 | | remove | Removes a character at an index | 101 | | removeRange | Removes a range of characters | 102 | | repeat | Repeats string n times | 103 | | reverse | Reverses all the characters | 104 | | split | Returns a slice based on delimiters and index | 105 | | splitAll | Returns a slice of slices based on delimiters | 106 | | splitToString | Returns a String based on delimiters and index | 107 | | splitAllToStrings | Returns a slice of Strings based on delimiters | 108 | | lines | Returns a slice of Strings split by newlines | 109 | | str | Returns the String as a slice | 110 | | substr | Creates a string from a range | 111 | | toLowercase | Converts (ASCII) characters to lowercase | 112 | | toOwned | Creates an owned slice of the String | 113 | | toUppercase | Converts (ASCII) characters to uppercase | 114 | | toCapitalized | Converts the first (ASCII) character of each word to uppercase | 115 | | trim | Removes whitelist from both ends | 116 | | trimEnd | Remove whitelist from the end | 117 | | trimStart | Remove whitelist from the start | 118 | | truncate | Realloc to the length | 119 | | setStr | Set's buffer value from string literal | 120 | | writer | Returns a std.io.Writer for the String | 121 | | startsWith | Determines if the given string begins with the given value | 122 | | endsWith | Determines if the given string ends with the given value | 123 | | replace | Replace all occurrences of the search string with the replacement string | 124 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | _ = b.addModule("string", .{ .root_source_file = b.path("zig-string.zig") }); 8 | 9 | var main_tests = b.addTest(.{ 10 | .root_source_file = b.path("zig-string-tests.zig"), 11 | .target = target, 12 | .optimize = optimize, 13 | }); 14 | 15 | const test_step = b.step("test", "Run library tests"); 16 | test_step.dependOn(&main_tests.step); 17 | } 18 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zig_string, 3 | .version = "0.10.0", 4 | .minimum_zig_version = "0.14.0", 5 | .fingerprint = 0xd2ee692e5a4bdae9, 6 | .paths = .{""}, 7 | } 8 | -------------------------------------------------------------------------------- /zig-string-tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const expect = std.testing.expect; 3 | const expectEqual = std.testing.expectEqual; 4 | const expectEqualStrings = std.testing.expectEqualStrings; 5 | 6 | const zig_string = @import("./zig-string.zig"); 7 | const String = zig_string.String; 8 | 9 | test "Basic Usage" { 10 | // Create your String 11 | var myString = String.init(std.testing.allocator); 12 | defer myString.deinit(); 13 | 14 | // Use functions provided 15 | try myString.concat("🔥 Hello!"); 16 | _ = myString.pop(); 17 | try myString.concat(", World 🔥"); 18 | 19 | // Success! 20 | try expect(myString.cmp("🔥 Hello, World 🔥")); 21 | } 22 | 23 | test "String Tests" { 24 | // This is how we create the String 25 | var myStr = String.init(std.testing.allocator); 26 | defer myStr.deinit(); 27 | 28 | // allocate & capacity 29 | try myStr.allocate(16); 30 | try expectEqual(myStr.capacity(), 16); 31 | try expectEqual(myStr.size, 0); 32 | 33 | // truncate 34 | try myStr.truncate(); 35 | try expectEqual(myStr.capacity(), myStr.size); 36 | try expectEqual(myStr.capacity(), 0); 37 | 38 | // concat 39 | try myStr.concat("A"); 40 | try myStr.concat("\u{5360}"); 41 | try myStr.concat("💯"); 42 | try myStr.concat("Hello🔥"); 43 | 44 | try expectEqual(myStr.size, 17); 45 | 46 | // pop & length 47 | try expectEqual(myStr.len(), 9); 48 | try expectEqualStrings(myStr.pop().?, "🔥"); 49 | try expectEqual(myStr.len(), 8); 50 | try expectEqualStrings(myStr.pop().?, "o"); 51 | try expectEqual(myStr.len(), 7); 52 | 53 | // str & cmp 54 | try expect(myStr.cmp("A\u{5360}💯Hell")); 55 | try expect(myStr.cmp(myStr.str())); 56 | 57 | // charAt 58 | try expectEqualStrings(myStr.charAt(2).?, "💯"); 59 | try expectEqualStrings(myStr.charAt(1).?, "\u{5360}"); 60 | try expectEqualStrings(myStr.charAt(0).?, "A"); 61 | 62 | // insert 63 | try myStr.insert("🔥", 1); 64 | try expectEqualStrings(myStr.charAt(1).?, "🔥"); 65 | try expect(myStr.cmp("A🔥\u{5360}💯Hell")); 66 | 67 | // find 68 | try expectEqual(myStr.find("🔥").?, 1); 69 | try expectEqual(myStr.find("💯").?, 3); 70 | try expectEqual(myStr.find("Hell").?, 4); 71 | 72 | // remove & removeRange 73 | try myStr.removeRange(0, 3); 74 | try expect(myStr.cmp("💯Hell")); 75 | try myStr.remove(myStr.len() - 1); 76 | try expect(myStr.cmp("💯Hel")); 77 | 78 | const whitelist = [_]u8{ ' ', '\t', '\n', '\r' }; 79 | 80 | // trimStart 81 | try myStr.insert(" ", 0); 82 | myStr.trimStart(whitelist[0..]); 83 | try expect(myStr.cmp("💯Hel")); 84 | 85 | // trimEnd 86 | _ = try myStr.concat("lo💯\n "); 87 | myStr.trimEnd(whitelist[0..]); 88 | try expect(myStr.cmp("💯Hello💯")); 89 | 90 | // clone 91 | var testStr = try myStr.clone(); 92 | defer testStr.deinit(); 93 | try expect(testStr.cmp(myStr.str())); 94 | 95 | // reverse 96 | myStr.reverse(); 97 | try expect(myStr.cmp("💯olleH💯")); 98 | myStr.reverse(); 99 | try expect(myStr.cmp("💯Hello💯")); 100 | 101 | // repeat 102 | try myStr.repeat(2); 103 | try expect(myStr.cmp("💯Hello💯💯Hello💯💯Hello💯")); 104 | 105 | // isEmpty 106 | try expect(!myStr.isEmpty()); 107 | 108 | // split 109 | try expectEqualStrings(myStr.split("💯", 0).?, ""); 110 | try expectEqualStrings(myStr.split("💯", 1).?, "Hello"); 111 | try expectEqualStrings(myStr.split("💯", 2).?, ""); 112 | try expectEqualStrings(myStr.split("💯", 3).?, "Hello"); 113 | try expectEqualStrings(myStr.split("💯", 5).?, "Hello"); 114 | try expectEqualStrings(myStr.split("💯", 6).?, ""); 115 | 116 | var splitStr = String.init(std.testing.allocator); 117 | defer splitStr.deinit(); 118 | 119 | try splitStr.concat("variable='value'"); 120 | try expectEqualStrings(splitStr.split("=", 0).?, "variable"); 121 | try expectEqualStrings(splitStr.split("=", 1).?, "'value'"); 122 | 123 | // splitAll 124 | var splitAllStr = try String.init_with_contents(std.testing.allocator, "THIS IS A TEST"); 125 | defer splitAllStr.deinit(); 126 | const splitAllSlices = try splitAllStr.splitAll(" "); 127 | 128 | try expectEqual(splitAllSlices.len, 5); 129 | try expectEqualStrings(splitAllSlices[0], "THIS"); 130 | try expectEqualStrings(splitAllSlices[1], "IS"); 131 | try expectEqualStrings(splitAllSlices[2], "A"); 132 | try expectEqualStrings(splitAllSlices[3], ""); 133 | try expectEqualStrings(splitAllSlices[4], "TEST"); 134 | 135 | // splitToString 136 | var newSplit = try splitStr.splitToString("=", 0); 137 | try expect(newSplit != null); 138 | defer newSplit.?.deinit(); 139 | 140 | try expectEqualStrings(newSplit.?.str(), "variable"); 141 | 142 | // splitAllToStrings 143 | var splitAllStrings = try splitAllStr.splitAllToStrings(" "); 144 | defer for (splitAllStrings) |*str| { 145 | str.deinit(); 146 | }; 147 | 148 | try expectEqual(splitAllStrings.len, 5); 149 | try expectEqualStrings(splitAllStrings[0].str(), "THIS"); 150 | try expectEqualStrings(splitAllStrings[1].str(), "IS"); 151 | try expectEqualStrings(splitAllStrings[2].str(), "A"); 152 | try expectEqualStrings(splitAllStrings[3].str(), ""); 153 | try expectEqualStrings(splitAllStrings[4].str(), "TEST"); 154 | 155 | // lines 156 | const lineSlice = "Line0\r\nLine1\nLine2"; 157 | 158 | var lineStr = try String.init_with_contents(std.testing.allocator, lineSlice); 159 | defer lineStr.deinit(); 160 | var linesSlice = try lineStr.lines(); 161 | defer for (linesSlice) |*str| { 162 | str.deinit(); 163 | }; 164 | 165 | try expectEqual(linesSlice.len, 3); 166 | try expect(linesSlice[0].cmp("Line0")); 167 | try expect(linesSlice[1].cmp("Line1")); 168 | try expect(linesSlice[2].cmp("Line2")); 169 | 170 | // toLowercase & toUppercase 171 | myStr.toUppercase(); 172 | try expect(myStr.cmp("💯HELLO💯💯HELLO💯💯HELLO💯")); 173 | myStr.toLowercase(); 174 | try expect(myStr.cmp("💯hello💯💯hello💯💯hello💯")); 175 | 176 | // substr 177 | var subStr = try myStr.substr(0, 7); 178 | defer subStr.deinit(); 179 | try expect(subStr.cmp("💯hello💯")); 180 | 181 | // clear 182 | myStr.clear(); 183 | try expectEqual(myStr.len(), 0); 184 | try expectEqual(myStr.size, 0); 185 | 186 | // writer 187 | const writer = myStr.writer(); 188 | const length = try writer.write("This is a Test!"); 189 | try expectEqual(length, 15); 190 | 191 | // owned 192 | const mySlice = try myStr.toOwned(); 193 | try expectEqualStrings(mySlice.?, "This is a Test!"); 194 | std.testing.allocator.free(mySlice.?); 195 | 196 | // StringIterator 197 | var i: usize = 0; 198 | var iter = myStr.iterator(); 199 | while (iter.next()) |ch| { 200 | if (i == 0) { 201 | try expectEqualStrings("T", ch); 202 | } 203 | i += 1; 204 | } 205 | 206 | try expectEqual(i, myStr.len()); 207 | 208 | // setStr 209 | const contents = "setStr Test!"; 210 | try myStr.setStr(contents); 211 | try expect(myStr.cmp(contents)); 212 | 213 | // non ascii supports in windows 214 | // try expectEqual(std.os.windows.kernel32.GetConsoleOutputCP(), 65001); 215 | } 216 | 217 | test "init with contents" { 218 | const initial_contents = "String with initial contents!"; 219 | 220 | // This is how we create the String with contents at the start 221 | var myStr = try String.init_with_contents(std.testing.allocator, initial_contents); 222 | defer myStr.deinit(); 223 | try expectEqualStrings(myStr.str(), initial_contents); 224 | } 225 | 226 | test "startsWith Tests" { 227 | var myString = String.init(std.testing.allocator); 228 | defer myString.deinit(); 229 | 230 | try myString.concat("bananas"); 231 | try expect(myString.startsWith("bana")); 232 | try expect(!myString.startsWith("abc")); 233 | } 234 | 235 | test "endsWith Tests" { 236 | var myString = String.init(std.testing.allocator); 237 | defer myString.deinit(); 238 | 239 | try myString.concat("asbananas"); 240 | try expect(myString.endsWith("nas")); 241 | try expect(!myString.endsWith("abc")); 242 | 243 | try myString.truncate(); 244 | try myString.concat("💯hello💯💯hello💯💯hello💯"); 245 | std.debug.print("", .{}); 246 | try expect(myString.endsWith("hello💯")); 247 | } 248 | 249 | test "replace Tests" { 250 | // Create your String 251 | var myString = String.init(std.testing.allocator); 252 | defer myString.deinit(); 253 | 254 | try myString.concat("hi,how are you"); 255 | var result = try myString.replace("hi,", ""); 256 | try expect(result); 257 | try expectEqualStrings(myString.str(), "how are you"); 258 | 259 | result = try myString.replace("abc", " "); 260 | try expect(!result); 261 | 262 | myString.clear(); 263 | try myString.concat("💯hello💯💯hello💯💯hello💯"); 264 | _ = try myString.replace("hello", "hi"); 265 | try expectEqualStrings(myString.str(), "💯hi💯💯hi💯💯hi💯"); 266 | } 267 | 268 | test "rfind Tests" { 269 | var myString = try String.init_with_contents(std.testing.allocator, "💯hi💯💯hi💯💯hi💯"); 270 | defer myString.deinit(); 271 | 272 | try expectEqual(myString.rfind("hi"), 9); 273 | } 274 | 275 | test "toCapitalized Tests" { 276 | var myString = try String.init_with_contents(std.testing.allocator, "love and be loved"); 277 | defer myString.deinit(); 278 | 279 | myString.toCapitalized(); 280 | 281 | try expectEqualStrings(myString.str(), "Love And Be Loved"); 282 | } 283 | 284 | test "includes Tests" { 285 | var myString = try String.init_with_contents(std.testing.allocator, "love and be loved"); 286 | defer myString.deinit(); 287 | 288 | var needle = try String.init_with_contents(std.testing.allocator, "be"); 289 | defer needle.deinit(); 290 | 291 | try expect(myString.includesLiteral("and")); 292 | try expect(myString.includesString(needle)); 293 | 294 | try needle.concat("t"); 295 | 296 | try expect(myString.includesLiteral("tiger") == false); 297 | try expect(myString.includesString(needle) == false); 298 | 299 | needle.clear(); 300 | 301 | try expect(myString.includesLiteral("") == false); 302 | try expect(myString.includesString(needle) == false); 303 | } -------------------------------------------------------------------------------- /zig-string.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const builtin = @import("builtin"); 4 | 5 | /// A variable length collection of characters 6 | pub const String = struct { 7 | /// The internal character buffer 8 | buffer: ?[]u8, 9 | /// The allocator used for managing the buffer 10 | allocator: std.mem.Allocator, 11 | /// The total size of the String 12 | size: usize, 13 | 14 | /// Errors that may occur when using String 15 | pub const Error = error{ 16 | OutOfMemory, 17 | InvalidRange, 18 | }; 19 | 20 | /// Creates a String with an Allocator 21 | /// ### example 22 | /// ```zig 23 | /// var str = String.init(allocator); 24 | /// // don't forget to deallocate 25 | /// defer _ = str.deinit(); 26 | /// ``` 27 | /// User is responsible for managing the new String 28 | pub fn init(allocator: std.mem.Allocator) String { 29 | // for windows non-ascii characters 30 | // check if the system is windows 31 | if (builtin.os.tag == std.Target.Os.Tag.windows) { 32 | _ = std.os.windows.kernel32.SetConsoleOutputCP(65001); 33 | } 34 | 35 | return .{ 36 | .buffer = null, 37 | .allocator = allocator, 38 | .size = 0, 39 | }; 40 | } 41 | 42 | pub fn init_with_contents(allocator: std.mem.Allocator, contents: []const u8) Error!String { 43 | var string = init(allocator); 44 | 45 | try string.concat(contents); 46 | 47 | return string; 48 | } 49 | 50 | /// Deallocates the internal buffer 51 | /// ### usage: 52 | /// ```zig 53 | /// var str = String.init(allocator); 54 | /// // deinit after the closure 55 | /// defer _ = str.deinit(); 56 | /// ``` 57 | pub fn deinit(self: *String) void { 58 | if (self.buffer) |buffer| self.allocator.free(buffer); 59 | } 60 | 61 | /// Returns the size of the internal buffer 62 | pub fn capacity(self: String) usize { 63 | if (self.buffer) |buffer| return buffer.len; 64 | return 0; 65 | } 66 | 67 | /// Allocates space for the internal buffer 68 | pub fn allocate(self: *String, bytes: usize) Error!void { 69 | if (self.buffer) |buffer| { 70 | if (bytes < self.size) self.size = bytes; // Clamp size to capacity 71 | self.buffer = self.allocator.realloc(buffer, bytes) catch { 72 | return Error.OutOfMemory; 73 | }; 74 | } else { 75 | self.buffer = self.allocator.alloc(u8, bytes) catch { 76 | return Error.OutOfMemory; 77 | }; 78 | } 79 | } 80 | 81 | /// Reallocates the the internal buffer to size 82 | pub fn truncate(self: *String) Error!void { 83 | try self.allocate(self.size); 84 | } 85 | 86 | /// Appends a character onto the end of the String 87 | pub fn concat(self: *String, char: []const u8) Error!void { 88 | try self.insert(char, self.len()); 89 | } 90 | 91 | /// Inserts a string literal into the String at an index 92 | pub fn insert(self: *String, literal: []const u8, index: usize) Error!void { 93 | // Make sure buffer has enough space 94 | if (self.buffer) |buffer| { 95 | if (self.size + literal.len > buffer.len) { 96 | try self.allocate((self.size + literal.len) * 2); 97 | } 98 | } else { 99 | try self.allocate((literal.len) * 2); 100 | } 101 | 102 | const buffer = self.buffer.?; 103 | 104 | // If the index is >= len, then simply push to the end. 105 | // If not, then copy contents over and insert literal. 106 | if (index == self.len()) { 107 | var i: usize = 0; 108 | while (i < literal.len) : (i += 1) { 109 | buffer[self.size + i] = literal[i]; 110 | } 111 | } else { 112 | if (String.getIndex(buffer, index, true)) |k| { 113 | // Move existing contents over 114 | var i: usize = buffer.len - 1; 115 | while (i >= k) : (i -= 1) { 116 | if (i + literal.len < buffer.len) { 117 | buffer[i + literal.len] = buffer[i]; 118 | } 119 | 120 | if (i == 0) break; 121 | } 122 | 123 | i = 0; 124 | while (i < literal.len) : (i += 1) { 125 | buffer[index + i] = literal[i]; 126 | } 127 | } 128 | } 129 | 130 | self.size += literal.len; 131 | } 132 | 133 | /// Removes the last character from the String 134 | pub fn pop(self: *String) ?[]const u8 { 135 | if (self.size == 0) return null; 136 | 137 | if (self.buffer) |buffer| { 138 | var i: usize = 0; 139 | while (i < self.size) { 140 | const size = String.getUTF8Size(buffer[i]); 141 | if (i + size >= self.size) break; 142 | i += size; 143 | } 144 | 145 | const ret = buffer[i..self.size]; 146 | self.size -= (self.size - i); 147 | return ret; 148 | } 149 | 150 | return null; 151 | } 152 | 153 | /// Compares this String with a string literal 154 | pub fn cmp(self: String, literal: []const u8) bool { 155 | if (self.buffer) |buffer| { 156 | return std.mem.eql(u8, buffer[0..self.size], literal); 157 | } 158 | return false; 159 | } 160 | 161 | /// Returns the String buffer as a string literal 162 | /// ### usage: 163 | ///```zig 164 | ///var mystr = try String.init_with_contents(allocator, "Test String!"); 165 | ///defer _ = mystr.deinit(); 166 | ///std.debug.print("{s}\n", .{mystr.str()}); 167 | ///``` 168 | pub fn str(self: String) []const u8 { 169 | if (self.buffer) |buffer| return buffer[0..self.size]; 170 | return ""; 171 | } 172 | 173 | /// Returns an owned slice of this string 174 | pub fn toOwned(self: String) Error!?[]u8 { 175 | if (self.buffer != null) { 176 | const string = self.str(); 177 | if (self.allocator.alloc(u8, string.len)) |newStr| { 178 | std.mem.copyForwards(u8, newStr, string); 179 | return newStr; 180 | } else |_| { 181 | return Error.OutOfMemory; 182 | } 183 | } 184 | 185 | return null; 186 | } 187 | 188 | /// Returns a character at the specified index 189 | pub fn charAt(self: String, index: usize) ?[]const u8 { 190 | if (self.buffer) |buffer| { 191 | if (String.getIndex(buffer, index, true)) |i| { 192 | const size = String.getUTF8Size(buffer[i]); 193 | return buffer[i..(i + size)]; 194 | } 195 | } 196 | return null; 197 | } 198 | 199 | /// Returns amount of characters in the String 200 | pub fn len(self: String) usize { 201 | if (self.buffer) |buffer| { 202 | var length: usize = 0; 203 | var i: usize = 0; 204 | 205 | while (i < self.size) { 206 | i += String.getUTF8Size(buffer[i]); 207 | length += 1; 208 | } 209 | 210 | return length; 211 | } else { 212 | return 0; 213 | } 214 | } 215 | 216 | /// Finds the first occurrence of the string literal 217 | pub fn find(self: String, literal: []const u8) ?usize { 218 | if (self.buffer) |buffer| { 219 | const index = std.mem.indexOf(u8, buffer[0..self.size], literal); 220 | if (index) |i| { 221 | return String.getIndex(buffer, i, false); 222 | } 223 | } 224 | 225 | return null; 226 | } 227 | 228 | /// Finds the last occurrence of the string literal 229 | pub fn rfind(self: String, literal: []const u8) ?usize { 230 | if (self.buffer) |buffer| { 231 | const index = std.mem.lastIndexOf(u8, buffer[0..self.size], literal); 232 | if (index) |i| { 233 | return String.getIndex(buffer, i, false); 234 | } 235 | } 236 | 237 | return null; 238 | } 239 | 240 | /// Removes a character at the specified index 241 | pub fn remove(self: *String, index: usize) Error!void { 242 | try self.removeRange(index, index + 1); 243 | } 244 | 245 | /// Removes a range of character from the String 246 | /// Start (inclusive) - End (Exclusive) 247 | pub fn removeRange(self: *String, start: usize, end: usize) Error!void { 248 | const length = self.len(); 249 | if (end < start or end > length) return Error.InvalidRange; 250 | 251 | if (self.buffer) |buffer| { 252 | const rStart = String.getIndex(buffer, start, true).?; 253 | const rEnd = String.getIndex(buffer, end, true).?; 254 | const difference = rEnd - rStart; 255 | 256 | var i: usize = rEnd; 257 | while (i < self.size) : (i += 1) { 258 | buffer[i - difference] = buffer[i]; 259 | } 260 | 261 | self.size -= difference; 262 | } 263 | } 264 | 265 | /// Trims all whitelist characters at the start of the String. 266 | pub fn trimStart(self: *String, whitelist: []const u8) void { 267 | if (self.buffer) |buffer| { 268 | var i: usize = 0; 269 | while (i < self.size) : (i += 1) { 270 | const size = String.getUTF8Size(buffer[i]); 271 | if (size > 1 or !inWhitelist(buffer[i], whitelist)) break; 272 | } 273 | 274 | if (String.getIndex(buffer, i, false)) |k| { 275 | self.removeRange(0, k) catch {}; 276 | } 277 | } 278 | } 279 | 280 | /// Trims all whitelist characters at the end of the String. 281 | pub fn trimEnd(self: *String, whitelist: []const u8) void { 282 | self.reverse(); 283 | self.trimStart(whitelist); 284 | self.reverse(); 285 | } 286 | 287 | /// Trims all whitelist characters from both ends of the String 288 | pub fn trim(self: *String, whitelist: []const u8) void { 289 | self.trimStart(whitelist); 290 | self.trimEnd(whitelist); 291 | } 292 | 293 | /// Copies this String into a new one 294 | /// User is responsible for managing the new String 295 | pub fn clone(self: String) Error!String { 296 | var newString = String.init(self.allocator); 297 | try newString.concat(self.str()); 298 | return newString; 299 | } 300 | 301 | /// Reverses the characters in this String 302 | pub fn reverse(self: *String) void { 303 | if (self.buffer) |buffer| { 304 | var i: usize = 0; 305 | while (i < self.size) { 306 | const size = String.getUTF8Size(buffer[i]); 307 | if (size > 1) std.mem.reverse(u8, buffer[i..(i + size)]); 308 | i += size; 309 | } 310 | 311 | std.mem.reverse(u8, buffer[0..self.size]); 312 | } 313 | } 314 | 315 | /// Repeats this String n times 316 | pub fn repeat(self: *String, n: usize) Error!void { 317 | try self.allocate(self.size * (n + 1)); 318 | if (self.buffer) |buffer| { 319 | for (1..n + 1) |i| { 320 | std.mem.copyForwards(u8, buffer[self.size * i ..], buffer[0..self.size]); 321 | } 322 | 323 | self.size *= (n + 1); 324 | } 325 | } 326 | 327 | /// Checks the String is empty 328 | pub inline fn isEmpty(self: String) bool { 329 | return self.size == 0; 330 | } 331 | 332 | /// Splits the String into a slice, based on a delimiter and an index 333 | pub fn split(self: *const String, delimiters: []const u8, index: usize) ?[]const u8 { 334 | if (self.buffer) |buffer| { 335 | var i: usize = 0; 336 | var block: usize = 0; 337 | var start: usize = 0; 338 | 339 | while (i < self.size) { 340 | const size = String.getUTF8Size(buffer[i]); 341 | if (size == delimiters.len) { 342 | if (std.mem.eql(u8, delimiters, buffer[i..(i + size)])) { 343 | if (block == index) return buffer[start..i]; 344 | start = i + size; 345 | block += 1; 346 | } 347 | } 348 | 349 | i += size; 350 | } 351 | 352 | if (i >= self.size - 1 and block == index) { 353 | return buffer[start..self.size]; 354 | } 355 | } 356 | 357 | return null; 358 | } 359 | 360 | /// Splits the String into slices, based on a delimiter. 361 | pub fn splitAll(self: *const String, delimiters: []const u8) ![][]const u8 { 362 | var splitArr = std.ArrayList([]const u8).init(std.heap.page_allocator); 363 | defer splitArr.deinit(); 364 | 365 | var i: usize = 0; 366 | while (self.split(delimiters, i)) |slice| : (i += 1) { 367 | try splitArr.append(slice); 368 | } 369 | 370 | return try splitArr.toOwnedSlice(); 371 | } 372 | 373 | /// Splits the String into a new string, based on delimiters and an index 374 | /// The user of this function is in charge of the memory of the new String. 375 | pub fn splitToString(self: *const String, delimiters: []const u8, index: usize) Error!?String { 376 | if (self.split(delimiters, index)) |block| { 377 | var string = String.init(self.allocator); 378 | try string.concat(block); 379 | return string; 380 | } 381 | 382 | return null; 383 | } 384 | 385 | /// Splits the String into a slice of new Strings, based on delimiters. 386 | /// The user of this function is in charge of the memory of the new Strings. 387 | pub fn splitAllToStrings(self: *const String, delimiters: []const u8) ![]String { 388 | var splitArr = std.ArrayList(String).init(std.heap.page_allocator); 389 | defer splitArr.deinit(); 390 | 391 | var i: usize = 0; 392 | while (try self.splitToString(delimiters, i)) |splitStr| : (i += 1) { 393 | try splitArr.append(splitStr); 394 | } 395 | 396 | return try splitArr.toOwnedSlice(); 397 | } 398 | 399 | /// Splits the String into a slice of Strings by new line (\r\n or \n). 400 | pub fn lines(self: *String) ![]String { 401 | var lineArr = std.ArrayList(String).init(std.heap.page_allocator); 402 | defer lineArr.deinit(); 403 | 404 | var selfClone = try self.clone(); 405 | defer selfClone.deinit(); 406 | 407 | _ = try selfClone.replace("\r\n", "\n"); 408 | 409 | return try selfClone.splitAllToStrings("\n"); 410 | } 411 | 412 | /// Clears the contents of the String but leaves the capacity 413 | pub fn clear(self: *String) void { 414 | if (self.buffer) |buffer| { 415 | for (buffer) |*ch| ch.* = 0; 416 | self.size = 0; 417 | } 418 | } 419 | 420 | /// Converts all (ASCII) uppercase letters to lowercase 421 | pub fn toLowercase(self: *String) void { 422 | if (self.buffer) |buffer| { 423 | var i: usize = 0; 424 | while (i < self.size) { 425 | const size = String.getUTF8Size(buffer[i]); 426 | if (size == 1) buffer[i] = std.ascii.toLower(buffer[i]); 427 | i += size; 428 | } 429 | } 430 | } 431 | 432 | /// Converts all (ASCII) uppercase letters to lowercase 433 | pub fn toUppercase(self: *String) void { 434 | if (self.buffer) |buffer| { 435 | var i: usize = 0; 436 | while (i < self.size) { 437 | const size = String.getUTF8Size(buffer[i]); 438 | if (size == 1) buffer[i] = std.ascii.toUpper(buffer[i]); 439 | i += size; 440 | } 441 | } 442 | } 443 | 444 | // Convert the first (ASCII) character of each word to uppercase 445 | pub fn toCapitalized(self: *String) void { 446 | if (self.size == 0) return; 447 | 448 | var buffer = self.buffer.?; 449 | var i: usize = 0; 450 | var is_new_word: bool = true; 451 | 452 | while (i < self.size) { 453 | const char = buffer[i]; 454 | 455 | if (std.ascii.isWhitespace(char)) { 456 | is_new_word = true; 457 | i += 1; 458 | continue; 459 | } 460 | 461 | if (is_new_word) { 462 | buffer[i] = std.ascii.toUpper(char); 463 | is_new_word = false; 464 | } 465 | 466 | i += 1; 467 | } 468 | } 469 | 470 | /// Creates a String from a given range 471 | /// User is responsible for managing the new String 472 | pub fn substr(self: String, start: usize, end: usize) Error!String { 473 | var result = String.init(self.allocator); 474 | 475 | if (self.buffer) |buffer| { 476 | if (String.getIndex(buffer, start, true)) |rStart| { 477 | if (String.getIndex(buffer, end, true)) |rEnd| { 478 | if (rEnd < rStart or rEnd > self.size) 479 | return Error.InvalidRange; 480 | try result.concat(buffer[rStart..rEnd]); 481 | } 482 | } 483 | } 484 | 485 | return result; 486 | } 487 | 488 | // Writer functionality for the String. 489 | pub usingnamespace struct { 490 | pub const Writer = std.io.Writer(*String, Error, appendWrite); 491 | 492 | pub fn writer(self: *String) Writer { 493 | return .{ .context = self }; 494 | } 495 | 496 | fn appendWrite(self: *String, m: []const u8) !usize { 497 | try self.concat(m); 498 | return m.len; 499 | } 500 | }; 501 | 502 | // Iterator support 503 | pub usingnamespace struct { 504 | pub const StringIterator = struct { 505 | string: *const String, 506 | index: usize, 507 | 508 | pub fn next(it: *StringIterator) ?[]const u8 { 509 | if (it.string.buffer) |buffer| { 510 | if (it.index == it.string.size) return null; 511 | const i = it.index; 512 | it.index += String.getUTF8Size(buffer[i]); 513 | return buffer[i..it.index]; 514 | } else { 515 | return null; 516 | } 517 | } 518 | }; 519 | 520 | pub fn iterator(self: *const String) StringIterator { 521 | return StringIterator{ 522 | .string = self, 523 | .index = 0, 524 | }; 525 | } 526 | }; 527 | 528 | /// Returns whether or not a character is whitelisted 529 | fn inWhitelist(char: u8, whitelist: []const u8) bool { 530 | var i: usize = 0; 531 | while (i < whitelist.len) : (i += 1) { 532 | if (whitelist[i] == char) return true; 533 | } 534 | 535 | return false; 536 | } 537 | 538 | /// Checks if byte is part of UTF-8 character 539 | inline fn isUTF8Byte(byte: u8) bool { 540 | return ((byte & 0x80) > 0) and (((byte << 1) & 0x80) == 0); 541 | } 542 | 543 | /// Returns the real index of a unicode string literal 544 | fn getIndex(unicode: []const u8, index: usize, real: bool) ?usize { 545 | var i: usize = 0; 546 | var j: usize = 0; 547 | while (i < unicode.len) { 548 | if (real) { 549 | if (j == index) return i; 550 | } else { 551 | if (i == index) return j; 552 | } 553 | i += String.getUTF8Size(unicode[i]); 554 | j += 1; 555 | } 556 | 557 | return null; 558 | } 559 | 560 | /// Returns the UTF-8 character's size 561 | inline fn getUTF8Size(char: u8) u3 { 562 | return std.unicode.utf8ByteSequenceLength(char) catch { 563 | return 1; 564 | }; 565 | } 566 | 567 | /// Sets the contents of the String 568 | pub fn setStr(self: *String, contents: []const u8) Error!void { 569 | self.clear(); 570 | try self.concat(contents); 571 | } 572 | 573 | /// Checks the start of the string against a literal 574 | pub fn startsWith(self: *String, literal: []const u8) bool { 575 | if (self.buffer) |buffer| { 576 | const index = std.mem.indexOf(u8, buffer[0..self.size], literal); 577 | return index == 0; 578 | } 579 | return false; 580 | } 581 | 582 | /// Checks the end of the string against a literal 583 | pub fn endsWith(self: *String, literal: []const u8) bool { 584 | if (self.buffer) |buffer| { 585 | const index = std.mem.lastIndexOf(u8, buffer[0..self.size], literal); 586 | const i: usize = self.size - literal.len; 587 | return index == i; 588 | } 589 | return false; 590 | } 591 | 592 | /// Replaces all occurrences of a string literal with another 593 | pub fn replace(self: *String, needle: []const u8, replacement: []const u8) !bool { 594 | if (self.buffer) |buffer| { 595 | const InputSize = self.size; 596 | const size = std.mem.replacementSize(u8, buffer[0..InputSize], needle, replacement); 597 | defer self.allocator.free(buffer); 598 | self.buffer = self.allocator.alloc(u8, size) catch { 599 | return Error.OutOfMemory; 600 | }; 601 | self.size = size; 602 | const changes = std.mem.replace(u8, buffer[0..InputSize], needle, replacement, self.buffer.?); 603 | if (changes > 0) { 604 | return true; 605 | } 606 | } 607 | return false; 608 | } 609 | 610 | /// Checks if the needle String is within the source String 611 | pub fn includesString(self: *String, needle: String) bool { 612 | 613 | if (self.size == 0 or needle.size == 0) return false; 614 | 615 | if (self.buffer) |buffer| { 616 | if (needle.buffer) |needle_buffer| { 617 | const found_index = std.mem.indexOf(u8, buffer[0..self.size], needle_buffer[0..needle.size]); 618 | 619 | if (found_index == null) return false; 620 | 621 | return true; 622 | } 623 | } 624 | 625 | return false; 626 | } 627 | 628 | /// Checks if the needle literal is within the source String 629 | pub fn includesLiteral(self: *String, needle: []const u8) bool { 630 | 631 | if (self.size == 0 or needle.len == 0) return false; 632 | 633 | if (self.buffer) |buffer| { 634 | const found_index = std.mem.indexOf(u8, buffer[0..self.size], needle); 635 | 636 | if (found_index == null) return false; 637 | 638 | return true; 639 | } 640 | 641 | return false; 642 | } 643 | }; 644 | --------------------------------------------------------------------------------