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 std.traits: isSomeChar;
11 
12 /// short syntax.
13 auto FixedString(string s)()
14 {
15 	import std.math.algebraic: nextPow2;
16 	return FixedString!(nextPow2(s.length))(s);
17 }
18 
19 ///
20 @safe @nogc nothrow unittest
21 {
22 	enum testString = "dlang is good";
23 	immutable foo = FixedString!(testString);
24 	immutable bar = FixedString!16("dlang is good");
25 
26 	assert(foo == bar);
27 }
28 
29 ///
30 struct FixedString(size_t maxSize, CharT = char)
31 {
32 	invariant (_length <= data.length);
33 
34 	enum size = maxSize; ///
35 
36 	private size_t _length;
37 	private CharT[maxSize] data = ' ';
38 
39 	///
40 	public size_t length() const pure @nogc @safe
41 	{
42 		return _length;
43 	}
44 
45 	/// ditto
46 	public void length(in size_t rhs) pure @safe @nogc
47 	in (rhs <= maxSize)
48 	{
49 		if (rhs >= length)
50 		{
51 			data[length .. rhs] = CharT.init;
52 		}
53 		_length = rhs;
54 	}
55 
56 	/// constructor
57 	public this(in CharT[] rhs) @safe @nogc nothrow pure
58 	in (rhs.length <= maxSize)
59 	{
60 		length = rhs.length;
61 		data[0 .. length] = rhs[];
62 	}
63 
64 	/// assignment
65 	public void opAssign(in CharT[] rhs) @safe @nogc nothrow pure
66 	in (rhs.length <= maxSize)
67 	{
68 		length = rhs.length;
69 		data[0 .. length] = rhs[];
70 	}
71 
72 	/// ditto
73 	public void opAssign(T : FixedString!n, size_t n)(in T rhs) @safe @nogc nothrow pure
74 	in (rhs.length <= maxSize)
75 	{
76 		length = rhs.length;
77 		data[0 .. length] = rhs[];
78 	}
79 
80 	/// ditto
81 	public void opOpAssign(string op)(in CharT[] rhs) @safe @nogc nothrow pure
82 	if (op == "~")
83 	in (length + rhs.length <= maxSize)
84 	{
85 		immutable oldLength = length;
86 		length = length + rhs.length;
87 		data[oldLength .. length] = rhs[];
88 	}
89 
90 	/// ditto
91 	public void opOpAssign(string op)(in CharT rhs) @safe @nogc nothrow pure
92 	if (op == "~")
93 	in (length + 1 <= maxSize)
94 	{
95 		length = length + 1;
96 		data[length - 1] = rhs;
97 	}
98 
99 	/// ditto
100 	public void opOpAssign(string op, T: FixedString!n, size_t n)(in T rhs) @safe @nogc nothrow pure
101 	if (op == "~")
102 	in (length + rhs.length <= maxSize)
103 	{
104 		immutable oldLength = length;
105 		length = length + rhs.length;
106 		data[oldLength .. length] = rhs[];
107 	}
108 
109 	/// array features...
110 	public auto opSlice(in size_t first, in size_t last) @safe @nogc nothrow const pure
111 	{
112 		auto temp = data[first .. last];
113 		return temp;
114 	}
115 
116 	/// ditto
117 	public size_t opDollar(size_t pos)() @safe @nogc nothrow const pure
118 	if (pos == 0)
119 	{
120 		return length;
121 	}
122 
123 	/// ditto
124 	public const(CharT)[] opIndex() @safe @nogc nothrow const pure
125 	{
126 		return data[0 .. length];
127 	}
128 
129 	/// ditto
130 	public CharT opIndex(in size_t index) @safe @nogc nothrow const pure
131 	in (index < length)
132 	{
133 		return data[index];
134 	}
135 
136 	/// ditto
137 	public void opIndexAssign(in CharT rhs, in size_t index) @safe @nogc nothrow pure
138 	in (index <= maxSize)
139 	{
140 		if (index >= length)
141 		{
142 			length = index + 1;
143 		}
144 		data[index] = rhs;
145 	}
146 
147 	/// equality
148 	public bool opEquals(T : FixedString!n, size_t n)(in T rhs) @safe @nogc nothrow const pure
149 	{
150 		return this[] == rhs[];
151 	}
152 
153 	/// ditto
154 	public bool opEquals(in CharT[] s) @safe @nogc nothrow const pure
155 	{
156 		if (length != s.length)
157 		{
158 			return false;
159 		}
160 
161 		return this[] == 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 	public auto concat(size_t s, T: FixedString!n, size_t n)(in T rhs) @safe @nogc nothrow const pure
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 	public auto opBinary(string op, T: FixedString!n, size_t n)(in T rhs) @safe @nogc nothrow const pure
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 		public const(CharT)[] toString() @safe nothrow const pure
188 		{
189 			return data[0 .. length].idup;
190 		}
191 	}
192 
193 	///
194 	public size_t toHash() @safe @nogc nothrow const pure
195 	{
196 		ulong result = length;
197 		foreach (CharT c; data[0 .. length])
198 		{
199 			result += c;
200 		}
201 		return result;
202 	}
203 
204 	/// range interface
205 	public bool empty() @safe @nogc nothrow const pure
206 	{
207 		return (length <= 0);
208 	}
209 
210 	/// ditto
211 	public CharT front() @safe @nogc nothrow const pure
212 	{
213 		return data[0];
214 	}
215 
216 	/// ditto
217 	public void popFront() @safe @nogc nothrow
218 	{
219 		for (auto i = 0; i < length; ++i)
220 		{
221 			data[i] = data[i + 1];
222 		}
223 		length = length - 1;
224 	}
225 
226 	mixin(opApplyWorkaround);
227 }
228 
229 /// readme example code
230 @safe @nogc nothrow unittest
231 {
232 	FixedString!14 foo = "clang";
233 	foo[0] = 'd';
234 	foo ~= " is cool";
235 	assert (foo == "dlang is cool");
236 
237 	foo.length = 9;
238 
239 	immutable bar = FixedString!"neat";
240 	assert (foo ~ bar == "dlang is neat");
241 
242 	// wchars and dchars are also supported
243 	assert(FixedString!(5, wchar)("áéíóú") == "áéíóú");
244 
245 	// in fact, any type is:
246 	immutable int[4] intArray = [1, 2, 3, 4];
247 	assert(FixedString!(5, int)(intArray) == intArray);
248 }
249 
250 private string resultAssign(in int n)
251 {
252 	switch (n)
253 	{
254 	case 1:
255 		return "result = dg(temp);";
256 	case 2:
257 		return "result = dg(i, temp);";
258 	default:
259 		assert(false, "this will never happen.");
260 	}
261 }
262 
263 private string delegateType(in int n, in string attributes)
264 {
265 	string params;
266 	switch (n)
267 	{
268 	case 1:
269 		params = "ref CharT";
270 		break;
271 	case 2:
272 		params = "ref int, ref CharT";
273 		break;
274 	default:
275 		assert(false, "unsupported number of parameters.");
276 	}
277 
278 	return "delegate(" ~ params ~ ") " ~ attributes;
279 }
280 
281 private string opApplyWorkaround()
282 {
283 	// dfmt off
284 	return paramNumbers("") ~
285 	paramNumbers("@safe") ~
286 	paramNumbers("@nogc") ~
287 	paramNumbers("@safe @nogc") ~
288 	paramNumbers("nothrow") ~
289 	paramNumbers("@safe nothrow") ~
290 	paramNumbers("@nogc nothrow") ~
291 	paramNumbers("@safe @nogc nothrow") ~
292 	paramNumbers("pure") ~
293 	paramNumbers("pure @safe") ~
294 	paramNumbers("pure @nogc") ~
295 	paramNumbers("pure @safe @nogc") ~
296 	paramNumbers("pure nothrow") ~
297 	paramNumbers("pure @safe nothrow") ~
298 	paramNumbers("pure @nogc nothrow") ~
299 	paramNumbers("pure @safe @nogc nothrow");
300 	// dfmt on
301 }
302 
303 private string paramNumbers(in string params)
304 {
305 	// dfmt off
306 	return good(1, params, true) ~
307 	good(2, params, true) ~
308 	good(1, params, false) ~
309 	good(2, params, false);
310 	// dfmt on
311 }
312 
313 private string good(in int n, in string parameters, in bool isConst)
314 {
315 	string s;
316 	if (isConst)
317 	{
318 		s = "const";
319 	}
320 	else
321 	{
322 		s = "";
323 	}
324 
325 	string result = "
326 		public int opApply(scope int " ~ delegateType(n, parameters) ~ " dg) " ~ s ~ "
327 		{
328 			int result = 0;
329 
330 			for (int i = 0; i != length; ++i)
331 			{
332 				CharT temp = data[i];
333 				" ~ resultAssign(n) ~ "
334 				if (result)
335 				{
336 					break;
337 				}
338 			}
339 
340 			return result;
341 		}";
342 
343 	return result;
344 }
345 
346 @safe @nogc nothrow unittest
347 {
348 	immutable string temp = "cool";
349 	auto a = FixedString!8(temp);
350 	assert(a[0] == 'c');
351 	assert(a == "cool");
352 	assert(a[] == "cool");
353 	assert(a[0 .. $] == "cool");
354 
355 	a[2] = 'd';
356 	assert(a == "codl");
357 	assert(a != "");
358 
359 	a[5] = 'd';
360 	assert(a == "codl\xffd");
361 
362 	a.length = 4;
363 	assert(a == "codl");
364 
365 	a.length = 6;
366 	assert(a == "codl\xff\xff");
367 
368 	a.length = 4;
369 	assert(a == "codl");
370 
371 	FixedString!10 b;
372 	b = " is nic";
373 	b ~= 'e';
374 	assert(a ~ b == "codl is nice");
375 	assert(a.concat!16(b) == "codl is nice");
376 	assert(a ~ b == a.concat!16(b));
377 
378 	FixedString!10 d;
379 	d = a;
380 
381 	foreach (i, char c; a)
382 	{
383 		switch (i)
384 		{
385 		case 0:
386 			assert(c == 'c');
387 			break;
388 
389 		case 1:
390 			assert(c == 'o');
391 			break;
392 
393 		case 2:
394 			assert(c == 'd');
395 			break;
396 
397 		case 3:
398 			assert(c == 'l');
399 			break;
400 
401 		default:
402 			assert(false);
403 		}
404 	}
405 
406 	a = "de";
407 	a ~= "ad";
408 	assert(a == "dead");
409 	b = "beef";
410 	a ~= b;
411 	assert(a == "deadbeef");
412 
413 	assert(FixedString!"aéiou" == "aéiou");
414 
415 	immutable char[4] dead = "dead";
416 	immutable(char)[4] beef = "beef";
417 
418 	immutable FixedString!4 deader = dead;
419 	immutable FixedString!4 beefer = beef;
420 	assert(deader ~ beefer == "deadbeef");
421 }
422 
423 @safe nothrow unittest
424 {
425 	immutable a = FixedString!16("bepis");
426 	assert (a.toString == "bepis");
427 
428 	int[FixedString!16] table;
429 	table[a] = 1;
430 
431 	immutable b = FixedString!16("conk");
432 	table[b] = 2;
433 
434 	assert(table[a] == 1);
435 	assert(table[b] == 2);
436 
437 }
438 
439 @system unittest
440 {
441 	import std.exception;
442 	import core.exception : AssertError;
443 
444 	assertThrown!AssertError(FixedString!2("too long"));
445 
446 	FixedString!2 a = "uh";
447 	assertThrown!AssertError(a[69] = 'a');
448 	assertThrown!AssertError(a.concat!1(a));
449 }