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