Skip to content

Instantly share code, notes, and snippets.

@jnm2
Created August 24, 2019 22:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnm2/18cde11aae7e765465f64f5e2d387616 to your computer and use it in GitHub Desktop.
Save jnm2/18cde11aae7e765465f64f5e2d387616 to your computer and use it in GitHub Desktop.
using System;
/// <summary>
/// Enables building an array by setting indexes in any order without having to use an <c>Add</c> method.
/// </summary>
public struct ArrayBuilder<T>
{
private T[] array;
private int usedLength;
/// <summary>
/// Initializes a new instance of the <see cref="ArrayBuilder{T}"/> struct.
/// </summary>
/// <param name="initialCapacity">
/// If non-zero, creates a starting array at the specified size. If zero, an array is not created until the
/// first index is set.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="initialCapacity"/> is less than zero.
/// </exception>
public ArrayBuilder(int initialCapacity)
{
if (initialCapacity <= 0)
{
if (initialCapacity < 0)
throw new ArgumentOutOfRangeException(nameof(initialCapacity), initialCapacity, "Initial capacity must be greater than or equal to zero.");
array = null;
}
else
{
array = new T[initialCapacity];
}
usedLength = 0;
}
/// <summary>
/// <para>
/// Gets or sets a value at the specified index in the array being built. The array is expanded as necessary to
/// accommodate setting any index.
/// </para>
/// <para>
/// Getting an index past the current end does not expand the array. It returns the default value of
/// <typeparamref name="T"/>, which is what getting an unset index within the array would return.
/// </para>
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="index"/> is less than zero.
/// </exception>
public T this[int index]
{
get
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index), index, "Index must be greater than or equal to zero.");
return array != null && index < array.Length ? array[index] : default;
}
set
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index), index, "Index must be greater than or equal to zero.");
var requiredLength = index + 1;
if (usedLength < requiredLength)
{
usedLength = requiredLength;
CommonUtils.EnsureCapacity(ref array, requiredLength);
}
array[index] = value;
}
}
/// <summary>
/// Obtains the built result in <see cref="ArraySegment{T}"/> form and clears the builder.
/// </summary>
public ArraySegment<T> MoveToArraySegment()
{
var segment = new ArraySegment<T>(array ?? Array.Empty<T>(), offset: 0, usedLength);
this = default;
return segment;
}
}
using System;
using System.Runtime.CompilerServices;
using NUnit.Framework;
using Shouldly;
public static class ArrayBuilderTests
{
[Test]
public static void Initial_capacity_must_not_be_negative()
{
var ex = Should.Throw<ArgumentOutOfRangeException>(() => new ArrayBuilder<int>(initialCapacity: -1));
ex.ParamName.ShouldBe("initialCapacity");
ex.ActualValue.ShouldBe(-1);
}
[Test]
public static void Initial_capacity_of_zero_is_identical_to_default_value()
{
RuntimeHelpers.Equals(new ArrayBuilder<int>(initialCapacity: 0), default(ArrayBuilder<int>)).ShouldBeTrue();
}
[Test]
public static void Default_instance_returns_valid_segment()
{
default(ArrayBuilder<int>).MoveToArraySegment().Array.ShouldBeSameAs(Array.Empty<int>());
}
[Test]
public static void Default_instance_allows_reads_at_any_nonnegative_index([Values(0, 1, int.MaxValue)] int index)
{
default(ArrayBuilder<int>)[index].ShouldBe(default);
}
[Test]
public static void Default_instance_allows_writes_at_any_nonnegative_index([Values(0, 1, 10_000)] int index)
{
var builder = default(ArrayBuilder<int>);
builder[index] = 42;
builder[index].ShouldBe(42);
builder.MoveToArraySegment().Array[index].ShouldBe(42);
}
[Test]
public static void Reads_are_not_allowed_at_negative_indexes()
{
var builder = new ArrayBuilder<int>();
var ex = Should.Throw<ArgumentOutOfRangeException>(() => _ = builder[-1]);
ex.ParamName.ShouldBe("index");
ex.ActualValue.ShouldBe(-1);
}
[Test]
public static void Writes_are_not_allowed_at_negative_indexes()
{
var builder = new ArrayBuilder<int>();
var ex = Should.Throw<ArgumentOutOfRangeException>(() => builder[-1] = 42);
ex.ParamName.ShouldBe("index");
ex.ActualValue.ShouldBe(-1);
}
[Test]
public static void Segment_length_is_less_than_array_length_when_not_all_capacity_is_used()
{
var builder = new ArrayBuilder<int>(initialCapacity: 10)
{
[8] = 42
};
var segment = builder.MoveToArraySegment();
segment.Array.Length.ShouldBe(10);
segment.Count.ShouldBe(9);
}
[Test]
public static void Writes_past_initial_capacity_doubles_the_current_capacity()
{
var builder = new ArrayBuilder<int>(initialCapacity: 10)
{
[10] = 42
};
builder.MoveToArraySegment().Array.Length.ShouldBe(20);
}
[Test]
public static void MoveToArraySegment_clears_the_builder()
{
var builder = new ArrayBuilder<int> { [0] = 42 };
_ = builder.MoveToArraySegment();
builder[0].ShouldBe(default);
builder.MoveToArraySegment().Array.ShouldBeSameAs(Array.Empty<int>());
}
[Test]
public static void Changes_to_builder_do_not_affect_build_segment()
{
var builder = new ArrayBuilder<int> { [0] = 42 };
var segment = builder.MoveToArraySegment();
builder[0] = 41;
segment.Array[0].ShouldBe(42);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment