├── .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 | [](https://github.com/JakubSzark/zig-string/actions/workflows/main.yml)  
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 |
--------------------------------------------------------------------------------