Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions Containers.Test/RingBufferTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,50 @@ public void ResampleWithSpecificValues_CorrectlyInterpolates()
// Resample to a larger size
buffer.Resample(5);

// Should interpolate to approximately: 10, 15, 20, 25, 30
Assert.AreEqual(10, buffer.At(0));
Assert.AreEqual(20, buffer.At(2));
Assert.AreEqual(30, buffer.At(4));
// Nearest-neighbour, not interpolation: five slots over three samples, so two of them
// appear twice and one once.
Assert.AreSequenceEqual([10, 10, 20, 30, 30], buffer);
}

[TestMethod]
[DataRow(new[] { 0, 1, 2, 3, 4 }, 9, new[] { 0, 0, 1, 1, 2, 3, 3, 4, 4 }, DisplayName = "5 to 9")]
[DataRow(new[] { 0, 10 }, 3, new[] { 0, 10, 10 }, DisplayName = "2 to 3")]
[DataRow(new[] { 0, 1, 2 }, 12, new[] { 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2 }, DisplayName = "3 to 12")]
[DataRow(new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, 3, new[] { 1, 4, 7 }, DisplayName = "9 to 3")]
public void Resample_PicksTheNearestSampleToEachSlotsCentre(int[] source, int length, int[] expected)
{
ArgumentNullException.ThrowIfNull(source);
RingBuffer<int> buffer = new(source, source.Length);

buffer.Resample(length);

Assert.AreSequenceEqual(expected, buffer);
}

[TestMethod]
public void Resample_Upsampling_RepeatsEverySampleEvenly()
{
for (int oldCount = 1; oldCount <= 12; oldCount++)
{
for (int length = oldCount; length <= 40; length++)
{
RingBuffer<int> buffer = new(Enumerable.Range(0, oldCount), oldCount);

buffer.Resample(length);

int fewest = length / oldCount;
int most = (length + oldCount - 1) / oldCount;
foreach (IGrouping<int, int> repeats in buffer.GroupBy(value => value))
{
Assert.IsTrue(
repeats.Count() >= fewest && repeats.Count() <= most,
$"{oldCount} to {length}: sample {repeats.Key} appears {repeats.Count()} times, expected {fewest} to {most}.");
}

Assert.AreEqual(0, buffer.Front(), $"{oldCount} to {length}: the first sample must come first.");
Assert.AreEqual(oldCount - 1, buffer.Back(), $"{oldCount} to {length}: the last sample must come last.");
}
}
}

[TestMethod]
Expand Down
24 changes: 13 additions & 11 deletions Containers/RingBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,15 @@ private static int NextPower2(int v)
public void Resize(int length) => AllocateBuffer(length);

/// <summary>
/// Resamples the buffer to a new length, interpolating or decimating the contents as needed.
/// Resamples the buffer to a new length, repeating or dropping elements as needed.
/// </summary>
/// <remarks>
/// The resampling process preserves the data pattern by applying simple linear interpolation.
/// If the new length is smaller than the old length, data is decimated (some values are dropped).
/// If the new length is larger, data is interpolated (new values are created between existing ones).
/// Each new slot takes the existing element nearest to its centre (nearest-neighbour), since a
/// generic <typeparamref name="T"/> cannot be interpolated. No new values are created.
/// If the new length is larger than the old length, every element is repeated as evenly as the
/// ratio allows: each appears either the rounded-down or the rounded-up number of times, and the
/// first and last elements stay first and last.
/// If the new length is smaller, elements are decimated: evenly spaced ones are kept and the rest dropped.
///
/// This is useful when you need to change the buffer size while preserving the overall pattern of the data.
/// For example, resampling time-series data when changing the sampling rate.
Expand Down Expand Up @@ -280,13 +283,12 @@ public void Resample(int length)
// Resample the data into the new buffer
for (int i = 0; i < length; i++)
{
// Map the new index to the old data range
// Multiply in double: the int product overflows once length * oldCount passes int.MaxValue
double oldIndex = (double)i * (oldCount - 1) / Math.Max(length - 1, 1);
int index = (int)Math.Round(oldIndex);

// Ensure we don't go out of bounds
index = Math.Min(index, oldCount - 1);
// Take the old element under the centre of new slot i: floor((i + 0.5) * oldCount / length).
// Sampling at the centre spreads repeats evenly where mapping the ends onto each other
// and rounding leaves ties that all round the same way. Doubling both sides keeps the
// arithmetic integral, so there are no floating-point ties, and the long product cannot
// overflow: (2 * i + 1) * oldCount stays below 2^63 for any int i and oldCount.
int index = (int)(((2L * i) + 1) * oldCount / (2L * length));

PushBack(oldData[index]);
}
Expand Down
Loading