1 /++
2 $(H1 Small String)
3 
4 The module contains self-contained generic small string implementaton.
5 
6 $(LREF SmallString) supports ASDF - Json Serialisation Library.
7 
8 See also `include/mir/small_series.h` for a C++ version of this type.
9 Both C++ and D implementations have the same ABI and name mangling.
10 
11 Copyright: 2020 Ilya Yaroshenko, Kaleidic Associates Advisory Limited, Symmetry Investments
12 Authors: Ilya Yaroshenko
13 +/
14 module mir.small_string;
15 
16 import mir.serde: serdeScoped, serdeProxy;
17 
18 private extern (C) @system nothrow @nogc pure size_t strnlen_s(scope const char* s, size_t n);
19 
20 private static immutable errorMsg = "Cannot create SmallString: input string exceeds maximum allowed length.";
21 version(D_Exceptions)
22     private static immutable exception = new Exception(errorMsg);
23 
24 extern(C++, "mir"):
25 
26 /++
27 Self-contained generic Small String implementaton.
28 +/
29 @serdeScoped @serdeProxy!(const(char)[])
30 struct SmallString(uint maxLength)
31     if (maxLength)
32 {
33 
34     import core.stdc..string: memcmp, memcpy, strlen;
35     import std.traits: Unqual, isIterable;
36 
37     // maxLength bytes
38     char[maxLength] _data = '\0';
39 
40 extern(D) @safe pure @nogc:
41 
42     /// Constructor
43     this(typeof(null))
44     {
45     }
46 
47     /// ditto
48     this(scope const(char)[] str)
49     {
50         this.opAssign(str);
51     }
52 
53     /// ditto
54     this(uint n)(auto ref scope const SmallString!n str)
55     {
56         this.opAssign(str);
57     }
58 
59     /// ditto
60     this(Range)(auto ref Range str)
61         if (isIterable!Range)
62     {
63         size_t i = 0;
64         foreach(char c; str)
65         {
66             if (i > _data.length)
67             {
68                 version(D_Exceptions) throw exception;
69                 else assert(0, errorMsg);
70             }
71             _data[i++] = c;
72         }
73     }
74 
75     /// `=` operator
76     ref typeof(this) opAssign(typeof(null)) return
77     {
78         _data = '\0';
79         return this;
80     }
81 
82     /// ditto
83     ref typeof(this) opAssign(scope const(char)[] str) return @trusted
84     {
85         if (str.length > _data.sizeof)
86         {
87             version(D_Exceptions) throw exception;
88             else assert(0, errorMsg);
89         }
90         if (__ctfe)
91             _data[0 .. str.length] =  str;
92         else
93             memcpy(_data.ptr, str.ptr, str.length);
94         _data[str.length .. $] = '\0';
95         return this;
96     }
97 
98     /// ditto
99     ref typeof(this) opAssign(uint n)(auto ref scope const SmallString!n rhs) return
100         if (n != maxLength)
101     {
102         static if (n < maxLength)
103         {
104             version (LDC)
105                 cast(char[n])(_data[0 .. n]) = rhs._data;
106             else
107                 _data[0 .. n] = rhs._data;
108             _data[n .. maxLength] = '\0';
109         }
110         else
111         {
112             if (rhs._data[maxLength])
113             {
114                 version(D_Exceptions) throw exception;
115                 else assert(0, errorMsg);
116             }
117             _data = cast(char[maxLength])(rhs._data[0 .. maxLength]);
118         }
119         return this;
120     }
121 
122     /// ditto
123     ref typeof(this) opAssign(uint n)(const SmallString!n rhs) return
124         if (n != maxLength)
125     {
126         static if (n < maxLength)
127         {
128             version (LDC)
129                 cast(char[n])(_data[0 .. n]) = rhs._data;
130             else
131                 _data[0 .. n] = rhs._data;
132             _data[n .. maxLength] = '\0';
133         }
134         else
135         {
136             if (rhs._data[maxLength])
137             {
138                 version(D_Exceptions) throw exception;
139                 else assert(0, errorMsg);
140             }
141             _data = cast(char[maxLength])(rhs._data[0 .. maxLength]);
142         }
143         return this;
144     }
145 
146     /// ditto
147     void trustedAssign(scope const(char)[] str) return @trusted nothrow
148     {
149         _data = '\0';
150         if (__ctfe)
151             _data[0 .. str.length] =  str;
152         else
153             memcpy(_data.ptr, str.ptr, str.length);
154     }
155 
156     ///
157     ref typeof(this) append(char c) @trusted
158     {
159         auto length = opIndex.length;
160         if (length == maxLength)
161         {
162             version(D_Exceptions) throw exception;
163             else assert(0, errorMsg);
164         }
165         _data[length] = c;
166         return this;
167     }
168 
169     ///
170     ref typeof(this) append(scope const(char)[] str) @trusted
171     {
172         auto length = opIndex.length;
173         if (length + str.length > maxLength)
174         {
175             version(D_Exceptions) throw exception;
176             else assert(0, errorMsg);
177         }
178         if (__ctfe)
179             _data[length .. str.length + length] = str;
180         else
181             memcpy(_data.ptr + length, str.ptr, str.length);
182         return this;
183     }
184 
185     /// ditto
186     alias put = append;
187 
188     /// ditto
189     alias opOpAssign(string op : "~") = append;
190 
191     ///
192     SmallString concat(scope const(char)[] str) scope const
193     {
194         SmallString c = this;
195         c.append(str);
196         return c; 
197     }
198 
199     /// ditto
200     alias opBinary(string op : "~") = concat;
201 
202 
203 scope nothrow:
204 
205     /++
206     Returns an scope common string.
207 
208     The property is used as common string representation self alias.
209 
210     The alias helps with `[]`, `[i]`, `[i .. j]`, `==`, and `!=` operations and implicit conversion to strings.
211     +/
212     inout(char)[] opIndex() inout @trusted scope return
213     {
214         size_t i;
215         if (__ctfe)
216             while (i < maxLength && _data[i]) i++;
217         else
218             i = _data[$ - 1] ? _data.length : strlen(_data.ptr);
219         return _data[0 .. i];
220     }
221 
222     ///
223     ref inout(char) opIndex(size_t index) inout scope return
224     {
225         return opIndex[index];
226     }
227 
228 const:
229 
230     ///
231     bool empty() @property
232     {
233         return _data[0] == 0;
234     }
235 
236     ///
237     size_t length() @property
238     {
239         return opIndex.length;
240     }
241 
242     ///
243     alias toString = opIndex;
244 
245     /// Hash implementation
246     size_t toHash()
247     {
248         return hashOf(opIndex);
249     }
250 
251     /// Comparisons operator overloads
252     bool opEquals(ref scope const SmallString rhs) scope
253     {
254         return _data == rhs._data;
255     }
256 
257     /// ditto
258     bool opEquals(SmallString rhs) scope
259     {
260         return _data == rhs._data;
261     }
262 
263     /// ditto
264     bool opEquals(uint rhsMaxLength)(auto ref scope const SmallString!rhsMaxLength rhs) scope
265         if (rhsMaxLength != maxLength)
266     {
267         return opIndex == rhs.opIndex;
268     }
269 
270     /// ditto
271     bool opEquals()(scope const(char)[] str) scope
272     {
273         return opIndex == str;
274     }
275 
276     /// ditto
277     int opCmp(uint rhsMaxLength)(auto ref scope const SmallString!rhsMaxLength rhs) scope
278     {
279         return __cmp(opIndex, rhs.opIndex);
280     }
281 
282     /// ditto
283     int opCmp()(scope const(char)[] str) scope
284     {
285         return __cmp(opIndex, str);
286     }
287 }
288 
289 ///
290 @safe pure @nogc version(mir_test) unittest
291 {
292     SmallString!16 s16;
293     assert(s16.empty);
294 
295     auto s8 = SmallString!8("Hellow!!");
296     assert(s8 == "Hellow!!", s8[]);
297 
298     s16 = s8;
299     assert(s16 == "Hellow!!", s16[]);
300     s16[7] = '@';
301     s8 = null;
302     assert(s8.empty);
303     s8 = s16;
304     assert(s8 == "Hellow!@");
305 
306     auto s8_2 = s8;
307     assert(s8_2 == "Hellow!@");
308     assert(s8_2 == s8);
309 
310     assert(s8 < "Hey");
311     assert(s8 > "Hellow!");
312 
313     assert(s8.opCmp("Hey") < 0);
314     assert(s8.opCmp(s8) == 0);
315 }
316 
317 /// Concatenation
318 @safe pure @nogc version(mir_test) unittest
319 {
320     auto a = SmallString!16("asdf");
321     a ~= " ";
322     auto b = a ~ "qwerty";
323     static assert(is(typeof(b) == SmallString!16));
324     assert(b == "asdf qwerty");
325     b.put('!');
326     b.put("!");
327     assert(b == "asdf qwerty!!");
328 }
329 
330 @safe pure @nogc nothrow version(mir_test) unittest
331 {
332     import mir.conv: emplaceRef;
333     SmallString!32 a, b;
334     emplaceRef!(const SmallString!32)(a, cast(const)b);
335 }