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