1 /**
2  * This file is part of the Amalthea library.
3  * Copyright (C) 2018-2024 Eugene 'Vindex' Stulin
4  * Distributed under the BSL 1.0 or (at your option) the GNU LGPL 3.0 or later.
5  *
6  * The module contains some useful functions for data processing,
7  * including functions for working with dynamic arrays and associative arrays,
8  * functions for working with command line arguments,
9  * operations on arrays as sets,
10  * some functions for working with strings, etc.
11  */
12 
13 module amalthea.dataprocessing;
14 
15 public import amalthea.libcore;
16 
17 import std.algorithm, std.array, std.json;
18 import std.format : formattedWrite;
19 import std.range.primitives : isInputRange;
20 static import std.traits;
21 
22 
23 /*******************************************************************************
24  * Implementation of the intersection operation for arrays.
25  */
26 T calcIntersection(T)(const T[] arrays ...) {
27     import std.algorithm.setops : setIntersection;
28     if (arrays.length == 1) return copyArray(arrays[0]);
29     T result = setIntersection(
30         sort(copyArray(arrays[0])), sort(copyArray(arrays[1]))
31     ).array;
32     for (auto i = 2; i < arrays.length; ++i) {
33         result = setIntersection(result, sort(copyArray(arrays[i]))).array;
34     }
35     return result;
36 }
37 ///
38 unittest {
39     int[] arr = calcIntersection([3, 4, 1, -15], [9, 3, 1], [1, 99, 3]);
40     assert(arr == [1, 3]);
41 }
42 
43 
44 /*******************************************************************************
45  * Implementation of the union operation for arrays.
46  */
47 T calcUnion(T)(const T[] arrays ...) {
48     if (arrays.length == 1) {
49         return copyArray(arrays[0]);
50     }
51     T result = copyArray(arrays[0]);
52     for (size_t i = 1; i < arrays.length; ++i) {
53         foreach(el; copyArray(arrays[i])) {
54             if (!canFind(result, el)) {
55                 result ~= el;
56             }
57         }
58     }
59     return std.algorithm.sort(result).array;
60 }
61 ///
62 unittest {
63     int[] arr = calcUnion([3, 4, 1, -15], [9, 3, 1], [1, 99, 3]);
64     assert(arr == [-15, 1, 3, 4, 9, 99]);
65 }
66 
67 
68 /*******************************************************************************
69  * Implementation of the complement operation for arrays.
70  */
71 T calcComplement(T)(const T[] arrays ...) {
72     if (arrays.length == 1) {
73         return arrays[0].copyArray;
74     }
75     T result = setDifference(
76         arrays[0].copyArray.sort,
77         arrays[1].copyArray.sort
78     ).array;
79     for (auto i = 2; i < arrays.length; ++i) {
80         result = array(setDifference(result, arrays[i].copyArray.sort));
81     }
82     return result;
83 }
84 ///
85 unittest {
86     int[] arr = calcComplement([3, 4, 1, -15], [9, 3, 1]);
87     assert(arr == [-15, 4]);
88 }
89 
90 
91 /*******************************************************************************
92  * The function removes consecutive string duplicates.
93  */
94 string removeDuplicateConsecutiveSubstring(string s, string substring) {
95     while(s.canFind(substring~substring)) {
96         s = s.replace(substring~substring, substring);
97     }
98     return s;
99 }
100 ///
101 unittest {
102     string s = removeDuplicateConsecutiveSubstring("AAAAbooeAAAuuvAww", "A");
103     assert(s == "AbooeAuuvAww");
104     s = removeDuplicateConsecutiveSubstring("AAAAbcooeAAAgguvAww", "AA");
105     assert(s == "AAbcooeAAAgguvAww");
106 }
107 
108 
109 /*******************************************************************************
110  * The function removes duplicate elements from an array.
111  */
112 ref T[] removeDuplicate(T)(ref T[] arr) {
113     if (arr.empty) return arr;
114     for (auto i = arr.length-1; i > 0; --i) {
115         if (arr[0 .. i].canFind(arr[i])) {
116             arr = arr.remove(i);
117         }
118     }
119     return arr;
120 }
121 ///
122 unittest {
123     int[] values = [1, 3, 1, 7, 2, 19, 5, 2, 2];
124     removeDuplicate(values);
125     assert(values == [1, 3, 7, 2, 19, 5]);
126 }
127 
128 
129 /*******************************************************************************
130  * Returns array of tuples with pairs of keys and values from associative array.
131  */
132 auto assocArrayItems(V, K)(V[K] aarray) {
133     Tuple!(K, V)[] pairs;
134     foreach(item; aarray.byPair) {
135         pairs ~= item;
136     }
137     return pairs;
138 }
139 
140 
141 /*******************************************************************************
142  * Makes key-sorted array of tuples with pairs of keys and values
143  * from associative array.
144  * Params:
145  *     aarray = Associative array.
146  *     desc = Flag for sorting (descending/ascending). By default, No.desc.
147  * Returns: Key-sorted array of tuples with elements of the associative array.
148  */
149 auto sortAssocArrayItemsByKeys(V, K)(V[K] aarray, Flag!"desc" desc = No.desc) {
150     if (!desc) {
151         return aarray.assocArrayItems.sort!((a, b) => a[0] < b[0]).array;
152     }
153     return aarray.assocArrayItems.sort!((a, b) => a[0] > b[0]).array;
154 }
155 unittest {
156     int[string] aarray = ["x": 9, "y": 7];
157     auto expected = [tuple("x", 9), tuple("y", 7)];
158     assert(expected == sortAssocArrayItemsByKeys(aarray));
159     expected = [tuple("y", 7), tuple("x", 9)];
160     assert(expected == sortAssocArrayItemsByKeys(aarray, Yes.desc));
161 }
162 
163 
164 /*******************************************************************************
165  * Makes value-sorted array of tuples with pairs of keys and values
166  * from associative array.
167  * Params:
168  *     aarray = Associative array.
169  *     desc = Flag for sorting (descending/ascending). By default, No.desc.
170  * Returns: Value-sorted array of tuples with elements of the associative array.
171  */
172 auto sortAssocArrayItemsByValues(V, K)(
173     V[K] aarray,Flag!"desc" desc = No.desc
174 ) {
175     if (!desc) {
176         return aarray.assocArrayItems.sort!((a, b) => a[1] < b[1]).array;
177     }
178     return aarray.assocArrayItems.sort!((a, b) => a[1] > b[1]).array;
179 }
180 unittest {
181     int[string] aarray = ["x": 9, "y": 7];
182     auto expected = [tuple("y", 7), tuple("x", 9)];
183     assert(expected == sortAssocArrayItemsByValues(aarray));
184     expected = [tuple("x", 9), tuple("y", 7)];
185     assert(expected == sortAssocArrayItemsByValues(aarray, Yes.desc));
186 }
187 
188 
189 /*******************************************************************************
190  * The function arranges the associative array
191  * by the required sorting method (string "by" is equal to "values" or "keys"),
192  * returns an array of keys and an array of values.
193  */
194 deprecated("Use sortAssocArrayItemsByKeys and sortAssocArrayItemsByValues")
195 void orderAssociativeArray(alias by, V, K)(
196     in V[K] aarray, out K[] orderedKeys, out V[] orderedValues
197 )
198 if (by == "values" || by == "keys") {
199     if (aarray.length == 0) {
200         return;
201     }
202     if (by == "keys") {
203         orderedKeys = aarray.keys.dup;
204         orderedKeys.sort;
205         orderedValues.length = orderedKeys.length;
206         foreach(i, k; orderedKeys) {
207             orderedValues[i] = aarray[k];
208         }
209     } else {
210         orderedValues = aarray.values.dup;
211         orderedValues.sort;
212         orderedKeys.length = orderedValues.length;
213         size_t[] passedIndexes;
214         foreach(k; aarray.keys) {
215             foreach(i, v; orderedValues) {
216                 if (passedIndexes.canFind(i)) {
217                     continue;
218                 }
219                 if (aarray[k] == v) {
220                     orderedKeys[i] = k;
221                     passedIndexes ~= i;
222                     break;
223                 }
224             }
225         }
226     }
227 }
228 
229 
230 /*******************************************************************************
231  * The function arranges the associative array by keys,
232  * returns an array of keys and an array of values.
233  */
234 deprecated("Use sortAssocArrayItemsByKeys")
235 void orderAssociativeArrayByKeys(V, K)(
236     in V[K] aarray, out K[] orderedKeys, out V[] orderedValues
237 ) {
238     orderAssociativeArray!"keys"(aarray, orderedKeys, orderedValues);
239 }
240 
241 
242 /*******************************************************************************
243  * The function arranges the associative array by values,
244  * returns an array of keys and an array of values.
245  */
246 deprecated("sortAssocArrayItemsByValues")
247 void orderAssociativeArrayByValues(V, K)(
248     in V[K] aarray, out K[] orderedKeys, out V[] orderedValues
249 ) {
250     orderAssociativeArray!"values"(aarray, orderedKeys, orderedValues);
251 }
252 
253 
254 /*******************************************************************************
255  * This function combines two associative arrays, returns new associative array.
256  * If two arrays have the same keys, the values from the first array are used.
257  */
258 V[K] mixAssociativeArrays(K,V)(V[K] firstArray, V[K] secondArray) {
259     V[K] array = firstArray.dup;
260     foreach(k, v; secondArray) {
261         if (k !in array) {
262             array[k] = v;
263         }
264     }
265     return array;
266 }
267 
268 
269 /*******************************************************************************
270  * Function to get a value by parameter.
271  * Used to process command line arguments.
272  * Params:
273  *     args = Command line arguments.
274  *     flag = Parameter with "--".
275  *     altFlag = Synonym for the flat. By default, empty.
276  * Returns: The value of the required parameter.
277  */
278 string getOptionValue(string[] args, string flag, string altFlag = "") {
279     auto tmpArgs = args.dup;
280     return extractValueForFlag(tmpArgs, flag, altFlag);
281 }
282 alias getValueForFlag = getOptionValue;
283 
284 
285 /*******************************************************************************
286  * Function for extracting of value by flag.
287  * Unlike getOptionValue(), this function changes the passed `args`,
288  * removing the flag with a value.
289  * Params:
290  *     args = Command line arguments. Changes while the function is running.
291  *     flag = Parameter with "--".
292  *     altFlag = Synonym for the flat. By default, empty.
293  * Returns: The value of the required parameter.
294  */
295 string extractOptionValue(ref string[] args, string flag, string altFlag = "") {
296     string value;
297     ssize_t index1 = -1;
298 
299     cycle1: foreach(option; [flag, altFlag]) {
300         foreach(i, arg; args) {
301             if (arg == option || arg.startsWith(option~"=")) {
302                 index1 = i;
303                 flag = option;
304                 break cycle1;
305             }
306         }
307     }
308     if (index1 == -1) return "";
309 
310     if (args[index1].startsWith(flag~"=")) {
311         value = args[index1][(flag~"=").length .. $];
312         auto tempArgs = args[0 .. index1];
313         if (args.length > index1+1) tempArgs ~= args[index1+1 .. $];
314         args = tempArgs;
315     } else {
316         if (args.length < index1+2) return "";
317         value = args[index1+1];
318         auto tempArgs = args[0 .. index1];
319         if (args.length > index1+2) tempArgs ~= args[index1+2 .. $];
320         args = tempArgs;
321     }
322     return value;
323 }
324 alias extractValueForFlag = extractOptionValue;
325 
326 ///
327 unittest {
328     string[] args = "app -a value1 value2 -b parameter --nothing".split;
329     assert("parameter" == args.getOptionValue("-b"));
330     assert("value1" == args.getOptionValue("-a"));
331     assert("" == args.getOptionValue("--nothing"));
332 
333     assert(args == "app -a value1 value2 -b parameter --nothing".split);
334 
335     assert("parameter" == args.extractValueForFlag("-b"));
336     assert("app -a value1 value2 --nothing".split == args);
337     assert("value1" == args.extractValueForFlag("-a"));
338     assert("app value2 --nothing".split == args);
339 
340     args = "-a value1 value2 -b parameter --nothing".split;
341     assert("parameter" == args.getOptionValue("-b"));
342     assert("value1" == args.getOptionValue("-a"));
343     assert("" == args.getOptionValue("--nothing"));
344     assert("parameter" == args.extractValueForFlag("-b"));
345     assert("-a value1 value2 --nothing".split == args);
346     assert("value1" == args.extractValueForFlag("-a"));
347     assert("value2 --nothing".split == args);
348 
349     args = `build --file=/home/user/Project_1/Makefile`.split;
350     assert("/home/user/Project_1/Makefile" == args.getOptionValue("--file"));
351 
352 }
353 
354 
355 /*******************************************************************************
356  * Function to get values as array by flag.
357  * Params:
358  *     args = Command line arguments.
359  *     flag = Parameter with "--".
360  *     altFlag = Synonym for the flat. By default, empty.
361  * Returns: array of values after the flag.
362  */
363 string[] getOptionRange(string[] args, string flag, string altFlag = "") {
364     auto argsCopy = args.dup;
365     return extractOptionRange(argsCopy, flag, altFlag);
366 }
367 
368 
369 /*******************************************************************************
370  * Function for extracting of value as array by flag.
371  * Unlike getOptionRange(), this function changes the passed arguments (args),
372  * removing the flag with a value.
373  * Params:
374  *     args = Command line arguments. Changes while the function is running.
375  *     flag = Parameter with "--".
376  *     altFlag = Synonym for the flat. By default, empty.
377  * Returns: array of values after the flag.
378  */
379 string[] extractOptionRange(ref string[] args,
380                             string flag,
381                             string altFlag = "") {
382     string[] range;
383     ssize_t index1 = args.countUntil(flag);
384     ssize_t index2 = -1;
385 
386     if (index1 == -1) index1 = args.countUntil(altFlag);
387     if (index1 == -1) return range;
388     if (index1 != args.length-1 && !args[index1+1].startsWith("-")) {
389         foreach(i, arg; args[index1+1 .. $]) {
390             if (arg.startsWith("-")) {
391                 index2 = index1 + i;
392                 break;
393             }
394         }
395         if (index2 != -1) {
396             range = args[index1+1 .. index2+1];
397             args = args[0 .. index1] ~ args[index2+1 .. $];
398         } else {
399             index2 = args.length - 1;
400             range = args[index1+1 .. index2+1];
401             args = args[0 .. index1];
402         }
403     }
404     return range;
405 }
406 
407 
408 ///
409 unittest {
410     string[] args = "app -a value1 value2 -b parameter --nothing".split;
411     assert(["value1", "value2"] == getOptionRange(args, "-a"));
412     assert(["parameter"] == getOptionRange(args, "-b"));
413     string[] nothing = getOptionRange(args, "--nothing");
414     assert(nothing.empty);
415 
416     args = "app --alt value1 value2 -b parameter --nothing".split;
417     assert(["value1", "value2"] == getOptionRange(args, "-a", "--alt"));
418 
419     args = "app -a -b thing".split;
420     string[] vacuum = getOptionRange(args, "-a");
421     assert(vacuum.empty);
422 
423     args = "app -a value1 value2 -b parameter --nothing".split;
424     assert("value1 value2".split == extractOptionRange(args, "-a"));
425     assert("app -b parameter --nothing".split == args);
426 
427     args = "-a value1 value2 -b parameter --nothing".split;
428     assert("value1 value2".split == extractOptionRange(args, "-a"));
429     assert("-b parameter --nothing".split == args);
430 }
431 
432 
433 /*******************************************************************************
434  * The function converts integer number into an octal form, returns as a string.
435  */
436 string getOctalForm(ulong value) {
437     auto writer = std.array.appender!string();
438     formattedWrite(writer, "%o", value);
439     return writer.data;
440 }
441 alias toOctalString = getOctalForm;
442 ///
443 unittest {
444     short eight = 8;
445     assert(getOctalForm(eight) == "10");
446     assert(33261.getOctalForm == "100755");
447 }
448 
449 
450 /*******************************************************************************
451  * The function converts integer number into an binary form.
452  */
453 string getBinaryForm(ulong value) {
454     auto writer = std.array.appender!string();
455     formattedWrite(writer, "%b", value);
456     return writer.data;
457 }
458 
459 
460 /*******************************************************************************
461  * The function returns the index of the array element by value.
462  * Params:
463  *     haystack = The array in which the search is performed.
464  *     needle = Element value in the array.
465  * Returns: The index of the array element. -1 if the element was not found.
466  */
467 ssize_t getIndex(T)(T[] haystack, T needle) {
468     foreach(i, el; haystack) {
469         if (el == needle) {
470             return i;
471         }
472     }
473     return -1;
474 }
475 
476 
477 /*******************************************************************************
478  * The function returns an array without unnecessary elements.
479  */
480 T[] removeElementsByContent(T)(T[] haystack, T needle) {
481     for(ssize_t i; i < haystack.length; ++i) {
482         if (haystack[i] == needle) {
483             if (haystack.length-1 != i) {
484                 haystack = haystack[0 .. i] ~ haystack[i+1 .. $];
485             } else {
486                 haystack = haystack[0 .. i];
487             }
488             --i;
489         }
490     }
491     return haystack.dup;
492 }
493 ///
494 unittest {
495     auto arr = [1, 8, 17, 4, 23, 8, 8, 3, 13, 19, 8];
496     auto res = arr.removeElementsByContent(8);
497     assert(res == [1, 17, 4, 23, 3, 13, 19]);
498 
499     auto lines = ["", "straw", "", "", "one more straw", "", ""];
500     auto straws = lines.removeElementsByContent("");
501     assert(straws == ["straw", "one more straw"]);
502 }
503 
504 
505 /*******************************************************************************
506  * Encode associative array using www-form-urlencoding (copied from std.uri).
507  * Params:
508  *     values = An associative array containing the values to be encoded.
509  * Returns: A string encoded using www-form-urlencoding.
510  */
511 string urlEncode(in string[string] values) {
512     import std.uri : encodeComponent;
513     import std.array : Appender;
514     import std.format : formattedWrite;
515     if (values.length == 0) return "";
516     Appender!string enc;
517     enc.reserve(values.length * 128);
518     bool first = true;
519     foreach (k, v; values) {
520         if (!first) enc.put('&');
521         formattedWrite(enc, "%s=%s", encodeComponent(k), encodeComponent(v));
522         first = false;
523     }
524     return enc.data;
525 }
526 
527 
528 /*******************************************************************************
529  * The function converts JSONValue to double.
530  */
531 double convJSONValueToDouble(std.json.JSONValue json) {
532     double number;
533     static if (__traits(compiles, JSONType.float_)) {
534         switch(json.type) {
535             case JSONType.float_:   number = json.floating;            break;
536             case JSONType.integer:  number = to!double(json.integer);  break;
537             case JSONType.uinteger: number = to!double(json.uinteger); break;
538             case JSONType..string:   number = to!double(json.str);      break;
539             default: break;
540         }
541     } else {
542         switch(json.type) {
543             case JSON_TYPE.FLOAT:    number = json.floating;            break;
544             case JSON_TYPE.INTEGER:  number = to!double(json.integer);  break;
545             case JSON_TYPE.UINTEGER: number = to!double(json.uinteger); break;
546             case JSON_TYPE.STRING:   number = to!double(json.str);      break;
547             default: break;
548         }
549     }
550     return number;
551 }
552 
553 
554 /*******************************************************************************
555  * The function similar to functions "canFind" and "among".
556  */
557 bool isAmong(alias pred = (a, b) => a == b, Value, Values...)
558             (Value value, Values values)
559 if (
560     Values.length != 0
561 ) {
562     foreach (ref v; values) {
563         import std.functional : binaryFun;
564         if (binaryFun!pred(value, v)) return true;
565     }
566     return false;
567 }
568 /// ditto
569 bool isAmong(alias pred = (a, b) => a == b, T)
570             (T value, const T[] values) {
571     foreach (ref v; values) {
572         import std.functional : binaryFun;
573         if (binaryFun!pred(value, v)) return true;
574     }
575     return false;
576 }
577 
578 
579 /*******************************************************************************
580  * Creates new array with specified content.
581  */
582 T[] makeFilledArray(T)(size_t len, T filler) {
583     T[] arr = new T[len];
584     std.algorithm.fill(arr, filler);
585     return arr;
586 }
587 ///
588 unittest {
589     assert(makeFilledArray(4, 5) == [5, 5, 5, 5]);
590     assert(makeFilledArray(10, ' ') == "          ");
591 }
592 
593 
594 /*******************************************************************************
595  * Cuts off consecutive unnecessary characters from the left side.
596  * Returns: truncated string.
597  */
598 string stripLeft(string line, in char symbol=' ') nothrow {
599     while (!line.empty && line[0] == symbol) {
600         line = line[1 .. $];
601     }
602     return line;
603 }
604 unittest {
605     assert(stripLeft("   string ") == "string ");
606     assert(stripLeft("ssstring", 's') == "tring");
607 }
608 
609 
610 /// ditto
611 string stripLeft(string line, in char[] symbols) nothrow {
612     foreach(char ch; line.dup) {
613         if (ch.isAmong(symbols)) {
614             line = line[1 .. $];
615         } else {
616             break;
617         }
618     }
619     return line;
620 }
621 unittest {
622     auto str = " \t\t string\t";
623     auto spaces = [' ', '\t'];
624     auto res = "string\t";
625     assert(stripLeft(str, spaces) == res);
626 }
627 
628 
629 /*******************************************************************************
630  * Cuts off consecutive unnecessary characters from the right side.
631  * Returns: truncated string.
632  */
633 string stripRight(string line, in char symbol=' ') nothrow {
634     while (!line.empty && line[$-1] == symbol) {
635         line = line[0 .. $-1];
636     }
637     return line;
638 }
639 unittest {
640     assert(stripRight("   string ") == "   string");
641     assert(stripRight("ssstringgg", 'g') == "ssstrin");
642 }
643 
644 
645 /// ditto
646 string stripRight(string line, in char[] symbols) nothrow {
647     foreach_reverse(char ch; line.dup) {
648         if (ch.isAmong(symbols)) {
649             line = line[0 .. $-1];
650         } else {
651             break;
652         }
653     }
654     return line;
655 }
656 unittest {
657     auto str = " \t\t string\t \t";
658     auto spaces = [' ', '\t'];
659     auto res = " \t\t string";
660     assert(stripRight(str, spaces) == res);
661 }
662 
663 
664 /*******************************************************************************
665  * Counts the number of required elements in a range.
666  * Params:
667  *     range = Some input range.
668  *     value = Value to find.
669  * Returns: number of required elements.
670  */
671 size_t countElements(R, E)(R range, E value) nothrow pure @safe
672 if (isInputRange!R)
673 do {
674     size_t res;
675     foreach(elem; range) {
676         res += (value == elem);
677     }
678     return res;
679 }
680 unittest {
681     int[] arr = [0, 1, 3, 1, 5, 8, 5, 1];
682     assert(countElements(arr, 1) == 3);
683     assert(countElements(arr, 5) == 2);
684     assert(countElements(arr, 9) == 0);
685 }
686 
687 
688 /*******************************************************************************
689  * Divides an array: makes a new array of arrays containing elements 
690  * from source array. Each subarray contains specified number of elements.
691  * Params:
692  *     arr = Source array.
693  *     n = Number of elements in each new subarray.
694  */
695 T[] divideByElemNumber(T)(T arr, size_t n) nothrow pure @safe
696 if (std.traits.isArray!T)
697 in {
698     assert(arr.length % n == 0);
699 } do {
700     T[] res;
701     for (size_t i = 0; i < arr.length; i += n) {
702         res ~= arr[i .. i + n];
703     }
704     return res;
705 }
706 unittest {
707     string s = "123456";
708     assert(divideByElemNumber(s, 2) == ["12", "34", "56"]);
709     assert(divideByElemNumber(s, 3) == ["123", "456"]);
710 }
711 
712 
713 /*******************************************************************************
714  * Creates a new dynamic array of the same size and copies the contents of
715  * the dynamic array into it. This function supports arrays with complex
716  * constant structures, unlike the 'dup' function.
717  * Params:
718  *     arr = The dynamic array.
719  * Returns:
720  *     A copy of the required array.
721  */
722 T[] copyArray(T)(const T[] arr) if (!is(T == class)) {
723     T[] copy = new T[arr.length];
724     for (size_t i = 0; i < arr.length; i++) {
725         T elem = arr[i];
726         copy[i] = elem;
727     }
728     return copy;
729 }
730 unittest {
731     // copied for testing
732     T[] copyArray(T)(const T[] arr) if (!is(T == class)) {
733         T[] copy = new T[arr.length];
734         for (size_t i = 0; i < arr.length; i++) {
735             // it doesn't work with postblit, only with modern copy ctor
736             T elem = arr[i];
737             copy[i] = elem;
738         }
739         return copy;
740     }
741     struct S {
742         int x, y;
743         bool[] a;
744         this(ref return scope const S rhs) {
745             this.x = rhs.x;
746             this.y = rhs.y;
747             this.a = rhs.a.dup;
748         }
749     }
750     const S[] arr = [
751         S(1, 2, [true, false]),
752         S(3, 4, [false, true])
753     ];
754     // S[] copy = arr.dup;  // It can't be compiled!
755     S[] copy = copyArray(arr);   // from const array to non-const one
756 
757     assert(copy.length == arr.length);
758     for (size_t i = 0; i < arr.length; i++) {
759         assert(arr[i].x == copy[i].x);
760         assert(arr[i].y == copy[i].y);
761         assert(arr[i].a == copy[i].a);
762         assert(arr[i].a.ptr != copy[i].a.ptr);
763     }
764 }