ObiWanKenobi
ObiWanKenobi

Reputation: 14892

Round integer to nearest high number in array

I have an array of ints like this: [32,128,1024,2048,4096]

Given a specific value, I need to get the closest value in the array that is equal to, or higher than, the value.

I have the following code

  private int GetNextValidSize(int size, int[] validSizes)
  {

      int returnValue = size;

      for (int i = 0; i < validSizes.Length; i++)
      {
          if (validSizes[i] >= size)
          {
              returnValue = validSizes[i];
              break;
          }
      }

      return returnValue;
  }

It works, but is there any better/faster way to do it? The array will never contain more than 5-10 elements.

Clarification: I actually want to return the original value/size if it is bigger than any of the valid sizes. The validSizes array can be considered to always be sorted, and it will always contain at least one value.

Upvotes: 1

Views: 2252

Answers (7)

Larry Watanabe
Larry Watanabe

Reputation: 10184

It doesn't work. Here are 3 test cases where it fails. In fact, the function interface doesn't have any return result for failure.

I wrote a corrected version, GetNextValidSize2. Since there is no way to return a failure message, I throw an exception for those cases. Here are the results of the run:

test1 : GetNextValidSize failed test1 : GetNextValidSize2 passed test2 : GetNextValidSize Object reference not set to an instance of an object. test2 : GetNextValidSize2 validSizes is nothing test3 : GetNextValidSize passed test3 : GetNextValidSize2 No items in validSizes

By the way, LINQ may be simpler or easier, but it can hardly be more efficient. It can probably be equally efficient if the query optimizer/CLR optimizer work well.

Here's the code - it's in VB since that's what I'm using at the moment, don't want to switch mental gears:

Module Module1

''' <summary>
''' Error - does not work if validSizes is Nothing, or has 0 elements, or if
''' the list contains a validSize that is not the closest one before a closer one,
''' or there are no valid sizes.
''' </summary>
Public Function GetNextValidSize(ByVal size As Integer, ByVal validSizes As List(Of Integer)) As Integer
    Dim returnValue As Integer = size

    For i As Integer = 0 To validSizes.Count - 1 Step 1
        If validSizes.Item(i) >= size Then
            returnValue = validSizes.Item(i)
            Exit For
        End If
    Next
    Return returnValue
End Function

''' <summary>
''' Returns the closest item in validSizes that is >= size. Throws an exception if one cannot 
''' be found.
''' </summary>
 Public Function GetNextValidSize2(ByVal size As Integer, ByVal validSizes As List(Of Integer)) As Integer
    Dim closestValue As Integer = Integer.MaxValue
    Dim found As Boolean = False

    If validSizes Is Nothing Then
        Throw New Exception("validSizes is nothing")
    End If

    If validSizes.Count = 0 Then
        Throw New Exception("No items in validSizes")
    End If

    For Each x In validSizes
        If x >= size Then
            found = True
            If x < closestValue Then
                closestValue = x
            End If
        End If
    Next
    If Not found Then
        Throw New Exception("No items found")
    End If
    Return closestValue
End Function

''' <summary>
''' Output the result of a test.
''' </summary>
 Public Sub outputResult(ByVal testName As String, ByVal result As Boolean, ByVal funcName As String)
    Dim passFail As String
    If result Then
        passFail = " passed"
    Else
        passFail = " failed"
    End If
    Console.WriteLine(testName & " : " & funcName & passFail)
End Sub

''' <summary>
''' Output the result of a test where an exception occurred.
''' </summary>
 Public Sub outputResult(ByVal testName As String, ByVal ex As Exception, ByVal funcName As String)

    Console.WriteLine(testName & " : " & funcName & " " & ex.Message())
End Sub

''' <summary>
''' Test with a list of 3 integers
''' </summary>
 Public Sub test1()
    Dim aList As New List(Of Integer)
    aList.Add(5)
    aList.Add(4)
    aList.Add(3)
    Dim result = GetNextValidSize(3, aList)
    outputResult("test1", 3 = GetNextValidSize(3, aList), "GetNextValidSize")
    outputResult("test1", 3 = GetNextValidSize2(3, aList), "GetNextValidSize2")
End Sub

''' <summary>
''' Test with a null reference
''' </summary>
Public Sub test2()
    Dim aList = Nothing
    Try
        outputResult("test2", GetNextValidSize(3, aList), "GetNextValidSize")
    Catch ex As Exception
        outputResult("test2", ex, "GetNextValidSize")
    End Try
    Try
        outputResult("test2", GetNextValidSize2(3, aList), "GetNextValidSize2")
    Catch ex As Exception
        outputResult("test2", ex, "GetNextValidSize2")
    End Try
End Sub

''' <summary>
''' Test with an empty array.
''' </summary>
Public Sub test3()
    Dim aList As New List(Of Integer)
    Try
        outputResult("test3", GetNextValidSize(3, aList), "GetNextValidSize")
    Catch ex As Exception
        outputResult("test3", ex, "GetNextValidSize")
    End Try
    Try
        outputResult("test3", GetNextValidSize2(3, aList), "GetNextValidSize2")
    Catch ex As Exception
        outputResult("test3", ex, "GetNextValidSize2")
    End Try
End Sub

''' <summary>
''' Run all tests.
''' </summary>
Public Sub testAll()
    test1()
    test2()
    test3()
End Sub

Sub Main()
    testAll()
    Console.ReadLine()
End Sub

End Module

Upvotes: 2

LBushkin
LBushkin

Reputation: 131716

You could use LINQ to simplify the query - it will probably be as fast as anything you could write if your list is sorted.

int someInitialValue;
int defaultIfNotFound = ... // set to some value, even initialValue
// attempt to find first value less than or equal
int bestMatch = myListOfValues.Concat( new []{defaultIfNotFound} )
                              .FirstOrDefault( x => x >= someInitialValue );

If the array is not ordered, or if you need better performance:

myListOfValues.OrderBy( x => x ).Concat( new []{defaultIfNotFound} )
                                .FirstOrDefault( x => x >= someInitialValue );

You mention that you list is relatively small (5-10 items) - so linear search is probably fast enough. However, on a larger list (dozens or hundreds of items), you may want to consider using a binary search to find the value:

// index is positive if an exact match is found
// if no exact match is found, the index returned will be two's complement and
// reference the next number immediately larger than the search target
int index = myListOfValues.BinarySearch( someInitialValue );
if( index < 0 && ~index > myListOfValues.Length )
   bestMatch = someInitialValue;
else
   bestMatch = index < 0 ? myListOfValues[~index] : myListOfValues[index];

Upvotes: 2

Jon Skeet
Jon Skeet

Reputation: 1500983

With only 5-10 elements, definitely go with the simplest solution. Getting a binary chop working would help with a larger array, but it's got at least the potential for off-by-one errors.

Rather than breaking, however, I would return directly from the loop to make it even simpler, and use foreach as well:

  private int GetNextValidSize(int size, int[] validSizes)
  {    
      int returnValue = size;

      foreach (int validSize in validSizes)
      {
          if (validSize >= size)
          {
              return validSizes;
          }
      }

      // Nothing valid    
      return size;
  }

You can make this even simpler with LINQ:

// Make sure we return "size" if none of the valid sizes are greater
return validSizes.Concat(new[] { size })
                 .First(validSize => validSize >= size);

It would be even simpler without the Concat step... or if there were a Concat method that just took a single element. That's easy to write, admittedly:

public static IEnumerable<T> Concat(this IEnumerable<T> source,
                                    T tail)
{
    foreach (T element in source)
    {
        yield return element;
    }
    yield return tail;
}

then it's just:

return validSizes.Concat(size).First(validSize => validSize >= size);

Alternatively (and I realise I'm presenting way more options than are really needed here!) an overload for FirstOrDefault which took the default value to return:

public static T FirstOrDefault(this IEnumerable<T> source,
                               Func<T, bool> predicate,
                               T defaultValue)
{
    foreach (T element in source)
    {
        if (predicate(element))
        {
            return element;
        }
    }
    return defaultValue;
}

Call it like this:

return validSizes.FirstOrDefault(validSize => validSize >= size, size);

Both of these are overkill for a single use, but if you're already building up a library of extra LINQ operators, it could be useful.

Upvotes: 8

Humberto
Humberto

Reputation: 7199

I think you will just get the first greater number, not necessarily the closest greater number.

If your array isn't sorted, you'll need to double-pass it to find the right number. First you'll find the greatest, and second the smaller value that is still greater than the original value.

Upvotes: -1

FWH
FWH

Reputation: 3223

If your array is ordered, you can fast this up by using a binary search algorithm.

See there: http://en.wikipedia.org/wiki/Binary_search_algorithm

Upvotes: 1

Dave Markle
Dave Markle

Reputation: 97701

int[] validSizes = new int[] { 32, 128, 1024, 2048, 4096 };

int sizeICareAbout = 4096;

Console.Write(validSizes.Max(i => i < sizeICareAbout ? i : Int32.MinValue));

This will return Int32.MinValue if you put in the smallest value. God, I love LINQ.

Upvotes: 2

tanascius
tanascius

Reputation: 53944

Given that you have only 5-10 elements I would consider this to be ok.

Upvotes: 7

Related Questions