The OpenD Programming Language

1 /++
2 $(H1 Index-series)
3 
4 The module contains $(LREF Series) data structure with special iteration and indexing methods.
5 It is aimed to construct index or time-series using Mir and Phobos algorithms.
6 
7 Public_imports: $(MREF mir,ndslice,slice).
8 
9 Copyright: 2020 Ilia Ki, Kaleidic Associates Advisory Limited, Symmetry Investments
10 Authors: Ilia Ki
11 
12 Macros:
13 NDSLICE = $(REF_ALTTEXT $(TT $2), $2, mir, ndslice, $1)$(NBSP)
14 T2=$(TR $(TDNW $(LREF $1)) $(TD $+))
15 +/
16 module mir.series;
17 
18 import mir.ndslice.iterator: IotaIterator;
19 import mir.ndslice.sorting: transitionIndex;
20 import mir.qualifier;
21 import mir.serde: serdeIgnore, serdeFields;
22 import std.traits;
23 ///
24 public import mir.ndslice.slice;
25 ///
26 public import mir.ndslice.sorting: sort;
27 
28 /++
29 See_also: $(LREF unionSeries), $(LREF troykaSeries), $(LREF troykaGalop).
30 +/
31 @safe version(mir_test) unittest
32 {
33     import mir.test;
34     import mir.ndslice;
35     import mir.series;
36 
37     import mir.array.allocation: array;
38     import mir.algorithm.setops: multiwayUnion;
39 
40     import mir.date: Date;
41     import core.lifetime: move;
42     import std.exception: collectExceptionMsg;
43 
44     //////////////////////////////////////
45     // Constructs two time-series.
46     //////////////////////////////////////
47     auto index0 = [
48         Date(2017, 01, 01),
49         Date(2017, 03, 01),
50         Date(2017, 04, 01)];
51 
52     auto data0 = [1.0, 3, 4];
53     auto series0 = index0.series(data0);
54 
55     auto index1 = [
56         Date(2017, 01, 01),
57         Date(2017, 02, 01),
58         Date(2017, 05, 01)];
59 
60     auto data1 = [10.0, 20, 50];
61     auto series1 = index1.series(data1);
62 
63     //////////////////////////////////////
64     // asSlice method
65     //////////////////////////////////////
66     assert(series0
67         .asSlice
68         // ref qualifier is optional
69         .map!((ref key, ref value) => key.yearMonthDay.month == value)
70         .all);
71 
72     //////////////////////////////////////
73     // get* methods
74     //////////////////////////////////////
75 
76     auto refDate = Date(2017, 03, 01);
77     auto missingDate = Date(2016, 03, 01);
78 
79     // default value
80     double defaultValue = 100;
81     assert(series0.get(refDate, defaultValue) == 3);
82     assert(series0.get(missingDate, defaultValue) == defaultValue);
83 
84     // Exceptions handlers
85     assert(series0.get(refDate) == 3);
86     assert(series0.get(refDate, new Exception("My exception msg")) == 3);
87     assert(series0.getVerbose(refDate) == 3);
88     assert(series0.getExtraVerbose(refDate, "My exception msg") == 3);
89 
90     collectExceptionMsg!Exception(series0.get(missingDate)).should
91         == "Series double[Date]: Missing required key";
92 
93     collectExceptionMsg!Exception(series0.get(missingDate, new Exception("My exception msg"))).should
94         == "My exception msg";
95 
96     collectExceptionMsg!Exception(series0.getVerbose(missingDate)).should
97         == "Series double[Date]: Missing 2016-03-01 key";
98 
99     collectExceptionMsg!Exception(series0.getExtraVerbose(missingDate, "My exception msg")).should
100         == "My exception msg. Series double[Date]: Missing 2016-03-01 key";
101 
102     // assign with get*
103     series0.get(refDate) = 100;
104     assert(series0.get(refDate) == 100);
105     series0.get(refDate) = 3;
106 
107     // tryGet
108     double val;
109     assert(series0.tryGet(refDate, val));
110     assert(val == 3);
111     assert(!series0.tryGet(missingDate, val));
112     assert(val == 3); // val was not changed
113 
114     //////////////////////////////////////
115     // Merges multiple series into one.
116     // Allocates using GC. M
117     // Makes exactly two allocations per merge:
118     // one for index/time and one for data.
119     //////////////////////////////////////
120     auto m0 = unionSeries(series0, series1);
121     auto m1 = unionSeries(series1, series0); // order is matter
122 
123     assert(m0.index == [
124         Date(2017, 01, 01),
125         Date(2017, 02, 01),
126         Date(2017, 03, 01),
127         Date(2017, 04, 01),
128         Date(2017, 05, 01)]);
129 
130     assert(m0.index == m1.index);
131     assert(m0.data == [ 1, 20,  3,  4, 50]);
132     assert(m1.data == [10, 20,  3,  4, 50]);
133 
134     //////////////////////////////////////
135     // Joins two time-series into a one with two columns.
136     //////////////////////////////////////
137     auto u = [index0, index1].multiwayUnion;
138     auto index = u.move.array;
139     auto data = slice!double([index.length, 2], 0); // initialized to 0 value
140     auto series = index.series(data);
141 
142     series[0 .. $, 0][] = series0; // fill first column
143     series[0 .. $, 1][] = series1; // fill second column
144 
145     assert(data == [
146         [1, 10],
147         [0, 20],
148         [3,  0],
149         [4,  0],
150         [0, 50]]);
151 }
152 
153 ///
154 version(mir_test)
155 unittest{
156 
157     import mir.series;
158 
159     double[int] map;
160     map[1] = 4.0;
161     map[2] = 5.0;
162     map[4] = 6.0;
163     map[5] = 10.0;
164     map[10] = 11.0;
165 
166     const s = series(map);
167 
168     double value;
169     int key;
170     assert(s.tryGet(2, value) && value == 5.0);
171     assert(!s.tryGet(8, value));
172 
173     assert(s.tryGetNext(2, value) && value == 5.0);
174     assert(s.tryGetPrev(2, value) && value == 5.0);
175     assert(s.tryGetNext(8, value) && value == 11.0);
176     assert(s.tryGetPrev(8, value) && value == 10.0);
177     assert(!s.tryGetFirst(8, 9, value));
178     assert(s.tryGetFirst(2, 10, value) && value == 5.0);
179     assert(s.tryGetLast(2, 10, value) && value == 11.0);
180     assert(s.tryGetLast(2, 8, value) && value == 10.0);
181 
182     key = 2; assert(s.tryGetNextUpdateKey(key, value) && key == 2 && value == 5.0);
183     key = 2; assert(s.tryGetPrevUpdateKey(key, value) && key == 2 && value == 5.0);
184     key = 8; assert(s.tryGetNextUpdateKey(key, value) && key == 10 && value == 11.0);
185     key = 8; assert(s.tryGetPrevUpdateKey(key, value) && key == 5 && value == 10.0);
186     key = 2; assert(s.tryGetFirstUpdateLower(key, 10, value) && key == 2 && value == 5.0);
187     key = 10; assert(s.tryGetLastUpdateKey(2, key, value) && key == 10 && value == 11.0);
188     key = 8; assert(s.tryGetLastUpdateKey(2, key, value) && key == 5 && value == 10.0);
189 }
190 
191 import mir.ndslice.slice;
192 import mir.ndslice.internal: is_Slice, isIndex;
193 import mir.math.common: fmamath;
194 
195 import std.meta;
196 
197 @fmamath:
198 
199 /++
200 Plain index/time observation data structure.
201 Observation are used as return tuple for for indexing $(LREF Series).
202 +/
203 struct mir_observation(Index, Data)
204 {
205     /// Date, date-time, time, or index.
206     Index index;
207     /// Value or ndslice.
208     Data data;
209 }
210 
211 /// ditto
212 alias Observation = mir_observation;
213 
214 /// Convenient function for $(LREF Observation) construction.
215 auto observation(Index, Data)(Index index, Data data)
216 {
217     alias R = mir_observation!(Index, Data);
218     return R(index, data);
219 }
220 
221 /++
222 Convinient alias for 1D Contiguous $(LREF Series).
223 +/
224 alias SeriesMap(K, V) = mir_series!(K*, V*);
225 
226 ///
227 version(mir_test) unittest
228 {
229     import std.traits;
230     import mir.series;
231 
232     static assert (is(SeriesMap!(string, double) == Series!(string*, double*)));
233 
234     /// LHS, RHS
235     static assert (isAssignable!(SeriesMap!(string, double), SeriesMap!(string, double)));
236     static assert (isAssignable!(SeriesMap!(string, double), typeof(null)));
237 
238     static assert (isAssignable!(SeriesMap!(const string, double), SeriesMap!(string, double)));
239     static assert (isAssignable!(SeriesMap!(string, const double), SeriesMap!(string, double)));
240     static assert (isAssignable!(SeriesMap!(const string, const double), SeriesMap!(string, double)));
241 
242     static assert (isAssignable!(SeriesMap!(immutable string, double), SeriesMap!(immutable string, double)));
243     static assert (isAssignable!(SeriesMap!(immutable string, const double), SeriesMap!(immutable string, double)));
244     static assert (isAssignable!(SeriesMap!(const string, const double), SeriesMap!(immutable string, double)));
245     static assert (isAssignable!(SeriesMap!(string, immutable double), SeriesMap!(string, immutable double)));
246     static assert (isAssignable!(SeriesMap!(const string, immutable double), SeriesMap!(string, immutable double)));
247     static assert (isAssignable!(SeriesMap!(const string, const double), SeriesMap!(string, immutable double)));
248     // etc
249 }
250 
251 /++
252 Plain index series data structure.
253 
254 `*.index[i]`/`*.key[i]`/`*.time` corresponds to `*.data[i]`/`*.value`.
255 
256 Index is assumed to be sorted.
257 $(LREF sort) can be used to normalise a series.
258 +/
259 @serdeFields
260 struct mir_series(IndexIterator_, Iterator_, size_t N_ = 1, SliceKind kind_ = Contiguous)
261 {
262     private enum doUnittest = is(typeof(this) == mir_series!(int*, double*));
263 
264     ///
265     alias IndexIterator = IndexIterator_;
266 
267     ///
268     alias Iterator = Iterator_;
269 
270     ///
271     @serdeIgnore enum size_t N = N_;
272 
273     ///
274     @serdeIgnore enum SliceKind kind = kind_;
275 
276     /++
277     Index series is assumed to be sorted.
278 
279     `IndexIterator` is an iterator on top of date, date-time, time, or numbers or user defined types with defined `opCmp`.
280     For example, `Date*`, `DateTime*`, `immutable(long)*`, `mir.ndslice.iterator.IotaIterator`.
281     +/
282     auto index() @property @trusted
283     {
284         return _index.sliced(this.data._lengths[0]);
285     }
286 
287     /// ditto
288     auto index() @property @trusted const
289     {
290         return _index.lightConst.sliced(this.data._lengths[0]);
291     }
292 
293     /// ditto
294     auto index() @property @trusted immutable
295     {
296         return _index.lightImmutable.sliced(this.data._lengths[0]);
297     }
298 
299     /// ditto
300     void index()(Slice!IndexIterator index) @property @trusted
301     {
302         import core.lifetime: move;
303         this._index = move(index._iterator);
304     }
305 
306     /// ditto
307     static if (doUnittest)
308     @safe version(mir_test) unittest
309     {
310         import mir.ndslice.slice: sliced;
311         auto s = ["a", "b"].series([5, 6]);
312         assert(s.index == ["a", "b"]);
313         s.index = ["c", "d"].sliced;
314         assert(s.index == ["c", "d"]);
315     }
316 
317     /++
318     Data is any ndslice with only one constraints,
319     `data` and `index` lengths should be equal.
320     +/
321     Slice!(Iterator, N, kind) data;
322 
323 @serdeIgnore:
324 
325     ///
326     IndexIterator _index;
327 
328     /// Index / Key / Time type aliases
329     alias Index = typeof(typeof(this).init.index.front);
330     /// Data / Value type aliases
331     alias Data = typeof(typeof(this).init.data.front);
332 
333     private enum defaultMsg() = "Series " ~ Unqual!(this.Data).stringof ~ "[" ~ Unqual!(this.Index).stringof ~ "]: Missing";
334     private static immutable defaultExc() = new Exception(defaultMsg!() ~ " required key");
335 
336     ///
337     void serdeFinalize()() @trusted scope
338     {
339         import mir.algorithm.iteration: any;
340         import mir.ndslice.topology: pairwise;
341         import std.traits: Unqual;
342         if (length <= 1)
343             return;
344         auto mutableOf = cast(Series!(Unqual!Index*, Unqual!Data*)) this.lightScope();
345         if (any(pairwise!"a > b"(mutableOf.index)))
346             sort(mutableOf);
347     }
348 
349 @fmamath:
350 
351     ///
352     this()(Slice!IndexIterator index, Slice!(Iterator, N, kind) data)
353     {
354         assert(index.length == data.length, "Series constructor: index and data lengths must be equal.");
355         this.data = data;
356         _index = index._iterator;
357     }
358 
359 
360     /// Construct from null
361     this(typeof(null))
362     {
363         this.data = this.data.init;
364         _index = _index.init;
365     }
366 
367     ///
368     bool opEquals(RIndexIterator, RIterator, size_t RN, SliceKind rkind, )(Series!(RIndexIterator, RIterator, RN, rkind) rhs) const
369     {
370         return this.lightScopeIndex == rhs.lightScopeIndex && this.data.lightScope == rhs.data.lightScope;
371     }
372 
373     private auto lightScopeIndex()() return scope @trusted
374     {
375         return .lightScope(_index).sliced(this.data._lengths[0]);
376     }
377 
378     private auto lightScopeIndex()() return scope @trusted const
379     {
380         return .lightScope(_index).sliced(this.data._lengths[0]);
381     }
382 
383     private auto lightScopeIndex()() return scope @trusted immutable
384     {
385         return .lightScope(_index).sliced(this.data._lengths[0]);
386     }
387 
388     ///
389     typeof(this) opBinary(string op : "~")(typeof(this) rhs)
390     {
391         scope typeof(this.lightScope)[2] lhsAndRhs = [this.lightScope, rhs.lightScope];
392         return unionSeriesImplPrivate!false(lhsAndRhs);
393     }
394 
395     /// ditto
396     auto opBinary(string op : "~")(const typeof(this) rhs) const @trusted
397     {
398         scope typeof(this.lightScope)[2] lhsAndRhs = [this.lightScope, rhs.lightScope];
399         return unionSeriesImplPrivate!false(lhsAndRhs);
400     }
401 
402     static if (doUnittest)
403     ///
404     @safe pure nothrow version(mir_test) unittest
405     {
406         import mir.date: Date;
407 
408         //////////////////////////////////////
409         // Constructs two time-series.
410         //////////////////////////////////////
411         auto index0 = [1,3,4];
412         auto data0 = [1.0, 3, 4];
413         auto series0 = index0.series(data0);
414 
415         auto index1 = [1,2,5];
416         auto data1 = [10.0, 20, 50];
417         auto series1 = index1.series(data1);
418 
419         //////////////////////////////////////
420         // Merges multiple series into one.
421         //////////////////////////////////////
422         // Order is matter.
423         // The first slice has higher priority.
424         auto m0 = series0 ~ series1;
425         auto m1 = series1 ~ series0;
426 
427         assert(m0.index == m1.index);
428         assert(m0.data == [ 1, 20,  3,  4, 50]);
429         assert(m1.data == [10, 20,  3,  4, 50]);
430     }
431 
432     static if (doUnittest)
433     @safe pure nothrow version(mir_test) unittest
434     {
435         import mir.date: Date;
436 
437         //////////////////////////////////////
438         // Constructs two time-series.
439         //////////////////////////////////////
440         auto index0 = [1,3,4];
441         auto data0 = [1.0, 3, 4];
442         auto series0 = index0.series(data0);
443 
444         auto index1 = [1,2,5];
445         auto data1 = [10.0, 20, 50];
446         const series1 = index1.series(data1);
447 
448         //////////////////////////////////////
449         // Merges multiple series into one.
450         //////////////////////////////////////
451         // Order is matter.
452         // The first slice has higher priority.
453         auto m0 = series0 ~ series1;
454         auto m1 = series1 ~ series0;
455 
456         assert(m0.index == m1.index);
457         assert(m0.data == [ 1, 20,  3,  4, 50]);
458         assert(m1.data == [10, 20,  3,  4, 50]);
459     }
460 
461     /++
462     Special `[] =` index-assign operator for index-series.
463     Assigns data from `r` with index intersection.
464     If a index index in `r` is not in the index index for this series, then no op-assign will take place.
465     This and r series are assumed to be sorted.
466 
467     Params:
468         r = rvalue index-series
469     +/
470     void opIndexAssign(IndexIterator_, Iterator_, size_t N_, SliceKind kind_)
471         (Series!(IndexIterator_, Iterator_, N_, kind_) r)
472     {
473         opIndexOpAssign!("", IndexIterator_, Iterator_, N_, kind_)(r);
474     }
475 
476     static if (doUnittest)
477     ///
478     version(mir_test) unittest
479     {
480         auto index = [1, 2, 3, 4];
481         auto data = [10.0, 10, 10, 10];
482         auto series = index.series(data);
483 
484         auto rindex = [0, 2, 4, 5];
485         auto rdata = [1.0, 2, 3, 4];
486         auto rseries = rindex.series(rdata);
487 
488         // series[] = rseries;
489         series[] = rseries;
490         assert(series.data == [10, 2, 10, 3]);
491     }
492 
493     /++
494     Special `[] op=` index-op-assign operator for index-series.
495     Op-assigns data from `r` with index intersection.
496     If a index index in `r` is not in the index index for this series, then no op-assign will take place.
497     This and r series are assumed to be sorted.
498 
499     Params:
500         rSeries = rvalue index-series
501     +/
502     void opIndexOpAssign(string op, IndexIterator_, Iterator_, size_t N_, SliceKind kind_)
503         (auto ref Series!(IndexIterator_, Iterator_, N_, kind_) rSeries)
504     {
505         scope l = this.lightScope;
506         scope r = rSeries.lightScope;
507         if (r.empty)
508             return;
509         if (l.empty)
510             return;
511         Unqual!(typeof(*r._index)) rf = *r._index;
512         Unqual!(typeof(*l._index)) lf = *l._index;
513         goto Begin;
514     R:
515         r.popFront;
516         if (r.empty)
517             goto End;
518         rf = *r._index;
519     Begin:
520         if (lf > rf)
521             goto R;
522         if (lf < rf)
523             goto L;
524     E:
525         static if (N != 1)
526             mixin("l.data.front[] " ~ op ~ "= r.data.front;");
527         else
528             mixin("l.data.front   " ~ op ~ "= r.data.front;");
529 
530         r.popFront;
531         if (r.empty)
532             goto End;
533         rf = *r._index;
534     L:
535         l.popFront;
536         if (l.empty)
537             goto End;
538         lf = *l._index;
539 
540         if (lf < rf)
541             goto L;
542         if (lf == rf)
543             goto E;
544         goto R;
545     End:
546     }
547 
548     static if (doUnittest)
549     ///
550     version(mir_test) unittest
551     {
552         auto index = [1, 2, 3, 4];
553         auto data = [10.0, 10, 10, 10];
554         auto series = index.series(data);
555 
556         auto rindex = [0, 2, 4, 5];
557         auto rdata = [1.0, 2, 3, 4];
558         auto rseries = rindex.series(rdata);
559 
560         series[] += rseries;
561         assert(series.data == [10, 12, 10, 13]);
562     }
563 
564     /++
565     This function uses a search with policy sp to find the largest left subrange on which
566     `t < key` is true for all `t`.
567     The search schedule and its complexity are documented in `std.range.SearchPolicy`.
568     +/
569     auto lowerBound(Index)(auto ref scope const Index key)
570     {
571         return opIndex(opSlice(0, lightScopeIndex.transitionIndex(key)));
572     }
573 
574     /// ditto
575     auto lowerBound(Index)(auto ref scope const Index key) const
576     {
577         return opIndex(opSlice(0, lightScopeIndex.transitionIndex(key)));
578     }
579 
580 
581     /++
582     This function uses a search with policy sp to find the largest right subrange on which
583     `t > key` is true for all `t`.
584     The search schedule and its complexity are documented in `std.range.SearchPolicy`.
585     +/
586     auto upperBound(Index)(auto ref scope const Index key)
587     {
588         return opIndex(opSlice(lightScopeIndex.transitionIndex!"a <= b"(key), length));
589     }
590 
591     /// ditto
592     auto upperBound(Index)(auto ref scope const Index key) const
593     {
594         return opIndex(opSlice(lightScopeIndex.transitionIndex!"a <= b"(key), length));
595     }
596 
597     /**
598     Gets data for the index.
599     Params:
600         key = index
601         _default = default value is returned if the series does not contains the index.
602     Returns:
603         data that corresponds to the index or default value.
604     */
605     ref get(Index, Value)(auto ref scope const Index key, return ref Value _default) @trusted
606         if (!is(Value : const(Exception)))
607     {
608         size_t idx = lightScopeIndex.transitionIndex(key);
609         return idx < this.data._lengths[0] && _index[idx] == key ? this.data[idx] : _default;
610     }
611 
612     /// ditto
613     ref get(Index, Value)(auto ref scope const Index key, return ref Value _default) const
614         if (!is(Value : const(Exception)))
615     {
616         return this.lightScope.get(key, _default);
617     }
618 
619     /// ditto
620     ref get(Index, Value)(auto ref scope const Index key, return ref Value _default) immutable
621         if (!is(Value : const(Exception)))
622     {
623         return this.lightScope.get(key, _default);
624     }
625 
626     auto get(Index, Value)(auto ref scope const Index key, Value _default) @trusted
627         if (!is(Value : const(Exception)))
628     {
629         size_t idx = lightScopeIndex.transitionIndex(key);
630         return idx < this.data._lengths[0] && _index[idx] == key ? this.data[idx] : _default;
631     }
632 
633     /// ditto
634     auto get(Index, Value)(auto ref scope const Index key, Value _default) const
635         if (!is(Value : const(Exception)))
636     {
637         import core.lifetime: forward;
638         return this.lightScope.get(key, forward!_default);
639     }
640 
641     /// ditto
642     auto get(Index, Value)(auto ref scope const Index key, Value _default) immutable
643         if (!is(Value : const(Exception)))
644     {
645         import core.lifetime: forward;
646         return this.lightScope.get(key, forward!_default);
647     }
648 
649     /**
650     Gets data for the index.
651     Params:
652         key = index
653         exc = (lazy, optional) exception to throw if the series does not contains the index.
654     Returns: data that corresponds to the index.
655     Throws:
656         Exception if the series does not contains the index.
657     See_also: $(LREF Series.getVerbose), $(LREF Series.tryGet)
658     */
659     auto ref get(Index)(auto ref scope const Index key) @trusted
660     {
661         size_t idx = lightScopeIndex.transitionIndex(key);
662         if (idx < this.data._lengths[0] && _index[idx] == key)
663         {
664             return this.data[idx];
665         }
666         import mir.exception : toMutable;
667         throw defaultExc!().toMutable;
668     }
669 
670     /// ditto
671     auto ref get(Index)(auto ref scope const Index key, lazy const Exception exc) @trusted
672     {
673         size_t idx = lightScopeIndex.transitionIndex(key);
674         if (idx < this.data._lengths[0] && _index[idx] == key)
675         {
676             return this.data[idx];
677         }
678         { import mir.exception : toMutable; throw exc.toMutable; }
679     }
680 
681     /// ditto
682     auto ref get(Index)(auto ref scope const Index key) const
683     {
684         return this.lightScope.get(key);
685     }
686 
687     /// ditto
688     auto ref get(Index)(auto ref scope const Index key, lazy const Exception exc) const
689     {
690         return this.lightScope.get(key, exc);
691     }
692 
693 
694     /// ditto
695     auto ref get(Index)(auto ref scope const Index key) immutable
696     {
697         return this.lightScope.get(key);
698     }
699 
700     /// ditto
701     auto ref get(Index)(auto ref scope const Index key, lazy const Exception exc) immutable
702     {
703         return this.lightScope.get(key, exc);
704     }
705 
706     /**
707     Gets data for the index (verbose exception).
708     Params:
709         key = index
710     Returns: data that corresponds to the index.
711     Throws:
712         Detailed exception if the series does not contains the index.
713     See_also: $(LREF Series.get), $(LREF Series.tryGet)
714     */
715     auto ref getVerbose(Index)(auto ref scope const Index key, string file = __FILE__, int line = __LINE__)
716     {
717         import std.format: format;
718         return this.get(key, new Exception(format("%s %s key", defaultMsg!(), key), file, line));
719     }
720 
721     /// ditto
722     auto ref getVerbose(Index)(auto ref scope const Index key, string file = __FILE__, int line = __LINE__) const
723     {
724         return this.lightScope.getVerbose(key, file, line);
725     }
726 
727     /// ditto
728     auto ref getVerbose(Index)(auto ref scope const Index key, string file = __FILE__, int line = __LINE__) immutable
729     {
730         return this.lightScope.getVerbose(key, file, line);
731     }
732 
733     /**
734     Gets data for the index (extra verbose exception).
735     Params:
736         key = index
737     Returns: data that corresponds to the index.
738     Throws:
739         Detailed exception if the series does not contains the index.
740     See_also: $(LREF Series.get), $(LREF Series.tryGet)
741     */
742     auto ref getExtraVerbose(Index)(auto ref scope const Index key, string exceptionInto, string file = __FILE__, int line = __LINE__)
743     {
744         import std.format: format;
745         return this.get(key, new Exception(format("%s. %s %s key", exceptionInto, defaultMsg!(), key), file, line));
746     }
747 
748     /// ditto
749     auto ref getExtraVerbose(Index)(auto ref scope const Index key, string exceptionInto, string file = __FILE__, int line = __LINE__) const
750     {
751         return this.lightScope.getExtraVerbose(key, exceptionInto, file, line);
752     }
753 
754     /// ditto
755     auto ref getExtraVerbose(Index)(auto ref scope const Index key, string exceptionInto, string file = __FILE__, int line = __LINE__) immutable
756     {
757         return this.lightScope.getExtraVerbose(key, exceptionInto, file, line);
758     }
759 
760     ///
761     bool contains(Index)(auto ref scope const Index key) const @trusted
762     {
763         size_t idx = lightScopeIndex.transitionIndex(key);
764         return idx < this.data._lengths[0] && _index[idx] == key;
765     }
766 
767     ///
768     auto opBinaryRight(string op : "in", Index)(auto ref scope const Index key) @trusted
769     {
770         size_t idx = lightScopeIndex.transitionIndex(key);
771         bool cond = idx < this.data._lengths[0] && _index[idx] == key;
772         static if (__traits(compiles, &this.data[size_t.init]))
773         {
774             if (cond)
775                 return &this.data[idx];
776             return null;
777         }
778         else
779         {
780             return bool(cond);
781         }
782     }
783 
784     /// ditto
785     auto opBinaryRight(string op : "in", Index)(auto ref scope const Index key) const
786     {
787         auto val = key in this.lightScope;
788         return val;
789     }
790 
791     /// ditto
792     auto opBinaryRight(string op : "in", Index)(auto ref scope const Index key) immutable
793     {
794         auto val = key in this.lightScope;
795         return val;
796     }
797 
798     /++
799     Tries to get the first value, such that `key_i == key`.
800 
801     Returns: `true` on success.
802     +/
803     bool tryGet(Index, Value)(Index key, scope ref Value val) @trusted
804     {
805         size_t idx = lightScopeIndex.transitionIndex(key);
806         auto cond = idx < this.data._lengths[0] && _index[idx] == key;
807         if (cond)
808             val = this.data[idx];
809         return cond;
810     }
811 
812     /// ditto
813     bool tryGet(Index, Value)(Index key, scope ref Value val) const
814     {
815         return this.lightScope.tryGet(key, val);
816     }
817 
818     /// ditto
819     bool tryGet(Index, Value)(Index key, scope ref Value val) immutable
820     {
821         return this.lightScope.tryGet(key, val);
822     }
823 
824     /++
825     Tries to get the first value, such that `key_i >= key`.
826 
827     Returns: `true` on success.
828     +/
829     bool tryGetNext(Index, Value)(auto ref scope const Index key, scope ref Value val)
830     {
831         size_t idx = lightScopeIndex.transitionIndex(key);
832         auto cond = idx < this.data._lengths[0];
833         if (cond)
834             val = this.data[idx];
835         return cond;
836     }
837 
838     /// ditto
839     bool tryGetNext(Index, Value)(auto ref scope const Index key, scope ref Value val) const
840     {
841         return this.lightScope.tryGetNext(key, val);
842     }
843 
844     /// ditto
845     bool tryGetNext(Index, Value)(auto ref scope const Index key, scope ref Value val) immutable
846     {
847         return this.lightScope.tryGetNext(key, val);
848     }
849 
850     /++
851     Tries to get the first value, such that `key_i >= key`.
852     Updates `key` with `key_i`.
853 
854     Returns: `true` on success.
855     +/
856     bool tryGetNextUpdateKey(Index, Value)(scope ref Index key, scope ref Value val) @trusted
857     {
858         size_t idx = lightScopeIndex.transitionIndex(key);
859         auto cond = idx < this.data._lengths[0];
860         if (cond)
861         {
862             key = _index[idx];
863             val = this.data[idx];
864         }
865         return cond;
866     }
867 
868     /// ditto
869     bool tryGetNextUpdateKey(Index, Value)(scope ref Index key, scope ref Value val) const
870     {
871         return this.lightScope.tryGetNextUpdateKey(key, val);
872     }
873 
874     /// ditto
875     bool tryGetNextUpdateKey(Index, Value)(scope ref Index key, scope ref Value val) immutable
876     {
877         return this.lightScope.tryGetNextUpdateKey(key, val);
878     }
879 
880     /++
881     Tries to get the last value, such that `key_i <= key`.
882 
883     Returns: `true` on success.
884     +/
885     bool tryGetPrev(Index, Value)(auto ref scope const Index key, scope ref Value val)
886     {
887         size_t idx = lightScopeIndex.transitionIndex!"a <= b"(key) - 1;
888         auto cond = 0 <= sizediff_t(idx);
889         if (cond)
890             val = this.data[idx];
891         return cond;
892     }
893 
894     /// ditto
895     bool tryGetPrev(Index, Value)(auto ref scope const Index key, scope ref Value val) const
896     {
897         return this.lightScope.tryGetPrev(key, val);
898     }
899 
900     /// ditto
901     bool tryGetPrev(Index, Value)(auto ref scope const Index key, scope ref Value val) immutable
902     {
903         return this.lightScope.tryGetPrev(key, val);
904     }
905 
906     /++
907     Tries to get the last value, such that `key_i <= key`.
908     Updates `key` with `key_i`.
909 
910     Returns: `true` on success.
911     +/
912     bool tryGetPrevUpdateKey(Index, Value)(scope ref Index key, scope ref Value val) @trusted
913     {
914         size_t idx = lightScopeIndex.transitionIndex!"a <= b"(key) - 1;
915         auto cond = 0 <= sizediff_t(idx);
916         if (cond)
917         {
918             key = _index[idx];
919             val = this.data[idx];
920         }
921         return cond;
922     }
923 
924     /// ditto
925     bool tryGetPrevUpdateKey(Index, Value)(scope ref Index key, scope ref Value val) const
926     {
927         return this.lightScope.tryGetPrevUpdateKey(key, val);
928     }
929 
930     /// ditto
931     bool tryGetPrevUpdateKey(Index, Value)(scope ref Index key, scope ref Value val) immutable
932     {
933         return this.lightScope.tryGetPrevUpdateKey(key, val);
934     }
935 
936     /++
937     Tries to get the first value, such that `lowerBound <= key_i <= upperBound`.
938 
939     Returns: `true` on success.
940     +/
941     bool tryGetFirst(Index, Value)(auto ref scope const Index lowerBound, auto ref scope const Index upperBound, scope ref Value val) @trusted
942     {
943         size_t idx = lightScopeIndex.transitionIndex(lowerBound);
944         auto cond = idx < this.data._lengths[0] && _index[idx] <= upperBound;
945         if (cond)
946             val = this.data[idx];
947         return cond;
948     }
949 
950     /// ditto
951     bool tryGetFirst(Index, Value)(Index lowerBound, auto ref scope const Index upperBound, scope ref Value val) const
952     {
953         return this.lightScope.tryGetFirst(lowerBound, upperBound, val);
954     }
955 
956     /// ditto
957     bool tryGetFirst(Index, Value)(Index lowerBound, auto ref scope const Index upperBound, scope ref Value val) immutable
958     {
959         return this.lightScope.tryGetFirst(lowerBound, upperBound, val);
960     }
961 
962     /++
963     Tries to get the first value, such that `lowerBound <= key_i <= upperBound`.
964     Updates `lowerBound` with `key_i`.
965 
966     Returns: `true` on success.
967     +/
968     bool tryGetFirstUpdateLower(Index, Value)(ref Index lowerBound, auto ref scope const Index upperBound, scope ref Value val) @trusted
969     {
970         size_t idx = lightScopeIndex.transitionIndex(lowerBound);
971         auto cond = idx < this.data._lengths[0] && _index[idx] <= upperBound;
972         if (cond)
973         {
974             lowerBound = _index[idx];
975             val = this.data[idx];
976         }
977         return cond;
978     }
979 
980     /// ditto
981     bool tryGetFirstUpdateLower(Index, Value)(ref Index lowerBound, auto ref scope const Index upperBound, scope ref Value val) const
982     {
983         return this.lightScope.tryGetFirstUpdateLower(lowerBound, upperBound, val);
984     }
985 
986     /// ditto
987     bool tryGetFirstUpdateLower(Index, Value)(ref Index lowerBound, auto ref scope const Index upperBound, scope ref Value val) immutable
988     {
989         return this.lightScope.tryGetFirstUpdateLower(lowerBound, upperBound, val);
990     }
991 
992     /++
993     Tries to get the last value, such that `lowerBound <= key_i <= upperBound`.
994 
995     Returns: `true` on success.
996     +/
997     bool tryGetLast(Index, Value)(Index lowerBound, auto ref scope const Index upperBound, scope ref Value val) @trusted
998     {
999         size_t idx = lightScopeIndex.transitionIndex!"a <= b"(upperBound) - 1;
1000         auto cond = 0 <= sizediff_t(idx) && _index[idx] >= lowerBound;
1001         if (cond)
1002             val = this.data[idx];
1003         return cond;
1004     }
1005 
1006     /// ditto
1007     bool tryGetLast(Index, Value)(Index lowerBound, auto ref scope const Index upperBound, scope ref Value val) const
1008     {
1009         return this.lightScope.tryGetLast(lowerBound, upperBound, val);
1010     }
1011 
1012     /// ditto
1013     bool tryGetLast(Index, Value)(Index lowerBound, auto ref scope const Index upperBound, scope ref Value val) immutable
1014     {
1015         return this.lightScope.tryGetLast(lowerBound, upperBound, val);
1016     }
1017 
1018     /++
1019     Tries to get the last value, such that `lowerBound <= key_i <= upperBound`.
1020     Updates `upperBound` with `key_i`.
1021 
1022     Returns: `true` on success.
1023     +/
1024     bool tryGetLastUpdateKey(Index, Value)(Index lowerBound, ref Index upperBound, scope ref Value val) @trusted
1025     {
1026         size_t idx = lightScopeIndex.transitionIndex!"a <= b"(upperBound) - 1;
1027         auto cond = 0 <= sizediff_t(idx) && _index[idx] >= lowerBound;
1028         if (cond)
1029         {
1030             upperBound = _index[idx];
1031             val = this.data[idx];
1032         }
1033         return cond;
1034     }
1035 
1036     /// ditto
1037     bool tryGetLastUpdateKey(Index, Value)(Index lowerBound, ref Index upperBound, scope ref Value val) const
1038     {
1039         return this.lightScope.tryGetLastUpdateKey(lowerBound, upperBound, val);
1040     }
1041 
1042     /// ditto
1043     bool tryGetLastUpdateKey(Index, Value)(Index lowerBound, ref Index upperBound, scope ref Value val) immutable
1044     {
1045         return this.lightScope.tryGetLastUpdateKey(lowerBound, upperBound, val);
1046     }
1047 
1048     /++
1049     Returns:
1050         1D Slice with creared with $(NDSLICE topology, zip) ([0] - key, [1] - value).
1051     See_also:
1052         $(NDSLICE topology, map) uses multiargument lambdas to handle zipped slices.
1053     +/
1054     auto asSlice()() @property
1055     {
1056         import mir.ndslice.topology: zip, map, ipack;
1057         static if (N == 1)
1058             return index.zip(data);
1059         else
1060             return index.zip(data.ipack!1.map!"a");
1061     }
1062 
1063     /// ditto
1064     auto asSlice()() const @property
1065     {
1066         return opIndex.asSlice;
1067     }
1068 
1069     /// ditto
1070     auto asSlice()() immutable @property
1071     {
1072         return opIndex.asSlice;
1073     }
1074 
1075     /// ndslice-like primitives
1076     bool empty(size_t dimension = 0)() const @property
1077         if (dimension < N)
1078     {
1079         return !length!dimension;
1080     }
1081 
1082     /// ditto
1083     size_t length(size_t dimension = 0)() const @property
1084         if (dimension < N)
1085     {
1086         return this.data.length!dimension;
1087     }
1088 
1089     /// ditto
1090     auto front(size_t dimension = 0)() @property
1091         if (dimension < N)
1092     {
1093         assert(!empty!dimension);
1094         static if (dimension)
1095         {
1096             return index.series(data.front!dimension);
1097         }
1098         else
1099         {
1100             return Observation!(Index, Data)(index.front, data.front);
1101         }
1102     }
1103 
1104     /// ditto
1105     auto back(size_t dimension = 0)() @property
1106         if (dimension < N)
1107     {
1108         assert(!empty!dimension);
1109         static if (dimension)
1110         {
1111             return index.series(this.data.back!dimension);
1112         }
1113         else
1114         {
1115             return index.back.observation(this.data.back);
1116         }
1117     }
1118 
1119     /// ditto
1120     void popFront(size_t dimension = 0)() @trusted
1121         if (dimension < N)
1122     {
1123         assert(!empty!dimension);
1124         static if (dimension == 0)
1125             _index++;
1126         this.data.popFront!dimension;
1127     }
1128 
1129     /// ditto
1130     void popBack(size_t dimension = 0)()
1131         if (dimension < N)
1132     {
1133         assert(!empty!dimension);
1134         this.data.popBack!dimension;
1135     }
1136 
1137     /// ditto
1138     void popFrontExactly(size_t dimension = 0)(size_t n) @trusted
1139         if (dimension < N)
1140     {
1141         assert(length!dimension >= n);
1142         static if (dimension == 0)
1143             _index += n;
1144         this.data.popFrontExactly!dimension(n);
1145     }
1146 
1147     /// ditto
1148     void popBackExactly(size_t dimension = 0)(size_t n)
1149         if (dimension < N)
1150     {
1151         assert(length!dimension >= n);
1152         this.data.popBackExactly!dimension(n);
1153     }
1154 
1155     /// ditto
1156     void popFrontN(size_t dimension = 0)(size_t n)
1157         if (dimension < N)
1158     {
1159         auto len = length!dimension;
1160         n = n <= len ? n : len;
1161         popFrontExactly!dimension(n);
1162     }
1163 
1164     /// ditto
1165     void popBackN(size_t dimension = 0)(size_t n)
1166         if (dimension < N)
1167     {
1168         auto len = length!dimension;
1169         n = n <= len ? n : len;
1170         popBackExactly!dimension(n);
1171     }
1172 
1173     /// ditto
1174     Slice!(IotaIterator!size_t) opSlice(size_t dimension = 0)(size_t i, size_t j) const
1175         if (dimension < N)
1176     in
1177     {
1178         assert(i <= j,
1179             "Series.opSlice!" ~ dimension.stringof ~ ": the left opSlice boundary must be less than or equal to the right bound.");
1180         enum errorMsg = ": difference between the right and the left bounds"
1181                         ~ " must be less than or equal to the length of the given dimension.";
1182         assert(j - i <= this.data._lengths[dimension],
1183               "Series.opSlice!" ~ dimension.stringof ~ errorMsg);
1184     }
1185     do
1186     {
1187         return typeof(return)(j - i, typeof(return).Iterator(i));
1188     }
1189 
1190     /// ditto
1191     size_t opDollar(size_t dimension = 0)() const
1192     {
1193         return this.data.opDollar!dimension;
1194     }
1195 
1196     /// ditto
1197     auto opIndex(Slices...)(Slices slices)
1198         if (allSatisfy!(templateOr!(is_Slice, isIndex), Slices))
1199     {
1200         static if (Slices.length == 0)
1201         {
1202             return this;
1203         }
1204         else
1205         static if (is_Slice!(Slices[0]))
1206         {
1207             return index[slices[0]].series(data[slices]);
1208         }
1209         else
1210         {
1211             return index[slices[0]].observation(data[slices]);
1212         }
1213     }
1214 
1215     /// ditto
1216     auto opIndex(Slices...)(Slices slices) const
1217         if (allSatisfy!(templateOr!(is_Slice, isIndex), Slices))
1218     {
1219         return lightConst.opIndex(slices);
1220     }
1221 
1222     /// ditto
1223     auto opIndex(Slices...)(Slices slices) immutable
1224         if (allSatisfy!(templateOr!(is_Slice, isIndex), Slices))
1225     {
1226         return lightImmutable.opIndex(slices);
1227     }
1228 
1229     ///
1230     ref opAssign(typeof(this) rvalue) scope return @trusted
1231     {
1232         import mir.utility: swap;
1233         this.data._structure = rvalue.data._structure;
1234         swap(this.data._iterator, rvalue.data._iterator);
1235         swap(this._index, rvalue._index);
1236         return this;
1237     }
1238 
1239     /// ditto
1240     ref opAssign(RIndexIterator, RIterator)(Series!(RIndexIterator, RIterator, N, kind) rvalue) scope return
1241         if (isAssignable!(IndexIterator, RIndexIterator) && isAssignable!(Iterator, RIterator))
1242     {
1243         import core.lifetime: move;
1244         this.data._structure = rvalue.data._structure;
1245         this.data._iterator = rvalue.data._iterator.move;
1246         this._index = rvalue._index.move;
1247         return this;
1248     }
1249 
1250     /// ditto
1251     ref opAssign(RIndexIterator, RIterator)(auto ref const Series!(RIndexIterator, RIterator, N, kind) rvalue) scope return
1252         if (isAssignable!(IndexIterator, LightConstOf!RIndexIterator) && isAssignable!(Iterator, LightConstOf!RIterator))
1253     {
1254         return this = rvalue.opIndex;
1255     }
1256 
1257     /// ditto
1258     ref opAssign(RIndexIterator, RIterator)(auto ref immutable Series!(RIndexIterator, RIterator, N, kind) rvalue) scope return
1259         if (isAssignable!(IndexIterator, LightImmutableOf!RIndexIterator) && isAssignable!(Iterator, LightImmutableOf!RIterator))
1260     {
1261         return this = rvalue.opIndex;
1262     }
1263 
1264     /// ditto
1265     ref opAssign(typeof(null)) scope return
1266     {
1267         return this = this.init;
1268     }
1269 
1270     /// ditto
1271     auto save()() @property
1272     {
1273         return this;
1274     }
1275 
1276     ///
1277     Series!(LightScopeOf!IndexIterator, LightScopeOf!Iterator, N, kind) lightScope()() return scope @trusted @property
1278     {
1279         return typeof(return)(lightScopeIndex, this.data.lightScope);
1280     }
1281 
1282     /// ditto
1283     Series!(LightConstOf!(LightScopeOf!IndexIterator), LightConstOf!(LightScopeOf!Iterator), N, kind) lightScope()() return scope @trusted const @property
1284     {
1285         return typeof(return)(lightScopeIndex, this.data.lightScope);
1286     }
1287 
1288     /// ditto
1289     Series!(LightConstOf!(LightScopeOf!IndexIterator), LightConstOf!(LightScopeOf!Iterator), N, kind) lightScope()() return scope @trusted immutable @property
1290     {
1291         return typeof(return)(lightScopeIndex, this.data.lightScope);
1292     }
1293 
1294     ///
1295     Series!(LightConstOf!IndexIterator, LightConstOf!Iterator, N, kind) lightConst()() const @property @trusted
1296     {
1297         return index[].series(data[]);
1298     }
1299 
1300     ///
1301     Series!(LightImmutableOf!IndexIterator, LightImmutableOf!Iterator, N, kind) lightImmutable()() immutable @property @trusted
1302     {
1303         return index[].series(data[]);
1304     }
1305 
1306     ///
1307     auto toConst()() const @property
1308     {
1309         return index.toConst.series(data.toConst);
1310     }
1311 
1312     ///
1313     void toString(Writer)(scope ref Writer w) scope const @safe
1314     {
1315         import mir.format: print;
1316         scope ls = lightScope;
1317         print(w, "{ index: ");
1318         print(w, ls.index);
1319         print(w, ", data: ");
1320         print(w, ls.data);
1321         print(w, " }");
1322     }
1323 
1324 }
1325 
1326 
1327 /// ditto
1328 alias Series = mir_series;
1329 
1330 /// 1-dimensional data
1331 @safe pure version(mir_test) unittest
1332 {
1333     auto index = [1, 2, 3, 4];
1334     auto data = [2.1, 3.4, 5.6, 7.8];
1335     auto series = index.series(data);
1336     const cseries = series;
1337 
1338     assert(series.contains(2));
1339     assert( ()@trusted{ return (2 in series) is &data[1]; }() );
1340 
1341     assert(!series.contains(5));
1342     assert( ()@trusted{ return (5 in series) is null; }() );
1343 
1344     assert(series.lowerBound(2) == series[0 .. 1]);
1345     assert(series.upperBound(2) == series[2 .. $]);
1346 
1347     assert(cseries.lowerBound(2) == cseries[0 .. 1]);
1348     assert(cseries.upperBound(2) == cseries[2 .. $]);
1349 
1350     // slicing type deduction for const / immutable series
1351     static assert(is(typeof(series[]) ==
1352         Series!(int*, double*)));
1353     static assert(is(typeof(cseries[]) ==
1354         Series!(const(int)*, const(double)*)));
1355     static assert(is(typeof((cast(immutable) series)[]) ==
1356         Series!(immutable(int)*, immutable(double)*)));
1357 
1358     /// slicing
1359     auto seriesSlice  = series[1 .. $ - 1];
1360     assert(seriesSlice.index == index[1 .. $ - 1]);
1361     assert(seriesSlice.data == data[1 .. $ - 1]);
1362     static assert(is(typeof(series) == typeof(seriesSlice)));
1363 
1364     /// indexing
1365     assert(series[1] == observation(2, 3.4));
1366 
1367     /// range primitives
1368     assert(series.length == 4);
1369     assert(series.front == observation(1, 2.1));
1370 
1371     series.popFront;
1372     assert(series.front == observation(2, 3.4));
1373 
1374     series.popBackN(10);
1375     assert(series.empty);
1376 }
1377 
1378 /// 2-dimensional data
1379 @safe pure version(mir_test) unittest
1380 {
1381     import mir.date: Date;
1382     import mir.ndslice.topology: canonical, iota;
1383 
1384     size_t row_length = 5;
1385 
1386     auto index = [
1387         Date(2017, 01, 01),
1388         Date(2017, 02, 01),
1389         Date(2017, 03, 01),
1390         Date(2017, 04, 01)];
1391 
1392     //  1,  2,  3,  4,  5
1393     //  6,  7,  8,  9, 10
1394     // 11, 12, 13, 14, 15
1395     // 16, 17, 18, 19, 20
1396     auto data = iota!int([index.length, row_length], 1);
1397 
1398     // canonical and universal ndslices are more flexible then contiguous
1399     auto series = index.series(data.canonical);
1400 
1401     /// slicing
1402     auto seriesSlice  = series[1 .. $ - 1, 2 .. 4];
1403     assert(seriesSlice.index == index[1 .. $ - 1]);
1404     assert(seriesSlice.data == data[1 .. $ - 1, 2 .. 4]);
1405 
1406     static if (kindOf!(typeof(series.data)) != Contiguous)
1407         static assert(is(typeof(series) == typeof(seriesSlice)));
1408 
1409     /// indexing
1410     assert(series[1, 4] == observation(Date(2017, 02, 01), 10));
1411     assert(series[2] == observation(Date(2017, 03, 01), iota!int([row_length], 11)));
1412 
1413     /// range primitives
1414     assert(series.length == 4);
1415     assert(series.length!1 == 5);
1416 
1417     series.popFront!1;
1418     assert(series.length!1 == 4);
1419 }
1420 
1421 /// Construct from null
1422 @safe pure nothrow @nogc version(mir_test) unittest
1423 {
1424     import mir.series;
1425     alias Map = Series!(string*, double*);
1426     Map a = null;
1427     auto b = Map(null);
1428     assert(a.empty);
1429     assert(b.empty);
1430 
1431     auto fun(Map a = null)
1432     {
1433 
1434     }
1435 }
1436 
1437  version(mir_test)
1438 ///
1439 @safe unittest
1440 {
1441     import mir.series: series, sort;
1442     auto s = ["b", "a"].series([9, 8]).sort;
1443 
1444     import mir.format : text;
1445     assert(s.text == `{ index: [a, b], data: [8, 9] }`);
1446 }
1447 
1448 /++
1449 Convenient function for $(LREF Series) construction.
1450 See_also: $(LREF assocArray)
1451 Attention:
1452     This overloads do not sort the data.
1453     User should call $(LREF directly) if index was not sorted.
1454 +/
1455 auto series(IndexIterator, Iterator, size_t N, SliceKind kind)
1456     (
1457         Slice!IndexIterator index,
1458         Slice!(Iterator, N, kind) data,
1459     )
1460 {
1461     assert(index.length == data.length);
1462     return Series!(IndexIterator, Iterator, N, kind)(index, data);
1463 }
1464 
1465 /// ditto
1466 auto series(Index, Data)(Index[] index, Data[] data)
1467 {
1468     assert(index.length == data.length);
1469     return .series(index.sliced, data.sliced);
1470 }
1471 
1472 /// ditto
1473 auto series(IndexIterator, Data)(Slice!IndexIterator index, Data[] data)
1474 {
1475     assert(index.length == data.length);
1476     return .series(index, data.sliced);
1477 }
1478 
1479 /// ditto
1480 auto series(Index, Iterator, size_t N, SliceKind kind)(Index[] index, Slice!(Iterator, N, kind) data)
1481 {
1482     assert(index.length == data.length);
1483     return .series(index.sliced, data);
1484 }
1485 
1486 /**
1487 Constructs a GC-allocated series from an associative array.
1488 Performs exactly two allocations.
1489 
1490 Params:
1491     aa = associative array or a pointer to associative array
1492 Returns:
1493     sorted GC-allocated series.
1494 See_also: $(LREF assocArray)
1495 */
1496 Series!(K*, V*) series(RK, RV, K = RK, V = RV)(RV[RK] aa)
1497     if (is(typeof(K.init < K.init)) && is(typeof(Unqual!K.init < Unqual!K.init)))
1498 {
1499     import mir.conv: to;
1500     const size_t length = aa.length;
1501     alias R = typeof(return);
1502     if (__ctfe)
1503     {
1504         K[] keys;
1505         V[] values;
1506         foreach(ref kv; aa.byKeyValue)
1507         {
1508             keys ~= kv.key.to!K;
1509             values ~= kv.value.to!V;
1510         }
1511         auto ret = series(keys, values);
1512         .sort((()@trusted=>cast(Series!(Unqual!K*, Unqual!V*))ret)());
1513         static if (is(typeof(ret) == typeof(return)))
1514             return ret;
1515         else
1516             return ()@trusted{ return *cast(R*) &ret; }();
1517     }
1518     import mir.ndslice.allocation: uninitSlice;
1519     Series!(Unqual!K*, Unqual!V*) ret = series(length.uninitSlice!(Unqual!K), length.uninitSlice!(Unqual!V));
1520     auto it = ret;
1521     foreach(ref kv; aa.byKeyValue)
1522     {
1523         import mir.conv: emplaceRef;
1524         emplaceRef!K(it.index.front, kv.key.to!K);
1525         emplaceRef!V(it.data.front, kv.value.to!V);
1526         it.popFront;
1527     }
1528     .sort(ret);
1529     static if (is(typeof(ret) == typeof(return)))
1530         return ret;
1531     else
1532         return ()@trusted{ return *cast(R*) &ret; }();
1533 }
1534 
1535 /// ditto
1536 Series!(RK*, RV*) series(K, V, RK = const K, RV = const V)(const V[K] aa)
1537     if (is(typeof(K.init < K.init)) && is(typeof(Unqual!K.init < Unqual!K.init)))
1538 {
1539     return .series!(K, V, RK, RV)((()@trusted => cast(V[K]) aa)());
1540 }
1541 
1542 /// ditto
1543 Series!(RK*, RV*)  series( K, V, RK = immutable K, RV = immutable V)(immutable V[K] aa)
1544     if (is(typeof(K.init < K.init)) && is(typeof(Unqual!K.init < Unqual!K.init)))
1545 {
1546     return .series!(K, V, RK, RV)((()@trusted => cast(V[K]) aa)());
1547 }
1548 
1549 /// ditto
1550 auto series(K, V)(V[K]* aa)
1551     if (is(typeof(K.init < K.init)) && is(typeof(Unqual!K.init < Unqual!K.init)))
1552 {
1553     return series(*a);
1554 }
1555 
1556 ///
1557 @safe pure nothrow version(mir_test) unittest
1558 {
1559     auto s = [1: 1.5, 3: 3.3, 2: 20.9].series;
1560     assert(s.index == [1, 2, 3]);
1561     assert(s.data == [1.5, 20.9, 3.3]);
1562     assert(s.data[s.findIndex(2)] == 20.9);
1563 }
1564 
1565 pure nothrow version(mir_test) unittest
1566 {
1567     immutable aa = [1: 1.5, 3: 3.3, 2: 2.9];
1568     auto s = aa.series;
1569     s = cast() s;
1570     s = cast(const) s;
1571     s = cast(immutable) s;
1572     s = s;
1573     assert(s.index == [1, 2, 3]);
1574     assert(s.data == [1.5, 2.9, 3.3]);
1575     assert(s.data[s.findIndex(2)] == 2.9);
1576 }
1577 
1578 
1579 /**
1580 Constructs a RC-allocated series from an associative array.
1581 Performs exactly two allocations.
1582 
1583 Params:
1584     aa = associative array or a pointer to associative array
1585 Returns:
1586     sorted RC-allocated series.
1587 See_also: $(LREF assocArray)
1588 */
1589 auto rcseries(RK, RV, K = RK, V = RV)(RV[RK] aa)
1590     if (is(typeof(K.init < K.init)) && is(typeof(Unqual!K.init < Unqual!K.init)))
1591 {
1592     import mir.rc.array;
1593     import mir.conv: to;
1594     alias R = Series!(RCI!K, RCI!V);
1595     const size_t length = aa.length;
1596     auto ret = series(length.mininitRcarray!(Unqual!K).asSlice, length.mininitRcarray!(Unqual!V).asSlice);
1597     auto it = ret.lightScope;
1598     foreach(ref kv; aa.byKeyValue)
1599     {
1600         import mir.conv: emplaceRef;
1601         emplaceRef!K(it.lightScopeIndex.front, kv.key.to!K);
1602         emplaceRef!V(it.data.front, kv.value.to!V);
1603         it.popFront;
1604     }
1605     import core.lifetime: move;
1606     .sort(ret.lightScope);
1607     static if (is(typeof(ret) == R))
1608         return ret;
1609     else
1610         return ()@trusted{ return (*cast(R*) &ret); }();
1611 }
1612 
1613 /// ditto
1614 auto rcseries(K, V, RK = const K, RV = const V)(const V[K] aa)
1615     if (is(typeof(K.init < K.init)) && is(typeof(Unqual!K.init < Unqual!K.init)))
1616 {
1617     return .rcseries!(K, V, RK, RV)((()@trusted => cast(V[K]) aa)());
1618 }
1619 
1620 /// ditto
1621 auto  rcseries( K, V, RK = immutable K, RV = immutable V)(immutable V[K] aa)
1622     if (is(typeof(K.init < K.init)) && is(typeof(Unqual!K.init < Unqual!K.init)))
1623 {
1624     return .rcseries!(K, V, RK, RV)((()@trusted => cast(V[K]) aa)());
1625 }
1626 
1627 /// ditto
1628 auto rcseries(K, V)(V[K]* aa)
1629     if (is(typeof(K.init < K.init)) && is(typeof(Unqual!K.init < Unqual!K.init)))
1630 {
1631     return rcseries(*a);
1632 }
1633 
1634 ///
1635 @safe pure nothrow version(mir_test) unittest
1636 {
1637     auto s = [1: 1.5, 3: 3.3, 2: 20.9].rcseries;
1638     assert(s.index == [1, 2, 3]);
1639     assert(s.data == [1.5, 20.9, 3.3]);
1640     assert(s.data[s.findIndex(2)] == 20.9);
1641 }
1642 
1643 // pure nothrow
1644 version(mir_test) unittest
1645 {
1646     import mir.rc.array;
1647     immutable aa = [1: 1.5, 3: 3.3, 2: 2.9];
1648     auto s = aa.rcseries;
1649     Series!(RCI!(const int), RCI!(const double)) c;
1650     s = cast() s;
1651     c = s;
1652     s = cast(const) s;
1653     s = cast(immutable) s;
1654     s = s;
1655     assert(s.index == [1, 2, 3]);
1656     assert(s.data == [1.5, 2.9, 3.3]);
1657     assert(s.data[s.findIndex(2)] == 2.9);
1658 }
1659 
1660 /++
1661 Constructs a manually allocated series from an associative array.
1662 Performs exactly two allocations.
1663 
1664 Params:
1665     aa == associative array or a pointer to associative array
1666 Returns:
1667     sorted manually allocated series.
1668 +/
1669 Series!(K*, V*) makeSeries(Allocator, K, V)(auto ref Allocator allocator, V[K] aa)
1670     if (is(typeof(K.init < K.init)) && is(typeof(Unqual!K.init < Unqual!K.init)))
1671 {
1672     import mir.ndslice.allocation: makeUninitSlice;
1673     import mir.conv: emplaceRef;
1674 
1675     immutable size_t length = aa.length;
1676 
1677     auto ret = series(
1678         allocator.makeUninitSlice!(Unqual!K)(length),
1679         allocator.makeUninitSlice!(Unqual!V)(length));
1680 
1681     auto it = ret;
1682     foreach(ref kv; aa.byKeyValue)
1683     {
1684         it.index.front.emplaceRef!K(kv.key);
1685         it.data.front.emplaceRef!V(kv.value);
1686         it.popFront;
1687     }
1688 
1689     ret.sort;
1690     static if (is(typeof(ret) == typeof(return)))
1691         return ret;
1692     else
1693         return ()@trusted{ return cast(typeof(return)) ret; }();
1694 }
1695 
1696 /// ditto
1697 Series!(K*, V*) makeSeries(Allocator, K, V)(auto ref Allocator allocator, V[K]* aa)
1698     if (is(typeof(K.init < K.init)) && is(typeof(Unqual!K.init < Unqual!K.init)))
1699 {
1700     return makeSeries(allocator, *a);
1701 }
1702 
1703 ///
1704 pure nothrow version(mir_test) unittest
1705 {
1706     import std.experimental.allocator;
1707     import std.experimental.allocator.building_blocks.region;
1708 
1709     InSituRegion!(1024) allocator;
1710     auto aa = [1: 1.5, 3: 3.3, 2: 2.9];
1711 
1712     auto s = (double[int] aa) @nogc @trusted pure nothrow {
1713         return allocator.makeSeries(aa);
1714     }(aa);
1715 
1716     auto indexArray = s.index.field;
1717     auto dataArray = s.data.field;
1718 
1719     assert(s.index == [1, 2, 3]);
1720     assert(s.data == [1.5, 2.9, 3.3]);
1721     assert(s.data[s.findIndex(2)] == 2.9);
1722 
1723     allocator.dispose(indexArray);
1724     allocator.dispose(dataArray);
1725 }
1726 
1727 /++
1728 Returns a newly allocated associative array from a range of key/value tuples.
1729 
1730 Params:
1731     series = index / time $(LREF Series), may not be sorted
1732 
1733 Returns: A newly allocated associative array out of elements of the input
1734 _series. Returns a null associative
1735 array reference when given an empty _series.
1736 
1737 Duplicates: Associative arrays have unique keys. If r contains duplicate keys,
1738 then the result will contain the value of the last pair for that key in r.
1739 +/
1740 auto assocArray(IndexIterator, Iterator, size_t N, SliceKind kind)
1741     (Series!(IndexIterator, Iterator, N, kind) series)
1742 {
1743     alias SK = series.Index;
1744     alias SV = series.Data;
1745     alias UK = Unqual!SK;
1746     alias UV = Unqual!SV;
1747     static if (isImplicitlyConvertible!(SK, UK))
1748         alias K = UK;
1749     else
1750         alias K = SK;
1751     static if (isImplicitlyConvertible!(SV, UV))
1752         alias V = UV;
1753     else
1754         alias V = SV;
1755     static assert(isMutable!V, "mir.series.assocArray: value type ( " ~ V.stringof ~ " ) must be mutable");
1756 
1757     V[K] aa;
1758     aa.insertOrAssign = series;
1759     return aa;
1760 }
1761 
1762 ///
1763 @safe pure version(mir_test) unittest
1764 {
1765     import mir.ndslice; //iota and etc
1766     import mir.series;
1767 
1768     auto s = ["c", "a", "b"].series(3.iota!int);
1769     assert(s.assocArray == [
1770         "c": 0,
1771         "a": 1,
1772         "b": 2,
1773     ]);
1774 }
1775 
1776 /// Returns: true if `U` is a $(LREF Series);
1777 enum isSeries(U) = is(U : Series!(IndexIterator, Iterator, N, kind), IndexIterator, Iterator, size_t N, SliceKind kind);
1778 
1779 /++
1780 Finds an index such that `series.index[index] == key`.
1781 
1782 Params:
1783     series = series
1784     key = index to find in the series
1785 Returns:
1786     `size_t.max` if the series does not contain the key and appropriate index otherwise.
1787 +/
1788 size_t findIndex(IndexIterator, Iterator, size_t N, SliceKind kind, Index)(Series!(IndexIterator, Iterator, N, kind) series, auto ref scope const Index key)
1789 {
1790     auto idx = series.lightScopeIndex.transitionIndex(key);
1791     if (idx < series.data._lengths[0] && series.index[idx] == key)
1792     {
1793         return idx;
1794     }
1795     return size_t.max;
1796 }
1797 
1798 ///
1799 @safe pure nothrow version(mir_test) unittest
1800 {
1801     auto index = [1, 2, 3, 4].sliced;
1802     auto data = [2.1, 3.4, 5.6, 7.8].sliced;
1803     auto series = index.series(data);
1804 
1805     assert(series.data[series.findIndex(3)] == 5.6);
1806     assert(series.findIndex(0) == size_t.max);
1807 }
1808 
1809 /++
1810 Finds a backward index such that `series.index[$ - backward_index] == key`.
1811 
1812 Params:
1813     series = series
1814     key = index key to find in the series
1815 Returns:
1816     `0` if the series does not contain the key and appropriate backward index otherwise.
1817 +/
1818 size_t find(IndexIterator, Iterator, size_t N, SliceKind kind, Index)(Series!(IndexIterator, Iterator, N, kind) series, auto ref scope const Index key)
1819 {
1820     auto idx = series.lightScopeIndex.transitionIndex(key);
1821     auto bidx = series.data._lengths[0] - idx;
1822     if (bidx && series.index[idx] == key)
1823     {
1824         return bidx;
1825     }
1826     return 0;
1827 }
1828 
1829 ///
1830 @safe pure nothrow version(mir_test) unittest
1831 {
1832     auto index = [1, 2, 3, 4].sliced;
1833     auto data = [2.1, 3.4, 5.6, 7.8].sliced;
1834     auto series = index.series(data);
1835 
1836     if (auto bi = series.find(3))
1837     {
1838         assert(series.data[$ - bi] == 5.6);
1839     }
1840     else
1841     {
1842         assert(0);
1843     }
1844 
1845     assert(series.find(0) == 0);
1846 }
1847 
1848 /++
1849 Iterates union using three functions to handle each intersection case separately.
1850 Params:
1851     lfun = binary function that accepts left side key (and left side value)
1852     cfun = trinary function that accepts left side key, (left side value,) and right side value
1853     rfun = binary function that accepts right side key (and right side value)
1854 +/
1855 template troykaGalop(alias lfun, alias cfun, alias rfun)
1856 {
1857     import mir.primitives: isInputRange;
1858 
1859     /++
1860     Params:
1861         lhs = left hand series
1862         rhs = right hand series
1863     +/
1864     pragma(inline, false)
1865     void troykaGalop(
1866         IndexIterL, IterL, size_t LN, SliceKind lkind,
1867         IndexIterR, IterR, size_t RN, SliceKind rkind,
1868     )(
1869         Series!(IndexIterL, IterL, LN, lkind) lhs,
1870         Series!(IndexIterR, IterR, RN, rkind) rhs,
1871     )
1872     {
1873         if (lhs.empty)
1874             goto R0;
1875         if (rhs.empty)
1876             goto L1;
1877         for(;;)
1878         {
1879             if (lhs.index.front < rhs.index.front)
1880             {
1881                 lfun(lhs.index.front, lhs.data.front);
1882                 lhs.popFront;
1883                 if (lhs.empty)
1884                     goto R1;
1885                 continue;
1886             }
1887             else
1888             if (lhs.index.front > rhs.index.front)
1889             {
1890                 rfun(rhs.index.front, rhs.data.front);
1891                 rhs.popFront;
1892                 if (rhs.empty)
1893                     goto L1;
1894                 continue;
1895             }
1896             else
1897             {
1898                 cfun(lhs.index.front, lhs.data.front, rhs.data.front);
1899                 lhs.popFront;
1900                 rhs.popFront;
1901                 if (rhs.empty)
1902                     goto L0;
1903                 if (lhs.empty)
1904                     goto R1;
1905                 continue;
1906             }
1907         }
1908 
1909     L0:
1910         if (lhs.empty)
1911             return;
1912     L1:
1913         do
1914         {
1915             lfun(lhs.index.front, lhs.data.front);
1916             lhs.popFront;
1917         } while(!lhs.empty);
1918         return;
1919 
1920     R0:
1921         if (rhs.empty)
1922             return;
1923     R1:
1924         do
1925         {
1926             rfun(rhs.index.front, rhs.data.front);
1927             rhs.popFront;
1928         } while(!rhs.empty);
1929         return;
1930     }
1931 
1932     /++
1933     Params:
1934         lhs = left hand input range
1935         rhs = right hand input range
1936     +/
1937     pragma(inline, false)
1938     void troykaGalop (LeftRange, RightRange)(LeftRange lhs, RightRange rhs)
1939         if (isInputRange!LeftRange && isInputRange!RightRange && !isSeries!LeftRange && !isSeries!RightRange)
1940     {
1941         if (lhs.empty)
1942             goto R0;
1943         if (rhs.empty)
1944             goto L1;
1945         for(;;)
1946         {
1947             if (lhs.front < rhs.front)
1948             {
1949                 lfun(lhs.front);
1950                 lhs.popFront;
1951                 if (lhs.empty)
1952                     goto R1;
1953                 continue;
1954             }
1955             else
1956             if (lhs.front > rhs.front)
1957             {
1958                 rfun(rhs.front);
1959                 rhs.popFront;
1960                 if (rhs.empty)
1961                     goto L1;
1962                 continue;
1963             }
1964             else
1965             {
1966                 cfun(lhs.front, rhs.front);
1967                 lhs.popFront;
1968                 rhs.popFront;
1969                 if (rhs.empty)
1970                     goto L0;
1971                 if (lhs.empty)
1972                     goto R1;
1973                 continue;
1974             }
1975         }
1976 
1977     L0:
1978         if (lhs.empty)
1979             return;
1980     L1:
1981         do
1982         {
1983             lfun(lhs.front);
1984             lhs.popFront;
1985         } while(!lhs.empty);
1986         return;
1987 
1988     R0:
1989         if (rhs.empty)
1990             return;
1991     R1:
1992         do
1993         {
1994             rfun(rhs.front);
1995             rhs.popFront;
1996         } while(!rhs.empty);
1997         return;
1998     }
1999 }
2000 
2001 /++
2002 Constructs union using three functions to handle each intersection case separately.
2003 Params:
2004     lfun = binary function that accepts left side key and left side value
2005     cfun = trinary function that accepts left side key, left side value, and right side value
2006     rfun = binary function that accepts right side key and right side value
2007 +/
2008 template troykaSeries(alias lfun, alias cfun, alias rfun)
2009 {
2010     /++
2011     Params:
2012         lhs = left hand series
2013         rhs = right hand series
2014     Returns:
2015         GC-allocated union series with length equal to $(LREF troykaLength)
2016     +/
2017     auto troykaSeries
2018     (
2019         IndexIterL, IterL, size_t LN, SliceKind lkind,
2020         IndexIterR, IterR, size_t RN, SliceKind rkind,
2021     )(
2022         Series!(IndexIterL, IterL, LN, lkind) lhs,
2023         Series!(IndexIterR, IterR, RN, rkind) rhs,
2024     )
2025     {
2026         alias I = CommonType!(typeof(lhs.index.front), typeof(rhs.index.front));
2027         alias E = CommonType!(
2028             typeof(lfun(lhs.index.front, lhs.data.front)),
2029             typeof(cfun(lhs.index.front, lhs.data.front, rhs.data.front)),
2030             typeof(rfun(rhs.index.front, rhs.data.front)),
2031         );
2032         alias R = Series!(I*, E*);
2033         alias UI = Unqual!I;
2034         alias UE = Unqual!E;
2035         const length = troykaLength(lhs.index, rhs.index);
2036         import mir.ndslice.allocation: uninitSlice;
2037         auto index = length.uninitSlice!UI;
2038         auto data = length.uninitSlice!UE;
2039         auto ret = index.series(data);
2040         alias algo = troykaSeriesImpl!(lfun, cfun, rfun);
2041         algo!(I, E)(lhs.lightScope, rhs.lightScope, ret);
2042         return (()@trusted => cast(R) ret)();
2043     }
2044 }
2045 
2046 ///
2047 version(mir_test) unittest
2048 {
2049     import mir.ndslice;
2050     auto a = [1, 2, 3, 9].sliced.series(iota!int([4], 1));
2051     auto b = [0, 2, 4, 9].sliced.series(iota!int([4], 1) * 10.0);
2052     alias unionAlgorithm = troykaSeries!(
2053         (key, left) => left,
2054         (key, left, right) => left + right,
2055         (key, right) => -right,
2056     );
2057     auto c = unionAlgorithm(a, b);
2058     assert(c.index == [0, 1, 2, 3, 4, 9]);
2059     assert(c.data == [-10, 1, 22, 3, -30, 44]);
2060 }
2061 
2062 /++
2063 Constructs union using three functions to handle each intersection case separately.
2064 Params:
2065     lfun = binary function that accepts left side key and left side value
2066     cfun = trinary function that accepts left side key, left side value, and right side value
2067     rfun = binary function that accepts right side key and right side value
2068 +/
2069 template rcTroykaSeries(alias lfun, alias cfun, alias rfun)
2070 {
2071     /++
2072     Params:
2073         lhs = left hand series
2074         rhs = right hand series
2075     Returns:
2076         RC-allocated union series with length equal to $(LREF troykaLength)
2077     +/
2078     auto rcTroykaSeries
2079     (
2080         IndexIterL, IterL, size_t LN, SliceKind lkind,
2081         IndexIterR, IterR, size_t RN, SliceKind rkind,
2082     )(
2083         auto ref Series!(IndexIterL, IterL, LN, lkind) lhs,
2084         auto ref Series!(IndexIterR, IterR, RN, rkind) rhs,
2085     )
2086     {
2087         import mir.rc.array;
2088         alias I = CommonType!(typeof(lhs.index.front), typeof(rhs.index.front));
2089         alias E = CommonType!(
2090             typeof(lfun(lhs.index.front, lhs.data.front)),
2091             typeof(cfun(lhs.index.front, lhs.data.front, rhs.data.front)),
2092             typeof(rfun(rhs.index.front, rhs.data.front)),
2093         );
2094         alias R = Series!(RCI!I, RCI!E);
2095         alias UI = Unqual!I;
2096         alias UE = Unqual!E;
2097         const length = troykaLength(lhs.index, rhs.index);
2098         import mir.ndslice.allocation: uninitSlice;
2099         auto ret = length.mininitRcarray!UI.asSlice.series(length.mininitRcarray!UE.asSlice);
2100         alias algo = troykaSeriesImpl!(lfun, cfun, rfun);
2101         algo!(I, E)(lhs.lightScope, rhs.lightScope, ret.lightScope);
2102         return (()@trusted => *cast(R*) &ret)();
2103     }
2104 }
2105 
2106 ///
2107 version(mir_test) unittest
2108 {
2109     import mir.ndslice;
2110     auto a = [1, 2, 3, 9].sliced.series(iota!int([4], 1));
2111     auto b = [0, 2, 4, 9].sliced.series(iota!int([4], 1) * 10.0);
2112     alias unionAlgorithm = rcTroykaSeries!(
2113         (key, left) => left,
2114         (key, left, right) => left + right,
2115         (key, right) => -right,
2116     );
2117     auto c = unionAlgorithm(a, b);
2118     assert(c.index == [0, 1, 2, 3, 4, 9]);
2119     assert(c.data == [-10, 1, 22, 3, -30, 44]);
2120 }
2121 
2122 
2123 /++
2124 Length for Troyka union handlers.
2125 Params:
2126     lhs = left hand side series/range
2127     rhs = right hand side series/range
2128 Returns: Total count of lambda function calls in $(LREF troykaGalop) union handler.
2129 +/
2130 size_t troykaLength(
2131     IndexIterL, IterL, size_t LN, SliceKind lkind,
2132     IndexIterR, IterR, size_t RN, SliceKind rkind,
2133 )(
2134     Series!(IndexIterL, IterL, LN, lkind) lhs,
2135     Series!(IndexIterR, IterR, RN, rkind) rhs,
2136 )
2137 {
2138     return troykaLength(lhs.index, rhs.index);
2139 }
2140 
2141 /// ditto
2142 size_t troykaLength(LeftRange, RightRange)(LeftRange lhs, RightRange rhs)
2143     if (!isSeries!LeftRange && !isSeries!RightRange)
2144 {
2145     size_t length;
2146     alias counter = (scope auto ref _) => ++length;
2147     alias ccounter = (scope auto ref _l, scope auto ref _r) => ++length;
2148     troykaGalop!(counter, ccounter, counter)(lhs, rhs);
2149     return length;
2150 }
2151 
2152 ///
2153 template troykaSeriesImpl(alias lfun, alias cfun, alias rfun)
2154 {
2155     ///
2156     void troykaSeriesImpl
2157     (
2158         I, E,
2159         IndexIterL, IterL, size_t LN, SliceKind lkind,
2160         IndexIterR, IterR, size_t RN, SliceKind rkind,
2161         UI, UE,
2162     )(
2163         Series!(IndexIterL, IterL, LN, lkind) lhs,
2164         Series!(IndexIterR, IterR, RN, rkind) rhs,
2165         Series!(UI*, UE*) uninitSlice,
2166     )
2167     {
2168         import mir.conv: emplaceRef;
2169         troykaGalop!(
2170             (auto ref key, auto ref value) {
2171                 uninitSlice.index.front.emplaceRef!I(key);
2172                 uninitSlice.data.front.emplaceRef!E(lfun(key, value));
2173                 uninitSlice.popFront;
2174             },
2175             (auto ref key, auto ref lvalue, auto ref rvalue) {
2176                 uninitSlice.index.front.emplaceRef!I(key);
2177                 uninitSlice.data.front.emplaceRef!E(cfun(key, lvalue, rvalue));
2178                 uninitSlice.popFront;
2179             },
2180             (auto ref key, auto ref value) {
2181                 uninitSlice.index.front.emplaceRef!I(key);
2182                 uninitSlice.data.front.emplaceRef!E(rfun(key, value));
2183                 uninitSlice.popFront;
2184             },
2185             )(lhs, rhs);
2186         assert(uninitSlice.length == 0);
2187     }
2188 }
2189 
2190 /**
2191 Merges multiple (time) series into one.
2192 Makes exactly one memory allocation for two series union
2193 and two memory allocation for three and more series union.
2194 
2195 Returns: sorted GC-allocated series.
2196 See_also $(LREF Series.opBinary) $(LREF makeUnionSeries)
2197 */
2198 auto unionSeries(IndexIterator, Iterator, size_t N, SliceKind kind)(
2199     Series!(IndexIterator, Iterator, N, kind) a,
2200     Series!(IndexIterator, Iterator, N, kind) b,
2201     ) @safe
2202 {
2203     import core.lifetime: move;
2204     Series!(IndexIterator, Iterator, N, kind)[2] ar = [move(a), move(b)];
2205     return unionSeriesImplPrivate!false(move(ar));
2206 }
2207 
2208 /// ditto
2209 auto unionSeries(IndexIterator, Iterator, size_t N, SliceKind kind)(
2210     Series!(IndexIterator, Iterator, N, kind) a,
2211     Series!(IndexIterator, Iterator, N, kind) b,
2212     Series!(IndexIterator, Iterator, N, kind) c,
2213     ) @safe
2214 {
2215     import core.lifetime: move;
2216     Series!(IndexIterator, Iterator, N, kind)[3] ar = [move(a), move(b), move(c)];
2217     return unionSeriesImplPrivate!false(move(ar));
2218 }
2219 
2220 /// ditto
2221 auto unionSeries(IndexIterator, Iterator, size_t N, SliceKind kind)(
2222     Series!(IndexIterator, Iterator, N, kind) a,
2223     Series!(IndexIterator, Iterator, N, kind) b,
2224     Series!(IndexIterator, Iterator, N, kind) c,
2225     Series!(IndexIterator, Iterator, N, kind) d,
2226     ) @safe
2227 {
2228     import core.lifetime: move;
2229     Series!(IndexIterator, Iterator, N, kind)[4] ar = [move(a), move(b), move(c), move(d)];
2230     return unionSeriesImplPrivate!false(move(ar));
2231 }
2232 
2233 
2234 ///
2235 @safe pure nothrow version(mir_test) unittest
2236 {
2237     import mir.date: Date;
2238 
2239     //////////////////////////////////////
2240     // Constructs two time-series.
2241     //////////////////////////////////////
2242     auto index0 = [1,3,4];
2243     auto data0 = [1.0, 3, 4];
2244     auto series0 = index0.series(data0);
2245 
2246     auto index1 = [1,2,5];
2247     auto data1 = [10.0, 20, 50];
2248     auto series1 = index1.series(data1);
2249 
2250     //////////////////////////////////////
2251     // Merges multiple series into one.
2252     //////////////////////////////////////
2253     // Order is matter.
2254     // The first slice has higher priority.
2255     auto m0 = unionSeries(series0, series1);
2256     auto m1 = unionSeries(series1, series0);
2257 
2258     assert(m0.index == m1.index);
2259     assert(m0.data == [ 1, 20,  3,  4, 50]);
2260     assert(m1.data == [10, 20,  3,  4, 50]);
2261 }
2262 
2263 ///
2264 @safe pure nothrow version(mir_test) unittest
2265 {
2266     import mir.date: Date;
2267 
2268     //////////////////////////////////////
2269     // Constructs three time-series.
2270     //////////////////////////////////////
2271     auto index0 = [1,3,4];
2272     auto data0 = [1.0, 3, 4];
2273     auto series0 = index0.series(data0);
2274 
2275     auto index1 = [1,2,5];
2276     auto data1 = [10.0, 20, 50];
2277     auto series1 = index1.series(data1);
2278 
2279     auto index2 = [1, 6];
2280     auto data2 = [100.0, 600];
2281     auto series2 = index2.series(data2);
2282 
2283     //////////////////////////////////////
2284     // Merges multiple series into one.
2285     //////////////////////////////////////
2286     // Order is matter.
2287     // The first slice has higher priority.
2288     auto m0 = unionSeries(series0, series1, series2);
2289     auto m1 = unionSeries(series1, series0, series2);
2290     auto m2 = unionSeries(series2, series0, series1);
2291 
2292     assert(m0.index == m1.index);
2293     assert(m0.index == m2.index);
2294     assert(m0.data == [  1, 20,  3,  4, 50, 600]);
2295     assert(m1.data == [ 10, 20,  3,  4, 50, 600]);
2296     assert(m2.data == [100, 20,  3,  4, 50, 600]);
2297 }
2298 
2299 /**
2300 Merges multiple (time) series into one.
2301 
2302 Params:
2303     allocator = memory allocator
2304     seriesTuple = variadic static array of composed of series.
2305 Returns: sorted manually allocated series.
2306 See_also $(LREF unionSeries)
2307 */
2308 auto makeUnionSeries(IndexIterator, Iterator, size_t N, SliceKind kind, size_t C, Allocator)(auto ref Allocator allocator, Series!(IndexIterator, Iterator, N, kind)[C] seriesTuple...)
2309     if (C > 1)
2310 {
2311     return unionSeriesImplPrivate!false(seriesTuple, allocator);
2312 }
2313 
2314 ///
2315 @system pure nothrow version(mir_test) unittest
2316 {
2317     import std.experimental.allocator;
2318     import std.experimental.allocator.building_blocks.region;
2319 
2320     //////////////////////////////////////
2321     // Constructs two time-series.
2322     //////////////////////////////////////
2323     auto index0 = [1,3,4];
2324 
2325     auto data0 = [1.0, 3, 4];
2326     auto series0 = index0.series(data0);
2327 
2328     auto index1 = [1,2,5];
2329 
2330     auto data1 = [10.0, 20, 50];
2331     auto series1 = index1.series(data1);
2332 
2333     //////////////////////////////////////
2334     // Merges multiple series into one.
2335     //////////////////////////////////////
2336 
2337     InSituRegion!(1024) allocator;
2338 
2339     auto m0 = allocator.makeUnionSeries(series0, series1);
2340     auto m1 = allocator.makeUnionSeries(series1, series0); // order is matter
2341 
2342     assert(m0.index == m1.index);
2343     assert(m0.data == [ 1, 20,  3,  4, 50]);
2344     assert(m1.data == [10, 20,  3,  4, 50]);
2345 
2346     /// series should have the same sizes as after allocation
2347     allocator.dispose(m0.index.field);
2348     allocator.dispose(m0.data.field);
2349     allocator.dispose(m1.index.field);
2350     allocator.dispose(m1.data.field);
2351 }
2352 
2353 /**
2354 Merges multiple (time) series into one.
2355 
2356 Returns: sorted manually allocated series.
2357 See_also $(LREF unionSeries)
2358 */
2359 auto rcUnionSeries(IndexIterator, Iterator, size_t N, SliceKind kind)(
2360     Series!(IndexIterator, Iterator, N, kind) a,
2361     Series!(IndexIterator, Iterator, N, kind) b,
2362     ) @safe
2363 {
2364     import core.lifetime: move;
2365     Series!(IndexIterator, Iterator, N, kind)[2] ar = [move(a), move(b)];
2366     return unionSeriesImplPrivate!true(move(ar));
2367 }
2368 
2369 ///ditto
2370 auto rcUnionSeries(IndexIterator, Iterator, size_t N, SliceKind kind)(
2371     Series!(IndexIterator, Iterator, N, kind) a,
2372     Series!(IndexIterator, Iterator, N, kind) b,
2373     Series!(IndexIterator, Iterator, N, kind) c,
2374     ) @safe
2375 {
2376     import core.lifetime: move;
2377     Series!(IndexIterator, Iterator, N, kind)[3] ar = [move(a), move(b), move(c)];
2378     return unionSeriesImplPrivate!true(move(ar));
2379 }
2380 
2381 ///ditto
2382 auto rcUnionSeries(IndexIterator, Iterator, size_t N, SliceKind kind)(
2383     Series!(IndexIterator, Iterator, N, kind) a,
2384     Series!(IndexIterator, Iterator, N, kind) b,
2385     Series!(IndexIterator, Iterator, N, kind) c,
2386     Series!(IndexIterator, Iterator, N, kind) d,
2387     ) @safe
2388 {
2389     import core.lifetime: move;
2390     Series!(IndexIterator, Iterator, N, kind)[4] ar = [move(a), move(b), move(c), move(d)];
2391     return unionSeriesImplPrivate!true(move(ar));
2392 }
2393 
2394 ///
2395 @safe pure nothrow version(mir_test) unittest
2396 {
2397     import mir.rc.array;
2398 
2399     //////////////////////////////////////
2400     // Constructs two time-series.
2401     //////////////////////////////////////
2402     auto index0 = [1,3,4];
2403 
2404     auto data0 = [1.0, 3, 4];
2405     auto series0 = index0.series(data0);
2406 
2407     auto index1 = [1,2,5];
2408 
2409     auto data1 = [10.0, 20, 50];
2410     auto series1 = index1.series(data1);
2411 
2412     //////////////////////////////////////
2413     // Merges multiple series into one.
2414     //////////////////////////////////////
2415 
2416     Series!(RCI!int, RCI!double) m0 = rcUnionSeries(series0, series1);
2417     Series!(RCI!int, RCI!double) m1 = rcUnionSeries(series1, series0); // order is matter
2418 
2419     assert(m0.index == m1.index);
2420     assert(m0.data == [ 1, 20,  3,  4, 50]);
2421     assert(m1.data == [10, 20,  3,  4, 50]);
2422 }
2423 
2424 /**
2425 Initialize preallocated series using union of multiple (time) series.
2426 Doesn't make any allocations.
2427 
2428 Params:
2429     seriesTuple = dynamic array composed of series.
2430     uninitSeries = uninitialized series with exactly required length.
2431 */
2432 pragma(inline, false)
2433 auto unionSeriesImpl(I, E,
2434     IndexIterator, Iterator, size_t N, SliceKind kind, UI, UE)(
2435     scope Series!(IndexIterator, Iterator, N, kind)[] seriesTuple,
2436     Series!(UI*, UE*, N) uninitSeries,
2437     ) @trusted
2438 {
2439     import mir.conv: emplaceRef;
2440     import mir.algorithm.setops: multiwayUnion;
2441 
2442     enum N = N;
2443     alias I = DeepElementType!(typeof(seriesTuple[0].index));
2444     alias E = DeepElementType!(typeof(seriesTuple[0].data));
2445 
2446     if(uninitSeries.length)
2447     {
2448         auto u = seriesTuple.multiwayUnion!"a.index < b.index";
2449         do
2450         {
2451             auto obs = u.front;
2452             emplaceRef!I(uninitSeries.index.front, obs.index);
2453             static if (N == 1)
2454                 emplaceRef!E(uninitSeries.data.front, obs.data);
2455             else
2456                 each!(emplaceRef!E)(uninitSeries.data.front, obs.data);
2457             u.popFront;
2458             uninitSeries.popFront;
2459         }
2460         while(uninitSeries.length);
2461     }
2462 }
2463 
2464 private auto unionSeriesImplPrivate(bool rc, IndexIterator, Iterator, size_t N, SliceKind kind, size_t C, Allocator...)(scope Series!(IndexIterator, Iterator, N, kind)[C] seriesTuple, ref Allocator allocator) @safe
2465     if (C > 1 && Allocator.length <= 1)
2466 {
2467     import mir.algorithm.setops: unionLength;
2468     import mir.ndslice.topology: iota;
2469     import mir.internal.utility: Iota;
2470     import mir.ndslice.allocation: uninitSlice, makeUninitSlice;
2471     static if (rc)
2472         import mir.rc.array;
2473 
2474     Slice!IndexIterator[C] indeces;
2475     foreach (i; Iota!C)
2476         indeces[i] = seriesTuple[i].index;
2477 
2478     immutable len = (()@trusted => indeces[].unionLength)();
2479 
2480     alias I = typeof(seriesTuple[0].index.front);
2481     alias E = typeof(seriesTuple[0].data.front);
2482     static if (rc)
2483         alias R = Series!(RCI!I, RCI!E, N);
2484     else
2485         alias R = Series!(I*, E*, N);
2486     alias UI = Unqual!I;
2487     alias UE = Unqual!E;
2488 
2489     static if (N > 1)
2490     {
2491         auto shape = seriesTuple[0].data._lengths;
2492         shape[0] = len;
2493 
2494         foreach (ref sl; seriesTuple[1 .. $])
2495             foreach (i; Iota!(1, N))
2496                 if (seriesTuple.data[0]._lengths[i] != sl.data._lengths[i])
2497                     assert(0, "shapes mismatch");
2498     }
2499     else
2500     {
2501         alias shape = len;
2502     }
2503 
2504     static if (rc == false)
2505     {
2506         static if (Allocator.length)
2507             auto ret = (()@trusted => allocator[0].makeUninitSlice!UI(len).series(allocator[0].makeUninitSlice!UE(shape)))();
2508         else
2509             auto ret = (()@trusted => len.uninitSlice!UI.series(shape.uninitSlice!UE))();
2510     }
2511     else
2512     {
2513         static if (Allocator.length)
2514             static assert(0, "rcUnionSeries with allocators is not implemented.");
2515         else
2516             auto ret = (()@trusted =>
2517                 len
2518                 .mininitRcarray!UI
2519                 .asSlice
2520                 .series(
2521                     shape
2522                     .iota
2523                     .elementCount
2524                     .mininitRcarray!UE
2525                     .asSlice
2526                     .sliced(shape)))();
2527     }
2528 
2529     static if (C == 2) // fast path
2530     {
2531         alias algo = troykaSeriesImpl!(
2532             ref (scope ref key, scope return ref left) => left,
2533             ref (scope ref key, scope return ref left, scope return ref right) => left,
2534             ref (scope ref key, scope return ref right) => right,
2535         );
2536         algo!(I, E)(seriesTuple[0], seriesTuple[1], ret.lightScope);
2537     }
2538     else
2539     {
2540         unionSeriesImpl!(I, E)((()@trusted => seriesTuple[])(), ret.lightScope);
2541     }
2542 
2543     return () @trusted {return *cast(R*) &ret; }();
2544 }
2545 
2546 /**
2547 Inserts or assigns a series to the associative array `aa`.
2548 Params:
2549     aa = associative array
2550     series = series
2551 Returns:
2552     associative array
2553 */
2554 ref V[K] insertOrAssign(V, K, IndexIterator, Iterator, size_t N, SliceKind kind)(return ref V[K] aa, auto ref Series!(IndexIterator, Iterator, N, kind) series) @property
2555 {
2556     auto s = series.lightScope;
2557     foreach (i; 0 .. s.length)
2558     {
2559         aa[s.index[i]] = s.data[i];
2560     }
2561     return aa;
2562 }
2563 
2564 ///
2565 @safe pure nothrow version(mir_test) unittest
2566 {
2567     auto a = [1: 3.0, 4: 2.0];
2568     auto s = series([1, 2, 3], [10, 20, 30]);
2569     a.insertOrAssign = s;
2570     assert(a.series == series([1, 2, 3, 4], [10.0, 20, 30, 2]));
2571 }
2572 
2573 /**
2574 Inserts a series to the associative array `aa`.
2575 Params:
2576     aa = associative array
2577     series = series
2578 Returns:
2579     associative array
2580 */
2581 ref V[K] insert(V, K, IndexIterator, Iterator, size_t N, SliceKind kind)(return ref V[K] aa, auto ref Series!(IndexIterator, Iterator, N, kind) series) @property
2582 {
2583     auto s = series.lightScope;
2584     foreach (i; 0 .. s.length)
2585     {
2586         if (s.index[i] in aa)
2587             continue;
2588         aa[s.index[i]] = s.data[i];
2589     }
2590     return aa;
2591 }
2592 
2593 ///
2594 @safe pure nothrow version(mir_test) unittest
2595 {
2596     auto a = [1: 3.0, 4: 2.0];
2597     auto s = series([1, 2, 3], [10, 20, 30]);
2598     a.insert = s;
2599     assert(a.series == series([1, 2, 3, 4], [3.0, 20, 30, 2]));
2600 }