After stumbling upon this question on StackOverflow, I started playing with some extension methods to trim null
objects from a bi-dimensional array.
This is what I've came with so far:
public static class TrimArray {
/// <summary>
/// Trims the outer layers of null objects;
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <returns>An array with the outer layers of null objects trimmed.</returns>
public static TSource[,] Trim<TSource>( this TSource[,] sourceArray ) where TSource : class {
return sourceArray.CalculateTrim( true, true, true, true );
}
/// <summary>
/// Trims the outer top and bottom layers of null objects;
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <returns>An array with the outer top and bottom layers of null objects trimmed.</returns>
public static TSource[,] TrimVertical<TSource>( this TSource[,] sourceArray ) where TSource : class {
return sourceArray.CalculateTrim( true, false, false, true );
}
/// <summary>
/// Trims the outer left and right layers of null objects;
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <returns>An array with the outer left and right layers of null objects trimmed.</returns>
public static TSource[,] TrimHorizontal<TSource>( this TSource[,] sourceArray ) where TSource : class {
return sourceArray.CalculateTrim( false, true, true, false );
}
/// <summary>
/// Trims the outer top layers of null objects;
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <returns>An array with the outer top layers of null objects trimmed.</returns>
public static TSource[,] TrimTop<TSource>( this TSource[,] sourceArray ) where TSource : class {
return sourceArray.CalculateTrim( true, false, false, false );
}
/// <summary>
/// Trims the outer left layers of null objects;
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <returns>An array with the outer left layers of null objects trimmed.</returns>
public static TSource[,] TrimLeft<TSource>( this TSource[,] sourceArray ) where TSource : class {
return sourceArray.CalculateTrim( false, true, false, false );
}
/// <summary>
/// Trims the outer right layers of null objects;
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <returns>An array with the outer right layers of null objects trimmed.</returns>
public static TSource[,] TrimRight<TSource>( this TSource[,] sourceArray ) where TSource : class {
return sourceArray.CalculateTrim( false, false, true, false );
}
/// <summary>
/// Trims the outer bottom layers of null objects;
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <returns>An array with the outer bottom layers of null objects trimmed.</returns>
public static TSource[,] TrimBottom<TSource>( this TSource[,] sourceArray ) where TSource : class {
return sourceArray.CalculateTrim( false, false, false, true );
}
/// <summary>
/// Calculates the layers to be trimmed.
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <param name="trimTop">True to trim the top of the array, otherwise false.</param>
/// <param name="trimLeft">True to trim the left of the array, otherwise false.</param>
/// <param name="trimRight">True to trim the right of the array, otherwise false.</param>
/// <param name="trimBottom">True to trim the bottom of the array, otherwise false.</param>
/// <returns>An array with the outer layers of null objects trimmed.</returns>
private static TSource[,] CalculateTrim<TSource>( this TSource[,] sourceArray, Boolean trimTop, Boolean trimLeft, Boolean trimRight, Boolean trimBottom ) where TSource : class {
if( sourceArray == null
|| ( sourceArray.GetLength( 0 ) == 0 && sourceArray.GetLength( 1 ) == 0 )
|| ( !trimTop && !trimLeft && !trimRight && !trimBottom ) ) {
return sourceArray;
}
Int32
top = 0,
left = 0,
right = sourceArray.GetLength( 1 ) - 1,
bottom = sourceArray.GetLength( 0 ) - 1;
if( trimTop ) {
top = sourceArray.CalculateTrimTop();
}
if( trimLeft ) {
left = sourceArray.CalculateTrimLeft();
}
if( trimRight ) {
right = sourceArray.CalculateTrimRight();
}
if( trimBottom ) {
bottom = sourceArray.CalculateTrimBottom();
}
return sourceArray.Trim( top, left, right, bottom );
}
/// <summary>
/// Calculates the top limit to be trimmed.
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <returns>The top limit to be trimmed.</returns>
private static Int32 CalculateTrimTop<TSource>( this TSource[,] sourceArray ) where TSource : class {
for( Int32 yIndex = 0, yIndexLimit = sourceArray.GetLength( 0 ); yIndex < yIndexLimit; yIndex++ ) {
for( Int32 xIndex = 0, xIndexLimit = sourceArray.GetLength( 1 ); xIndex < xIndexLimit; xIndex++ ) {
if( sourceArray[ yIndex, xIndex ] != null ) {
return yIndex;
}
}
}
return 0;
}
/// <summary>
/// Calculates the left limit to be trimmed.
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <returns>The left limit to be trimmed.</returns>
private static Int32 CalculateTrimLeft<TSource>( this TSource[,] sourceArray ) where TSource : class {
for( Int32 xIndex = 0, xIndexLimit = sourceArray.GetLength( 1 ); xIndex < xIndexLimit; xIndex++ ) {
for( Int32 yIndex = 0, yIndexLimit = sourceArray.GetLength( 0 ); yIndex < yIndexLimit; yIndex++ ) {
if( sourceArray[ yIndex, xIndex ] != null ) {
return xIndex;
}
}
}
return 0;
}
/// <summary>
/// Calculates the right limit to be trimmed.
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <returns>The right limit to be trimmed.</returns>
private static Int32 CalculateTrimRight<TSource>( this TSource[,] sourceArray ) where TSource : class {
for( Int32 xIndex = sourceArray.GetLength( 1 ) - 1; xIndex >= 0; xIndex-- ) {
for( Int32 yIndex = sourceArray.GetLength( 0 ) - 1; yIndex >= 0; yIndex-- ) {
if( sourceArray[ yIndex, xIndex ] != null ) {
return xIndex;
}
}
}
return sourceArray.GetLength( 1 ) - 1;
}
/// <summary>
/// Calculates the bottom limit to be trimmed.
/// </summary>
/// <typeparam name="TSource">A <see cref="Nullable"/> type object.</typeparam>
/// <param name="sourceArray">The source array to be trimmed</param>
/// <returns>The bottom limit to be trimmed.</returns>
private static Int32 CalculateTrimBottom<TSource>( this TSource[,] sourceArray ) where TSource : class {
for( Int32 yIndex = sourceArray.GetLength( 0 ) - 1; yIndex >= 0; yIndex-- ) {
for( Int32 xIndex = sourceArray.GetLength( 1 ) - 1; xIndex >= 0; xIndex-- ) {
if( sourceArray[ yIndex, xIndex ] != null ) {
return yIndex;
}
}
}
return sourceArray.GetLength( 0 ) - 1;
}
/// <summary>
/// Trims an array
/// </summary>
/// <typeparam name="TSource">The array object type.</typeparam>
/// <param name="sourceArray">The source array to be trimmed.</param>
/// <param name="top">The exclusive top limit to start the vertical trim.</param>
/// <param name="left">The exclusive left limit to start the horizontal trim.</param>
/// <param name="right">The exclusive right limit to end the horizontal trim.</param>
/// <param name="bottom">The exclusive bottom limit to end the vertical trim.</param>
/// <returns>The array trimmed.</returns>
public static TSource[,] Trim<TSource>( this TSource[,] sourceArray, Int32 top, Int32 left, Int32 right, Int32 bottom ) {
if( sourceArray == null || ( sourceArray.GetLength( 0 ) == 0 && sourceArray.GetLength( 1 ) == 0 ) ) {
return sourceArray;
}
if( top > bottom ) {
throw new ArgumentException( $"'{nameof( top )}' must be lower or equal to '{nameof( bottom )}'" );
}
if( left > right ) {
throw new ArgumentException( $"'{nameof( left )}' must be lower or equal to '{nameof( right )}'" );
}
if( top < 0 ) {
throw new ArgumentException( $"'{nameof( top )}' ({top}) must be greater or equal to 0", nameof( top ) );
}
if( left < 0 ) {
throw new ArgumentException( $"'{nameof( left )}' ({left}) must be greater or equal to 0", nameof( left ) );
}
if( right >= sourceArray.GetLength( 1 ) ) {
throw new ArgumentException( $"'{nameof( right )}' ({right}) must be lower than the {nameof( sourceArray )} 1-dimension ({sourceArray.GetLength( 1 )})", nameof( right ) );
}
if( bottom >= sourceArray.GetLength( 0 ) ) {
throw new ArgumentException( $"'{nameof( bottom )}' ({bottom}) must be lower than the {nameof( sourceArray )} 0-dimension ({sourceArray.GetLength( 0 )})", nameof( bottom ) );
}
TSource[,]
trimmedArray = new TSource[ bottom - top + 1, right - left + 1 ];
for( Int32 yIndex = 0; yIndex < trimmedArray.GetLength( 0 ); yIndex++ ) {
for( Int32 xIndex = 0; xIndex < trimmedArray.GetLength( 1 ); xIndex++ ) {
trimmedArray[ yIndex, xIndex ] = sourceArray[ top + yIndex, left + xIndex ];
}
}
return trimmedArray;
}
}
So far, these methods only perform the basic trimming -- it searches for boundaries where a non null
objects are present, and limits to those limits to create a trimmed array.
Things that I'm absolutely sure I need to improve: Methods summaries -- I still need to learn better ways to explain the inputs and outputs plus limitations to it.
Thoughts on this?
1 Answer 1
Constraint & SRP
where TSource : class
This constraint should not be there. Instead you should have another extension that calculates the depth of each trim and pass those numbers into another extnsion that works with numbers. This way you would separate the logic for searching null
from trimming and the trim could be more general.
If you have done that, you could have use a single loop from the question you've liked to.
When those two concerns are separated you can also test and optimize them separately.
Order of trim arguments
Boolean trimTop, Boolean trimLeft, Boolean trimRight, Boolean trimBottom
The order the trim arguments is weird. It doesn't follow any logic. It should be clockwise like in WPF or CSS: top, right, bottom, left.
LINQ
You have really a lot of loops there. They can all be replaced with LINQ and if you only use a jagged array instead of a multidimensional one it's quite easy to achieve.
public static IEnumerable<IEnumerable<T>> Trim<T>(this ICollection<ICollection<T>> values, int top, int right, int bottom, int left)
{
return
values
.Skip(top)
.Select(x => x.Skip(left).Take(x.Count - right - left))
.Take(values.Count - bottom - top);
}
I already hear the voices saying oh no!, this is so slow!. Well, if you think that, then you can implement your own Skip
and Take
that are aware of ICollection
and don't have to iterate the entire collection. Then it'll be much faster but only if you have arrays with an insane number of items.
var array = new[]
{
new [] { 1, 2, 3 },
new [] { 4, 5, 6 },
new [] { 7, 8, 9 }
};
var result = array.Trim(1, 0, 0, 1);
Anonymous tuples
With C# 7 you could use anonymous tuples for the trim arguments where the first extension searches for null
in any last row/column
public static (int top, int right, int bottom, int left) CalcTrim<T>(this ICollection<ICollection<T>> values) where T : class
{
return (
values.FirstOrDefault().Any(x => x == null) ? 1 : 0,
values.Select(x => x.Last()).Any(x => x == null) ? 1 : 0,
values.LastOrDefault().Any(x => x == null) ? 1 : 0,
values.Select(x => x.First()).Any(x => x == null) ? 1 : 0
);
}
and the previous extension now instead of single values, uses a tuple:
public static IEnumerable<IEnumerable<T>> Trim<T>(this ICollection<ICollection<T>> values, (int top, int right, int bottom, int left) trim)
{
return
values
.Skip(trim.top)
.Select(x => x.Skip(trim.left).Take(x.Count - trim.right - trim.left))
.Take(values.Count - trim.bottom - trim.top);
}
var array = new[]
{
new [] { null, "2", "3" },
new [] { "4", "5", "6" },
new [] { "7", "8", "9" }
};
var trimmed = array.Trim(array.CalcTrim());
-
\$\begingroup\$ Constraint & SRP: I initially was building this only for
Nullable
objects, hence thewhere TSource : class
( probably not the best way ) , and giving non-Nullable
s love later. Order of trim arguments I agree with you on this one. LINQ I'll need to study this one seriously. I initially tried to calculate everything on a single block with only 2 loops, but since I might not need everything, I've separated them into methods. Anonymous tuples This is simply scary. Amusing, but scary. \$\endgroup\$auhmaan– auhmaan2017年05月25日 13:49:36 +00:00Commented May 25, 2017 at 13:49 -
\$\begingroup\$ If I may ask a question here, does this code leak? I'm trying to do some insane tests -- arrays with a dimension of 10.000 by 10.000 -- and the memory does not stop getting higher ( usually 400MB -> 600MB -> 800MB ). Unless I'm blind, I'm not forgetting to clear vars -- nor should I, since none of them are
IDisposible
, or should I? \$\endgroup\$auhmaan– auhmaan2017年05月25日 16:45:33 +00:00Commented May 25, 2017 at 16:45 -
1\$\begingroup\$ @auhmaan assuming you test it with
int
and sincesizeof(int) == 4
bytes, so4 * 10.000 * 10.000 = 400mb
this is just for the input, then the extension generates an output which is approx of the same size, thus800mb
. A lot of data requires a lot of memory. \$\endgroup\$t3chb0t– t3chb0t2017年05月25日 17:43:36 +00:00Commented May 25, 2017 at 17:43 -
\$\begingroup\$ I'm aware of the memory space required for it. What I meant was, after calling a
Trim
, the memory wasn't freed, and only went up. The program gets400 MB
at startup for the initial array, then up to800 MB
after calling one of theTrim
extensions, then I would expect toGC
collect the400 MB
from the extension, which doesn't happen. Is this a leak, or something badly coded? \$\endgroup\$auhmaan– auhmaan2017年05月26日 13:54:33 +00:00Commented May 26, 2017 at 13:54 -
\$\begingroup\$ @auhmaan there is no leak and you cannot tell when the garbage collector runs. It does that when it finds it's suitable. There is nothing badly coded. Besides as long as the data is referenced anywhere, it'll remain in memory. You can trigger the GC to run but usially this isn't a good idea. \$\endgroup\$t3chb0t– t3chb0t2017年05月26日 13:59:19 +00:00Commented May 26, 2017 at 13:59