mirror of https://github.com/deavmi/niknaks
1015 lines
24 KiB
D
1015 lines
24 KiB
D
/**
|
|
* Container types
|
|
*
|
|
* Authors: Tristan Brice Velloza Kildaire (deavmi)
|
|
*/
|
|
module niknaks.containers;
|
|
|
|
import core.sync.mutex : Mutex;
|
|
import std.datetime : Duration, dur;
|
|
import std.datetime.stopwatch : StopWatch, AutoStart;
|
|
import core.thread : Thread;
|
|
import core.sync.condition : Condition;
|
|
import std.functional : toDelegate;
|
|
|
|
version(unittest)
|
|
{
|
|
import std.stdio : writeln;
|
|
}
|
|
|
|
/**
|
|
* Represents an entry of
|
|
* some value of type `V`
|
|
*
|
|
* Associated with this
|
|
* is a timer used to
|
|
* check against for
|
|
* expiration
|
|
*/
|
|
private template Entry(V)
|
|
{
|
|
/**
|
|
* The entry type
|
|
*/
|
|
public struct Entry
|
|
{
|
|
private V value;
|
|
private StopWatch timer;
|
|
|
|
@disable
|
|
private this();
|
|
|
|
/**
|
|
* Creates a new entry
|
|
* with the given value
|
|
*
|
|
* Params:
|
|
* value = the value
|
|
*/
|
|
public this(V value)
|
|
{
|
|
setValue(value);
|
|
timer = StopWatch(AutoStart.yes);
|
|
}
|
|
|
|
/**
|
|
* Sets the value of this
|
|
* entry
|
|
*
|
|
* Params:
|
|
* value = the value
|
|
*/
|
|
public void setValue(V value)
|
|
{
|
|
this.value = value;
|
|
}
|
|
|
|
/**
|
|
* Returns the value associated
|
|
* with this entry
|
|
*
|
|
* Returns: the value
|
|
*/
|
|
public V getValue()
|
|
{
|
|
return this.value;
|
|
}
|
|
|
|
/**
|
|
* Resets the timer back
|
|
* to zero
|
|
*/
|
|
public void bump()
|
|
{
|
|
timer.reset();
|
|
}
|
|
|
|
/**
|
|
* Gets the time elapsed
|
|
* since this entry was
|
|
* instantiated
|
|
*
|
|
* Returns: the elapsed
|
|
* time
|
|
*/
|
|
public Duration getElapsedTime()
|
|
{
|
|
return timer.peek();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A `CacheMap` with a key type of `K`
|
|
* and value type of `V`
|
|
*/
|
|
public template CacheMap(K, V)
|
|
{
|
|
/**
|
|
* A replacement function which takes
|
|
* in the key of type `K` and returns
|
|
* a value of type `V`
|
|
*
|
|
* This is the delegate-based variant
|
|
*/
|
|
public alias ReplacementDelegate = V delegate(K);
|
|
|
|
/**
|
|
* A replacement function which takes
|
|
* in the key of type `K` and returns
|
|
* a value of type `V`
|
|
*
|
|
* This is the function-based variant
|
|
*/
|
|
public alias ReplacementFunction = V function(K);
|
|
|
|
/**
|
|
* A caching map which when queried
|
|
* for a key which does not exist yet
|
|
* will call a so-called replacement
|
|
* function which produces a result
|
|
* which will be stored at that key's
|
|
* location
|
|
*
|
|
* After this process a timer is started,
|
|
* and periodically entries are checked
|
|
* for timeouts, if they have timed out
|
|
* then they are removed and the process
|
|
* begins again.
|
|
*
|
|
* Accessing an entry will reset its
|
|
* timer ONLY if it has not yet expired
|
|
* however accessing an entry which
|
|
* has expired causing an on-demand
|
|
* replacement function call, just not
|
|
* a removal in between
|
|
*/
|
|
public class CacheMap
|
|
{
|
|
private Entry!(V)[K] map;
|
|
private Mutex lock;
|
|
private Duration expirationTime;
|
|
private ReplacementDelegate replFunc;
|
|
|
|
private Thread checker;
|
|
private bool isRunning;
|
|
private Condition condVar;
|
|
private Duration sweepInterval;
|
|
|
|
/**
|
|
* Constructs a new cache map with the
|
|
* given replacement delegate and the
|
|
* expiration deadline.
|
|
*
|
|
* Params:
|
|
* replFunc = the replacement delegate
|
|
* expirationTime = the expiration
|
|
* deadline
|
|
* sweepInterval = the interval at
|
|
* which the sweeper thread should
|
|
* run at to check for expired entries
|
|
*/
|
|
this(ReplacementDelegate replFunc, Duration expirationTime = dur!("seconds")(10), Duration sweepInterval = dur!("seconds")(10))
|
|
{
|
|
this.replFunc = replFunc;
|
|
this.lock = new Mutex();
|
|
this.expirationTime = expirationTime;
|
|
|
|
this.sweepInterval = sweepInterval;
|
|
this.condVar = new Condition(this.lock);
|
|
this.checker = new Thread(&checkerFunc);
|
|
this.isRunning = true;
|
|
this.checker.start();
|
|
|
|
}
|
|
|
|
/**
|
|
* Constructs a new cache map with the
|
|
* given replacement function and the
|
|
* expiration deadline.
|
|
*
|
|
* Params:
|
|
* replFunc = the replacement function
|
|
* expirationTime = the expiration
|
|
* deadline
|
|
* sweepInterval = the interval at
|
|
* which the sweeper thread should
|
|
* run at to check for expired entries
|
|
*/
|
|
this(ReplacementFunction replFunc, Duration expirationTime = dur!("seconds")(10), Duration sweepInterval = dur!("seconds")(10))
|
|
{
|
|
this(toDelegate(replFunc), expirationTime, sweepInterval);
|
|
}
|
|
|
|
/**
|
|
* Creates an entry for the given
|
|
* key by creating the `Entry`
|
|
* at the key and then setting
|
|
* that entry's value with the
|
|
* replacement function
|
|
*
|
|
* Params:
|
|
* key = the key
|
|
* Returns: the value set
|
|
*/
|
|
private V makeKey(K key)
|
|
{
|
|
// Lock the mutex
|
|
this.lock.lock();
|
|
|
|
// On exit
|
|
scope(exit)
|
|
{
|
|
// Unlock the mutex
|
|
this.lock.unlock();
|
|
}
|
|
|
|
// Run the replacement function for this key
|
|
V newValue = replFunc(key);
|
|
|
|
// Create a new entry with this value
|
|
Entry!(V) newEntry = Entry!(V)(newValue);
|
|
|
|
// Save this entry into the hashmap
|
|
this.map[key] = newEntry;
|
|
|
|
return newValue;
|
|
}
|
|
|
|
/**
|
|
* Called to update an existing
|
|
* `Entry` (already present) in
|
|
* the map. This will run the
|
|
* replacement function and update
|
|
* the value present.
|
|
*
|
|
* Params:
|
|
* key = the key
|
|
* Returns: the value set
|
|
*/
|
|
private V updateKey(K key)
|
|
{
|
|
// Lock the mutex
|
|
this.lock.lock();
|
|
|
|
// On exit
|
|
scope(exit)
|
|
{
|
|
// Unlock the mutex
|
|
this.lock.unlock();
|
|
}
|
|
|
|
// Run the replacement function for this key
|
|
V newValue = replFunc(key);
|
|
|
|
// Update the value saved at this key's entry
|
|
this.map[key].setValue(newValue);
|
|
|
|
return newValue;
|
|
}
|
|
|
|
/**
|
|
* Check's a specific key for expiration,
|
|
* and if expired then refreshes it if
|
|
* not it leaves it alone.
|
|
*
|
|
* Returns the key's value
|
|
*
|
|
* Params:
|
|
* key = the key to check
|
|
* Returns: the key's value
|
|
*/
|
|
private V expirationCheck(K key)
|
|
{
|
|
// Lock the mutex
|
|
this.lock.lock();
|
|
|
|
// On exit
|
|
scope(exit)
|
|
{
|
|
// Unlock the mutex
|
|
this.lock.unlock();
|
|
}
|
|
|
|
// Obtain the entry at this key
|
|
Entry!(V)* entry = key in this.map;
|
|
|
|
// If the key exists
|
|
if(entry != null)
|
|
{
|
|
// If this entry expired, run the refresher
|
|
if(entry.getElapsedTime() >= this.expirationTime)
|
|
{
|
|
version(unittest) { writeln("Expired entry for key '", key, "', refreshing"); }
|
|
|
|
updateKey(key);
|
|
}
|
|
// Else, if not, then bump the entry
|
|
else
|
|
{
|
|
entry.bump();
|
|
}
|
|
}
|
|
// If it does not exist (then make it)
|
|
else
|
|
{
|
|
version(unittest) { writeln("Hello there, we must MAKE key as it does not exist"); }
|
|
makeKey(key);
|
|
version(unittest) { writeln("fic"); }
|
|
}
|
|
|
|
return this.map[key].getValue();
|
|
}
|
|
|
|
/**
|
|
* Gets the value of
|
|
* the entry at the
|
|
* provided key
|
|
*
|
|
* This may or may not
|
|
* call the replication
|
|
* function
|
|
*
|
|
* Params:
|
|
* key = the key to
|
|
* lookup by
|
|
*
|
|
* Returns: the value
|
|
*/
|
|
public V get(K key)
|
|
{
|
|
// Lock the mutex
|
|
this.lock.lock();
|
|
|
|
// On exit
|
|
scope(exit)
|
|
{
|
|
// Unlock the mutex
|
|
this.lock.unlock();
|
|
}
|
|
|
|
// The key's value
|
|
V keyValue;
|
|
|
|
// On access expiration check
|
|
keyValue = expirationCheck(key);
|
|
|
|
return keyValue;
|
|
}
|
|
|
|
/**
|
|
* See_Also: get
|
|
*/
|
|
public V opIndex(K key)
|
|
{
|
|
return get(key);
|
|
}
|
|
|
|
/**
|
|
* Removes the given key
|
|
* returning whether or
|
|
* not it was a success
|
|
*
|
|
* Params:
|
|
* key = the key to
|
|
* remove
|
|
* Returns: `true` if the
|
|
* key existed, `false`
|
|
* otherwise
|
|
*/
|
|
public bool removeKey(K key)
|
|
{
|
|
// Lock the mutex
|
|
this.lock.lock();
|
|
|
|
// On exit
|
|
scope(exit)
|
|
{
|
|
// Unlock the mutex
|
|
this.lock.unlock();
|
|
}
|
|
|
|
// Remove the key
|
|
return this.map.remove(key);
|
|
}
|
|
|
|
/**
|
|
* Runs at the latest every
|
|
* `expirationTime` ticks
|
|
* and checks the entire
|
|
* map for expired
|
|
* entries
|
|
*/
|
|
private void checkerFunc()
|
|
{
|
|
while(this.isRunning)
|
|
{
|
|
// Lock the mutex
|
|
this.lock.lock();
|
|
|
|
// On loop exit
|
|
scope(exit)
|
|
{
|
|
// Unlock the mutex
|
|
this.lock.unlock();
|
|
}
|
|
|
|
// Sleep until sweep interval
|
|
this.condVar.wait(this.sweepInterval);
|
|
|
|
// Run the expiration check
|
|
K[] marked;
|
|
foreach(K curKey; this.map.keys())
|
|
{
|
|
Entry!(V) curEntry = this.map[curKey];
|
|
|
|
// If entry has expired mark it for removal
|
|
if(curEntry.getElapsedTime() >= this.expirationTime)
|
|
{
|
|
version(unittest) { writeln("Marked entry '", curEntry, "' for removal"); }
|
|
marked ~= curKey;
|
|
}
|
|
}
|
|
|
|
foreach(K curKey; marked)
|
|
{
|
|
Entry!(V) curEntry = this.map[curKey];
|
|
|
|
version(unittest) { writeln("Removing entry '", curEntry, "'..."); }
|
|
this.map.remove(curKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wakes up the checker
|
|
* immediately such that
|
|
* it can perform a cycle
|
|
* over the map and check
|
|
* for expired entries
|
|
*/
|
|
private void doLiveCheck()
|
|
{
|
|
// Lock the mutex
|
|
this.lock.lock();
|
|
|
|
// Signal wake up
|
|
this.condVar.notify();
|
|
|
|
// Unlock the mutex
|
|
this.lock.unlock();
|
|
}
|
|
|
|
/**
|
|
* On destruction, set
|
|
* the running status
|
|
* to `false`, then
|
|
* wake up the checker
|
|
* and wait for it to
|
|
* exit
|
|
*/
|
|
~this()
|
|
{
|
|
version(unittest)
|
|
{
|
|
writeln("Dtor running");
|
|
|
|
scope(exit)
|
|
{
|
|
writeln("Dtor running [done]");
|
|
}
|
|
}
|
|
|
|
// Set run state to false
|
|
this.isRunning = false;
|
|
|
|
// Signal to stop
|
|
doLiveCheck();
|
|
|
|
// Wait for it to stop
|
|
this.checker.join();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests the usage of the `CacheMap` type
|
|
* along with the expiration of entries
|
|
* mechanism
|
|
*/
|
|
unittest
|
|
{
|
|
int i = 0;
|
|
int getVal(string)
|
|
{
|
|
i++;
|
|
return i;
|
|
}
|
|
|
|
// Create a CacheMap with 10 second expiration and 10 second sweeping interval
|
|
CacheMap!(string, int) map = new CacheMap!(string, int)(&getVal, dur!("seconds")(10));
|
|
|
|
// Get the value
|
|
int tValue = map["Tristan"];
|
|
assert(tValue == 1);
|
|
|
|
// Get the value (should still be cached)
|
|
tValue = map["Tristan"];
|
|
assert(tValue == 1);
|
|
|
|
// Wait for expiry (by sweeping thread)
|
|
Thread.sleep(dur!("seconds")(11));
|
|
|
|
// Should call replacement function
|
|
tValue = map["Tristan"];
|
|
assert(tValue == 2);
|
|
|
|
// Wait for expiry (by sweeping thread)
|
|
writeln("Sleeping now 11 secs");
|
|
Thread.sleep(dur!("seconds")(11));
|
|
|
|
// Destroy the map (such that it ends the sweeper)
|
|
destroy(map);
|
|
}
|
|
|
|
/**
|
|
* Creates a `CacheMap` which tests out
|
|
* the on-access expiration checking of
|
|
* entries by accessing an entry faster
|
|
* then the sweep interval and by
|
|
* having an expiration interval below
|
|
* the aforementioned interval
|
|
*/
|
|
unittest
|
|
{
|
|
int i = 0;
|
|
int getVal(string)
|
|
{
|
|
i++;
|
|
return i;
|
|
}
|
|
|
|
// Create a CacheMap with 5 second expiration and 10 second sweeping interval
|
|
CacheMap!(string, int) map = new CacheMap!(string, int)(&getVal, dur!("seconds")(5), dur!("seconds")(10));
|
|
|
|
// Get the value
|
|
int tValue = map["Tristan"];
|
|
assert(tValue == 1);
|
|
|
|
// Wait for 5 seconds (the entry should then be expired by then for on-access check)
|
|
Thread.sleep(dur!("seconds")(5));
|
|
|
|
// Get the value (should have replacement function run)
|
|
tValue = map["Tristan"];
|
|
assert(tValue == 2);
|
|
|
|
// Destroy the map (such that it ends the sweeper
|
|
destroy(map);
|
|
}
|
|
|
|
/**
|
|
* Tests the usage of the `CacheMap`,
|
|
* specifically the explicit key
|
|
* removal method
|
|
*/
|
|
unittest
|
|
{
|
|
int i = 0;
|
|
int getVal(string)
|
|
{
|
|
i++;
|
|
return i;
|
|
}
|
|
|
|
// Create a CacheMap with 10 second expiration and 10 second sweeping interval
|
|
CacheMap!(string, int) map = new CacheMap!(string, int)(&getVal, dur!("seconds")(10), dur!("seconds")(10));
|
|
|
|
// Get the value
|
|
int tValue = map["Tristan"];
|
|
assert(tValue == 1);
|
|
|
|
// Remove the key
|
|
assert(map.removeKey("Tristan"));
|
|
|
|
// Get the value
|
|
tValue = map["Tristan"];
|
|
assert(tValue == 2);
|
|
|
|
// Destroy the map (such that it ends the sweeper
|
|
destroy(map);
|
|
}
|
|
|
|
private struct Sector(T)
|
|
{
|
|
private T[] data;
|
|
|
|
this(T[] data)
|
|
{
|
|
this.data = data;
|
|
}
|
|
|
|
public T opIndex(size_t idx)
|
|
{
|
|
return this.data[idx];
|
|
}
|
|
|
|
public void opIndexAssign(T value, size_t index)
|
|
{
|
|
this.data[index] = value;
|
|
}
|
|
|
|
// Contract: Obtaining the length must be present
|
|
public size_t opDollar()
|
|
{
|
|
return this.data.length;
|
|
}
|
|
|
|
// Contract: Obtaining the length must be present
|
|
@property
|
|
public size_t length()
|
|
{
|
|
return opDollar();
|
|
}
|
|
|
|
public T[] opSlice(size_t start, size_t end)
|
|
{
|
|
return this.data[start..end];
|
|
}
|
|
|
|
public T[] opSlice()
|
|
{
|
|
return opSlice(0, opDollar);
|
|
}
|
|
|
|
// Contract: Rezising must be implemented
|
|
// TODO: This would then be the very reason for
|
|
// using ref actually, as resizing may only
|
|
// change a local copy when extding on
|
|
// the tail-end "extent" (SectorType)
|
|
|
|
// Actually should resizing even be done here?
|
|
|
|
}
|
|
|
|
// TODO: Make a bit better
|
|
private bool isSector(S)()
|
|
{
|
|
return __traits(hasMember, S, "opIndex");
|
|
}
|
|
|
|
import core.exception : ArrayIndexError;
|
|
import core.exception : RangeError;
|
|
|
|
public struct View(T, SectorType = Sector!(T))
|
|
if(isSector!(SectorType)())
|
|
{
|
|
private SectorType[] sectors;
|
|
// private
|
|
|
|
// Maybe current size should be here as we
|
|
// are a view, we should allow modofication
|
|
// but not make any NEW arrays
|
|
private size_t curSize;
|
|
|
|
private size_t computeTotalLen()
|
|
{
|
|
size_t l;
|
|
foreach(SectorType sector; this.sectors)
|
|
{
|
|
l += sector.opDollar();
|
|
}
|
|
return l;
|
|
}
|
|
|
|
public size_t opDollar()
|
|
{
|
|
return this.length;
|
|
}
|
|
|
|
public T opIndex(size_t idx)
|
|
{
|
|
// Within range of "fake" size
|
|
if(!(idx < this.length))
|
|
{
|
|
throw new ArrayIndexError(idx, this.length);
|
|
}
|
|
|
|
size_t thunk;
|
|
foreach(SectorType sector; this.sectors)
|
|
{
|
|
if(idx-thunk < sector.opDollar())
|
|
{
|
|
return sector[idx-thunk];
|
|
}
|
|
else
|
|
{
|
|
thunk += sector.opDollar();
|
|
}
|
|
}
|
|
|
|
throw new ArrayIndexError(idx, this.length);
|
|
}
|
|
|
|
public void opIndexAssign(T value, size_t idx)
|
|
{
|
|
// Within range of "fake" size
|
|
if(!(idx < this.length))
|
|
{
|
|
throw new ArrayIndexError(idx, this.length);
|
|
}
|
|
|
|
size_t thunk;
|
|
// TODO: Should be ref, else it is just a local struct copy
|
|
// could cheat if sector is never replaced, hence why it works
|
|
foreach(SectorType sector; this.sectors)
|
|
{
|
|
writeln(sector);
|
|
writeln("idx: ", idx);
|
|
writeln("thunk: ", thunk);
|
|
if(idx-thunk < sector.opDollar())
|
|
{
|
|
sector[idx-thunk] = value;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
thunk += sector.opDollar();
|
|
}
|
|
}
|
|
|
|
throw new ArrayIndexError(idx, this.length);
|
|
}
|
|
|
|
public T[] opSlice()
|
|
{
|
|
T[] buff;
|
|
foreach(SectorType sector; this.sectors)
|
|
{
|
|
buff ~= sector[];
|
|
}
|
|
|
|
// Trim to "fake" size
|
|
buff.length = this.curSize;
|
|
|
|
return buff;
|
|
}
|
|
|
|
public T[] opSlice(size_t start, size_t end)
|
|
{
|
|
// Invariant of start < end
|
|
if(!(start <= end))
|
|
{
|
|
// TODO: Check
|
|
throw new RangeError("Starting index must be smaller than or equal to ending index");
|
|
}
|
|
// Within range of "fake" size
|
|
else if(!((start < this.length) && (end <= this.length)))
|
|
{
|
|
throw new RangeError("start index or end index not under range");
|
|
// throw new ArrayIndexError(idx, this.length);
|
|
}
|
|
|
|
T[] collected;
|
|
|
|
size_t thunk;
|
|
foreach(SectorType sector; this.sectors)
|
|
{
|
|
// If the current sector contains
|
|
// both the starting AND ending
|
|
// indices
|
|
if(start-thunk < sector.opDollar() && end-thunk <= sector.opDollar())
|
|
{
|
|
return sector[start-thunk..end-thunk];
|
|
}
|
|
// If the current sector's starting
|
|
// index (only) is included
|
|
else if(start-thunk < sector.opDollar() && !(end-thunk <= sector.opDollar()))
|
|
{
|
|
collected ~= sector[start-thunk..$];
|
|
}
|
|
// If the current sector's ending
|
|
// index (only) is included
|
|
else if(!(start-thunk < sector.opDollar()) && end-thunk <= sector.opDollar())
|
|
{
|
|
collected ~= sector[0..end-thunk];
|
|
}
|
|
// If the current sector's entirety
|
|
// is to be included
|
|
else
|
|
{
|
|
collected ~= sector[];
|
|
}
|
|
|
|
thunk += sector.opDollar();
|
|
}
|
|
|
|
|
|
|
|
// FIXME: This is lazy, do a check for up to where
|
|
// and actually make THIS the real implementation
|
|
|
|
writeln(collected);
|
|
|
|
// TODO: Also if the range matches the bounds
|
|
// of a given range exactly then extract directly
|
|
return collected;
|
|
}
|
|
|
|
private static bool isArrayAppend(P)()
|
|
{
|
|
return __traits(isSame, P, T[]);
|
|
}
|
|
|
|
private static bool isElementAppend(P)()
|
|
{
|
|
return __traits(isSame, P, T);
|
|
}
|
|
|
|
// Append
|
|
public void opOpAssign(string op, E)(E value)
|
|
if(op == "~" && (isArrayAppend!(E) || isElementAppend!(E)))
|
|
{
|
|
static if(isArrayAppend!(E))
|
|
{
|
|
add(value);
|
|
}
|
|
else
|
|
{
|
|
add([value]);
|
|
}
|
|
}
|
|
|
|
// Takes the data, constructs a kind-of SectorType
|
|
// and adds it
|
|
private void add(T[] data)
|
|
{
|
|
// Create a new sector
|
|
SectorType sec = SectorType(data);
|
|
|
|
// Update the tracking size
|
|
this.curSize += sec.length;
|
|
|
|
// Concatenate it to the view
|
|
this.sectors ~= SectorType(data);
|
|
}
|
|
|
|
@property
|
|
public size_t length()
|
|
{
|
|
return this.curSize;
|
|
}
|
|
|
|
@property
|
|
public void length(size_t size)
|
|
{
|
|
// TODO: Add support for sizing down
|
|
// TODO: Add support for sizing up
|
|
|
|
// TODO: Need we continuously compute this?
|
|
// ... we should have a tracking field for
|
|
// ... this
|
|
size_t actualSize = computeTotalLen();
|
|
|
|
// On successful exit, update the "fake" size
|
|
scope(success)
|
|
{
|
|
this.curSize = size;
|
|
}
|
|
|
|
|
|
// Don't allow sizing up (doesn't make sense for a view)
|
|
if(size > actualSize)
|
|
{
|
|
auto r = new RangeError();
|
|
r.msg = "Cannot extend the size of a view past its total size (of all attached sectors)";
|
|
throw r;
|
|
}
|
|
// If nothing changes
|
|
else if(size == actualSize)
|
|
{
|
|
// Nothing
|
|
}
|
|
// If shrinking to zero
|
|
else if(size == 0)
|
|
{
|
|
// Just drop everything
|
|
this.sectors.length = 0;
|
|
}
|
|
// If shrinking (arbitrary)
|
|
else
|
|
{
|
|
// Sectors from left-to-right to keep
|
|
size_t sectorCnt;
|
|
|
|
// Accumulator
|
|
size_t accumulator;
|
|
|
|
foreach(SectorType sector; this.sectors)
|
|
{
|
|
accumulator += sector.length;
|
|
sectorCnt++;
|
|
if(size <= accumulator)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.sectors.length = sectorCnt;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
unittest
|
|
{
|
|
View!(int) view;
|
|
assert(view.opDollar() == 0);
|
|
|
|
try
|
|
{
|
|
view[1];
|
|
assert(false);
|
|
}
|
|
catch(ArrayIndexError e)
|
|
{
|
|
assert(e.index == 1);
|
|
assert(e.length == 0);
|
|
}
|
|
|
|
view ~= [1,3,45];
|
|
assert(view.opDollar() == 3);
|
|
assert(view.length == 3);
|
|
|
|
view ~= 2;
|
|
assert(view.opDollar() == 4);
|
|
assert(view.length == 4);
|
|
|
|
assert(view[0] == 1);
|
|
assert(view[1] == 3);
|
|
assert(view[2] == 45);
|
|
assert(view[3] == 2);
|
|
assert(view[0..2] == [1,3]);
|
|
assert(view[0..4] == [1,3,45,2]);
|
|
|
|
// Update elements
|
|
view[0] = 71;
|
|
view[3] = 50;
|
|
|
|
// Set size to same size
|
|
view.length = view.length;
|
|
|
|
// Check that update is present
|
|
// and size unchanged
|
|
int[] all = view[];
|
|
assert(all == [71,3,45,50]);
|
|
|
|
// Truncate by 1 element
|
|
view.length = view.length-1;
|
|
all = view[];
|
|
assert(all == [71,3,45]);
|
|
|
|
// This should fail
|
|
try
|
|
{
|
|
view[3] = 3;
|
|
assert(false);
|
|
}
|
|
catch(RangeError e)
|
|
{
|
|
}
|
|
|
|
// This should fail
|
|
try
|
|
{
|
|
int j = view[3];
|
|
assert(false);
|
|
}
|
|
catch(RangeError e)
|
|
{
|
|
}
|
|
|
|
// Up-sizing past real size should not be allowed
|
|
try
|
|
{
|
|
view.length = view.length+1;
|
|
assert(false);
|
|
}
|
|
catch(RangeError e)
|
|
{
|
|
}
|
|
|
|
// Size to zero
|
|
view.length = 0;
|
|
assert(view.length == 0);
|
|
assert(view[] == []);
|
|
}
|
|
|
|
unittest
|
|
{
|
|
View!(int) view;
|
|
view ~= 1;
|
|
view ~= [2,3,4];
|
|
view ~= 5;
|
|
|
|
assert(view[0..5] == [1,2,3,4,5]);
|
|
} |