class Vector

MiniD's built-in array type is useful for many things, but its performance is nothing special, since its elements are dynamically typed. It's fine for most tasks, but sometimes you need an array of numbers that's fast to operate on, or sometimes as a way of interfacing with lower-level APIs (like modifying sound or image data). This is where Vectors come in.

A Vector is a dynamically-resizable typed array of numerical values. It has several types, both integral and floating-point, which can be used as its type. The name is derived more from the STL "vector" type than from the traditional linear algebra definition.

Vectors are also used by the Stream Library. Vectors can be directly read from or written to streams. A single Vector can also be used multiple times as a buffer into which data is read, for better efficiency.

There are ten possible types that a Vector can hold. Each type has an associated "type code" which is just a string. These type codes are used when creating Vectors, as well as being returned from the type method and appearing in the string representation of Vectors. They are as follows:

Type CodeNative D TypeDefinition
u8ubyteUnsigned 8-bit integer
u16ushortUnsigned 16-bit integer
u32uintUnsigned 32-bit integer
u64ulongUnsigned 64-bit integer
i8byteSigned 8-bit integer
i16shortSigned 16-bit integer
i32intSigned 32-bit integer
i64longSigned 64-bit integer
f32floatSingle-precision IEEE 754 float
f64doubleDouble-precision IEEE 754 float

These type codes are case-sensitive, so for example, passing "u8" to the constructor is legal, whereas "U8" is not.

A note on the "u64" type: MiniD's int type is a signed 64-bit integer, which does not have the range to represent all possible values that an unsigned 64-bit integer can. So, when dealing with "u64" arrays, values larger than (263 - 1) will be represented as negative MiniD integers. However, internally, all the operations on these Vectors will respect unsigned integer rules.

A note on all types: this object does not check that the ranges of values that you set are valid for the type of the Vector. If you assign an integer into a Vector whose type cannot represent a number that large, the extra bits will be truncated. If you assign a float into an "f32", it will be rounded to the nearest representable value.

All methods, unless otherwise documented, return the Vector object on which they were called.

All Methods

  1. this(type: string, size: int = 0 [, filler])
  2. range(type: string, val1 [, val2 [, step]])
  3. fromArray(type: string, a: array)
  4. append
  5. apply(f: function)
  6. `copyRange(lo: int = 0, hi: int = #this, other: Vector, otherLo: int = …
  7. dup()
  8. fill(v: int|float|function|array|Vector)
  9. `fillRange(lo: int = 0, hi: int = #this, v: …
  10. insert(idx: int, v: int|float|Vector)
  11. itemSize()
  12. map(f: function)
  13. max()
  14. min()
  15. pop(idx: int = -1)
  16. product()
  17. remove(start: int, end: int = start + 1)
  18. reverse()
  19. sort()
  20. sum()
  21. toArray()
  22. toString()
  23. type([new: string])
  24. readXxx(idx: int)
  25. writeXxx(idx: int, value: int|float)
  26. opLength()
  27. opLengthAssign(len: int)
  28. opIndex(idx: int)
  29. opIndexAssign(idx: int, val: int|float)
  30. opSlice(lo: int = 0, hi: int = #this)
  31. opSliceAssign
  32. opApply([reverse: string])
  33. opSerialize, opDeserialize
  34. opEquals(other: Vector)
  35. opCat(other: int|float|Vector)
  36. opCat_r(other: int|float)
  37. opCatAssign(vararg)
  38. `opAdd, opSub, opMul, opDiv, opMod, opAdd_r, opSub_r, opMul_r, opDiv_r, …
  39. opAddAssign, opSubAssign, opMulAssign, opDivAssign, opModAssign
  40. revSub, revDiv, revMod

this(type: string, size: int = 0 [, filler])

Constructs a Vector object. The constructor may only be called once on a Vector instance; attempting to call it a second time will result in a runtime error. The type parameter must be a string containing one of the type codes described above. The size parameter is how many items long the Vector should be (note that it's not the number of bytes). If no filler parameter is passed, the data in the Vector will be uninitialized. This means that it will likely contain random garbage. If you pass a filler parameter, this instance will have the fill method called on it with filler as the parameter.

range(type: string, val1 [, val2 [, step]])

This is usually meant to be called as a static method (like "Vector.range(...)"), but can be called on an instance too. This creates a new Vector object whose values are generated from a range of values with a given step. This is very similar to the array.range function. If the type code given is an integral type, the other three parameters must be integers. If the type code given is a floating-point type, the other three parameters must be ints or floats.

If called with just val1, it specifies a noninclusive ending index. The beginning index will be 0 and the step will be 1. So "Vector.range("i32", 5)" gives "Vector(i32)[0, 1, 2, 3, 4]", and "Vector.range("i32", -5)" gives "Vector(i32)[0, -1, -2, -3, -4]".

If called with val1 and val2, val1 becomes the inclusive start index and val2 the noninclusive end index. The step will be 1.

If the step parameter is specified, it indicates how large the stride between successive elements should be.

fromArray(type: string, a: array)

This is usually meant to be called as a static method (like "Vector.range(...)"), but can be called on an instance too. This creates a new Vector object whose values are taken from the given array. This is really just a convenience function; the exact same effect could be achieved with "Vector(type, #a, a)".

append

This is just an alias to opCatAssign. Any time you write "v ~= a ~ b ~ c", it can be written equivalently "v.append(a, b, c)", and vice versa.

apply(f: function)

Similar to the apply method defined for arrays, this method takes a function, calls it once on each element, and assigns the result back into the Vector. For integral types, the function should accept a single integer and return a single integer. For floating-point types, the function should accept a single float and return a single int or float.

copyRange(lo: int = 0, hi: int = #this, other: Vector, otherLo: int = 0, otherHi: int = otherLo + (hi - lo))

This function's signature looks scary, but it's not bad!

If you want to copy all the data from one Vector into a slice of another, something like "a[x .. y] = b" will work. However, if you want to copy a slice of one Vector to another, there's a performance issue: slicing a Vector always creates a new vector. So "a[x .. y] = b[z .. w]" looks nice but creates an unnecessary temporary. This function allows you to avoid the creation of such a temporary and will copy data from a slice of one Vector to another directly.

The first two parameters are the destination slice indices. The third parameter is the source Vector. The last two parameters are the source slice indices. If you omit both source slice indices, it will copy the first (hi - lo) items from the source into the destination slice. If you specify just the low source index, it will copy (hi - lo) items starting from otherLo from the source into the destination slice. Otherwise, the length of the source and destination slices must match.

Here are a few examples:

local v = Vector("i32", 5, 0)       // v = Vector(i32)[0, 0, 0, 0, 0]
local u = Vector.range("i32", 1, 6) // u = Vector(i32)[1, 2, 3, 4, 5]
v.copyRange(0, 3, u, 2, 5)          // v = Vector(i32)[3, 4, 5, 0, 0]
v.copyRange(0, 4, u, 0)             // v = Vector(i32)[1, 2, 3, 4, 0]
v.copyRange(0, 5, u)                // v = Vector(i32)[1, 2, 3, 4, 5]

dup()

Returns a new Vector that is a duplicate of this one: same type, same size, same data.

fill(v: int|float|function|array|Vector)

This is a flexible way to fill a Vector with some data. It never changes the size of the Vector; it always operates in-place and replaces the data that already exists.

If the Vector is integral-typed, you can pass an int, in which case all the items in the Vector will be set to that value.

If the Vector is floating-point-typed, you can pass an int or float, in which case all the items in the Vector will be set to that value (cast to the appropriate type).

If you pass a function, it must take a single integer and return a single value (an integer for integer vectors and an int or float for float vectors). This function is called once for each index in the Vector, and the returned value is assigned into that index.

If you pass an array, it must be the same length of the Vector. For int Vectors, all the items in the array must be ints, and for float Vectors, all the items must be ints or floats.

Lastly, if you pass another Vector, it must be the same length and the same type. The data is copied directly from it.

fillRange(lo: int = 0, hi: int = #this, v: int|float|function|array|Vector)

Just like the fill method, but this only operates on a subrange of the Vector.

insert(idx: int, v: int|float|Vector)

This inserts the given value before the given index. The index can be equal to the length of the Vector, in which case the item is appended to the end of the Vector. For int Vectors, the value must be an integer, and for float Vectors, the value must be an int or float. For any kind of Vector, if the value is another Vector, it must be the same type, and all its elements are inserted at the given index.

itemSize()

Returns the size, in bytes, of a single item in this Vector.

map(f: function)

Very similar to apply, except that this returns a new Vector containing the outputs of the given function, leaving the original Vector intact.

max()

Returns the largest item in this Vector, or throws an exception if it is empty.

min()

Returns the smallest item in this Vector, or throws an exception if it is empty.

pop(idx: int = -1)

Just like the pop method of arrays, this removes a single item from the Vector and returns it. By default, it removes an item from the end, but you can remove it from anywhere you like; in that case, all the items after it will be shifted down a slot.

product()

Returns the product of all the elements in this Vector. Returns an int for int Vectors and a float for float Vectors.

remove(start: int, end: int = start + 1)

Removes a range of items from this Vector. The start index is inclusive and the end index is noninclusive. Either can be negative. The end index defaults to the item after the start, meaning that calling this method with just one parameter will remove one item (similar to pop, except the removed item is not returned; 'this' is returned instead).

reverse()

Reverses the order all the items in this Vector.

sort()

Sorts the items of this Vector in-place in ascending order.

sum()

Returns the sum of all the elements in this Vector Returns an int for int Vectors and a float for float Vectors.

toArray()

Returns a MiniD array the same length as this Vector and containing the same values.

toString()

Returns a string representation of this Vector in the form "Vector(type)[items]"; for example, "Vector.range("i32", 1, 5).toString()" would yield "Vector(i32)[1, 2, 3, 4]".

type([new: string])

If called with no params, returns the type code string of this Vector.

If called with a param, it must be a type code string. This Vector's type will be changed to the new type. The byte length of this Vector must be a multiple of the item size of the new type, or else an error will be thrown. For instance, if you have a Vector(u8) of length 7 and attempt to set its type to "u16", you will get an error, since 7 is not a multiple of 2.

readXxx(idx: int)

This is not one function, but a whole set of functions. They include readByte, readUByte, readShort, readUShort, readInt, readUInt, readLong, readULong, readFloat, and readDouble. These allow you to reinterpret the data in a Vector as some other type. This is useful, for example, if you read a chunk of data out of a file and want to read values out of it, or for accessing structured data given to you by the host application (such as in a native structure). The idx parameter is specified as a byte offset rather than an element offset. It must be in the range 0 <= idx < (length * itemSize) - valueSize, where valueSize is the size of the value being read. The index can be negative, in which case the index will be from the end of the Vector. If the Vector is not large enough to hold a value of the given type, an error will be thrown, since there are no valid indices in that case.

writeXxx(idx: int, value: int|float)

Like the readXxx functions, this is another set of functions, including writeByte, writeUByte, writeShort, writeUShort, writeInt, writeUInt, writeLong, writeULong, writeFloat, and writeDouble. These are just the inverse of the corresponding read methods. The idx parameter is specified as a byte offset rather than an element offset. It must be in the range 0 <= idx < (length * itemSize) - valueSize, where valueSize is the size of the value being read. The index can be negative, in which case the index will be from the end of the Vector. If the Vector is not large enough to hold a value of the given type, an error will be thrown, since there are no valid indices in that case. Like for all other Vector methods, if the type you're writing is an int, the value param must be an int, and if it's a float, the value may be an int or a float.

opLength()

Returns the length of this Vector.

opLengthAssign(len: int)

Resizes this Vector. If you make it smaller, the extra elements are discarded. If you make it larger, the extra elements will be uninitialized, meaning they will contain garbage.

opIndex(idx: int)

Gets a single item out of this Vector at the given index. The index can be negative.

opIndexAssign(idx: int, val: int|float)

Sets a single item in this Vector at the given index, which can be negative. For int Vectors, the value must be an int. For float Vectors, the value can be an int or a float.

opSlice(lo: int = 0, hi: int = #this)

Returns a new Vector, the same type as this, whose data is a copy of the data in the given slice in this Vector.

opSliceAssign

This is just an alias to fillRange. So any slice-assignment of the form "a[x .. y] = b" can be written equivalently "a.fillRange(x, y, b)", for any x, y, and b.

opApply([reverse: string])

This lets you iterate over Vectors using foreach loops. Just like the array iterator, you can pass in the "reverse" string to iterate backwards. For example:

local v = Vector.range("i32", 1, 6)

foreach(val; v)
    writeln(val) // prints 1 through 5

foreach(val; v, "reverse")
    writeln(val) // prints 5 through 1

opSerialize, opDeserialize

The serialization and deserialization methods which are to be used with the MiniD serialization framework. It is not recommended that you call these methods directly.

opEquals(other: Vector)

Compares two Vectors for equality. If 'this' is other, returns true. If the two Vectors are of different types, throws an exception. If they have the same length and data, returns true; otherwise, returns false.

opCat(other: int|float|Vector)

Concatenates this Vector with the given value and returns a new Vector. For int Vectors, other must be an int. For float Vectors, other must be an int or float. For all Vectors, if other is a Vector, it must be the same type as this Vector.

opCat_r(other: int|float)

Overload to allow reverse concatenation with basic types. Also always returns a new Vector.

opCatAssign(vararg)

Appends all the items to the end of this Vector. The types of all the items must all be the same as the argument to opCat. This works in-place, resizing this Vector, instead of creating a new Vector.

opAdd, opSub, opMul, opDiv, opMod, opAdd_r, opSub_r, opMul_r, opDiv_r, opMod_r

These methods all overload binary arithmetic operators for Vectors. The operand passed to all these must be an int, float, or Vector of the same type as 'this'. I think by now you get what types are allowed.

opAddAssign, opSubAssign, opMulAssign, opDivAssign, opModAssign

These methods all overload reflexive arithmetic operators for Vectors. The operand passed to all these must be an int, float, or Vector of the same type as 'this'.

revSub, revDiv, revMod

These allow you to perform in-place reflexive operations where the left-hand side is to be used as the second operand rather than the first. For example, doing "v -= 5" will subtract 5 from each element in v; but doing "v.revSub(5)" will subtract each element in v from 5. The operand passed to all these must be an int, float, or Vector of the same type as 'this'.