1 module magpie.group;
2 
3 import magpie.axis: Axis, DataType;
4 import magpie.dataframe: DataFrame;
5 import magpie.index: Index;
6 import magpie.helper: dropper, transposed, toArr, vectorize, isHomogeneous, suitableType, auxDispatch;
7 
8 import std.meta: staticMap, AliasSeq;
9 import mir.ndslice;
10 
11 /// Struct for groupBy Operation
12 struct Group(GrpRowType...)
13 {
14     /// Group names
15     string[][] groups;
16 
17     /// number of elements before the particular group
18     int[] elementCountTill;
19 
20     /// Flag to check if the Group is Homogeneous
21     enum bool isHomogeneousType = isHomogeneous!(GrpRowType);
22 
23     static if(!isHomogeneous!(GrpRowType))
24         alias GrpType = staticMap!(toArr, GrpRowType);
25     else
26         alias GrpType = toArr!(GrpRowType[0])[GrpRowType.length];
27     /// Data of Group
28     GrpType data;
29 
30     /// Index for group
31     Index grpIndex;
32 
33 private:
34     ptrdiff_t positionInGroup(int axis)(string[] index, ptrdiff_t grpPos)
35     {
36         import std.array: appender;
37         import std.algorithm: countUntil;
38         import std.conv: to;
39         auto codes = appender!(int[]);
40 
41         foreach(i; 0 .. grpIndex.indexing[axis].codes.length)
42         {
43             if(grpIndex.indexing[axis].index[i].length == 0)
44                 codes.put(to!int(index[i]));
45             else
46             {
47                 int indxpos = cast(int)countUntil(grpIndex.indexing[axis].index[i], index[i]);
48                 if(indxpos < 0)
49                     return -1;
50                 codes.put(indxpos);
51             }
52         }
53 
54         foreach(i; 0 .. (axis == 0)? elementCountTill[grpPos +1] - elementCountTill[grpPos]: GrpType.length)
55         {
56             bool flag = true;
57             foreach(j; 0 .. grpIndex.indexing[axis].codes.length)
58             {
59                 if(grpIndex.indexing[axis].codes[j][i + ((axis == 0)? elementCountTill[grpPos]: 0)] != codes.data[j])
60                     flag = false;
61             }
62 
63             if(flag)
64                 return i;
65         }
66 
67         return -1;
68     }
69 
70     auto asSliceInternal(Type, SliceKind kind)(size_t start, size_t stop)
71     {
72         static if(__traits(isArithmetic, Type))
73             alias RetType = Type;
74         else
75             alias RetType =  string;
76 
77         static if(kind == Universal)
78             Slice!(RetType*, 2, kind) ret = slice!(RetType)(stop - start, GrpRowType.length).universal;
79         else static if(kind == Canonical)
80             Slice!(RetType*, 2, kind) ret = slice!(RetType)(stop - start, GrpRowType.length).canonical;
81         else
82             Slice!(RetType*, 2, kind) ret = slice!(RetType)(stop - start, GrpRowType.length);
83 
84         static foreach(i; 0 .. GrpRowType.length)
85         {
86             static if(__traits(isArithmetic, RetType, GrpRowType[i]))
87             {
88                 foreach(j; start .. stop)
89                     ret[j - start][i] = cast(RetType)data[i][j];
90             }
91             else static if(is(RetType == string))
92             {
93                 import std.conv: to;
94                 foreach(j; start .. stop)
95                     ret[j - start][i] = to!(string)(data[i][j]);
96             }
97         }
98 
99         return ret;
100     }
101 
102     template resolverInternal(T, Ops...)
103     {
104         import std.traits: ReturnType;
105         static if(Ops.length)
106         {
107             alias Op = Ops[0];
108             static if(__traits(compiles, ReturnType!(Op)))
109                 alias resolverInternal = AliasSeq!(ReturnType!(Op), resolverInternal!(T, Ops[1 .. $]));
110             else static if(__traits(compiles, ReturnType!(Op!T)))
111                 alias resolverInternal = AliasSeq!(ReturnType!(Op!T), resolverInternal!(T, Ops[1 .. $]));
112             else static if(__traits(compiles, ReturnType!(Op!(toArr!T))))
113                 alias resolverInternal = AliasSeq!(ReturnType!(Op!(toArr!T)), resolverInternal!(T, Ops[1 .. $]));
114             else
115                 alias resolverInternal = AliasSeq!(ReturnType!(Op!(T,T)), resolverInternal!(T, Ops[1 .. $]));
116         }
117         else 
118             alias resolverInternal = AliasSeq!();
119     }
120 
121     template aggregateType(Ops...)
122     {
123         alias fwdType = suitableType!(GrpRowType);
124         alias Resolved = resolverInternal!(fwdType, Ops);
125         alias aggregateType = Resolved;
126     }
127 
128 public:
129     /++
130     ptrdiff_t getGroupPosition(string[] grpTitles)
131     Description: Get position of a particular group in the sruct
132     @params: grpTitles - the group you wnant to search for
133     +/
134     ptrdiff_t getGroupPosition(string[] grpTitles)
135     {
136         foreach(i, ele; groups)
137             if(grpTitles == ele)
138                 return cast(int)i;
139 
140         return -1;
141     }
142 
143     /++
144     void createGroup(T)(DataFrame!T df, int[] levels)
145     Description: Funtion to create a groupBy object from a DataFrame
146     @parmas: df - DataFrame on which groupBy is operated
147     @params: levels - levels to consider for grouping
148     +/
149     void createGroup(int[] dataLevels = [], T...)(DataFrame!T df, int[] indexLevels)
150     {
151         assert(dataLevels.length + indexLevels.length > 0, "Cannot group without specifying any levels");
152         import std.algorithm: reduce, max, min, sort;
153 
154         if(indexLevels.length > 0)
155         {
156             assert(indexLevels.reduce!max < df.indx.row.index.length);
157             assert(indexLevels.reduce!min > -1);
158         }
159 
160         static if(dataLevels.length > 0)
161         {
162             assert(dataLevels.reduce!max < df.RowType.length);
163             assert(dataLevels.reduce!min > -1);
164         }
165 
166         string[][] levels;
167         levels.length = df.rows;
168         foreach(i; 0 .. df.rows)
169         {
170             levels[i].length = indexLevels.length + dataLevels.length;
171             foreach(j, ele; indexLevels)
172             {
173                 if(df.indx.row.index[ele].length == 0)
174                 {
175                     import std.conv: to;
176                     levels[i][j] = to!string(df.indx.row.codes[ele][i]);
177                 }
178                 else
179                     levels[i][j] = df.indx.row.index[ele][df.indx.row.codes[ele][i]];
180             }
181 
182             static foreach(j, ele; dataLevels)
183             {
184                 static if(is(df.RowType[ele] == string))
185                     levels[i][indexLevels.length + j] = df.data[ele][i];
186                 else
187                 {
188                     import std.conv: to;
189                     levels[i][indexLevels.length + j] = to!string(df.data[ele][i]);
190                 }
191             }
192         }
193 
194         // This is required as dropper!(dataLevels, df.data) won't work
195         auto dropped = df.drop!(1, dataLevels);
196 
197         static foreach(i; 0 .. GrpRowType.length)
198             data[i] = dropped.data[i];
199         
200         grpIndex.column = dropped.indx.column;
201         grpIndex.row.titles = dropper(indexLevels, df.indx.row.titles);
202         grpIndex.row.index = dropper(indexLevels, df.indx.row.index);
203 
204         import std.range: zip;
205         int[] codes = vectorize(levels);
206         int[][] rcodes = transposed(dropper(indexLevels, df.indx.row.codes));
207         // Simultaneously arranging all the relavent fields using a zip as displayed in the docs
208 
209         static if(isHomogeneousType)
210             auto sortdata = transposed(data);
211         else
212             alias sortdata = data;
213 
214         auto arrange = zip(levels, codes[1 .. $], sortdata, rcodes).sort!((a, b) => a[1] < b[1]);
215 
216         static if(isHomogeneousType)
217             foreach(i, ele; sortdata)
218                 foreach(j, iele; ele)
219                     data[j][i] = iele;
220 
221         grpIndex.row.codes = transposed(rcodes);
222 
223         elementCountTill.length = codes[0] + 1;
224         groups.length = codes[0];
225         groups[0] = levels[0];
226         elementCountTill[0] = 0;
227         int uele = 0, count = 0;
228 
229         foreach(i; codes[1 .. $])
230         {
231             if(i != uele)
232             {
233                 groups[uele + 1] = levels[count];
234                 elementCountTill[uele + 1] = count;
235                 ++uele;
236             }
237             count++;
238         }
239 
240         elementCountTill[uele + 1] = count;
241     }
242 
243     /++
244     string display(T)(T[] groupIndex = [],bool getStr = false, int termianlw = 0)
245     Description: Display groupBy on terminal
246     @params: groupIndex - Group index in form of integer array or string array
247     @params: getStr - Returns the generated display sring
248     @params: termianlw - Set terminal width
249     +/
250     string display(T)(T[] groupIndex = [],bool getStr = false, int termianlw = 0)
251     {
252         import std.array: appender;
253         auto retstr = appender!(string);
254 
255         ptrdiff_t[] pos;
256         static if(is(T == int))
257         {
258             import std.algorithm: reduce, max, min;
259             assert(groupIndex.reduce!max < groups.length, "Index out of bound");
260             assert(groupIndex.reduce!min > -1, "Index out of bound");
261             pos.length = groupIndex.length;
262             foreach(i, ele; groupIndex)
263                 pos[i] = ele;
264         }
265         else static if(is(T == string))
266         {
267             pos.length = 1;
268             ptrdiff_t indxpos = getGroupPosition(groupIndex);
269             assert(indxpos > -1, "Group not found");
270             pos[0] = indxpos;
271         }
272         else static if(is(T == string[]))
273         {
274             pos.length = groupIndex.length;
275             foreach(i, ele; groupIndex)
276             {
277                 ptrdiff_t indxpos = getGroupPosition(ele);
278                 assert(indxpos > -1, "Group not found");
279                 pos[i] = indxpos;
280             }
281         }
282         else static assert(0, "Group Indexes must be an array of integer, 1D array of string or 2D array of string");
283 
284         DataFrame!(true, GrpRowType) displayHelper;
285         displayHelper.indx.column = grpIndex.column;
286         displayHelper.indx.row.titles = grpIndex.row.titles;
287         displayHelper.indx.row.index = grpIndex.row.index;
288         displayHelper.indx.row.codes.length = grpIndex.row.codes.length;
289 
290         foreach(i, ele; pos)
291         {
292             foreach(j; 0 .. displayHelper.indx.row.codes.length)
293             {
294                 displayHelper.indx.row.codes[j] = grpIndex.row.codes[j][elementCountTill[ele] .. elementCountTill[ele + 1]];
295             }
296 
297             static foreach(j; 0 .. GrpRowType.length)
298                 displayHelper.data[j] = data[j][elementCountTill[ele] .. elementCountTill[ele + 1]];
299 
300             displayHelper.rows = elementCountTill[ele + 1] - elementCountTill[ele];
301             string display = displayHelper.display(true, termianlw);
302 
303             if(getStr)
304             {
305                 retstr.put(display);
306                 retstr.put("\n");
307             }
308             else
309             {
310                 import std.stdio: writeln;
311                 writeln("Group: ", groups[i]);
312                 writeln("Group Dimension: [ ", elementCountTill[ele + 1] - elementCountTill[ele]," X ", GrpType.length, " ]");
313                 writeln(display);
314             }
315         }
316 
317         return retstr.data;
318     }
319 
320     /++
321     string display(bool getStr = false, int termianlw = 0)
322     Description: Display complete groupBy on terminal
323     @params: getStr - Returns the generated display sring
324     @params: termianlw - Set terminal width
325     +/
326     string display(bool getStr = false, int termianlw = 0)
327     {
328         int[] pos;
329         pos.length = groups.length;
330         foreach(i; 0 .. groups.length)
331             pos[i] = cast(int)i;
332 
333         return display(pos, getStr, termianlw);
334     }
335 
336     /++
337     string[][] getGroups() @property
338     Description: Get the generated string array containing all the groups
339     +/
340     string[][] getGroups() @property
341     {
342         return groups;
343     }
344 
345     /++
346     auto combine(T)(T[] groupIndex)
347     Description: Combines selective groups into a DataFrame
348     @params: groupIndex - Group index in form of integer array or string array
349     +/
350     auto combine(T)(T[] groupIndex)
351     {
352         ptrdiff_t[] pos;
353         static if(is(T == int))
354         {
355             import std.algorithm: reduce, max, min;
356             assert(groupIndex.reduce!max < groups.length, "Index out of bound");
357             assert(groupIndex.reduce!min > -1, "Index out of bound");
358             pos.length = groupIndex.length;
359             foreach(i, ele; groupIndex)
360                 pos[i] = ele;
361         }
362         else static if(is(T == string))
363         {
364             pos.length = 1;
365             ptrdiff_t indxpos = getGroupPosition(groupIndex);
366             assert(indxpos > -1, "Group not found");
367             pos[0] = indxpos;
368         }
369         else static if(is(T == string[]))
370         {
371             pos.length = groupIndex.length;
372             foreach(i, ele; groupIndex)
373             {
374                 ptrdiff_t indxpos = getGroupPosition(ele);
375                 assert(indxpos > -1, "Group not found");
376                 pos[i] = indxpos;
377             }
378         }
379         else
380         {
381             assert(0, "Group Indexes must be an array of integer, 1D array of string or 2D array of string");
382         }
383 
384         DataFrame!(true, GrpRowType) combinator;
385         combinator.indx.column = grpIndex.column;
386         foreach(i; 0 .. groups[0].length)
387         {
388             import std.conv: to;
389             ++combinator.indx.row.titles.length;
390             combinator.indx.row.titles[i] = "GroupL" ~ to!(string)(i + 1);
391         }
392 
393         combinator.indx.row.titles ~= grpIndex.row.titles;
394         combinator.indx.row.index.length = combinator.indx.row.titles.length;
395         combinator.indx.row.codes.length = combinator.indx.row.titles.length;
396 
397         foreach(i; 0 .. grpIndex.row.index.length)
398             combinator.indx.row.index[groups[0].length + i] = grpIndex.row.index[i];
399 
400         foreach(i, elei; pos)
401         {
402             foreach(j, elej; groups[pos[i]])
403             {
404                 combinator.indx.row.index[j].length += elementCountTill[elei + 1] - elementCountTill[elei];
405                 foreach(k; 0 .. elementCountTill[pos[i] + 1] - elementCountTill[pos[i]])
406                     combinator.indx.row.index[j][$ - 1 - k] = elej;
407             }
408 
409             foreach(j; 0 .. grpIndex.row.index.length)
410                 combinator.indx.row.codes[groups[0].length + j] ~= grpIndex.row.codes[j][elementCountTill[elei] .. elementCountTill[elei + 1]];
411 
412             static foreach(j; 0 .. GrpRowType.length)
413                 combinator.data[j] ~= data[j][elementCountTill[elei] .. elementCountTill[elei + 1]];
414 
415             combinator.rows += elementCountTill[elei + 1] - elementCountTill[elei];
416         }
417 
418         combinator.indx.generateCodes();
419 
420         return combinator;
421     }
422 
423     /++
424     auto combine(T)(T[] groupIndex)
425     Description: Combines all te groups into a DataFrame
426     +/
427     auto combine()
428     {
429         int[] pos;
430         pos.length = groups.length;
431         foreach(i; 0 .. groups.length)
432             pos[i] = cast(int)i;
433 
434         return combine(pos);
435     }
436 
437     /++
438     Index operation of the form gp[size_t, size_t, size_t]
439     +/
440     auto opIndex(size_t i1, size_t i2, size_t i3)
441     {
442         auto returnAux(ptrdiff_t si = -1)(size_t ri = 0) @property
443         {
444             static if(si > -1)
445                 alias i = si;
446             else
447                 size_t i = ri;
448 
449             return data[i][elementCountTill[i1] + i2];
450         }
451         
452         mixin auxDispatch!(returnAux, isHomogeneousType, GrpRowType);
453         return auxDispatch(i3);
454     }
455 
456     /++
457     Index operation of the form gp[["Grp Index"], ["Row-Index"], ["Column-Index"]]
458     +/
459     auto opIndex(T, U, V)(T i1, U i2, V i3)
460         if((is(T == string) || is(T == string[]))
461         && (is(U == string) || is(U == string[]))
462         && (is(V == string) || is(V == string[])))
463     {
464         ptrdiff_t[3] pos;
465         static if(is(T == string))
466             pos[0] = getGroupPosition([i1]);
467         else
468             pos[0] = getGroupPosition(i1);
469         assert(pos[0] > -1, "Index out of bound");
470 
471         static if(is(U == string))
472             pos[1] = positionInGroup!(0)([i2], pos[0]);
473         else
474             pos[1] = positionInGroup!(0)(i2, pos[0]);
475         assert(pos[1] > -1, "Index out of bound");
476 
477         static if(is(V == string))
478             pos[2] = positionInGroup!(1)([i3], pos[0]);
479         else
480             pos[2] = positionInGroup!(1)(i3, pos[0]);
481         assert(pos[2] > -1, "Index out of bound");
482 
483         auto returnAux(ptrdiff_t si = -1)(size_t ri) @property
484         {
485             static if(si > -1)
486                 alias i = si;
487             else
488                 size_t i = ri;
489 
490             return data[i][elementCountTill[pos[0]] + pos[1]];
491         }
492         
493         mixin auxDispatch!(returnAux, isHomogeneousType, GrpRowType);
494         return auxDispatch(pos[2]);
495     }
496 
497     /++
498     opIndex that returns Axis for column/row binary operations
499     +/
500     auto opIndex(T, U, Args...)(T groupIndex, U index, Args args)
501         if((is(T == string) || is(T == string[]))
502         && (is(U == string) || is(U == string[]))
503         && (Args.length == 0 || (Args.length == 1 && is(Args[0] == int))))
504     {
505         ptrdiff_t[2] pos;
506         static if(is(T == string))
507         {
508             ptrdiff_t grpPos = getGroupPosition([groupIndex]);
509             assert(grpPos > -1, "Index out of bound");
510             pos[0] = grpPos;
511         }
512         else
513         {
514             ptrdiff_t grpPos = getGroupPosition(groupIndex);
515             assert(grpPos > -1, "Index out of bound");
516             pos[0] = grpPos;
517         }
518 
519         static if(is(U == string))
520         {
521             ptrdiff_t axisPos = positionInGroup!(1 - Args.length)([index], pos[0]);
522             assert(axisPos > -1, "Index out of bound");
523             pos[1] = axisPos;
524         }
525         else
526         {
527             ptrdiff_t axisPos = positionInGroup!(1 - Args.length)(index, pos[0]);
528             assert(axisPos > -1, "Index out of bound");
529             pos[1] = axisPos;
530         }
531 
532         static if(1 - Args.length)
533         {
534             Axis!(void) ret;
535             void dataSetterAux(ptrdiff_t si = -1)(size_t ri = 0) @property
536             {
537                 static if(si > -1)
538                     alias i = si;
539                 else
540                     size_t i = ri;
541 
542                 foreach(j; data[i][elementCountTill[pos[0]] .. elementCountTill[pos[0] + 1]])
543                     ret.data ~= DataType(j);
544             }
545             
546             mixin auxDispatch!(dataSetterAux, isHomogeneousType, GrpRowType);
547             auxDispatch(pos[1]);
548 
549             return ret;
550         }
551         else
552         {
553             Axis!(GrpRowType) ret;
554             static foreach(i; 0 .. GrpRowType.length)
555             {
556                 ret.data[i] = data[i][elementCountTill[pos[0]] + pos[1]];
557             }
558 
559             return ret;
560         }
561     }
562 
563     /// Binary Operation on DataFrame rows/columns
564     void opIndexAssign(T...)(Axis!T elements, string[] groupTitle, string[] index, int axis = 1)
565     {
566         opIndexOpAssign!("")(elements, groupTitle, index, axis);
567     }
568 
569     /// Short Hand Binary operation
570     void opIndexOpAssign(string op, T...)(Axis!T elements, string[] groupTitle, string[] index, int axis = 1)
571     {
572         ptrdiff_t[2] pos;
573         ptrdiff_t p = getGroupPosition(groupTitle);
574         assert(p > -1, "Group index out of bound");
575         pos[0] = p;
576 
577         if(axis)
578         {
579             p = positionInGroup!(1)(index, pos[0]);
580             assert(p > -1, "Index out of bound");
581             pos[1] = p;
582         }
583         else
584         {
585             p = positionInGroup!(0)(index, pos[0]);
586             assert(p > -1, "Index out of bound");
587             pos[1] = p;
588         }
589 
590         import std.traits: isArray;
591         static if(is(T[0] == void) || (T.length == 1 && isArray!(T[0])))
592         {
593             assert(elements.data.length == elementCountTill[pos[0] + 1] - elementCountTill[pos[0]],
594                 "Size of data doesn't match size of group column");
595 
596             void mixinAux(ptrdiff_t si = -1)(size_t ri = 0) @property
597             {
598                 static if(si > -1)
599                 {
600                     alias i = si;
601                     enum size_t typepos = si;
602                 }
603                 else
604                 {
605                     size_t i = ri;
606                     enum size_t typepos = 0;
607                 }
608 
609                 static if(is(T[0] == void))
610                     enum string append = ".get!(GrpRowType[typepos]);";
611                 else
612                     enum string append = ";";
613 
614                 foreach(j; elementCountTill[pos[0]] .. elementCountTill[pos[0] + 1])
615                 {
616                     mixin("data[i][j] " ~ op ~ "= elements.data[j - elementCountTill[pos[0]]]" ~ append);
617                 }
618             }
619             
620             mixin auxDispatch!(mixinAux, isHomogeneousType, GrpRowType);
621             auxDispatch(pos[1]);
622         }
623         else
624         {
625             assert(elements.data.length == GrpRowType.length, "Size of data doesn't match size of group column");
626             static foreach(i; 0.. GrpRowType.length)
627             {
628                 mixin("data[i][elementCountTill[pos[0]] + pos[1]] " ~ op ~ "= elements.data[i];");
629             }
630         }
631     }
632 
633     /// Slice assignment operation
634     void opIndexAssign(T, SliceKind kind)(Slice!(T*, 1, kind) sl, string[] groupTitle, string[] index, int axis = 1)
635     {
636         opIndexOpAssign!("")(sl, groupTitle, index, axis);
637     }
638 
639     /// Slice assignment operation
640     void opIndexOpAssign(string op, T, SliceKind kind)(Slice!(T*, 1, kind) sl, string[] groupTitle, string[] index, int axis = 1)
641     {
642         if(axis)
643         {
644             Axis!void fwd;
645             fwd.data.length = sl.shape[0];
646             foreach(i; 0 .. sl.shape[0])
647                 fwd.data[i] = DataType(sl[i]);
648 
649             opIndexOpAssign!(op)(fwd, groupTitle, index, axis);
650         }
651         else
652         {
653             Axis!(GrpRowType) fwd;
654             static foreach(i; 0 .. GrpRowType.length)
655                 static if(__traits(isArithmetic, GrpRowType[i], T))
656                     fwd.data[i] = cast(GrpRowType[i])sl[i];
657                 else
658                 {
659                     import std.conv: to;
660                     fwd.data[i] = to!(GrpRowType[i])(sl[i]);
661                 }
662 
663             opIndexOpAssign!(op)(fwd, groupTitle, index, axis);
664         }
665     }
666 
667     /// Assigning a Slice to group
668     void opIndexAssign(T, SliceKind kind)(Slice!(T*, 2, kind) sl, string[] groupTitle)
669     {
670         assert(sl.shape[1] <= GrpRowType.length, "Slice is larger than the Group that it is being assigned to");
671 
672         ptrdiff_t pos = getGroupPosition(groupTitle);
673         assert(pos > -1, "Group not found");
674         assert(sl.shape[0] <= elementCountTill[pos + 1] - elementCountTill[pos], "Slice is larger than the Group that it is being assigned to");
675 
676         static foreach(i; 0 .. GrpRowType.length)
677         {
678             if(__traits(isArithmetic, T, GrpRowType[i]))
679                 foreach(j; elementCountTill[pos] .. elementCountTill[pos + 1])
680                     data[i][j] = cast(GrpRowType[i])sl[j - elementCountTill[pos]][i];
681             else
682             {
683                 import std.conv: to;
684                 foreach(j; elementCountTill[pos] .. elementCountTill[pos + 1])
685                     data[i][j] = to!(GrpRowType[i])(sl[j - elementCountTill[pos]][i]);
686             }
687         }
688     }
689 
690     /// Assign a Slice to the Group
691     void opAssign(T, SliceKind kind)(Slice!(T*, 2, kind) input)
692     {
693         assert(input.shape[0] <= elementCountTill[$ - 1] && input.shape[1] <= GrpRowType.length, "Given Slice is larger than group");
694         static foreach(i; 0 .. GrpRowType.length)
695         {
696             static if(__traits(isArithmetic, T, GrpRowType[i]))
697                 foreach(j; 0 .. input.shape[0])
698                     data[i][j] = cast(GrpRowType[i])input[j][i];
699             else
700             {
701                 import std.conv: to;
702                 foreach(j; 0 .. input.shape[0])
703                     data[i][j] = to!(GrpRowType[i])(input[j][i]);
704             }
705 
706         }
707     }
708 
709     /// Get entire 
710     auto asSlice(T, SliceKind kind)() @property
711     {
712         return asSliceInternal!(T, kind)(0, elementCountTill[$ - 1]);
713     }
714 
715     /// Get a specific group
716     auto asSlice(T, SliceKind kind, U)(U grp)
717         if(is(U == string) || is(U == string[]))
718     {
719         ptrdiff_t pos;
720         static if(is(U == string))
721             pos = getGroupPosition([grp]);
722         else
723             pos = getGroupPosition(grp);
724 
725         assert(pos > -1, "Group not found");
726         return asSliceInternal!(T, kind)(elementCountTill[pos], elementCountTill[pos + 1]);
727     }
728 
729     auto asSlice(SliceKind kind, Type = string, int axis = 0, U)(U grp, U index)
730         if(is(U == string[]) || is(U == int))
731     {
732         ptrdiff_t[2] pos;
733         static if(is(U == string[]))
734         {
735             pos[0] = getGroupPosition(grp);
736             pos[1] = positionInGroup!(axis)(index, pos[0]);
737         }
738         else
739         {
740             pos[0] = grp;
741             pos[1] = index;
742         }
743 
744         assert(pos[0] > -1, "Group not found");
745         assert(pos[1] > -1, "Index not found");
746 
747         static if(axis)
748         {
749 
750             static if(kind == Universal)
751                 Slice!(Type*, 1, kind) ret = slice!(Type)(elementCountTill[pos[0] + 1] - elementCountTill[pos[0]]).universal;
752             else static if(kind == Canonical)
753                 Slice!(Type*, 1, kind) ret = slice!(Type)(elementCountTill[pos[0] + 1] - elementCountTill[pos[0]]).canonical;
754             else
755                 Slice!(Type*, 1, kind) ret = slice!(Type)(elementCountTill[pos[0] + 1] - elementCountTill[pos[0]]);
756 
757             void assignmentAux(ptrdiff_t si = -1)(size_t ri = 0) @property
758             {
759                 static if(si > -1)
760                 {
761                     alias i = si;
762                     enum size_t typepos = si;
763                 }
764                 else
765                 {
766                     size_t i = ri;
767                     enum size_t typepos = 0;
768                 }
769 
770                 static if(__traits(isArithmetic, Type, GrpRowType[typepos]) || is(Type == GrpRowType[typepos]))
771                     foreach(j; elementCountTill[pos[0]] .. elementCountTill[pos[0] + 1])
772                         ret[j - elementCountTill[pos[0]]] = cast(Type)data[i][j];
773 
774             }
775 
776             mixin auxDispatch!(assignmentAux, isHomogeneousType, GrpRowType);
777             auxDispatch(pos[1]);
778             
779             return ret;
780         }
781         else
782         {
783             Slice!(string*, 1, kind) ret;
784             static if(kind == Universal)
785                 ret = slice!(string)(GrpRowType.length).universal;
786             else static if(kind == Canonical)
787                 ret = slice!(string)(GrpRowType.length).canonical;
788             else
789                 ret = slice!(string)(GrpRowType.length);
790 
791             static foreach(i; 0 .. GrpRowType.length)
792             {
793                 import std.conv: to;
794                 ret[i] = to!string(data[i][elementCountTill[pos[0]] + pos[1]]);
795             }
796 
797             return ret;
798         }
799     }
800 
801     auto aggregate(int axis, Ops...)() @property
802     {
803         void init(int axis, T)(ref T arg)
804         {
805             enum int perpendicular = ((axis)? 0: 1);
806 
807             arg.groups = groups;
808             arg.grpIndex.indexing[axis] = grpIndex.indexing[axis];
809             arg.grpIndex.indexing[perpendicular].index.length = 1;
810             arg.grpIndex.indexing[perpendicular].codes.length = 1;
811 
812             static if(axis)
813             {
814                 ret.grpIndex.row.titles = ["Operation"];
815                 ret.grpIndex.row.index[0].length = ret.groups.length * Ops.length;
816                 ret.elementCountTill.length = groups.length + 1;
817 
818                 foreach(i; 0 .. ret.elementCountTill.length)
819                     ret.elementCountTill[i] = cast(int)(i * Ops.length);
820 
821                 static foreach(i; 0 .. GrpType.length)
822                     ret.data[i].length = ret.groups.length * Ops.length;
823                 
824                 foreach(i; 0 .. ret.groups.length)
825                     static foreach(j; 0 .. Ops.length)
826                         ret.grpIndex.row.index[0][i * Ops.length + j] = __traits(identifier, Ops[j]);
827             }
828             else
829             {
830                 ret.elementCountTill = elementCountTill;
831                 ret.grpIndex.column.index[0].length = Ops.length;
832 
833                 static foreach(i; 0 .. Ops.length)
834                     ret.grpIndex.column.index[0][i] = __traits(identifier, Ops[i]);
835 
836                 static foreach(i; 0 .. Ops.length)
837                     ret.data[i].length = ret.elementCountTill[$ -1];
838             }
839         }
840 
841         static if(axis)
842         {
843             import std.meta: staticMap;
844 
845             alias Resolve(T) = suitableType!(resolverInternal!(T, Ops));
846             alias ResolvedTypes = staticMap!(Resolve, GrpRowType);
847             Group!(ResolvedTypes) ret;
848             init!(axis)(ret);
849             
850             foreach(i; 0 .. ret.groups.length)
851                 static foreach(j; 0 .. Ops.length)
852                     static foreach(k; 0 .. GrpRowType.length)
853                         static if(__traits(isArithmetic, GrpRowType[k]))
854                         {
855                             static if(__traits(compiles, Ops[j](data[k][elementCountTill[i] .. elementCountTill[i + 1]])))
856                                 ret.data[k][i * Ops.length + j] = Ops[j](data[k][elementCountTill[i] .. elementCountTill[i + 1]]);
857                             else
858                             {
859                                 import std.algorithm: map, reduce;
860                                 if(__traits(compiles, data[k][elementCountTill[i] .. elementCountTill[i + 1]].map!(e => e).reduce!(Ops[j])))
861                                     ret.data[k][i * Ops.length + j] = cast(ResolvedTypes[k])data[k][elementCountTill[i] .. elementCountTill[i + 1]].map!(e => e).reduce!(Ops[j]);
862                             }
863                         }
864         }
865         else
866         {
867             alias AggregateType = aggregateType!(Ops);
868             Group!(AggregateType) ret;
869             init!(axis)(ret);
870 
871             suitableType!(GrpRowType)[GrpRowType.length] opArr;
872             size_t k;
873 
874             foreach(i; 0 .. ret.elementCountTill[$ - 1])
875             {
876                 k = 0;
877                 static foreach(j; 0 .. GrpType.length)
878                     static if(__traits(isArithmetic, GrpRowType[j]))
879                     {
880                         opArr[k] = data[j][i];
881                         ++k;
882                     }    
883 
884                 if(k == 0)
885                     break;
886                 
887                 static foreach(j; 0 .. Ops.length)
888                 {
889                     static if(__traits(compiles, Ops[j](opArr[0 .. k])))
890                         ret.data[j][i] = Ops[j](opArr[0 .. k]);
891                     else
892                     {
893                         import std.algorithm: map, reduce;
894                         if(__traits(compiles, opArr[0 .. k].map!(e => e).reduce!(Ops[j])))
895                             ret.data[j][i] = cast(AggregateType[j])opArr[0 .. k].map!(e => e).reduce!(Ops[j]);
896                     }
897                 }
898             }
899         }
900 
901         ret.grpIndex.generateCodes();
902         return ret;
903     }
904 }
905 
906 // Basic groubBy operation done manually
907 unittest
908 {
909     DataFrame!(int, 5) df;
910     Index inx;
911     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
912     df.setFrameIndex(inx);
913     df.assign!1(2, [1,2,3]);
914     df.assign!1(4, [1,2,3]);
915     string str1 = df.display(true, 200);
916 
917     Group!(int, int, int, int) gp;
918     gp.createGroup!([2])(df, [0, 1]);
919 
920     assert(gp.groups.length == 3);
921     assert(gp.groups == [["Hello", "Hi", "1"], ["Hi", "Hello", "2"], ["Hey", "Hey", "3"]]);
922     assert(gp.elementCountTill == [0, 1, 2, 3]);
923     assert(gp.data[3] == [1, 2, 3]);
924 
925     string str2 = df.display(true, 200);
926     assert(str1 == str2);
927 }
928 
929 // getGroupPosition
930 unittest
931 {
932     DataFrame!(int, 5) df;
933     Index inx;
934     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
935     df.setFrameIndex(inx);
936     df.assign!1(2, [1,2,3]);
937 
938     auto gp = df.groupBy!([2])([0, 1]);
939     // gp.display();
940     assert(gp.getGroupPosition(["Hello", "Hi", "1"]) == 0);
941     assert(gp.getGroupPosition(["Hi", "Hello", "2"]) == 1);
942     assert(gp.getGroupPosition(["Hey", "Hey", "3"]) == 2);
943 }
944 
945 // Display
946 unittest
947 {
948     DataFrame!(int, 5) df;
949     Index inx;
950     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
951     df.setFrameIndex(inx);
952     df.assign!1(2, [1,2,3]);
953     df.assign!1(4, [1,2,3]);
954     string str1 = df.display(true, 200);
955 
956     Group!(int, int, int, int) gp;
957     gp.createGroup!([2])(df, [0, 1]);
958 
959     // Full Group
960     assert(gp.display(true, 200) == "3    0  1  3  4\n"
961         ~ "Hey  0  0  0  1\n\n"
962         ~ "3      0  1  3  4\n"
963         ~ "Hello  0  0  0  2\n\n"
964         ~ "3   0  1  3  4\n"
965         ~ "Hi  0  0  0  3\n\n"
966     );
967 
968     // Single Group
969     assert(gp.display(["Hello", "Hi", "1"], true, 200) == "3    0  1  3  4\n"
970         ~ "Hey  0  0  0  1\n\n"
971     );
972 
973     // Array of groups
974     assert(gp.display([["Hello", "Hi", "1"], ["Hi", "Hello", "2"]], true, 200) == "3    0  1  3  4\n"
975         ~ "Hey  0  0  0  1\n\n"
976         ~ "3      0  1  3  4\n"
977         ~ "Hello  0  0  0  2\n\n"
978     );
979 
980     // Array of groups
981     assert(gp.display([["Hello", "Hi", "1"], ["Hi", "Hello", "2"], ["Hey", "Hey", "3"]], true, 200) == "3    0  1  3  4\n"
982         ~ "Hey  0  0  0  1\n\n"
983         ~ "3      0  1  3  4\n"
984         ~ "Hello  0  0  0  2\n\n"
985         ~ "3   0  1  3  4\n"
986         ~ "Hi  0  0  0  3\n\n"
987     );
988 
989     // Single Group - integer indexes
990     assert(gp.display([0], true, 200) == "3    0  1  3  4\n"
991         ~ "Hey  0  0  0  1\n\n"
992     );
993 
994     // Array of groups - integer indexes
995     assert(gp.display([0, 1], true, 200) == "3    0  1  3  4\n"
996         ~ "Hey  0  0  0  1\n\n"
997         ~ "3      0  1  3  4\n"
998         ~ "Hello  0  0  0  2\n\n"
999     );
1000 
1001     // Array of groups - integer indexes
1002     assert(gp.display([0, 1, 2], true, 200) == "3    0  1  3  4\n"
1003         ~ "Hey  0  0  0  1\n\n"
1004         ~ "3      0  1  3  4\n"
1005         ~ "Hello  0  0  0  2\n\n"
1006         ~ "3   0  1  3  4\n"
1007         ~ "Hi  0  0  0  3\n\n"
1008     );
1009 }
1010 
1011 // Pandas groupBy Example
1012 unittest
1013 {
1014     DataFrame!(double) df;
1015     Index inx;
1016     inx.constructFromLevels!(0)([["Falcon", "Parrot"], ["Captive", "Wild"]], ["Animal", "Type"]);
1017     inx.constructFromLevels!(1)([["Max-Speed"]]);
1018     df.setFrameIndex(inx);
1019     df.assign!1(0, [380.0, 370.0, 24.0, 26.0]);
1020     // df.display();
1021 
1022     Group!(double) gp;
1023     gp.createGroup(df, [0]);
1024     assert(gp.groups.length == 2);
1025     assert(gp.groups == [["Falcon"], ["Parrot"]]);
1026     assert(gp.elementCountTill == [0, 2, 4]);
1027     assert(gp.data[0] == [380.0, 370.0, 24.0, 26.0]);
1028 
1029     // Display full
1030     assert(gp.display(true, 200) == "Type     Max-Speed\n"
1031         ~ "Captive  380      \n"
1032         ~ "Wild     370      \n\n"
1033         ~ "Type     Max-Speed\n"
1034         ~ "Captive  24       \n"
1035         ~ "Wild     26       \n\n"
1036     );
1037 
1038     // Display Falcon
1039     assert(gp.display(["Falcon"], true, 200) == "Type     Max-Speed\n"
1040         ~ "Captive  380      \n"
1041         ~ "Wild     370      \n\n"
1042     );
1043 
1044     // Display Parrot
1045     assert(gp.display(["Parrot"], true, 200) == "Type     Max-Speed\n"
1046         ~ "Captive  24       \n"
1047         ~ "Wild     26       \n\n"
1048     );
1049 
1050     // Display Falcon - Integer Index
1051     assert(gp.display([0], true, 200) == "Type     Max-Speed\n"
1052         ~ "Captive  380      \n"
1053         ~ "Wild     370      \n\n"
1054     );
1055 
1056     // Display Parrot - Integer Index
1057     assert(gp.display([1], true, 200) == "Type     Max-Speed\n"
1058         ~ "Captive  24       \n"
1059         ~ "Wild     26       \n\n"
1060     );
1061 }
1062 
1063 // getGroups
1064 unittest
1065 {
1066     DataFrame!(double) df1;
1067     Index inx1;
1068     inx1.constructFromLevels!(0)([["Falcon", "Parrot"], ["Captive", "Wild"]], ["Animal", "Type"]);
1069     inx1.constructFromLevels!(1)([["Max-Speed"]]);
1070     df1.setFrameIndex(inx1);
1071     df1.assign!1(0, [380.0, 370.0, 24.0, 26.0]);
1072     // df.display();
1073 
1074     Group!(double) gp1;
1075     gp1.createGroup(df1, [0]);
1076     assert(gp1.getGroups == [["Falcon"], ["Parrot"]]);
1077 
1078     DataFrame!(int, 5) df2;
1079     Index inx2;
1080     inx2.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
1081     df2.setFrameIndex(inx2);
1082     df2.assign!1(2, [1,2,3]);
1083     df2.assign!1(4, [1,2,3]);
1084 
1085     Group!(int, int, int, int) gp2;
1086     gp2.createGroup!([2])(df2, [0, 1]);
1087     assert(gp2.getGroups == [["Hello", "Hi", "1"], ["Hi", "Hello", "2"], ["Hey", "Hey", "3"]]);
1088 }
1089 
1090 // Combine groups to get a DataFrame
1091 unittest
1092 {
1093     DataFrame!(double) df1;
1094     Index inx1;
1095     inx1.constructFromLevels!(0)([["Falcon", "Parrot"], ["Captive", "Wild"]], ["Animal", "Type"]);
1096     inx1.constructFromLevels!(1)([["Max-Speed"]]);
1097     df1.setFrameIndex(inx1);
1098     df1.assign!1(0, [380.0, 370.0, 24.0, 26.0]);
1099     // df1.display();
1100 
1101     Group!(double) gp1;
1102     gp1.createGroup(df1, [0]);
1103     // gp1.display();
1104 
1105     // Combine one group
1106     assert(gp1.combine([0]).display(true, 200) == "GroupL1  Type     Max-Speed\n"
1107         ~ "Falcon   Captive  380      \n"
1108         ~ "Falcon   Wild     370      \n"
1109     );
1110 
1111     // Combine both the groups
1112     assert(gp1.combine([0, 1]).display(true, 200) == "GroupL1  Type     Max-Speed\n"
1113         ~ "Falcon   Captive  380      \n"
1114         ~ "Falcon   Wild     370      \n"
1115         ~ "Parrot   Captive  24       \n"
1116         ~ "Parrot   Wild     26       \n"
1117     );
1118 
1119     // Cobine all - overload
1120     assert(gp1.combine().display(true, 200) == "GroupL1  Type     Max-Speed\n"
1121         ~ "Falcon   Captive  380      \n"
1122         ~ "Falcon   Wild     370      \n"
1123         ~ "Parrot   Captive  24       \n"
1124         ~ "Parrot   Wild     26       \n"
1125     );
1126 
1127     assert(gp1.combine(["Falcon"]).display(true, 200) == "GroupL1  Type     Max-Speed\n"
1128         ~ "Falcon   Captive  380      \n"
1129         ~ "Falcon   Wild     370      \n"
1130     );
1131 
1132     // Combine both the groups
1133     assert(gp1.combine([["Falcon"], ["Parrot"]]).display(true, 200) == "GroupL1  Type     Max-Speed\n"
1134         ~ "Falcon   Captive  380      \n"
1135         ~ "Falcon   Wild     370      \n"
1136         ~ "Parrot   Captive  24       \n"
1137         ~ "Parrot   Wild     26       \n"
1138     );
1139 
1140     // Just Parrot
1141     assert(gp1.combine(["Parrot"]).display(true, 200) == "GroupL1  Type     Max-Speed\n"
1142         ~ "Parrot   Captive  24       \n"
1143         ~ "Parrot   Wild     26       \n"
1144     );
1145 }
1146 
1147 unittest
1148 {
1149     DataFrame!(int, 5) df;
1150     Index inx;
1151     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
1152     df.setFrameIndex(inx);
1153     df.assign!1(2, [1,2,3]);
1154     df.assign!1(4, [1,2,3]);
1155 
1156     Group!(int, int, int, int) gp;
1157     gp.createGroup!([2])(df, [0, 1]);
1158 
1159     // Complete Group
1160     assert(gp.combine().display(true, 200) == "GroupL1  GroupL2  GroupL3  3      0  1  3  4\n"
1161         ~ "Hello    Hi       1        Hey    0  0  0  1\n"
1162         ~ "Hi       Hello    2        Hello  0  0  0  2\n"
1163         ~ "Hey      Hey      3        Hi     0  0  0  3\n"
1164     );
1165 
1166     // Single Group
1167     assert(gp.combine([0]).display(true, 200) == "GroupL1  GroupL2  GroupL3  3    0  1  3  4\n"
1168         ~ "Hello    Hi       1        Hey  0  0  0  1\n"
1169     );
1170 
1171     // 2 Groups combine
1172     assert(gp.combine([0, 1]).display(true, 200) == "GroupL1  GroupL2  GroupL3  3      0  1  3  4\n"
1173         ~ "Hello    Hi       1        Hey    0  0  0  1\n"
1174         ~ "Hi       Hello    2        Hello  0  0  0  2\n"
1175     );
1176 
1177     // Complete group again
1178     assert(gp.combine([0, 1, 2]).display(true, 200) == "GroupL1  GroupL2  GroupL3  3      0  1  3  4\n"
1179         ~ "Hello    Hi       1        Hey    0  0  0  1\n"
1180         ~ "Hi       Hello    2        Hello  0  0  0  2\n"
1181         ~ "Hey      Hey      3        Hi     0  0  0  3\n"
1182     );
1183 
1184     // Groups using string array
1185     assert(gp.combine(["Hello", "Hi", "1"]).display(true, 200) == "GroupL1  GroupL2  GroupL3  3    0  1  3  4\n"
1186         ~ "Hello    Hi       1        Hey  0  0  0  1\n"
1187     );
1188 
1189     // Groups using 2D string array
1190     assert(gp.combine([["Hello", "Hi", "1"], ["Hi", "Hello", "2"]]).display(true, 200) == "GroupL1  GroupL2  GroupL3  3      0  1  3  4\n"
1191         ~ "Hello    Hi       1        Hey    0  0  0  1\n"
1192         ~ "Hi       Hello    2        Hello  0  0  0  2\n"
1193     );
1194 }
1195 
1196 // Index operation
1197 unittest
1198 {
1199     DataFrame!(int, 5) df;
1200     Index inx;
1201     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
1202     df.setFrameIndex(inx);
1203     df.assign!1(2, [1,2,3]);
1204     df.assign!1(4, [1,2,3]);
1205 
1206     Group!(int, int, int, int) gp;
1207     gp.createGroup!([2])(df, [0, 1]);
1208 
1209     assert(gp[["Hello", "Hi", "1"], ["4"]].data == [1]);
1210     assert(gp[["Hello", "Hi", "1"], ["3"]].data == [0]);
1211 
1212     assert(gp[["Hi", "Hello", "2"], ["4"]].data == [2]);
1213     assert(gp[["Hi", "Hello", "2"], ["3"]].data == [0]);
1214 
1215     assert(gp[["Hey", "Hey", "3"], ["4"]].data == [3]);
1216     assert(gp[["Hey", "Hey", "3"], ["3"]].data == [0]);
1217 
1218     assert(gp[["Hello", "Hi", "1"], ["Hey"], 0].data.length == 4);
1219     assert(gp[["Hello", "Hi", "1"], ["Hey"], 0].data[3] == 1);
1220 
1221     assert(gp[["Hi", "Hello", "2"], ["Hello"], 0].data.length == 4);
1222     assert(gp[["Hi", "Hello", "2"], ["Hello"], 0].data[3] == 2);
1223 
1224     assert(gp[["Hey", "Hey", "3"], ["Hi"], 0].data.length == 4);
1225     assert(gp[["Hey", "Hey", "3"], ["Hi"], 0].data[3] == 3);
1226 }
1227 
1228 // Index operation
1229 unittest
1230 {
1231     DataFrame!(double) df1;
1232     Index inx1;
1233     inx1.constructFromLevels!(0)([["Falcon", "Parrot"], ["Captive", "Wild"]], ["Animal", "Type"]);
1234     inx1.constructFromLevels!(1)([["Max-Speed"]]);
1235     df1.setFrameIndex(inx1);
1236     df1.assign!1(0, [380.0, 370.0, 24.0, 26.0]);
1237     // df.display();
1238 
1239     Group!(double) gp;
1240     gp.createGroup(df1, [0]);
1241     assert(gp[["Falcon"], ["Max-Speed"]].data == [380.0, 370.0]);
1242     assert(gp[["Parrot"], ["Max-Speed"]].data == [24.0, 26.0]);
1243 
1244     assert(gp[["Falcon"], ["Captive"], 0].data[0] == 380);
1245     assert(gp[["Falcon"], ["Wild"], 0].data[0] == 370);
1246     assert(gp[["Parrot"], ["Captive"], 0].data[0] == 24);
1247     assert(gp[["Parrot"], ["Wild"], 0].data[0] == 26);
1248 
1249     // No need to pass array if the level of indexing or grouping is 1
1250     assert(gp["Falcon", "Max-Speed"].data == [380.0, 370.0]);
1251     assert(gp["Parrot", "Max-Speed"].data == [24.0, 26.0]);
1252 
1253     assert(gp["Falcon", "Captive", 0].data[0] == 380);
1254     assert(gp["Falcon", "Wild", 0].data[0] == 370);
1255     assert(gp["Parrot", "Captive", 0].data[0] == 24);
1256     assert(gp["Parrot", "Wild", 0].data[0] == 26);
1257 }
1258 
1259 // Element access using indexes
1260 unittest
1261 {
1262     DataFrame!(double) df1;
1263     Index inx1;
1264     inx1.constructFromLevels!(0)([["Falcon", "Parrot"], ["Captive", "Wild"]], ["Animal", "Type"]);
1265     inx1.constructFromLevels!(1)([["Max-Speed"]]);
1266     df1.setFrameIndex(inx1);
1267     df1.assign!1(0, [380.0, 370.0, 24.0, 26.0]);
1268     // df.display();
1269 
1270     Group!(double) gp;
1271     gp.createGroup(df1, [0]);
1272 
1273     assert(gp["Falcon", "Captive", "Max-Speed"] == 380);
1274     assert(gp[0, 0, 0] == 380);
1275     assert(gp["Falcon", "Wild", "Max-Speed"] == 370);
1276     assert(gp[0, 1, 0] == 370);
1277 
1278     assert(gp["Parrot", "Captive", "Max-Speed"] == 24);
1279     assert(gp[1, 0, 0] == 24);
1280     assert(gp["Parrot", "Wild", "Max-Speed"] == 26);
1281     assert(gp[1, 1, 0] == 26);
1282 }
1283 
1284 // Element access using index
1285 unittest
1286 {
1287     DataFrame!(int, 5) df;
1288     Index inx;
1289     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
1290     df.setFrameIndex(inx);
1291     df.assign!1(2, [1,2,3]);
1292     df.assign!1(4, [1,2,3]);
1293 
1294     Group!(int, int, int, int) gp;
1295     gp.createGroup!([2])(df, [0, 1]);
1296 
1297     assert(gp[["Hello", "Hi", "1"], ["Hey"], ["4"]] == 1);
1298     assert(gp[0, 0, 3] == 1);
1299     assert(gp[["Hello", "Hi", "1"], ["Hey"], ["3"]] == 0);
1300     assert(gp[0, 0, 2] == 0);
1301 
1302     assert(gp[["Hi", "Hello", "2"], ["Hello"], ["4"]] == 2);
1303     assert(gp[1, 0, 3] == 2);
1304     assert(gp[["Hi", "Hello", "2"], ["Hello"], ["3"]] == 0);
1305     assert(gp[1, 0, 2] == 0);
1306 
1307     assert(gp[["Hey", "Hey", "3"], ["Hi"], ["4"]] == 3);
1308     assert(gp[2, 0, 3] == 3);
1309     assert(gp[["Hey", "Hey", "3"], ["Hi"], ["3"]] == 0);
1310     assert(gp[2, 0, 2] == 0);
1311 
1312     // Checking different variations
1313     assert(gp[["Hi", "Hello", "2"], "Hello", ["3"]] == 0);
1314     assert(gp[["Hi", "Hello", "2"], ["Hello"], "3"] == 0);
1315     assert(gp[["Hi", "Hello", "2"], "Hello", "3"] == 0);
1316 }
1317 
1318 // Assignment operation on rows
1319 unittest
1320 {
1321     DataFrame!(double) df1;
1322     Index inx1;
1323     inx1.constructFromLevels!(0)([["Falcon", "Parrot"], ["Captive", "Wild"]], ["Animal", "Type"]);
1324     inx1.constructFromLevels!(1)([["Max-Speed"]]);
1325     df1.setFrameIndex(inx1);
1326     df1.assign!1(0, [380.0, 370.0, 24.0, 26.0]);
1327     // df.display();
1328 
1329     Group!(double) gp;
1330     gp.createGroup(df1, [0]);
1331 
1332     // Row assignment within the same group
1333     gp[["Falcon"], ["Captive"], 0] = gp[["Falcon"], ["Wild"], 0];
1334     assert(gp[0, 0, 0] == gp[0, 1, 0]);
1335 
1336     // Row assignment within the same group
1337     gp[["Parrot"], ["Captive"], 0] = gp[["Parrot"], ["Wild"], 0];
1338     assert(gp[1, 0, 0] == gp[1, 1, 0]);
1339 
1340     // Row assignment cross group
1341     gp[["Parrot"], ["Captive"], 0] = gp[["Falcon"], ["Wild"], 0];
1342     assert(gp[0, 0, 0] == gp[1, 0, 0]);
1343 }
1344 
1345 unittest
1346 {
1347     DataFrame!(int, 5) df;
1348     Index inx;
1349     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
1350     df.setFrameIndex(inx);
1351     df.assign!1(2, [1,2,3]);
1352     df.assign!1(4, [1,2,3]);
1353 
1354     Group!(int, int, int, int) gp;
1355     gp.createGroup!([2])(df, [0, 1]);
1356 
1357     gp[["Hello", "Hi", "1"], ["3"]] = gp[["Hello", "Hi", "1"], ["4"]];
1358     assert(gp[["Hello", "Hi", "1"], ["3"]].data == [1]);
1359 
1360     gp[["Hello", "Hi", "1"], ["3"]] = gp[["Hello", "Hi", "1"], ["0"]] + gp[["Hello", "Hi", "1"], ["3"]] + gp[["Hello", "Hi", "1"], ["4"]];
1361     assert(gp[["Hello", "Hi", "1"], ["3"]].data == [2]);
1362 
1363     gp[["Hello", "Hi", "1"], ["3"]] = gp[["Hello", "Hi", "1"], ["4"]] + gp[["Hi", "Hello", "2"], ["4"]];
1364     assert(gp[["Hello", "Hi", "1"], ["3"]].data == [3]);
1365 }
1366 
1367 // Short hand operations
1368 unittest
1369 {
1370     DataFrame!(int, 5) df;
1371     Index inx;
1372     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
1373     df.setFrameIndex(inx);
1374     df.assign!1(2, [1,2,3]);
1375     df.assign!1(4, [1,2,3]);
1376 
1377     Group!(int, int, int, int) gp;
1378     gp.createGroup!([2])(df, [0, 1]);
1379 
1380     gp[["Hello", "Hi", "1"], ["3"]] += gp[["Hello", "Hi", "1"], ["4"]];
1381     assert(gp[["Hello", "Hi", "1"], ["3"]].data == [1]);
1382 
1383     gp[["Hello", "Hi", "1"], ["3"]] -= gp[["Hello", "Hi", "1"], ["0"]] + gp[["Hello", "Hi", "1"], ["3"]] + gp[["Hello", "Hi", "1"], ["4"]];
1384     assert(gp[["Hello", "Hi", "1"], ["3"]].data == [-1]);
1385 
1386     gp[["Hello", "Hi", "1"], ["3"]] *= gp[["Hello", "Hi", "1"], ["4"]] + gp[["Hi", "Hello", "2"], ["4"]];
1387     assert(gp[["Hello", "Hi", "1"], ["3"]].data == [-3]);
1388 
1389     gp[["Hello", "Hi", "1"], ["3"]] /= gp[["Hello", "Hi", "1"], ["3"]];
1390     assert(gp[["Hello", "Hi", "1"], ["3"]].data == [1]);
1391 }
1392 
1393 // Retrieving as Slice and assignment of Slice to DataFrame 
1394 unittest
1395 {
1396     DataFrame!(int, 5) df;
1397     Index inx;
1398     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
1399     df.setFrameIndex(inx);
1400     df.assign!1(2, [1,2,3]);
1401     df.assign!1(4, [1,2,3]);
1402 
1403     Group!(int, int, int, int) gp;
1404     gp.createGroup!([2])(df, [0, 1]);
1405 
1406     DataFrame!(int, 5) df2;
1407     df2.setFrameIndex(inx);
1408     df2.assign!1(2, [1,2,3]);
1409 
1410     Group!(int, int, int, int) gp2;
1411     gp2.createGroup!([2])(df2, [0, 1]);
1412 
1413     gp2 = gp.asSlice!(int, Universal);
1414     assert(gp.display(true, 200) == gp2.display(true, 200));
1415 }
1416 
1417 // Slice assignment operation
1418 unittest
1419 {
1420     DataFrame!(int, 5) df;
1421     Index inx;
1422     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
1423     df.setFrameIndex(inx);
1424     df.assign!1(2, [1,2,3]);
1425     df.assign!1(4, [1,2,3]);
1426 
1427     Group!(int, int, int, int) gp;
1428     gp.createGroup!([2])(df, [0, 1]);
1429 
1430     // Assign a column
1431     gp[["Hello", "Hi", "1"], ["0"]] = ([42]).sliced(1).universal;
1432     assert(gp[["Hello", "Hi", "1"], ["Hey"], ["0"]] == 42);
1433 
1434     // Assign a row
1435     gp[["Hello", "Hi", "1"], ["Hey"], 0] = ([42, 42, 42, 42]).sliced(4).universal;
1436     assert(gp.data[0][0] == 42 && gp.data[1][0] == 42 && gp.data[2][0] == 42 && gp.data[3][0] == 42);
1437 }
1438 
1439 // Getting a Group as Slice
1440 unittest
1441 {
1442     DataFrame!(int, 5) df;
1443     Index inx;
1444     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
1445     df.setFrameIndex(inx);
1446     df.assign!1(2, [1,2,3]);
1447     df.assign!1(4, [1,2,3]);
1448 
1449     Group!(int, int, int, int) gp;
1450     gp.createGroup!([2])(df, [0, 1]);
1451 
1452     // Assigning values of one group to another using Slice
1453     gp[["Hello", "Hi", "1"]] = gp.asSlice!(int, Universal)(["Hi", "Hello", "2"]);
1454     assert(gp[["Hello", "Hi", "1"], ["Hey"], ["4"]] == 2);
1455 }
1456 
1457 // asSlice
1458 unittest
1459 {
1460     DataFrame!(int, 5) df;
1461     Index inx;
1462     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
1463     df.setFrameIndex(inx);
1464     df.assign!1(2, [1,2,3]);
1465     df.assign!1(4, [1,2,3]);
1466 
1467     Group!(int, int, int, int) gp;
1468     gp.createGroup!([2])(df, [0, 1]);
1469 
1470     assert(gp.asSlice!(Universal, int, 1)(["Hello", "Hi", "1"], ["4"]) == [1]);
1471     assert(gp.asSlice!(Universal)(["Hello", "Hi", "1"], ["Hey"]) == ["0", "0", "0", "1"]);
1472 }
1473 
1474 // asSlice - row
1475 unittest
1476 {
1477     DataFrame!(int, 5) df;
1478     Index inx;
1479     inx.setIndex([["Hello", "Hi", "Hey"], ["Hi", "Hello", "Hey"], ["Hey", "Hello", "Hi"]], ["1", "2", "3"]);
1480     df.setFrameIndex(inx);
1481     df.assign!1(2, [1,2,3]);
1482     df.assign!1(4, [1,2,3]);
1483 
1484     Group!(int, int, int, int) gp;
1485     gp.createGroup!([2])(df, [0, 1]);
1486 
1487     gp[["Hello", "Hi", "1"], ["Hey"], 0] = gp.asSlice!(Universal)(["Hi", "Hello", "2"], ["Hello"]);
1488     assert(gp[["Hello", "Hi", "1"], ["Hey"], ["4"]] == 2);
1489 }
1490 
1491 // Aggregate on Group column
1492 unittest
1493 {
1494     DataFrame!(int, 2, double, 2) df;
1495     Index inx;
1496     inx[0] = [["Hello", "Hello", "Hi", "Hi"], ["1", "2", "3", "4"]];
1497     inx[1] = ["1", "2", "3", "4"];
1498 
1499     df.setFrameIndex(inx);
1500     df = [[1, 2, 3.3, 4.7], [1, 3, 4.8, 5.2], [2, 5, 6, 7.6], [2, 6, 7, 8.9]];
1501     // df.display();
1502 
1503     auto grp = df.groupBy!([0])([0]);
1504     // grp.display();
1505 
1506     import std.algorithm: max, min;
1507     import std.math: approxEqual;
1508 
1509     auto maxgrp = grp.aggregate!(1, max);
1510     static assert(is(maxgrp.GrpType == AliasSeq!(int[], double[], double[])));
1511     assert(maxgrp.data[0] == [3, 6]);
1512     assert(approxEqual(maxgrp.data[1][0], 4.8, 1e-8) && approxEqual(maxgrp.data[1][1], 7, 1e-8));
1513     assert(approxEqual(maxgrp.data[2][0], 5.2, 1e-8) && approxEqual(maxgrp.data[2][1], 8.9, 1e-8));
1514 
1515     auto dub = grp.aggregate!(1, max, min);
1516     static assert(is(dub.GrpType == AliasSeq!(int[], double[], double[])));
1517     assert(dub.data[0] == [3, 2, 6, 5]);
1518     assert(approxEqual(dub.data[1][0], 4.8, 1e-8) && approxEqual(dub.data[1][1], 3.3, 1e-8) && approxEqual(dub.data[1][2], 7, 1e-8) && approxEqual(dub.data[1][3], 6, 1e-8));
1519     assert(approxEqual(dub.data[2][0], 5.2, 1e-8) && approxEqual(dub.data[2][1], 4.7, 1e-8) && approxEqual(dub.data[2][2], 8.9, 1e-8) && approxEqual(dub.data[2][3], 7.6, 1e-8));
1520 }
1521 
1522 // Aggregate on Group Row
1523 unittest
1524 {
1525     DataFrame!(int, 2, double, 2) df;
1526     Index inx;
1527     inx[0] = [["Hello", "Hello", "Hi", "Hi"], ["1", "2", "3", "4"]];
1528     inx[1] = ["1", "2", "3", "4"];
1529 
1530     df.setFrameIndex(inx);
1531     df = [[1, 2, 3.3, 4.7], [1, 3, 4.8, 5.2], [2, 5, 6, 7.6], [2, 6, 7, 8.9]];
1532     // df.display();
1533 
1534     auto grp = df.groupBy!([0])([0]);
1535     // grp.display();
1536 
1537     import std.algorithm: max, min;
1538     import std.math: approxEqual;
1539 
1540     auto maxgrp = grp.aggregate!(0, max);
1541     static assert(is(maxgrp.GrpType == double[][1]));
1542     assert(approxEqual(maxgrp.data[0][0], 4.7, 1e-8) && approxEqual(maxgrp.data[0][1], 5.2, 1e-8) && approxEqual(maxgrp.data[0][2], 7.6, 1e-8) && approxEqual(maxgrp.data[0][3], 8.9, 1e-8));
1543 
1544     auto dub = grp.aggregate!(0, max, min);
1545     assert(approxEqual(dub.data[0][0], 4.7, 1e-8) && approxEqual(dub.data[0][1], 5.2, 1e-8) && approxEqual(dub.data[0][2], 7.6, 1e-8) && approxEqual(dub.data[0][3], 8.9, 1e-8));
1546     assert(approxEqual(dub.data[1][0], 2, 1e-8) && approxEqual(dub.data[1][1], 3, 1e-8) && approxEqual(dub.data[1][2], 5, 1e-8) && approxEqual(dub.data[1][3], 6, 1e-8));
1547 }
1548 
1549 // Aggregate with custom function
1550 unittest
1551 {
1552     static auto customFunc(T)(T[] arr)
1553     {
1554         T res = 0;
1555         foreach(i, ele; arr)
1556             res += i * ele;
1557         
1558         return res;
1559     }
1560 
1561     DataFrame!(int, 2, double, 2) df;
1562     Index inx;
1563     inx[0] = [["Hello", "Hello", "Hi", "Hi"], ["1", "2", "3", "4"]];
1564     inx[1] = ["1", "2", "3", "4"];
1565 
1566     df.setFrameIndex(inx);
1567     df = [[1, 2, 3.3, 4.7], [1, 3, 4.8, 5.2], [2, 5, 6, 7.6], [2, 6, 7, 8.9]];
1568     // df.display();
1569 
1570     auto grp = df.groupBy!([0])([0]);
1571     // grp.display();
1572 
1573     import std.algorithm: max, min;
1574     import std.math: approxEqual;
1575 
1576     auto maxgrp = grp.aggregate!(1, customFunc);
1577     assert(maxgrp.data[0] == [3, 6]);
1578     assert(approxEqual(maxgrp.data[1][0], 4.8, 1e-8) && approxEqual(maxgrp.data[1][1], 7, 1e-8));
1579     assert(approxEqual(maxgrp.data[2][0], 5.2, 1e-8) && approxEqual(maxgrp.data[2][1], 8.9, 1e-8));
1580 
1581     auto custom = grp.aggregate!(0, customFunc);
1582     assert(approxEqual(custom.data[0][0], 12.7, 1e-8) && approxEqual(custom.data[0][1], 15.2, 1e-8) && approxEqual(custom.data[0][2], 21.2, 1e-8) && approxEqual(custom.data[0][3], 24.8, 1e-8));
1583 }