1 /**
2 * a @safe, @nogc-compatible template string type.
3 * Authors: Susan
4 * Date: 2021-11-06
5 * Licence: AGPL-3.0 or later
6 * Copyright: Susan, 2021
7 */
8 module fixedstring;
9 
10 import fixedstring.fixedstringrange;
11 import fixedstring.opapplymixin;
12 import std.traits: isSomeChar;
13 
14 /// short syntax.
15 auto fixedString(string s)()
16 {
17 	import std.math.algebraic: nextPow2;
18 	return FixedString!(nextPow2(s.length))(s);
19 }
20 
21 ///
22 @safe @nogc nothrow unittest
23 {
24 	enum testString = "dlang is good";
25 	immutable foo = fixedString!(testString);
26 	immutable bar = FixedString!16(testString);
27 
28 	assert(foo == bar);
29 }
30 
31 ///
32 struct FixedString(size_t maxSize, CharT = char)
33 {
34 	invariant (length_ <= data.length);
35 
36 	enum size = maxSize; ///
37 
38 	private size_t length_;
39 	private CharT[maxSize] data = ' ';
40 
41 	///
42 	size_t length() const @nogc nothrow pure @safe
43 	{
44 		return length_;
45 	}
46 
47 	/// ditto
48 	void length(in size_t rhs) @nogc nothrow pure @safe
49 	in (rhs <= maxSize)
50 	{
51 		if (rhs >= length)
52 		{
53 			data[length .. rhs] = CharT.init;
54 		}
55 		length_ = rhs;
56 	}
57 
58 	/// constructor
59 	this(in CharT[] rhs)
60 	in (rhs.length <= maxSize)
61 	{
62 		length = rhs.length;
63 		data[0 .. length] = rhs[];
64 	}
65 
66 	/// assignment
67 	void opAssign(in CharT[] rhs)
68 	in (rhs.length <= maxSize)
69 	{
70 		length = rhs.length;
71 		data[0 .. length] = rhs[];
72 	}
73 
74 	/// ditto
75 	void opAssign(T: CharT, size_t n)(in FixedString!(n, T) rhs)
76 	in (rhs.length <= maxSize)
77 	{
78 		length = rhs.length;
79 		data[0 .. length] = rhs[0 .. length];
80 	}
81 
82 	/// ditto
83 	void opOpAssign(string op)(in CharT[] rhs)
84 	if (op == "~")
85 	in (length + rhs.length <= maxSize)
86 	{
87 		immutable oldLength = length;
88 		length = length + rhs.length;
89 		data[oldLength .. length] = rhs[];
90 	}
91 
92 	/// ditto
93 	void opOpAssign(string op)(in CharT rhs)
94 	if (op == "~")
95 	in (length + 1 <= maxSize)
96 	{
97 		length = length + 1;
98 		data[length - 1] = rhs;
99 	}
100 
101 	/// ditto
102 	void opOpAssign(string op, T: CharT, size_t n)(in FixedString!(n, T) rhs)
103 	if (op == "~")
104 	in (length + rhs.length <= maxSize)
105 	{
106 		immutable oldLength = length;
107 		length = length + rhs.length;
108 		data[oldLength .. length] = rhs[0 .. rhs.length];
109 	}
110 
111 	/// array features...
112 	auto opSlice(in size_t first, in size_t last) const @nogc nothrow pure @safe
113 	in (first <= length && last <= length)
114 	{
115 		return data[first .. last];
116 	}
117 
118 	/// ditto
119 	size_t opDollar(size_t pos)() const @nogc nothrow pure @safe
120 	if (pos == 0)
121 	{
122 		return length;
123 	}
124 
125 	/// ditto
126 	auto opIndex() const
127 	{
128 		return FixedStringRange!CharT(data[0 .. length]);
129 	}
130 
131 	/// ditto
132 	CharT opIndex(in size_t index) const @nogc nothrow pure @safe
133 	in (index < length)
134 	{
135 		return data[index];
136 	}
137 
138 	/// ditto
139 	void opIndexAssign(in CharT rhs, in size_t index)
140 	in (index < length)
141 	{
142 		data[index] = rhs;
143 	}
144 
145 	/// equality
146 	bool opEquals(T : CharT, size_t n)(in FixedString!(n, T) rhs) const
147 	{
148 		import std.algorithm.comparison: equal;
149 		return this[].equal(rhs[]);
150 	}
151 
152 	/// ditto
153 	bool opEquals(in CharT[] s) const
154 	{
155 		if (length != s.length)
156 		{
157 			return false;
158 		}
159 
160 		import std.algorithm.comparison: equal;
161 		return this[].equal(s[]);
162 	}
163 
164 	/// concatenation. note that you should probably use the ~ operator instead - only use this version when you are pressed for ram and aren't making many calls, or you will end up with template bloat.
165 	auto concat(size_t s, T: CharT, size_t n)(in FixedString!(n, T) rhs) const
166 	in (s >= length + rhs.length)
167 	{
168 		FixedString!(s) result;
169 
170 		result = this;
171 		result ~= rhs;
172 
173 		return result;
174 	}
175 
176 	/// concatenation operator. generally, you should prefer this version.
177 	auto opBinary(string op, T: CharT, size_t n)(in FixedString!(n, T) rhs) const
178 	if (op == "~")
179 	{
180 		import std.math.algebraic: nextPow2;
181 		return concat!(nextPow2(size + rhs.size))(rhs);
182 	}
183 
184 	static if (isSomeChar!CharT)
185 	{
186 		///
187 		const(CharT)[] toString() const nothrow pure @safe
188 		{
189 			return data[0 .. length].idup;
190 		}
191 	}
192 
193 	///
194 	size_t toHash() const @nogc nothrow pure @safe
195 	{
196 		ulong result = length;
197 		foreach (CharT c; data[0 .. length])
198 		{
199 			result += c;
200 		}
201 		return result;
202 	}
203 
204 	mixin(opApplyWorkaround);
205 }
206 
207 ///
208 @safe @nogc nothrow pure unittest
209 {
210 	immutable string temp = "cool";
211 	auto foo = FixedString!8(temp);
212 	assert(foo[0] == 'c');
213 	assert(foo == "cool");
214 
215 	import std.algorithm.comparison: equal;
216 	assert(foo[].equal("cool"));
217 	assert(foo[0 .. $] == "cool");
218 
219 	foo[2] = 'd';
220 	assert(foo == "codl");
221 	assert(foo != "");
222 
223 	foo ~= "d";
224 	foo.length = 4;
225 	assert(foo == "codl");
226 
227 	foo.length = 6;
228 	assert(foo == "codl\xff\xff");
229 
230 	foo.length = 4;
231 	assert(foo == "codl");
232 
233 	import std.range: retro;
234 	assert(equal(retro(foo[]), "ldoc"));
235 
236 	import std.range: radial;
237 	assert(equal(radial(foo[]), "odcl"));
238 
239 	import std.range: cycle;
240 	assert(foo[].cycle[4 .. 8].equal(foo[]));
241 
242 	assert(foo[].save == foo[]);
243 
244 	FixedString!10 bar;
245 	bar = " is nic";
246 	bar ~= 'e';
247 
248 	immutable truth = fixedString!"codl is nice";
249 
250 	assert(foo ~ bar == truth);
251 	assert(foo.concat!16(bar) == truth);
252 	assert(foo ~ bar == foo.concat!16(bar));
253 
254 	FixedString!10 d;
255 	d = foo;
256 	d.length = 4;
257 
258 	foreach (i, char c; d)
259 	{
260 		assert(c == foo[i]);
261 	}
262 
263 	foo = "de";
264 	foo ~= "ad";
265 	assert(foo == "dead");
266 	bar = "beef";
267 	foo ~= bar;
268 	assert(foo == "deadbeef");
269 
270 	assert(fixedString!"aéiou" == "aéiou");
271 
272 	immutable char[4] dead = "dead";
273 	immutable(char)[4] beef = "beef";
274 
275 	immutable FixedString!4 deader = dead;
276 	immutable FixedString!4 beefer = beef;
277 	assert(deader ~ beefer == "deadbeef");
278 }
279 
280 ///
281 pure @safe nothrow unittest
282 {
283 	immutable a = FixedString!16("bepis");
284 	assert(a.toString == "bepis");
285 
286 	int[FixedString!16] table;
287 	table[a] = 1;
288 
289 	immutable b = FixedString!16("conk");
290 	table[b] = 2;
291 
292 	assert(table[a] == 1);
293 	assert(table[b] == 2);
294 }
295 
296 /// readme example code
297 @safe @nogc nothrow pure unittest
298 {
299 	FixedString!14 foo = "clang";
300 	foo[0] = 'd';
301 	foo ~= " is cool";
302 	assert(foo == "dlang is cool");
303 
304 	foo.length = 9;
305 
306 	immutable bar = fixedString!"neat";
307 	assert(foo ~ bar == "dlang is neat");
308 
309 	// wchars and dchars are also supported
310 	assert(FixedString!(5, wchar)("áéíóú") == "áéíóú");
311 
312 	// in fact, any type is:
313 	immutable int[4] intArray = [1, 2, 3, 4];
314 	assert(FixedString!(5, int)(intArray) == intArray);
315 }
316 
317 @system unittest
318 {
319 	import std.exception: assertThrown;
320 	import core.exception: AssertError;
321 
322 	assertThrown!AssertError(FixedString!2("too long"));
323 
324 	FixedString!2 a = "uh";
325 	assertThrown!AssertError(a[69] = 'a');
326 	assertThrown!AssertError(a.concat!1(a));
327 }