This package implements a variety of functions for performing operations upon iterables, typically either ranges or ones that are valid as ranges. The majority of functions that are possible to implement as lazy sequences do in fact return ranges.
Please note that fully documenting this package is a work in progress. If a module isn't documented here, comments and unit tests in the module source should hopefully provide sufficient explanation of usage and functionality.
A range must provide these methods and properites:
emptyfrontas a readable property, and optionally as an assignable one.popFront
A bidirectional range is one which implements:
backas a readable property, and optionally as an assignable one.popBack
A random access range is one which implements:
opIndexaccepting a single integer argument.- optionally
opIndexAssignaccepting an element to be assigned in addition to a single integer argument.
A slicing range is one which implements:
opSliceaccepting two integer arguments.
A saving range is one which implements:
save
And some additional properties that are common for ranges to support:
mutablelengthandopDollarremainingremoveFrontremoveBack
All ranges must implement an empty property which returns true when the
range has been fully consumed, and false when there are any elements remaining.
import mach.range.asrange : asrange;
auto range = "hi".asrange;
assert(!range.empty); // Not empty...
range.popFront();
assert(!range.empty); // Still not empty...
range.popFront();
assert(range.empty); // Empty!// A range whose `empty` property is known at compile time to be `false`
// is considered to be an infinite range.
import mach.traits : isInfiniteRange;
import mach.range.rangeof : infrangeof;
auto infrange = infrangeof('x');
static assert(isInfiniteRange!(typeof(infrange)));
static assert(infrange.empty == false); // Value known at compile time// When the `empty` property is known at compile time but is true,
// the range is always and completely empty.
import mach.range.rangeof : emptyrangeof;
auto emptyrange = emptyrangeof!int;
static assert(is(typeof(emptyrange.front) == int));
static assert(emptyrange.empty == true); // Value known at compile timeAll ranges must implement a front property which accesses the element
under the range's front cursor.
import mach.range.rangeof : rangeof;
auto range = rangeof(1, 2, 3);
assert(range.front == 1);Some ranges may allow front to be used in assigments.
When an expression like range.front = value; compiles, and when
range.mutable == true (except for incorrect implementations, the first
condition should always imply the second — though not vice versa),
the range is considered to have a mutable front.
When a range is created from some backing data set, the satisfaction of those two conditions implies that when the value is changed in the range, it will persist when the front is re-accessed, and the change will also be present in that backing data set. (Ranges not created from backing data sets, and so having nowhere to persist information to, are never mutable.)
import mach.traits : isMutableFrontRange;
import mach.range.asrange : asrange;
auto array = [1, 2, 3];
auto range = array.asrange;
// A range created from an array with mutable elements also allows mutation of its elements.
static assert(isMutableFrontRange!(typeof(range)));
assert(range.front == 1);
range.front = 100;
assert(range.front == 100);
assert(array == [100, 2, 3]);Note that, in cases like this, mutating the backing data set will also mutate the range. Depending on the form of mutation, this may result in strange behavior. For practical purposes, one should always assume that modifying a backing data set will invalidate any ranges currently enumerating that data set.
import mach.range.asrange : asrange;
auto array = [1, 2, 3];
auto range = array.asrange;
array.length = 2; // `range` may no longer behave correctly!All ranges must implement a popFront method which consumes the front element,
progressing the front cursor to the next element.
After all the elements in a range have been consumed, e.g. by calls to
popFront, range.empty will be true and any further calls to front or
popFront will be thwarted by errors. (Except for in release mode, in which
case the checks necessary to perform such error reporting may be omitted,
and undefined behavior or nasty crashes will result instead.)
import mach.range.rangeof : rangeof;
auto range = rangeof(1, 2);
assert(range.front == 1); // Check the first element,
range.popFront(); // Consume it.
assert(range.front == 2); // Now check the second element,
range.popFront(); // And consume that one, too.
assert(range.empty);
// Now that the range has been fully consumed...
import mach.test.assertthrows : assertthrows;
assertthrows({
range.front; // Accessing its front produces an error,
});
assertthrows({
range.popFront(); // And so does calling `popFront`.
});Ranges which support both the back property and popBack method are
bidirectional ranges.
Such ranges have not only a front cursor accessed and progressed by front
and popFront but also a back cursor accessed and progressed by their
complementary back and popBack.
import mach.traits : isBidirectionalRange;
import mach.range.asrange : asrange;
// Ranges produced from arrays are always bidirectional.
auto range = [1, 2, 3].asrange;
static assert(isBidirectionalRange!(typeof(range)));
assert(range.back == 3);
range.popBack();
assert(range.back == 2);
range.popBack();
assert(range.back == 1);
range.popBack();
assert(range.empty);
// Like `front` and `popFront`, `back` and `popBack` also fail for
// ranges that have already been fully consumed.
import mach.test.assertthrows : assertthrows;
assertthrows({
range.back;
});
assertthrows({
range.popBack();
});Like front, back may also allow assignment.
import mach.traits : isMutableBackRange;
import mach.range.asrange : asrange;
auto array = [1, 2, 3];
auto range = array.asrange;
static assert(isMutableBackRange!(typeof(range)));
assert(range.back == 3);
range.back = 300;
assert(range.back == 300);
assert(array == [1, 2, 300]);Any bidirectional range allows any combination of calls to front and back,
popFront and popBack over the course of consuming that range.
Though enumerating a range strictly in forward or reverse order is the most
common need, it's important to note that enumeration is not restricted to
that form of usage.
import mach.range.rangeof : rangeof;
auto range = rangeof(1, 2, 3);
assert(range.front == 1);
assert(range.back == 3);
range.popFront();
assert(range.front == 2);
range.popBack();
// Now the front and back cursor both point at the same, single remaining element.
assert(range.back == 2);
range.popFront();
assert(range.empty);Unlike forward ranges (those which do not support back and popBack)
whose elements may be enumerated using foreach but not foreach_reverse,
bidirectional ranges may be operated upon using both loop types.
// The range produced by `rangeof` is bidirectional, and so it works with both...
import mach.range.rangeof : rangeof;
foreach(i; rangeof(1, 2, 3)){} // Forward enumeration via `foreach`,
foreach_reverse(i; rangeof(4, 5, 6)){} // And backwards via `foreach_reverse`.// `recur` produces a forward range, so it works only with `foreach`.
import mach.range.recur : recur;
auto range = recur!(n => n + 1, n => n >= 10)(0);
foreach(i; range){} // Forward enumeration ok!
static assert(!is(typeof({ // Backwards enumeration not ok.
foreach_reverse(i; range){}
})));A range may have a compile-time mutable boolean property.
When range.mutable == true, that range supports some form of mutation of
its contents. (There are several such forms, and they may be present in
any combination — the only guarantee is that at least one of them is valid.)
When range.mutable == false, or when no mutable property is present,
the range supports no mutation of its contents.
The isMutableRange template implemented in mach.traits may be used
to determine whether range.mutable is both present and true.
import mach.traits : isMutableRange;
import mach.range.asrange : asrange;
// A mutable range built from a mutable array:
int[] mutablearray = [1, 2, 3];
auto mutablerange = mutablearray.asrange;
static assert(isMutableRange!(typeof(mutablerange)));
mutablerange.front = 100; // Ok!
// A range of immutable elements built from an array of immutable elements:
const(int)[] immutablearray = [4, 5, 6];
auto immutablerange = immutablearray.asrange;
static assert(!isMutableRange!(typeof(immutablerange)));
static assert(!is(typeof({
immutablerange.front = 200; // Not ok!
})));Many range types support a length property which indicates the total number
of elements that the range will have to consume from when it was initialized
before the range becomes empty.
When a range is enumerating some backing data set, length is typically the
number of elements in that data set.
The length property doesn't change depending on the location of a range's
front or back cursors.
Whenever a range has a length property, its type must be an integer.
Though it should be considered an error for generic code to incorrectly handle
length types other than size_t, the usefulness of using any other type
for a range's length is dubious at best.
import mach.range.asrange : asrange;
auto range = "hello".asrange;
assert(range.length == 5);
foreach(i; 0 .. range.length){
range.popFront();
}
assert(range.empty);
assert(range.length == 5); // Length doesn't change as a result of consumption!Any range with a length property should also override the opDollar operator
and, when implemented, the value of a range's opDollar must always be the
same as its length property.
import mach.range.rangeof : rangeof;
auto range = rangeof(1, 2, 3);
assert(range[$-1] == 3);Many ranges support a remaining property which indicates the total number
of elements that have yet to be consumed before the range is empty.
Like length, remaining must be an integer and should very probably always
be of type size_t.
When the range has just been initialized, range.remaining == range.length.
When the range has been fully consumed, range.remaining == 0.
import mach.range.rangeof : rangeof;
auto range = rangeof(1, 2, 3);
assert(range.remaining == 3);
range.popFront();
assert(range.remaining == 2);
range.popBack();
assert(range.remaining == 1);
range.popFront();
assert(range.remaining == 0);
assert(range.empty);Ranges may support random access reading and/or writing by overloading the
opIndex and opIndexAssign operators.
Though a range may overload these operators with various arguments specific to
that range type, a range is considered to have random access reading when
range[some_non_negative_integer] is valid and is considered to have random
access writing when range[some_non_negative_integer] = value; is valid and
when range.mutable == true. (The first condition should always imply the
second; anything else is an error in the implementation.)
Attempting to read or write a negative index of a random access range should not
be expected to be valid.
Attempting to read or write an index greater than or equal to a random access
range's length (if it has a length property) should not be expected to be
valid.
(Though a range type is not absolutely required to fulfill either of these
expectations.)
import mach.traits : isRandomAccessRange;
import mach.range.rangeof : rangeof;
auto range = rangeof(0, 1, 2);
static assert(isRandomAccessRange!(typeof(range)));
assert(range[0] == 0);
assert(range[1] == 1);
assert(range[2] == 2);
// Like many range types, those produced by `rangeof` throw a `IndexOutOfBoundsError`
// when an index is out of bounds.
import mach.test.assertthrows : assertthrows;
import mach.error : IndexOutOfBoundsError;
assertthrows!IndexOutOfBoundsError({
auto nope = range[3]; // Index out of bounds!
});import mach.traits : isMutableRandomRange;
import mach.range.asrange : asrange;
auto array = [1, 2, 3];
auto range = array.asrange;
static assert(isMutableRandomRange!(typeof(range)));
assert(range[0] == 1);
range[0] = 100;
assert(range[0] == 100);
assert(array == [100, 2, 3]);Ranges may support slicing via the opSlice operator overload.
Attempting to acquire a slice which begins at an index less than 0 or which
ends at an index greater than a range's length should not be expected to be
a valid operation.
(Though a range type is not absolutely required to behave according to this
expectation.)
Most slicing ranges, when sliced, return the same type as the range that was sliced. (This behavior should be maintained everywhere possible.) Some ranges, however, may return a different type when slicing because the operator could not otherwise be supported.
Currently, a range is strictly considered to support slicing (as it pertains
to ranges created from other ranges inheriting such traits, or to templates
such as isSlicingRange evaluating true for a range type) only when the
type returned by slicing is the same as the type that is being sliced.
Whether to extend support for slices of differing types, and how such support
should be implemented, is a design decision that has not yet been made.
Range slicing may or may not assume a more lenient definition in the future.
import mach.traits : isSlicingRange;
import mach.range.asrange : asrange;
import mach.range.compare : equals;
auto range = "hello world".asrange;
static assert(isSlicingRange!(typeof(range)));
assert(range[0 .. 5].equals("hello"));
assert(range[6 .. $].equals("world"));Ranges may provide a save property, which returns a range that occupies the
same state as the saved range, but whose state is not affected by consuming
that saved range. (And similarly, consuming the produced range does not affect
the state of the original, saved range.)
save must always return a value of the same type as the range being saved.
import mach.traits : isSavingRange;
import mach.range.rangeof : rangeof;
auto range = rangeof(1, 2, 3);
static assert(isSavingRange!(typeof(range)));
auto saved = range.save;
assert(range.front == 1);
assert(saved.front == 1);
range.popFront();
assert(range.front == 2);
assert(saved.front == 1);Mutating the contents of a range for which a saved copy has been acquired, or the contents of that saved copy where the original range is still being operated upon, should be considered to invalidate the behavior of the range for which the mutation did not take place.
And as with any mutable ranges with a backing data set, mutating the backing data set may invalidate the original range — as well as any saved copies of that range.
import mach.range.asrange : asrange;
auto range = [1, 2, 3].asrange;
auto saved = range.save;
range.front = 2; // `saved` may no longer behave correctly!
auto savedagain = range.save; // Ok...
savedagain.back = 4; // `range` may no longer behave correctly!Some ranges may support a removeFront and/or a removeBack method;
these would generally be ranges which enumerate the contents of a backing
data set.
Calling these methods consumes the front or back element, respectively,
removes it from the range and the backing data set, and reduces the range's
length and remaining properties (when available) each by 1.
When either of these methods are implemented for a range, range.mutable
must be true.
Additionally, removeBack is only valid for ranges that also support back
and popBack.
import mach.traits : isMutableRemoveFrontRange, isMutableRemoveBackRange;
import mach.range.compare : equals;
import mach.collect : DoublyLinkedList;
auto list = new DoublyLinkedList!int([1, 2, 3]);
auto range = list.values; // Acquire a range for enumerating the list's contents.
static assert(isMutableRemoveFrontRange!(typeof(range)));
static assert(isMutableRemoveBackRange!(typeof(range)));
assert(list.values.equals([1, 2, 3]));
// Remove the front element,
assert(range.front == 1);
range.removeFront();
assert(range.front == 2);
assert(list.values.equals([2, 3]));
// Remove the back element,
assert(range.back == 3);
range.removeBack();
assert(range.back == 2);
assert(list.values.equals([2]));
// And now remove the final remaining element.
range.removeFront();
assert(range.empty);
assert(list.empty);The asarray function can be applied to an iterable to produce a fully
in-memory array of its contents.
When the input is known to be finite, the function can be evaluated with the iterable as the only argument.
Note that when the input is itself an array, the function returns array.dup.
import mach.range.filter : filter;
auto range = [0, 1, 2, 3].filter!(n => n % 2);
auto array = range.asarray;
assert(array == [1, 3]);import mach.range.rangeof : rangeof;
auto array = rangeof(0, 1, 2, 3).asarray;
assert(array == [0, 1, 2, 3]);For known finite inputs, the asarray function can receive an optional
argument indicating maximum length;
any elements in the input past that length will be excluded from the array.
For infinite inputs, the maximum length argument is mandatory.
When the input is itself an array, the function returns slice.dup where
slice is the first so many elements of the array, as determined by the
specified maximum length.
import mach.range.rangeof : rangeof;
auto array = rangeof(0, 1, 2, 3).asarray(2);
assert(array == [0, 1]);import mach.range.repeat : repeat;
auto range = [0, 1, 2].repeat; // Repeat infinitely
auto array = range.asarray(5);
testeq(array, [0, 1, 2, 0, 1]);import mach.range.repeat : repeat;
auto range = [0, 1, 2].repeat; // Repeat infinitely
static assert(!is(typeof(
range.asarray // Fails because a maximum length is not provided.
)));The asarray function is also implemented for associative arrays.
The constructed array is a sequence of key, value pairs represented
by the KeyValuePair type defined in mach.types.keyvaluepair.
auto array = ["hello": "world"].asarray;
assert(array.length == 1);
assert(array[0].key == "hello");
assert(array[0].value == "world");Many iterables, though they are not themselves ranges, should logically be valid as ranges. For example, though an array is not itself a range, a range can be created which enumerates its elements.
The asrange method may be implemented for any type; for example, the
collections in mach.collect typically have an asrange method.
The purpose of this package is to provide default asrange implementations
for primitive types: Specifically, arrays and associative arrays.
asrange is additionally implemented for ranges, in this case the function
simply returns its input.
Functions in mach which require a range to operate upon accept any iterable
valid as a range and internally call asrange with that iterable in order
to acquire a range. It is strongly recommended that code utilizing or extending
this library duplicate this pattern in its own functions operating upon ranges.
// Acquire a range from an array.
auto range = [0, 1, 2, 3].asrange;
assert(range.front == 0);
assert(range.back == 3);// Acquire a range from an associative array.
auto range = ["hello": "world"].asrange;
assert(range.length == 1);
assert(range.front.key == "hello");
assert(range.front.value == "world");
foreach(key, value; range){
// The ranges produced for associative arrays
// can be enumerated like associative arrays.
}/// Acquire a range from a range.
import mach.range.rangeof : rangeof;
auto range = rangeof(0, 1, 2, 3);
assert(range.asrange is range);The asstaticarray function can be used to produce a static array from either
a list of variadic arguments, or from an iterable whose length is known at
compile time.
int[4] array = asstaticarray(0, 1, 2, 3);
assert(array == [0, 1, 2, 3]);import mach.range.rangeof : rangeof;
auto range = rangeof!int(0, 1, 2, 3);
int[4] array = range.asstaticarray!4;
assert(array == [0, 1, 2, 3]);When constructing a static array from an iterable, asstaticarray will produce
an error if the actual length of the input iterable does not match the expected
length.
Except for in release mode, the function throws an AsStaticArrayError by way
of reporting this error. In release mode, the conditionals required to perform
this error reporting are ommitted.
import mach.range.rangeof : rangeof;
import mach.test.assertthrows : assertthrows;
auto range = rangeof!int(0, 1, 2, 3);
assertthrows!AsStaticArrayError({
range.asstaticarray!10; // Fails because of incorrect length.
});The bytecontent function can be used to produce a range which enumerates
the bytes comprising the elements of some input iterable.
The function accepts an optional template argument of the type Endian —
implemented in mach.sys.endian — to indicate byte order of the produced
range.
If the byte order template argument isn't provided, then a little-endian
range is produced by default.
The range produced by bytecontent has elements of type ubyte.
It is bidirectional when the input is bidirectional and has a remaining
property.
It supports length and remaining when the input supports them, as well
as random access, slicing, and saving.
If the input to bytecontent is an iterable with elements that are already
ubytes, it will simply return its input.
If the input is an iterable with elements that are not ubytes, but are the
same size as ubytes, it will return a range which maps the input's elements
to values casted to ubyte.
import mach.range.compare : equals;
ushort[] shorts = [0x1234, 0x5678];
auto range = shorts.bytecontent!(Endian.LittleEndian);
assert(range.equals([0x34, 0x12, 0x78, 0x56]));The module also provides bytecontentle and bytecontentbe functions
for convenience which are equivalent to bytecontent!(Endian.LittleEndian)
and bytecontent!(Endian.BigEndian), respectively.
import mach.range.compare : equals;
ushort[] shorts = [0xabcd, 0x0123];
assert(shorts.bytecontentbe.equals([0xab, 0xcd, 0x01, 0x23])); // Big endian
assert(shorts.bytecontentle.equals([0xcd, 0xab, 0x23, 0x01])); // Little endianThe cache function produces a range which enumerates the elements of its
input, but in such a way that the front or back properties of the input
range (or the range constructed from an input iterable) are called only once
per element.
This may be useful when the front or back methods of an input range are
costly to compute, but can be expected to be accessed more than once.
This benefit would also apply to transient ranges for which, contrary to the
standard, accessing front or back also consumes that element.
import mach.test.assertthrows : assertthrows;
import core.exception : AssertError;
// A range which causes an error when `front`
// is accessed more than once.
struct Test{
enum bool empty = false;
int element = 0;
bool accessed = false;
@property auto front(){
assert(!this.accessed);
this.accessed = true;
return this.element;
}
void popFront(){
this.accessed = false;
}
}
// For example:
assertthrows!AssertError({
Test test;
test.front; // Ok
test.front; // Not ok
});
// But, using `cache`...
auto range = Test(0).cache;
assert(range.front == 0);
assert(range.front == 0); // Repeated access ok!The cartpower function produces a range that is the
n-ary Cartesian product
of an input iterable with itself.
It accepts a single input iterable that is valid as a saving range to perform
the operation upon, and an unsigned integer representing the dimensionality of
the exponentiation as a template argument.
import mach.types : tuple;
import mach.range.compare : equals;
assert(cartpower!2([1, 2, 3]).equals([
tuple(1, 1), tuple(1, 2), tuple(1, 3),
tuple(2, 1), tuple(2, 2), tuple(2, 3),
tuple(3, 1), tuple(3, 2), tuple(3, 3),
]));The function accepts an optional template argument deciding whether outputs
containing the same elements in different orders should be considered
duplicates, and the duplicates omitted.
The two options are CartesianPowerType.Ordered and CartesianPowerType.Unordered.
The chain function serves two similar but differing purposes.
It accepts a sequence of iterables as arguments, producing a range which
enumerates the elements of the inputs in sequence.
Or it accepts an iterable of iterables, producing a range which
enumerates the elements of the input's own elements in sequence.
import mach.range.compare : equals;
// Chain several iterables
assert(chain("hello", " ", "world").equals("hello world"));
// Chain an iterable of iterables
assert(["hello", " ", "world"].chain.equals("hello world"));Though the chain function should in almost all cases be able to discern
intention, the package provides chainiter and chainiters functions when
it becomes necessary to explicitly specify which form of chaining is desired.
import mach.range.compare : equals;
assert(chainiters("hello", " ", "world").equals("hello world"));import mach.range.compare : equals;
assert(chainiter(["hello", " ", "world"]).equals("hello world"));The chunk function returns a range for enumerating sequential chunks of an
input, its first argument being the input iterable and its second argument being
the size of the chunks to produce.
If the iterable is not evenly divisible by the given chunk size, then the
final element of the chunk range will be shorter than the given size.
The input iterable must support slicing and have numeric length. The outputted
range is bidirectional, has length and remaining properties, allows random
access and slicing operations, and can be saved.
auto range = "abc123xyz!!".chunk(3);
assert(range.length == 4);
assert(range[0] == "abc");
assert(range[1] == "123");
assert(range[2] == "xyz");
// Final chunk is shorter because the input wasn't evenly divisble by 3.
assert(range[3] == "!!");The headis and tailis functions can be used to compare the leading or
trailing elements of one iterable to another, respectively.
The first argument to either function is an iterable to be searched in,
and the second argument is the subject to be searched for. In the case of
headis, the leading elements of the first iterable must be equal to all the
elements of the second. In the case of tailis, the trailing elements of
the first iterable must be equal to all the elements of the second.
Note that attempting to call either function with two infinite iterables will result in a compile error.
assert("hello world".headis("hello"));
assert("hello world".tailis("world"));// An iterable always begins with an empty one.
assert("greetings".headis(""));
assert("salutations".tailis(""));// A finite iterable never begins or ends with an infinite one.
import mach.range.rangeof : infrangeof;
assert(!"yo".headis(infrangeof('k')));
assert(!"hi".tailis(infrangeof('k')));Both functions optionally accept a comparison function as a template argument. By default, the comparison between elements of the inputs is simple equality.
import mach.text.ascii : tolower;
alias compare = (a, b) => (a.tolower == b.tolower);
assert("Hello World".headis!compare("HELLO"));The consume function consumes an input iterable.
This is primarily useful for ranges which may modify state while they are
being enumerated.
For example, the tap function adds a callback for each element of an input
iterable. The callback is only evaluated as that element is popped from the
range.
import mach.range.tap : tap;
int count = 0;
// Increment `count` every time an element is popped.
auto range = [0, 1, 2, 3].tap!((e){count++;});
assert(count == 0);
range.consume; // Consume the range
assert(count == 4);The module also provides a consumereverse function for performing the same
consumption operation, but in reverse.
import mach.range.tap : tap;
string str = "";
auto range = "forwards".tap!((ch){str ~= ch;});
assert(str == "");
range.consumereverse;
assert(str == "sdrawrof");This module implements a contains function capable of searching an input
iterable for an element satisfying a predicate, for an element equal to an
input, or for a substring.
// Search for an equivalent element.
assert("hello".contains('h'));
assert(!"hello".contains('x'));// Search for an element satisfying a predicate.
import mach.text.ascii : isupper;
assert("upper CASE".contains!isupper);
assert(!"lower case".contains!isupper);// Search for a substring.
assert("hello world".contains("hello"));
assert("hello world".contains("world"));
assert(!"hello world".contains("nope"));// Search for a case-insensitive substring.
import mach.text.ascii : tolower;
alias compare = (a, b) => (a.tolower == b.tolower);
assert("Hello World".contains!compare("HELLO"));
assert(!"Hello World".contains!compare("Nope"));The count function can be used to determine the number of elements in an
iterable which satisfy a predicate function.
Alternatively, it can be passed a value instead of a predicate and the function
counts the number of elements in the input which are equal to that value.
// Count the number of even numbers.
assert([0, 1, 2, 3, 4].count!(n => n % 2 == 0) == 3);
// Count the number of occurrences of the character 'l'.
assert("hello".count('l') == 2);Though the return value of count may be treated like a number, it in fact
is not. The produced type supports exactly, atleast, atmost,
morethan, and lessthan methods for performing optimized comparisons upon
the number of elements. They are more efficient than acquiring the total count
and then comparing because they are able to short-circuit when the condition
has been met, or has ceased to be met.
Due to a limitation of the language at the time of writing, the comparison
operator overloads implemented by this type are not able to use the optimized
methods. So while input.count > n may be supported, input.count.morethan(n)
will perform more efficiently.
Note that the equality == and inequality != operators suffer no such
disadvantage.
assert("greetings".count('e').atleast(2));
assert("how are you?".count('?').lessthan(5));
assert("I'm well thank you".count('k').atmost(1));
assert("ok".count('o').morethan(0));// Comparison operators are supported, but are not efficiently implemented.
assert([0, 1, 2, 3, 4].count!(n => n > 0) > 2);
assert([5, 6, 7].count!(n => n == 5) <= 1);When necessary, the actual number of matching elements may be accessed using
the total property of a value returned by count.
This module implements a distinct function, which enumerates only the unique
elements of an input iterable.
Uniqueness is dictated by keys, which must be hashable. A transformation function for acquiring these keys can optionally be provided as a template argument; by default the elements are themselves used as the keys.
assert("hello world".distinct.equals("helo wrd"));// Use each element itself as the uniqueness key.
assert([2, 1, 2, 11, 10, 12].distinct.equals([2, 1, 11, 10, 12]));
// Use (element % 10) as the uniqueness key.
assert([2, 1, 2, 11, 10, 12].distinct!(e => e % 10).equals([2, 1, 10]));The each function eagerly evaluates a function for every element in
an iterable. The function to be applied is passed as a template argument, and
it must accept a single element from the iterable as its input.
Its lazily-evaluated equivalent is tap, defined in mach.range.tap.
string hello = "";
"hello".each!(e => hello ~= e);
assert(hello == "hello");The module also implements an eachreverse function, which operates the same
as each, except elements are evaluated in reverse order.
string greetings = "";
"sgniteerg".eachreverse!(e => greetings ~= e);
assert(greetings == "greetings");A notable difference between ranges and other iterables in mach is that while
they both indicate the number of elements they contain with the length
property, for a partially-consumed range that property no longer represents
the number of elements that should be expected to be handled when, from that
state, enumerating the range. To get the number of elements that enumerating
a range in its present state would result in, the remaining property is used.
The elementcount function is intended as a way to get the number of elements
that iteration would turn up, e.g. via foreach, given the current state of
the input. For ranges the function returns the remaining property and for
other types it returns the length property.
The function isn't valid for ranges that don't have a numeric remaining
property or for other types that don't have a numeric length.
assert("hello".elementcount == 5);
assert([0, 1, 2, 3].elementcount == 4);import mach.range.rangeof : rangeof;
auto range = rangeof(0, 1, 2, 3);
assert(range.elementcount == 4);
range.popFront();
assert(range.elementcount == 3); // Correct even for partially-consumed ranges.This module implements head and tail functions, which produce ranges for
enumerating the front or back elements of a range, up to a specified limit.
The length of a range returned by head or tail is either the limit passed
to the function or the length of its input, whichever is shorter.
The head function will work for any input iterable valid as a range.
tail will work only for inputs with random access and known numeric length.
import mach.range.compare : equals;
assert("hello world".head(5).equals("hello"));
assert("hello world".tail(5).equals("world"));import mach.range.compare : equals;
assert([0, 1, 2].head(10).equals([0, 1, 2]));This module implements an enumerate function, which returns a range whose
elements are those of its source iterable, wrapped in a type that includes,
in addition to an element of the source iterable, a zero-based index
representing the location of that element within the iterable.
The elements of the range returned by enumerate behave like tuples.
auto array = ["hi", "how", "are", "you"];
foreach(index, element; array.enumerate){
assert(element == array[index]);
}auto range = ["zero", "one", "two"].enumerate;
assert(range.front.index == 0);
assert(range.front.value == "zero");When the input iterable is bidirectional, so is the range outputted by
enumerate. The range provides length and remaining properties when
the input does, and propagates infiniteness. It supports random access and
slicing operators when the input iterable supports them, as well as
removal of elements.
The enumerate range allows mutation of its front and back elements, as well
as random access writing, using the element type of its input iterable.
They cannot be assigned using the element type of the range itself.
auto array = [0, 1, 2, 3];
auto range = array.enumerate;
// Values can be reassigned using the element type of the input iterable.
range.front = 10;
assert(range.front.value == 10);
range.back = 20;
assert(range.back.value == 20);
// But not using the element type of the `enumerate` range.
static assert(!is(typeof({
range.front = range.back;
})));The fill function is an abstraction of the mutate function in
mach.range.mutate which assigns every element in a mutable input iterable
to a given value.
int[] array = [0, 1, 2, 3];
array.fill(10);
assert(array == [10, 10, 10, 10]);This module implements the filter higher-order function for iterable inputs.
The filter function produces a range enumerating only those elements of an
input iterable which satisfy its predicate.
The predicate is passed as a template argument, and the input iterable can be
anything that is valid as a range.
The range returned by filter supports bidirectionality, saving, removal,
and mutation when the input range supports them. Infiniteness is similarly
propagated.
The range does not provide length or remaining properties, as the only way
to determine those values in advance is to traverse the outputted sequence.
To acquire these properties, or to get a slice or element at an index, the
walklength, walkindex, and walkslice functions in mach.range.walk may
be used.
import mach.range.compare : equals;
auto range = [0, 1, 2, 3, 4].filter!(n => n % 2 == 0);
assert(range.equals([0, 2, 4]));import mach.range.compare : equals;
auto range = "h e l l o".filter!(ch => ch != ' ');
assert(range.equals("hello"));When filter does not receive an explicit predicate argument, it enumerates
only the truthy values of the input iterable.
import mach.range.compare : equals;
auto range = [1, 0, 2, 3, 0];
assert(range.filter().equals([1, 2, 3]));This module implements first and last functions for finding the first or
the last element of some iterable meeting a predicate function.
The predicate function is passed as a template argument for first and last
or, if the argument is omitted, a default function matching any element is
used.
(In this case, the functions simply retrieve the first or last element in the
iterable.)
Note that while last will work for any iterable, the implementation is
necessarily inefficient for inputs that are not bidirectional.
assert([0, 1, 2].first == 0);
assert([0, 1, 2].first!(n => n % 2) == 1);assert([0, 1, 2].last == 2);
assert([0, 1, 2].last!(n => n % 2) == 1);first and last can optionally be called with a fallback value to be returned
when no element of the input satisfies the predicate function, or when the
input iterable is empty.
assert([0, 1, 2].first!(n => n > 10)(-1) == -1);
assert([0, 1, 2].last!(n => n > 10)(-1) == -1);auto empty = new int[0];
assert(empty.first(1) == 1);
assert(empty.last(1) == 1);If in these cases a fallback is not provided, an error is produced:
first throws a NoFirstElementError and last a NoLastElementError.
import mach.test.assertthrows : assertthrows;
assertthrows!NoFirstElementError({
[0, 1, 2].first!(n => n > 10); // No elements satisfy the predicate.
});Where an iterable is an iterable of iterables, the flatten function
sequentially enumerates the elements of each iterable within that input
iterable, down to the lowest level of nesting. When the input is an iterable
whose elements are not themselves iterables, flatten simply returns its input.
The effect of this is that the elements of an output produced by flatten will
never be iterables.
Except for the case where flatten returns its input, the function returns
a range, lazily enumerating the flattened contents.
// Flatten an array of arrays of ints, producing a sequence of ints.
import mach.range.compare : equals;
int[][] array = [[0, 1, 2], [3], [4, 5]];
assert(array.flatten.equals([0, 1, 2, 3, 4, 5]));// Flatten an array of strings, producing a string.
import mach.range.compare : equals;
assert(["hello", " ", "world"].flatten.equals("hello world"));// Flatten an array of arrays of arrays of ints, producing a sequence of ints.
import mach.range.compare : equals;
int[][][] array = [[[0, 1], [2]], [[3], [], [4, 5]]];
assert(array.flatten.equals([0, 1, 2, 3, 4, 5]));The join function accepts an iterable of iterables, then enumerates the
contents of those nested iterables in sequence, interrupting the output
with a separator whenever a nested iterable ends or begins.
The first argument accepted by join is the iterable of iterables to be
joined, and the second argument is the separator to insert between its
constituent iterables. The separator may either be an iterable with the
same element type as the iterables in the input iterable, or may be an
element of the type that such an iterable would possess.
join is most commonly used to manipulate strings; when using it for this
purpose mind that the outputted iterable is lazily-evaluated, and a function
like asarray from mach.range.asarray will be needed to create an in-memory
string from the range.
Its complement is split, in mach.range.split.
// Join on a separator that is an element of the constituent iterables.
import mach.range.compare : equals;
assert(["hello", "how", "are", "you"].join(' ').equals("hello how are you"));// Join on a separator that is also an iterable.
import mach.range.compare : equals;
assert(["100", "200", "300"].join("___").equals("100___200___300"));Though the default behavior is to insert the separator only in between
elements, and not at the beginning or the end of the range, the join
function accepts optional template arguments which may alter this behavior.
import mach.range.compare : equals;
// Include the separator at the front of the output.
assert(["abc", "xyz"].join!(true, false)('.').equals(".abc.xyz"));
// Include the separator at the back of the output.
assert(["abc", "xyz"].join!(false, true)('.').equals("abc.xyz."));
// Include the separator at the front and back of the output.
assert(["abc", "xyz"].join!(true, true)('.').equals(".abc.xyz."));The lexorder function implements a generalized
lexicographical ordering
algorithm that, given any two input iterables, will return a value indicating
the correct ordering.
assert(lexorder("hello", "hello") == 0); // Both inputs are equal
assert(lexorder("apple", "zed") == -1); // "apple" precedes "zed"
assert(lexorder("watch", "bear") == +1); // "watch" follows "bear"assert(lexorder([3, 2, 1], [3, 2, 1]) == 0);
assert(lexorder([3, 2, 1], [3, 2, 2]) == -1);
assert(lexorder([3, 2, 1], [3, 2, 0]) == +1);
assert(lexorder([3, 2, 1, 0], [3, 2, 1]) == +1);lexorder optionally accepts an ordering function that is applied to each
pair of correlating elements. It should return 0 when the elements are equal,
-1 when the first element precedes the second, and +1 when the first element
follows the second.
import mach.text.ascii;
alias order = (a, b){ // Case-insensitive ASCII character comparison
if(a.tolower() > b.tolower()) return 1;
else if(a.tolower() < b.tolower()) return -1;
return 0;
};
assert(lexorder!order("HELLO", "hello") == 0);
assert(lexorder!order("apple", "Zed") == -1);This module provides any, all, and none functions which operate based on
whether elements in an input iterable satisfy a predicate.
any returns true when at least one element satisfies the predicate.
all returns true when no element fails to satisfy the predicate.
none returns true when no element satisfies the predicate.
// Any element is greater than 1?
assert([0, 1, 2, 3].any!(n => n > 1));
// All elements are greater than or equal to 10?
assert([10, 11, 12, 13].all!(n => n >= 10));
// No elements are evenly divisible by 7?
assert([5, 10, 15, 20].none!(n => n % 7 == 0));The predicate can be passed to these functions as a template argument. When no predicate is passed, the default predicate evaluates truthiness or falsiness of the elements themselves.
assert([false, false, true].any);
assert([true, true, true].all);
assert([false, false, false].none);This package implements the map higher-order function for input iterables.
The map function creates a range for which each element is the result of a
transformation applied to the corresponding elements of the input iterables,
where the transformation function is passed as a template argument.
map comes in both singular and plural varieties. Singular map represents
the higher-order map function in its common form, where the transformation
operates upon the elements of a single input iterable.
import mach.range.compare : equals;
auto squares = [1, 2, 3, 4].map!(n => n * n);
assert(squares.equals([1, 4, 9, 16]));Plural map is an expansion upon that singular form, in that it accepts
multiple input iterables which are enumerated simultaneously; their
corresponding elements are passed collectively to a transformation function.
The length of a plural map function is equal to the length of its shortest
input. If all of the inputs are infinite, then so is the range produced
by map.
import mach.range.compare : equals;
auto intsa = [1, 2, 3, 4];
auto intsb = [3, 5, 7, 9];
// The transformation function must accept the same number of
// elements as there are input iterables, in this case two.
auto sums = map!((a, b) => a + b)(intsa, intsb);
// Output is a sequences of sums of elements of the input.
assert(sums.equals([4, 7, 10, 13]));This plural map operation may be more commonly expressed as a combination of
zip and singular map functions, for example:
auto sums = zip(intsa, intsb).map!(tup => tup[0] + tup[1]);
Notably, the zip function implemented in mach.range.zip is in fact a
very simple abstraction of the plural map function.
import mach.types.tuple : tuple;
// The `zip` function in `mach.range.zip` performs this same operation.
auto zipped = map!tuple([0, 1, 2], [3, 4, 5]);
assert(zipped.front == tuple(0, 3));Neither singular nor plural map ranges allow mutation of their elements.
The singular map function provides length and remaining properties when
its input iterable does. The plural map function provides these properties
in the case that all inputs either support the corresponding property, or are
of known infinite length.
The singular map function supports bidirectionality when its input does.
The plural map function supports bidirectionality only when all inputs
are finite, are bidirectional, and have a valid remaining property.
Please note that bidirectionality for plural ranges requires a potentially nontrivial amount of overhead to account for the case where its inputs are of varying lengths.
import mach.meta.varreduce : varmax;
auto intsa = [1, 2, 3, 4];
auto intsb = [5, 0, 4, 0, 3, 0];
auto intsc = [3, 2, 1, 1, 2];
auto range = map!varmax(intsa, intsb, intsc);
// Length is that of the shortest input.
assert(range.length == intsa.length);
// Greatest of elements [1, 5, 3]
assert(range.front == 5);
// Greatest of elements [4, 0, 1]
assert(range.back == 4);The mutate function is similar to map, but the values produced by the
transformation function are persisted to the underlying data.
Note that in order to acquire elements in the range, the transformation
function is called and its output returned. The element in the
underlying data is only actually mutated upon popping, however.
The range's nextfront and nextback methods can be called to acquire
and mutate the front or back elements without calling the transformation
twice, which is what will happen when separately calling range.front
and range.popFront.
The range produced by mutate supports length and remaining when the input
does, and infiniteness is propagated. It supports bidirectionality and random
access when the input does.
The range cannot be saved or sliced, on the basis that such operations are
likely to produce unsafe behavior by calling the transformation function
for and accordingly mutating an element more than once.
import mach.range.consume : consume;
int[] array = [0, 1, 2, 3];
array.mutate!(n => n + 1).consume;
assert(array == [1, 2, 3, 4]);int[] array = [5, 6, 7, 8];
auto range = array.mutate!(n => n * 2);
// The array is not actually modified when accessing elements.
assert(range.front == 10);
assert(array[0] == 5);
assert(range.back == 16);
assert(array[$-1] == 8);
assert(range[1] == 12);
assert(array[1] == 6);
// The elements are modified when popping.
range.popFront();
assert(array == [10, 6, 7, 8]);
range.popBack();
assert(array == [10, 6, 7, 16]);This module provides, very simply, methods for simultaneously retrieving the
front or back element of a range and popping that element, in the form of
nextfront and nextback.
The nextfront method can additionally be referenced by the name next.
import mach.range.rangeof : rangeof;
auto range = rangeof(0, 1, 2);
assert(range.next == 0);
assert(range.next == 1);
assert(range.next == 2);
assert(range.empty);import mach.range.rangeof : rangeof;
auto range = rangeof(5, 6, 7);
assert(range.nextback == 7);
assert(range.nextback == 6);
assert(range.nextback == 5);
assert(range.empty);The ngrams function can be used to generate n-grams given an input iterable.
The length of each n-gram is given as a template argument: Calling ngrams!2
enumerates bigrams, ngrams!3 enumerates trigrams, and so on.
The elements of a range produced by ngrams are static arrays containing
a number of elements equal to the count specified using the function's
template argument.
assert("hello".ngrams!2.equals(["he", "el", "ll" ,"lo"]));One of the more practical uses for this function is to generate n-grams from a sequence of words.
import mach.range.split : split;
auto text = "hello how are you";
auto words = text.split(' ');
auto bigrams = words.ngrams!2;
assert(bigrams.equals([["hello", "how"], ["how", "are"], ["are", "you"]]));When the input passed to ngrams provides length and remaining properties,
so does the outputted range. Infiniteness is similarly propagated.
When the input supports random access and slicing operations, so does the output.
auto range = [0, 1, 2, 3].ngrams!2;
assert(range.length == 3);
assert(range[0] == [0, 1]);This module implements the pad function and its derivatives, which produce
a range enumerating the contents of an input iterable, with additional elements
added to its front or back.
The padfront and padback functions can be used to perform the common
string manipulation of adding elements to the front or back such that the
total length of the output is at most or at minimum a given length.
import mach.range.compare : equals;
assert("123".padfront('0', 6).equals("000123"));
assert("345".padback('0', 6).equals("345000"));Since these functions generate lazy sequences, functions like string
concatenation will require using a function like asarray in order to
create an in-memory array from the padded output.
import mach.range.asarray : asarray;
auto text = "hello" ~ "world".padfront('_', 7).asarray;
assert(text == "hello__world");The pad function provides several overloads for producing an output
which enumerates the input with a given number of elements appended or
prepended to the input.
// Pad with two underscores at the front and the back.
assert("hi".pad('_', 2).equals("__hi__"));
// Pad with one underscore at the front and three at the back.
assert("yo".pad('_', 1, 3).equals("_yo___"));
// Pad with two underscores at the front and three bangs at the back.
assert("bro".pad('_', 2, '!', 3).equals("__bro!!!"));This module implements pluck, which is a simple abstraction of the map
function in mach.range.map.
pluck can be called with a template argument indicating a property to be
extracted from each element of an input iterable, or with runtime arguments
used to index the elements of the input iterable.
import mach.range.compare : equals;
struct Test{int x; int y; int z;}
// Equivalent to `input.map!(e => e.x)`.
auto range = [Test(0, 1, 2), Test(2, 3, 4)].pluck!`x`;
assert(range.equals([0, 2]));import mach.range.compare : equals;
string[] array = ["abc", "xyz", "123"];
// Equivalent to `input.map!(e => e[0])`.
assert(array.pluck(0).equals("ax1"));The recur function can be used to generate a range from repeatedly applying
a function to its input, either infinitely or until the output satisfies a
predicate. When such a predicate is provided, recur allows specifying whether
that final, matching element should be included in the outputted range via
a template argument. Alternately, the recuri function can be used when the
matching element should be included in the output.
import mach.range.compareends : headis;
auto range = 0.recur!(n => n + 1); // Repeatedly increment, starting at 0.
assert(range.headis([0, 1, 2, 3, 4, 5]));import mach.range.compare : equals;
// Repeat until 4.
auto exclusive = 0.recur!(n => n + 1, n => n >= 4);
assert(exclusive.equals([0, 1, 2, 3]));
// Repeat until and including 4.
auto inclusive = 0.recuri!(n => n + 1, n => n >= 4);
assert(inclusive.equals([0, 1, 2, 3, 4]));This module implements the reduce higher-order function for iterable inputs.
The reduce function accepts an accumulation function as its template argument,
and applies that function sequentially to the elements of an input iterable.
The function optionally accepts a seed, which sets the initial value of the
accumulator.
This module implements both lazyreduce and eagerreduce; the reduce symbol
aliases eagerreduce because it is by far the more common application of the
function.
An accumulation function must accept two arguments. The first is an accumulator,
which is either explicitly seeded or taken from the first element of the input.
The first is an element from the input. The function must return a new value
for the accumulator. The reduce function operates by repeatedly applying this
function, sequentially updating the accumulator value with the elements of the
input.
For example, a simple sum function can be implemented using reduce.
alias sum = (acc, next) => (acc + next);
assert([1, 2, 3, 4].reduce!sum(10) == 20); // With seedalias sum = (acc, next) => (acc + next);
assert([5, 6, 7].reduce!sum == 18); // No seedBoth lazy and eager reduce functions will produce an error if the function
is not seeded with an initial value, and the input is empty.
In this case a ReduceEmptyError is thrown, except for code compiled in
release mode, for which this check is ommitted.
import mach.test.assertthrows : assertthrows;
alias sum = (acc, next) => (acc + next);
assertthrows!ReduceEmptyError({
new int[0].reduceeager!sum;
});
assertthrows!ReduceEmptyError({
new int[0].reducelazy!sum;
});The repeat function can be used to repeat an inputted iterable either
infinitely or a specified number of times, depending on whether a limit is
passed to it.
In order for an iterable to repeated either finitely or infinitely, it must be infinite, have random access and length, or be valid as a saving range. Repeating an already-infinite iterable returns the selfsame iterable, regardless of whether the repeating was intended to be finite or infinite.
When repeat is called for an iterable without any additional arguments,
that iterable is repeated infinitely.
import mach.range.compareends : headis;
auto range = "hi".repeat;
assert(range.headis("hihihihihi"));// Fun fact: Infinitely repeated ranges are technically bidirectional.
assert("NICE".repeat.back == 'E');When the function is called with an additional integer argument, that argument dictates how many times the input will be repeated before the resulting range is exhausted.
Finitely repeated ranges support length and remaining properties when their
inputs support them. They allow random access when the input has random access
and numeric length. All finitely and infinitely repeated ranges allow saving.
import mach.range.compare : equals;
auto range = "yo".repeat(3);
assert(range.equals("yoyoyo"));The repeat function does not support infinitely repeating an empty iterable,
because attempting to do so would invalidate many of the compile-time checks
made possible by assuming the result of infinitely repeating an input is in
fact an infinite range.
An InfiniteRepeatEmptyError is thrown when this operation is attempted,
unless the code has been compiled in release mode, in which case the check is
omitted and a nastier error may occur instead.
import mach.test.assertthrows : assertthrows;
assertthrows!InfiniteRepeatEmptyError({
"".repeat; // Can't infinitely repeat an empty input.
});The retro function returns a range which enumerates the elements of its input
in reverse order. Its input must be an iterable valid as a bidirectional range.
When the input iterable provides length and remaining properties, so does
the output of retro. Infiniteness is similarly propagated. Saving, slicing,
random access reading and writing, front and back mutability, and element
removal are supported when the inputted iterable supports them.
import mach.range.compare : equals;
assert("hello".retro.equals("olleh"));The rotate function accepts any number of input iterables, where all of
the iterables are valid as ranges which allow modification of their front
element.
Up to the length of its shortest input, rotate will eagerly rotate the
positions of elements in the iterables such that —
in the case of three inputs, for example —
the elements of the first input are placed into the second,
the elements of the second input are placed into the third,
and the elements of the third input are placed into the first.
int[] a = [0, 1, 2];
int[] b = [3, 4, 5];
int[] c = [6, 7, 8];
rotate(a, b, c);
assert(a == [6, 7, 8]); // Previously the elements of c.
assert(b == [0, 1, 2]); // Previously the elements of a.
assert(c == [3, 4, 5]); // Previously the elements of b.// Rotation is performed only up to the length of the shortest input.
int[] a = [0, 1, 2, 3];
int[] b = [10, 11];
rotate(a, b);
assert(a == [10, 11, 2, 3]);
assert(b == [0, 1]);In the case of rotate receiving one or fewer inputs, no operation is actually
performed.
int[] a = [0, 1, 2, 3];
rotate(a); // Does nothing.rotate(); // Valid, but also does nothing.The split returns a range enumerating portions of an input iterable as
delimited by a separator, where the separator can either be an element,
an element predicate, or a substring.
It is a complement to the join function in mach.range.join.
split is most commonly useful as a string manipulation function; note that
when using it this way, because its output is a range, a function such as
asarray in mach.range.asarray may be required so that the range's contents
can be placed into an in-memory array.
import mach.range.compare : equals;
// Split on occurrences of an element.
assert("hello world".split(' ').equals(["hello", "world"]));
// Split on elements meeting a predicate.
assert("1.2,3.4".split!(ch => ch == '.' || ch == ',').equals(["1", "2", "3", "4"]));// Split on occurrences of a substring.
import mach.range.compare : equals;
assert("1, 2, 3".split(", ").equals(["1", "2", "3"]));// Split on occurrences of a substring, given a comparison function.
import mach.range.compare : equals;
import mach.text.ascii : tolower;
// Case-insensitive character comparison
alias compare = (a, b) => (a.tolower == b.tolower);
assert("123and456AND789".split!compare("and").equals(["123", "456", "789"]));The striphead and striptail functions can be used to get a range from
an input which, if it begins or ends with a subject, enumerates only those
elements following or preceding that subject.
striphead accepts two inputs valid as a range. If the first input begins
with the second, as determined by a given comparison function, then the
range that striphead returns enumerates the elements following those of
the subject. Otherwise, the range it returns enumerates the entire input.
striptail also accepts two inputs valid as a range. If the first input ends
with the second, as determined by a given comparison function, then the
range that striptail returns enumerates the elements preceding those of
the subject. Otherwise, the range it returns enumerates the entire input.
import mach.range.compare : equals;
assert("hello".striphead("he").equals("llo"));
assert("world".striptail("ld").equals("wor"));// When the input doesn't begin with the subject,
// the returned range enumerates the entire input.
import mach.range.compare : equals;
assert("hello".striphead("xyz").equals("hello"));
assert("hello".striphead("hey").equals("hello"));
assert("world".striptail("xld").equals("world"));The tap function lazily applies a callback function to each element in an
input iterable. It accepts an input iterable valid as a range for its single
runtime argument, and it accepts a template argument representing the function
to apply to the input's elements as the range is consumed.
When the input is bidirectional, so is the range produced by tap.
It provides length and remaining properties when the input does,
and propagates infiniteness.
It supports random access and slicing operations when the input does.
The outputted range applies the callback upon each element being popped, not upon access or assignment. If the range's elements are removed, then those elements are consumed without the callback being applied to them.
For an eagerly-evaluated analog to tap, see each in mach.range.each.
import mach.range.consume : consume;
string hello;
auto range = "hello".tap!((ch){hello ~= ch;});
range.consume;
assert(hello == "hello");int[] array;
// Produce a range which appends to `array` as elements are consumed.
auto range = [1, 2, 3, 4].tap!((n){array ~= n;});
assert(range.front == 1);
assert(array.length == 0);
range.popFront(); // The callback is applied upon popping.
assert(array == [1]);
while(!range.empty) range.popFront();
assert(array == [1, 2, 3, 4]);import mach.collect : DoublyLinkedList;
// The callback appends elements to this array.
int[] array;
// Use a range produced from a list because it supports removal of elements.
auto list = new DoublyLinkedList!int([1, 2, 3, 4]);
auto range = list.values.tap!((n){array ~= n;});
// The callback is applied to the front value upon popping it.
range.popFront();
assert(array == [1]);
// And the callback is applied to the back value upon popping it.
range.popBack();
assert(array == [1, 4]);
// When assigning elements, the callback is applied to the new value.
range.front = 10;
range.popFront();
assert(array == [1, 4, 10]);
// When removing elements, the callback is not applied.
range.removeFront();
assert(array == [1, 4, 10]);The top and bottom functions can be used to find a minimum or maximum
element according to a comparison function. With the default comparison
function, top can be used to find a minimum value in an iterable input
and bottom used to find the maximum.
Note that the mach.sort package provides functions for fully or
partially sorting inputs, rather than simply acquiring a minimum or maximum.
assert([1, 2, 3].top == 1);
assert([1, 2, 3].bottom == 3);auto strings = ["hello", "how", "are", "you?"];
alias compare = (a, b) => (a.length < b.length);
assert(strings.top!compare, "how"); // Get the first shortest string.
assert(strings.bottom!compare, "hello"); // Get the longest string.This module implements walklength, walkindex, and walkslice functions
which acquire the number of elements in an iterable, the element at an index,
and a slice of elements, respectively, by actually traversing the input.
These functions are useful for determining these properties even for ranges
which do not support them because no more efficient implementation than
traversal would be available.
When the input of walkslice is valid as a range, it will itself return a
range to lazily enumerate the elements of the slice.
When the input is not valid as a range, the slice will be accumulated in-memory
in an array and returned.
import mach.range.recur : recur;
import mach.range.compare : equals;
auto range = 0.recur!(n => n + 1, n => n >= 10); // Enumerate numbers 0 through 10.
assert(range.walklength == 10);
assert(range.walkindex(4) == 4);
assert(range.walkslice(2, 4).equals([2, 3]));The walkindex and walkslice functions will produce errors if the passed
indexes are out of bounds.
(In release mode, some of these checks necessary to helpfully report errors are
omitted and nastier errors may occur instead.)
These errors can be circumvented by providing a fallback value to walkindex
and walkslice, where if the indexes being acquired exceed the length of the
input, the fallback is returned for the missing elements instead.
import mach.test.assertthrows : assertthrows;
import mach.range.consume : consume;
assertthrows({
"hello".walkindex(100);
});
assertthrows({
// The error is thrown upon the invalid index being encountered,
// not upon creation of the `walkslice` range.
"hello".walkslice(0, 100).consume;
});// A fallback can be used to compensate for out-of-bounds indexes.
import mach.range.compare : equals;
assert("hi".walkindex(10, '_') == '_');
assert("hello".walkslice(3, 8, '_').equals("lo___"));The zip function accepts any number of input iterables as variadic arguments
and from them produces a range of tuples, where each tuple represents the
corresponding elements in those iterables.
The length of a range returned by zip is equal to the length of its shortest
input.
zip is a very simple abstraction of the plural map function defined
in mach.range.map; see its documentation for more detailed information
regarding the range that is returned.
import mach.types : tuple;
auto range = zip("apple", "bear", "car", "dumpling");
assert(range.front == tuple('a', 'b', 'c', 'd'));
assert(range.length == 3); // Length is that of the shortest input, "car".auto range = zip([0, 1, 2, 3], [0, 2, 4, 6]);
foreach(first, second; range){
assert(first * 2 == second);
}