4
\$\begingroup\$

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?

asked May 25, 2017 at 12:11
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

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());
answered May 25, 2017 at 12:43
\$\endgroup\$
5
  • \$\begingroup\$ Constraint & SRP: I initially was building this only for Nullable objects, hence the where TSource : class ( probably not the best way ) , and giving non-Nullables 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\$ Commented 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\$ Commented May 25, 2017 at 16:45
  • 1
    \$\begingroup\$ @auhmaan assuming you test it with int and since sizeof(int) == 4 bytes, so 4 * 10.000 * 10.000 = 400mb this is just for the input, then the extension generates an output which is approx of the same size, thus 800mb. A lot of data requires a lot of memory. \$\endgroup\$ Commented 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 gets 400 MB at startup for the initial array, then up to 800 MB after calling one of the Trim extensions, then I would expect to GC collect the 400 MB from the extension, which doesn't happen. Is this a leak, or something badly coded? \$\endgroup\$ Commented 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\$ Commented May 26, 2017 at 13:59

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.