From 7d799e46df304b9459953113f9d390862b56eb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cosmin=20Cre=C8=9Bu?= Date: Mon, 1 Sep 2025 23:04:20 +0100 Subject: [PATCH 1/4] Create EnumerableBatch.cs --- src/SimpliSharp/Extensions/EnumerableBatch.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/SimpliSharp/Extensions/EnumerableBatch.cs diff --git a/src/SimpliSharp/Extensions/EnumerableBatch.cs b/src/SimpliSharp/Extensions/EnumerableBatch.cs new file mode 100644 index 0000000..3927a18 --- /dev/null +++ b/src/SimpliSharp/Extensions/EnumerableBatch.cs @@ -0,0 +1,67 @@ +namespace SimpliSharp.Extensions.Batch; + +public static class EnumerableExtensions +{ + /// + /// Splits an enumerable sequence into batches of a specified size, creating batches with original object references. + /// + /// + /// This method is optimized for arrays (`T[]`) for potential performance benefits. + /// For other `IEnumerable` types, it uses a standard iteration approach. + /// The method uses deferred execution (yield return). + /// The last batch may contain fewer items than if the total number of items + /// is not evenly divisible by . + /// + /// The type of elements in the sequence. + /// The source enumerable sequence to batch. + /// The desired maximum size for each batch. + /// An `IEnumerable` where each element is an array representing a batch. + /// Thrown if is null. + /// Thrown if is less than or equal to 0. + public static IEnumerable Batch(this IEnumerable source, int batchSize) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (batchSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(batchSize), "Batch size must be positive."); + } + + // Handle array case for efficiency. No new copies of objects are made. + if (source is T[] sourceArray) + { + for (int i = 0; i < sourceArray.Length;) + { + int currentBatchSize = Math.Min(batchSize, sourceArray.Length - i); + T[] batch = new T[currentBatchSize]; + Array.Copy(sourceArray, i, batch, 0, currentBatchSize); + yield return batch; + i += currentBatchSize; + } + } + else + { + // Handle non-array enumerables + List currentBatch = new List(batchSize); + using IEnumerator enumerator = source.GetEnumerator(); + while (enumerator.MoveNext()) + { + currentBatch.Add(enumerator.Current); + if (currentBatch.Count == batchSize) + { + yield return currentBatch.ToArray(); + currentBatch.Clear(); // Reuse the list + } + } + + // Yield the last partial batch if it has items + if (currentBatch.Count > 0) + { + yield return currentBatch.ToArray(); + } + } + } +} \ No newline at end of file From 7078ab6e7a82d33ff15da03dd3cfa1ca504a5214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cosmin=20Cre=C8=9Bu?= Date: Mon, 1 Sep 2025 23:28:59 +0100 Subject: [PATCH 2/4] Adding sliding batching --- src/SimpliSharp/Extensions/EnumerableBatch.cs | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/src/SimpliSharp/Extensions/EnumerableBatch.cs b/src/SimpliSharp/Extensions/EnumerableBatch.cs index 3927a18..b3e8baa 100644 --- a/src/SimpliSharp/Extensions/EnumerableBatch.cs +++ b/src/SimpliSharp/Extensions/EnumerableBatch.cs @@ -3,14 +3,13 @@ namespace SimpliSharp.Extensions.Batch; public static class EnumerableExtensions { /// - /// Splits an enumerable sequence into batches of a specified size, creating batches with original object references. + /// Splits an enumerable sequence into non-overlapping batches of a specified size. /// /// - /// This method is optimized for arrays (`T[]`) for potential performance benefits. - /// For other `IEnumerable` types, it uses a standard iteration approach. - /// The method uses deferred execution (yield return). - /// The last batch may contain fewer items than if the total number of items - /// is not evenly divisible by . + /// This method partitions the source sequence into chunks. + /// For example, `[1,2,3,4,5,6]` with a batch size of 3 results in `[1,2,3]` and `[4,5,6]`. + /// The last batch may contain fewer items. + /// This method uses deferred execution. /// /// The type of elements in the sequence. /// The source enumerable sequence to batch. @@ -64,4 +63,48 @@ public static IEnumerable Batch(this IEnumerable source, int batchSiz } } } + + /// + /// Creates overlapping batches from an enumerable sequence using a sliding window approach. + /// + /// + /// This method generates batches by taking sections of the source sequence. + /// For example, `[1,2,3,4,5,6]` with a window size of 3 results in `[1,2,3]`, `[2,3,4]`, `[3,4,5]`, and `[4,5,6]`. + /// Unlike the `Batch` method, all returned batches will have the exact size of . + /// If the source sequence contains fewer items than the , no batches will be returned. + /// This method uses deferred execution. + /// + /// The type of elements in the sequence. + /// The source enumerable sequence. + /// The exact size for each sliding window batch. + /// An `IEnumerable` where each element is an array representing a sliding window batch. + /// Thrown if is null. + /// Thrown if is less than or equal to 0. + public static IEnumerable BatchSliding(this IEnumerable source, int windowSize) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (windowSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(windowSize), "Window size must be positive."); + } + + // Eagerly convert to an array to allow for indexed access, which is required for a sliding window. + var sourceArray = source.ToArray(); + + // Determine the number of possible windows we can create. + int possibleWindows = sourceArray.Length - windowSize + 1; + + // Iterate from the first possible window to the last. + for (int i = 0; i < possibleWindows; i++) + { + // Create a new array for the current window. + T[] window = new T[windowSize]; + Array.Copy(sourceArray, i, window, 0, windowSize); + yield return window; + } + } } \ No newline at end of file From fbad393c9bd9e7a48a95572afcb12ce5fed1b8d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cosmin=20Cre=C8=9Bu?= Date: Tue, 2 Sep 2025 18:28:33 +0100 Subject: [PATCH 3/4] Unit tests --- .../Extensions/BatchSlidingTests.cs | 83 ++++++++++++++ .../Extensions/BatchTests.cs | 103 ++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 tests/SimpliSharp.Tests/Extensions/BatchSlidingTests.cs create mode 100644 tests/SimpliSharp.Tests/Extensions/BatchTests.cs diff --git a/tests/SimpliSharp.Tests/Extensions/BatchSlidingTests.cs b/tests/SimpliSharp.Tests/Extensions/BatchSlidingTests.cs new file mode 100644 index 0000000..36b3342 --- /dev/null +++ b/tests/SimpliSharp.Tests/Extensions/BatchSlidingTests.cs @@ -0,0 +1,83 @@ +using SimpliSharp.Extensions.Batch; + +namespace SimpliSharp.Tests.MSTest; + +[TestClass] +public class BatchSlidingTests +{ + [TestMethod] + public void BatchSliding_WithStandardInput_ShouldReturnOverlappingWindows() + { + // Arrange + int[] source = { 1, 2, 3, 4, 5, 6 }; + int windowSize = 3; + var expected = new[] + { + new[] { 1, 2, 3 }, + new[] { 2, 3, 4 }, + new[] { 3, 4, 5 }, + new[] { 4, 5, 6 } + }; + + // Act + var result = source.BatchSliding(windowSize).ToList(); + + // Assert + Assert.AreEqual(expected.Length, result.Count); + for (int i = 0; i < expected.Length; i++) + { + CollectionAssert.AreEqual(expected[i], result[i]); + } + } + + [TestMethod] + public void BatchSliding_WithWindowSizeEqualToSourceLength_ShouldReturnOneWindow() + { + // Arrange + int[] source = { 1, 2, 3, 4 }; + int windowSize = 4; + + // Act + var result = source.BatchSliding(windowSize).ToList(); + + // Assert + Assert.AreEqual(1, result.Count); + CollectionAssert.AreEqual(source, result[0]); + } + + [TestMethod] + public void BatchSliding_WithWindowSizeLargerThanSource_ShouldReturnEmptyResult() + { + // Arrange + int[] source = { 1, 2, 3 }; + int windowSize = 4; + + // Act + var result = source.BatchSliding(windowSize).ToList(); + + // Assert + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void BatchSliding_WithNullSource_ShouldThrowArgumentNullException() + { + // Arrange + IEnumerable? source = null; + + // Act & Assert + Assert.ThrowsException(() => source!.BatchSliding(3).ToList()); + } + + [DataTestMethod] + [DataRow(0)] + [DataRow(-1)] + public void BatchSliding_WithNonPositiveWindowSize_ShouldThrowArgumentOutOfRangeException(int windowSize) + { + // Arrange + int[] source = { 1, 2, 3 }; + + // Act & Assert + Assert.ThrowsException(() => source.BatchSliding(windowSize).ToList()); + } +} \ No newline at end of file diff --git a/tests/SimpliSharp.Tests/Extensions/BatchTests.cs b/tests/SimpliSharp.Tests/Extensions/BatchTests.cs new file mode 100644 index 0000000..74b1ceb --- /dev/null +++ b/tests/SimpliSharp.Tests/Extensions/BatchTests.cs @@ -0,0 +1,103 @@ +using SimpliSharp.Extensions.Batch; + +namespace SimpliSharp.Tests.MSTest; + +[TestClass] +public class BatchTests +{ + [TestMethod] + public void Batch_WithArraySourceAndEvenlyDivisible_ShouldReturnFullBatches() + { + // Arrange + int[] source = { 1, 2, 3, 4, 5, 6 }; + int batchSize = 3; + var expected = new[] + { + new[] { 1, 2, 3 }, + new[] { 4, 5, 6 } + }; + + // Act + var result = source.Batch(batchSize).ToList(); + + // Assert + Assert.AreEqual(expected.Length, result.Count); + CollectionAssert.AreEqual(expected[0], result[0]); + CollectionAssert.AreEqual(expected[1], result[1]); + } + + [TestMethod] + public void Batch_WithListSourceAndUnevenlyDivisible_ShouldReturnLastPartialBatch() + { + // Arrange + var source = new List { 1, 2, 3, 4, 5, 6, 7 }; + int batchSize = 3; + var expected = new[] + { + new[] { 1, 2, 3 }, + new[] { 4, 5, 6 }, + new[] { 7 } + }; + + // Act + var result = source.Batch(batchSize).ToList(); + + // Assert + Assert.AreEqual(expected.Length, result.Count); + for (int i = 0; i < expected.Length; i++) + { + CollectionAssert.AreEqual(expected[i], result[i]); + } + } + + [TestMethod] + public void Batch_WithBatchSizeLargerThanSource_ShouldReturnSingleBatch() + { + // Arrange + int[] source = { 1, 2, 3 }; + int batchSize = 5; + + // Act + var result = source.Batch(batchSize).ToList(); + + // Assert + Assert.AreEqual(1, result.Count); + CollectionAssert.AreEqual(source, result[0]); + } + + [TestMethod] + public void Batch_WithEmptySource_ShouldReturnEmptyResult() + { + // Arrange + var source = Enumerable.Empty(); + int batchSize = 5; + + // Act + var result = source.Batch(batchSize).ToList(); + + // Assert + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void Batch_WithNullSource_ShouldThrowArgumentNullException() + { + // Arrange + IEnumerable? source = null; + + // Act & Assert + Assert.ThrowsException(() => source!.Batch(3).ToList()); + } + + [DataTestMethod] + [DataRow(0)] + [DataRow(-1)] + public void Batch_WithNonPositiveBatchSize_ShouldThrowArgumentOutOfRangeException(int batchSize) + { + // Arrange + int[] source = { 1, 2, 3 }; + + // Act & Assert + Assert.ThrowsException(() => source.Batch(batchSize).ToList()); + } +} \ No newline at end of file From f1eb07df96e2412f8119ebffff1980bff34fa0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cosmin=20Cre=C8=9Bu?= Date: Wed, 3 Sep 2025 08:46:09 +0100 Subject: [PATCH 4/4] Updating demo and readme --- README.md | 27 ++++- samples/SimpliSharp.Demo/Program.cs | 160 ++++++++++++++++++++-------- 2 files changed, 141 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 3fd1033..c56818e 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,7 @@ SimpliSharp is a .NET library designed to simplify common programming tasks with a focus on performance and ease of use. As the library grows, it will be populated with a variety of tools and utilities to help developers write cleaner, more efficient code. -## Features - -### SmartDataProcessor +## SmartDataProcessor The `SmartDataProcessor` is designed to process a queue of items in parallel, while automatically adjusting the level of concurrency to stay within a specified CPU usage limit. @@ -27,3 +25,26 @@ for (...) ``` ![Alt text for your image](https://raw.githubusercontent.com/cretucosmin3/SimpliSharp/refs/heads/main/assets/75-cpu-usage.png) + + +## Enumerable Extensions + +### Batching + +Batching enumerables using `Enumerables.Batch(batchSize)` or `Enumerables.BatchSliding(windowSize)` will simply yield the requested batches, + +```csharp +string[] sample = ["Red", "Blue", "Purple", "Black", "Yellow", "Pink"]; +string[][] batches = sample.Batch(2).ToArray(); + +// Batch 1: [Red, Blue, Purple] +// Batch 2: [Black, Yellow, Pink] +``` + +```csharp +int[] sample = [1, 2, 3]; +int[][] batches = sample.BatchSliding(2).ToArray(); + +// Batch 1: [1, 2] +// Batch 2: [2, 3] +``` \ No newline at end of file diff --git a/samples/SimpliSharp.Demo/Program.cs b/samples/SimpliSharp.Demo/Program.cs index f37abe4..331180f 100644 --- a/samples/SimpliSharp.Demo/Program.cs +++ b/samples/SimpliSharp.Demo/Program.cs @@ -1,53 +1,127 @@ -using SimpliSharp.Utilities.Process; +using SimpliSharp.Extensions.Batch; +using SimpliSharp.Utilities.Process; -class Program +Console.WriteLine("SimpliSharp Demo Application"); +Console.WriteLine("---------------------------"); + +Console.WriteLine("Available Demos:"); +Console.WriteLine("1. SmartDataProcessor Example"); +Console.WriteLine("2. Enumerable.Batch"); +Console.WriteLine("3. Enumerable.BatchSliding"); + +Console.WriteLine("[Enter] to exit"); + +var choice = Console.ReadKey(); + +switch (choice.Key) { - static void Main() + case ConsoleKey.D1: + case ConsoleKey.NumPad1: + SmartDataProcessor_Example(); + break; + case ConsoleKey.D2: + case ConsoleKey.NumPad2: + EnumerableBatch_Example(); + break; + case ConsoleKey.D3: + case ConsoleKey.NumPad3: + EnumerableBatchSliding_Example(); + break; + default: + Console.WriteLine("\nExiting..."); + return; +} + +static void SmartDataProcessor_Example() +{ + Console.Clear(); + Console.WriteLine("Starting data processing..."); + + using var processor = new SmartDataProcessor(maxCpuUsage: 90); + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + var tasksCount = 2000; + + for (int i = 0; i < tasksCount; i++) { - Console.WriteLine("Starting data processing..."); + int line = i; - using var processor = new SmartDataProcessor(maxCpuUsage: 90); - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - var tasksCount = 2000; - - for (int i = 0; i < tasksCount; i++) + processor.EnqueueOrWait(line, data => { - int line = i; + int simMax = Random.Shared.Next(5_000_000, 20_000_000); + double sum = 0; - processor.EnqueueOrWait(line, data => + for (int j = 0; j < 10_000_000; j++) { - int simMax = Random.Shared.Next(5_000_000, 20_000_000); - double sum = 0; - - for (int j = 0; j < 10_000_000; j++) - { - double value = Math.Sqrt(j) * Math.Sin(j % 360) + Math.Log(j + 1); - if (value > 1000) - sum -= value / 3.0; - else - sum += value * 2.5; - } - }); - - Console.SetCursorPosition(0, Console.CursorTop); - Console.Write($"Processing item {i + 1} of {tasksCount} | queued: {processor.Metrics.QueueLength}"); - } - - processor.WaitForAllAsync().Wait(); - stopwatch.Stop(); - - Console.WriteLine(); - Console.WriteLine($"Processing completed in {stopwatch.Elapsed.TotalSeconds} seconds"); - - var finalMetrics = processor.Metrics; - Console.WriteLine(); - Console.WriteLine("--- Final Metrics ---"); - Console.WriteLine($"Max Concurrency: {finalMetrics.MaxConcurrency}"); - Console.WriteLine($"Fastest Job: {finalMetrics.MinTaskTime:F2}ms"); - Console.WriteLine($"Slowest Job: {finalMetrics.MaxTaskTime:F2}ms"); - Console.WriteLine($"Average Job: {finalMetrics.AvgTaskTime:F2}ms"); - - Console.WriteLine("All processing done"); + double value = Math.Sqrt(j) * Math.Sin(j % 360) + Math.Log(j + 1); + if (value > 1000) + sum -= value / 3.0; + else + sum += value * 2.5; + } + }); + + Console.SetCursorPosition(0, Console.CursorTop); + Console.Write($"Processing item {i + 1} of {tasksCount} | queued: {processor.Metrics.QueueLength}"); + } + + processor.WaitForAllAsync().Wait(); + stopwatch.Stop(); + + Console.WriteLine(); + Console.WriteLine($"Processing completed in {stopwatch.Elapsed.TotalSeconds} seconds"); + + var finalMetrics = processor.Metrics; + Console.WriteLine(); + Console.WriteLine("--- Final Metrics ---"); + Console.WriteLine($"Max Concurrency: {finalMetrics.MaxConcurrency}"); + Console.WriteLine($"Fastest Job: {finalMetrics.MinTaskTime:F2}ms"); + Console.WriteLine($"Slowest Job: {finalMetrics.MaxTaskTime:F2}ms"); + Console.WriteLine($"Average Job: {finalMetrics.AvgTaskTime:F2}ms"); + + Console.WriteLine("All processing done"); +} + +static void EnumerableBatch_Example() +{ + Console.Clear(); + Console.WriteLine("Starting Enumerable.Batch demo..."); + + // yields: [ ["Red", "Blue"], ["Purple", "Black"], ["Yellow", "Pink"] ] + + + string[] sample = ["Red", "Blue", "Purple", "Black", "Yellow", "Pink"]; + int batchSize = 3; + + var batches = sample.Batch(batchSize).ToList(); + + Console.WriteLine($"Sample numbers: [{string.Join(", ", batches)}]"); + Console.WriteLine($"Batch size: {batchSize}"); + Console.WriteLine(); + + Console.WriteLine($"{batches.Count} Batches created:"); + for (int i = 0; i < batches.Count; i++) + { + Console.WriteLine($"Batch {i + 1}: [{string.Join(", ", batches[i])}]"); } } +static void EnumerableBatchSliding_Example() +{ + Console.Clear(); + Console.WriteLine("Starting Enumerable.BatchSliding demo..."); + + var numbers = Enumerable.Range(1, 3); + int batchSize = 2; + + var batches = numbers.BatchSliding(batchSize).ToList(); + + Console.WriteLine(); + Console.WriteLine($"Sample numbers: [{string.Join(", ", numbers)}]"); + Console.WriteLine(); + + Console.WriteLine($"{batches.Count} batches created:"); + for (int i = 0; i < batches.Count; i++) + { + Console.WriteLine($"Batch {i + 1}: [{string.Join(", ", batches[i])}]"); + } +} \ No newline at end of file