Context
I have a big macro where I have declared lots of global constant in a dedicated module (i.e, a module which contains only Public Const declarations).
About 100 of those global constants are used to assign columns names to each column of my main Data worksheet :
Public Const columnName = "A"
Public Const columnCity = "B"
Public Const columnPhone = "C"
...
Public Const columnColor = "CX"
This let me reference to the columns (of my Data worksheet, from the 10 other modules) using .Range(columnColor & l) instead of using .Range("CX" & l) (where l is obviously the row number) . This is much easier to code (I don't need to search for the right column) or to update if I decide to insert a column before "F" (I only have to update my const module and not the 10 other code modules).
However, it looks like using .Range(columnCity & l) is notably slower than using .Range("A" & l). (SEE EDIT BELOW)
The most processors intensive tasks are done using big 2D arrays. But I'm still probably calling those global column variables 100 000 times in some subs, since I'm not only checking/updating values/formulas (which I could do on a 2D array) but also dealing with cell's .Interior.Color, .Comment.Text ...
Question
How bad an idea is it to use such global variables (Public Const columnName...) to reference columns ?
Is there some standard way of doing so ?
Edit
As pointed by Tim, I think I indeed spent time changing every .Cells(l, 1) to .Range(columnName & 1) when I refactored my code to use the column variable. That means that :
My problem probably comes from using .Range vs .Cells rather than from the global variables.
I should probably refactor back to .Cells(l, colIndexName).
There is no "standard" way to do this, so you should run some performance tests and figure out the most efficient method if performance is an issue for you.
For example, using a numeric constant and Cells() seems to be about twice as fast as using Range():
Option Explicit
Public Const columnName As String = "A"
Public Const colIndexName As Long = 1
Sub Tester()
Dim l As Long, v, t
t = Timer
With Sheet1
For l = 1 To 300000#
v = .Range(columnName & 1).Value
Next l
End With
Debug.Print Timer - t '>> approx. 1.3 sec
t = Timer
With Sheet1
For l = 1 To 300000#
v = .Cells(1, colIndexName).Value
Next l
End With
Debug.Print Timer - t '>> approx. 0.6 sec
End Sub
However, it's likely only twice as fast if that's all you're doing - as soon as you add in other tasks that difference may wash out.
Need an Answer to provide formatting and results, though this is more of a comment.
I have found no significant difference between .Range(columnCity & l) and .Range("A" & l). Can you provide more insight on how you came to this conclusion?
Here is my code for speed comparison:
Public Const p_sCol As String = "A"
Sub tgr()
Dim ws As Worksheet
Dim i As Long, l As Long
Dim sTemp As String
Dim dTimer As Double
Dim aResults(1 To 1, 1 To 2) As Double
Set ws = ActiveWorkbook.ActiveSheet
l = 1
dTimer = Timer
For i = 1 To 100000
sTemp = vbNullString
sTemp = ws.Range(p_sCol & l).Value
Next i
aResults(1, 1) = Timer - dTimer
dTimer = Timer
For i = 1 To 100000
sTemp = vbNullString
sTemp = ws.Range("A" & l).Value
Next i
aResults(1, 2) = Timer - dTimer
ws.Range("C1:D1").Value = aResults
End Sub
I ran the test 10 times, and the average result for the public variable concatenation over 100,000 iterations was 0.4375 seconds while the average result for hard coding the column letter was 0.429688 seconds, which is a difference of 0.007813 seconds. Sure, the hard coded method was slightly faster, but not noticeably and certainly not significantly.
Related
Have a piece of code that looks for matches between 2 sheets (sheet1 is customer list and rData is copied pdf with invoices). It usually is exact match but in some cases I'm looking for 6 first characters that matches rData
Dim rData As Variant
Dim r As Variant
Dim r20 As Variant
Dim result As Variant
Dim i As Long
rData = ActiveWorkbook.Sheets(2).Range("A1:A60000")
r20 = ActiveWorkbook.Sheets(1).Range("C2:C33")
For Each r In r20
result = Application.Match(r, rData, 0)
If Not IsError(result) Then
For i = 1 To 5
If (result - i) > 0 Then
If (Left(Trim(rData(result - i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result - i, 1)
End If
End If
Next
For i = 1 To 15
If (result + i) > 0 Then
If (Left(Trim(rData(result + i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result + i, 1)
End If
End If
Next
End If
Next r
End Sub
Only part of this that is giving me a headache is this part result = Application.Match(r, rData, 0). How do it get match for not exact match?
Sample of Sheet1
This is what more or less looks like. Matching after CustomerNumber# is easy because they are the same every invoice. BUT sometimes invoice does not have it so I'm searching after CustomerName and sometimes they have uppercase letters, sometimes there is extra stuff behind it and therefore it cannot find exact match.
Hope it makes sense.
To match the customer name from your customer list to the customer name in the invoice even if it has extra characters appended, you can use the wildcard * in Match().
You also have a typo in the Match() function. r20 should be rData.
This is your code with the fixes applied:
Sub Test()
'v4
Dim rData As Variant
Dim r As Variant
Dim r20 As Variant
Dim result As Variant
Dim i As Long
rData = ActiveWorkbook.Sheets(2).Range("A1:A60000")
r20 = ActiveWorkbook.Sheets(1).Range("C2:C33")
For Each r In r20
result = Application.Match(r & "*", rData, 0) ' <~ Fixed here
If Not IsError(result) Then
For i = 1 To 5
If (result - i) > 0 Then
If (Left(Trim(rData(result - i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result - i, 1)
End If
End If
Next
For i = 1 To 15
If (result + i) > 0 Then
If (Left(Trim(rData(result + i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result + i, 1)
End If
End If
Next
End If
Next r
End Sub
Notes:
Match() is case insensitive, so it works with different capitalisations.
The data in Sheets(2) must all be text for Match() to work correctly with wildcards.
EDIT1: New better version
EDIT2: Refactored constants and made data ranges dynamic
EDIT3: Allows for any prefix to an invoice number of a fixed length
The following is a better, rewritten version of your code:
Sub MuchBetter()
'v3
Const s_InvoiceDataWorksheet As String = "Sheet2"
Const s_InvoiceDataColumn As String = "A:A"
Const s_CustomerWorksheet As String = "Sheet1"
Const s_CustomerStartCell As String = "C2"
Const s_InvoiceNumPrefix As String = "418"
Const n_InvoiceNumLength As Long = 8
Const n_InvScanStartOffset As Long = -5
Const n_InvScanEndOffset As Long = 15
Dim ƒ As Excel.WorksheetFunction: Set ƒ = Excel.WorksheetFunction ' Shortcut
With Worksheets(s_InvoiceDataWorksheet).Range(s_InvoiceDataColumn)
With .Parent.Range(.Cells(1), .Cells(Cells.Rows.Count).End(xlUp))
Dim varInvoiceDataArray As Variant
varInvoiceDataArray = ƒ.Transpose(.Cells.Value2)
End With
End With
With Worksheets(s_CustomerWorksheet).Range(s_CustomerStartCell)
With .Parent.Range(.Cells(1), .EntireColumn.Cells(Cells.Rows.Count).End(xlUp))
Dim varCustomerArray As Variant
varCustomerArray = ƒ.Transpose(.Cells.Value2)
End With
End With
Dim varCustomer As Variant
For Each varCustomer In varCustomerArray
Dim dblCustomerIndex As Double
dblCustomerIndex = Application.Match(varCustomer & "*", varInvoiceDataArray, 0)
If Not IsError(dblCustomerIndex) _
And varCustomer <> vbNullString _
Then
Dim i As Long
For i = ƒ.Max(dblCustomerIndex + n_InvScanStartOffset, 1) _
To ƒ.Min(dblCustomerIndex + n_InvScanEndOffset, UBound(varInvoiceDataArray))
Dim strInvoiceNum As String
strInvoiceNum = Right$(Trim$(varInvoiceDataArray(i)), n_InvoiceNumLength)
If (Left$(strInvoiceNum, Len(s_InvoiceNumPrefix)) = s_InvoiceNumPrefix) Then
MsgBox "customer: " & varCustomer & ". invoice: " & strInvoiceNum
End If
Next
End If
Next varCustomer
End Sub
Notes:
It is a good idea to use constants so all literal values are typed once only and kept grouped together.
Using the RVBA naming convention greatly increases the readability of the code, and reduces the likelihood of bugs.
Using long, appropriately named variables makes the code essentially self-documenting.
Using .Value2 whenever reading cell values is highly recommended (it avoids implicit casting, making it slightly faster as well as eliminating certain issues caused by the casting ).
Surprisingly, in VBA there are good reasons to put a variable declaration as close as possible to the first use of the variable. Two such reasons are 1) it improves readability, and 2) it simplifies future refactoring. Just remember that the variable is not reinitialised every time the Dim is encountered. Initialisation only occurs the first time.
The twin loops have been rolled into one according to the DRY principle.
Whilst the check for an empty customer name/number is not strictly necessary if you can guarantee it will never be so, it is good defensive programming as an empty value will cause erroneous results.
The negative index check inside the loop has been removed and replaced with the one-time use of the Max() worksheet function in the For statement.
The Min() worksheet function is also used in the For statement to avoid trying to read past the end of the array.
Always use worksheet functions on the WorksheetFunction object unless you are explicitly checking for errors, in which case use the Application object.
I'm trying to make MATCH function work like FIND function. First of all, I generate the dummy data to be use for testing. Here is the routine I use:
Sub Data_Generator()
Randomize
Dim Data(1 To 100000, 1 To 1)
Dim p As Single
For i = 1 To 100000
p = Rnd()
If p < 0.4 Then
Data(i, 1) = "A"
ElseIf p >= 0.4 And p <= 0.7 Then
Data(i, 1) = "B"
Else
Data(i, 1) = "C"
End If
Next i
Range("A1:A100000") = Data
End Sub
Now, I create a sub-routine to find the string A in the range Data. There are two methods I use here that employ MATCH function. The first method is to reset the range of lookup array like the following code:
Sub Find_Match_1()
T0 = Timer
Dim i As Long, j As Long, k As Long, Data As Range
Dim Output(1 To 100000, 1 To 1)
On Error GoTo Finish
Do
Set Data = Range(Cells(j + 1, 1), "A100000") 'Reset the range of lookup array
i = WorksheetFunction.Match("A", Data, 0)
j = j + i
Output(j, 1) = j 'Label the position of A
k = k + 1 'Counting the number of [A] found
Loop
Finish:
Range("B1:B100000") = Output
InputBox "The number of [A] found are " & k & " in", "Process is complete", Timer - T0
End Sub
And for the second method, I assign the cell of range where A is located by value vbNullString instead of resetting Range("A1:A100000"). The idea is to delete the string A after being found and to expect MATCH function to find the next string A in the Range("A1:A100000"). Here is the code to implement the second method:
Sub Find_Match_2()
T0 = Timer
Dim n As Long, i As Long, j As Long
Dim Data_Store()
Dim Output(1 To 100000, 1 To 1)
Data_Store = Range("A1:A100000")
On Error GoTo Finish
Do
j = WorksheetFunction.Match("A", Range("A1:A100000"), 0)
Output(j, 1) = j
Cells(j, 1) = vbNullString
n = n + 1
Loop
Finish:
Range("A1:A100000") = Data_Store
Range("B1:B100000") = Output
InputBox "The number of [A] found are " & n & " in", "Process is complete", Timer - T0
End Sub
The goal is to determine which method is better at employing MATCH function in its performance. It turns out the first method only completes less than 0.4 seconds meanwhile the second method completes about a minute on my PC. So my questions are:
Why does the second method take time too long to complete?
How does one improve the performance of the second method?
Can MATCH function be used in an array?
I agree that this is more of a Code Review question, but I chose to look into it for my own curiosity, so I'll share what I found.
I think you're hitting a very classic case of N vs N^2 computational complexity. Look at your two methods, which seem remarkably similar, and consider what they're actually doing, keeping in mind that the MATCH function is probably just a linear search when you use Match_type=0 (because your data is unsorted, whereas other match types could do a binary search on your sorted data).
Method 1:
Start at A1
Continue down the range until an "A" is found
Restart at the cell below the MATCH
Method 2:
Start at A1
Continue down the range until an "A" is found
Clear the "A"
Restart at A1
It should be instantly apparent that while one method is continually shrinking the range it searches, the other is always starting at the first cell and searching the whole range. This will account for some of the speedup, and already boosts Method 1 to a nice lead, but it's not even nearly the full story.
The real key lies in the amount of work Match has to do for each situation. Because its range constantly shrinks and moves its start further down the list, whichever cell Method 1's Match starts from, it only has to search a small number of cells before it hits an A and resumes the outer loop. Meanwhile, Method 2 is continually destroying A's, making them less and less dense and forcing itself to search more and more of the range before getting any hits. By the end, Method 2 is looping through almost 100,000 empty cells/B's/C's before finding its next A.
So on average, the Match for Method 1 is only looking through a couple of cells each time, while the Match for Method 2 is taking longer and longer as time goes on, until the end when it is forced to loop through the entire range. On top of that, Method 2 is doing a bunch of writes to cell values, which is slower than you might think when you have to do it tens of thousands of times.
In all honesty, your best bet would be to just loop through the cells yourself once, looking for A's and handling them as you go. MATCH brings no advantage to the table, and Method 1 is basically just a more complicated version of the loop I described.
I'd write this something like:
Sub Find_Match_3()
T0 = Timer
Dim k As Long, r As Range
Dim Output(1 To 100000, 1 To 1)
For Each r In Range("A1:A100000").Cells
If r.Value = "A" Then
Output(r.Row, 1) = r.Row 'Label the position of A
k = k + 1 'Counting the number of [A] found
End If
Next
Range("B1:B100000") = Output
InputBox "The number of [A] found are " & k & " in", "Process is complete", Timer - T0
End Sub
Which is about 30% faster on my machine.
I have a very basic knowledge and VBA but due to coding in other programming languages, I have a thought if I can copy-paste the data using specific column names in vba-excel. I use the following code till now but want to get over as I get stuck too often.
ThisWorkbook.Worksheets("Sheets2").Activate
lastrow = Worksheets("Sheet2").Cells(Rows.Count, "A").End(xlUp).Row + 1
Range("B2:B" & lastrow).Copy Worksheets("Sheet1").Range("a65536").End(xlUp).Offset(1, 2)
Range("A2:A" & lastrow).Copy Worksheets("Sheet1").Range("a65536").End(xlUp).Offset(1, 1)
Application.CutCopyMode = False
This will copy A and B column from Sheet2 to B and C column in Sheet1. Now can I do something like
Sheet2.colname.copy Sheet1.colname.offset
I am just curious and any help is appreciated. Thanks.
I often declare Const for the columns I'm referring to throughout complex code so I can refer to them by 'name'.
Const ItemNumCol = 1
Const DescCol = 2
Const CostCol = 3
etc...
It's not automatic, but by declaring them globally, it minimizes the pain of rearranging columns, and prevents me from having to go back to my spreadsheet to count every 2 minutes.
Usage Example
Dim ws1 as Worksheet
Dim ws2 as Worksheet
Set ws1 = ThisWorkbook.Worksheets("Sheets1")
Set ws2 = ThisWorkbook.Worksheets("Sheets2")
LastRow = ws2.UsedRange.Rows.Count + 1
ws.range(cells(2,DescCol),cells(LastRow,DescCol).copy ws1.range(ws1.UsedRange.Rows.Count + 1, DescCol)
You may have to flip the "Row, Col" references within Cells(), I can never remember which way they go, but IntelliSense in the VBE IDE will tell you.
In facts if you want just to use names, you can't do it in general, maybe in tables (select your column and add an array) but not sure...
to use something like this :
Sheet2.colname.copy Sheet1.colname.offset
you can use :
Sheet2.columns(index).copy Sheet1.columns(index).offset
Index is a number starting at 1, and you can find it with a simple For structure and then inject your result in your copies instructions! ;)
Here is a way to do it :
Sub MasterCopy()
Copy_Columns 0, 0
End Sub
Here is a routine that will help you use the function below (and you can reuse parameters if you want to unite them in only one routine)
Sub Copy_Columns(ByVal HorizontalOffset As Integer, ByVal VerticalOffset As Integer)
Dim ColCopy As Integer
Dim ColPaste As Integer
ColCopy = GetColName("SheetCopy", "ColToCopy")
ColPaste = GetColName("SheetPaste", "ColToPaste")
Sheets("SheetCopy").Columns(ColCopy).Copy Sheets("SheetPaste").Columns(ColPaste).Offset(VerticalOffset, HorizontalOffset)
End Sub
And so you can use that function everywhere to find your columns by name in a specific sheet
Public Function GetColName(ByVal SheetName As String, ByVal ColName As String)
For j = 1 To Sheets(SheetName).Cells(1, 1).End(xlToRight).Column
If LCase(Sheets(SheetName).Cells(1, j)) <> LCase(Column_Name) Then
Else
GetColName = j
Exit For
End If
Next j
If IsEmpty(GetColName) Then GetColName = 0
End Function
Enjoy! ;)
Excel specifications and limits says:
Total number of characters that a cell can contain: 32,767 characters
Is there a way to get this number programatically?
I'm asking because hardcoding constants should, in general, be avoided if and when feasible. This number may conceivably change by Office version (It hasn't changed between 2003 and 2013, but who knows what Microsoft has in store for us).
It's pretty easy to get the maximum number of rows in a worksheet:
Sheet1.Rows.Count ' returns 65,536 in Office 2003 and 1,048,576 in Office 2007-2013
but apparently, getting the maximum number of characters that a cell can contain isn't as straightforward.
Note that writing too many characters to a cell will not result in an error; it will silently fail and truncate the string — so proper error handling isn't an option here.
In a loop, append characters one by one to the cell contents. Each time, read cell contents, check if the last character added is present. If it isn't then that's the limit.
Upside: Works and is 100% reliable.
Downside: Really slow. It takes 10-15 seconds to complete, due to the many read-writes to/from sheet.
Obviously, this could be optimised by using a good guess (e.g. 32,767) as the initial condition, and using a hunt & bisect search algorithm rather than incrementing by 1. However if the answer is far enough away from the initial guess, this might still take ~1 second to run — not something you would want to call repeatedly.
Function MaximumNumberOfCharactersACellCanContain(r As Range)
'NB: Range r will be overwritten.
Dim sIn As String
Dim sOut As String
Dim i As Long
Application.ScreenUpdating = False
Do
i = i + 1
sIn = sIn & Chr(97 + (i - 1) Mod 26)
r.Cells(1, 1).Value = sIn
sOut = r.Cells(1, 1).Value
If Right(sOut, 1) <> Right(sIn, 1) Then Exit Do
'If Len(sOut) <> Len(sIn) Then Exit Do
Loop
Application.ScreenUpdating = True
MaximumNumberOfCharactersACellCanContain = i - 1
End Function
Example usage:
MsgBox MaximumNumberOfCharactersACellCanContain(Range("A1"))
Alternative: Loop appending a chunk until the assigned length is not whats expected
Const INT_MAX As Integer = 32767
Dim i As Long
ActiveCell.Value = ""
Dim buff As String: buff = Space$(INT_MAX)
Do
i = i + 1
ActiveCell.Value = ActiveCell.Value & buff
If Len(ActiveCell.Value) <> (i * INT_MAX) Then
MaxLen = Len(ActiveCell.Value)
Exit Function
End If
Loop
Or even
ActiveCell.Value = Space$(A_BIG_NUMBER)
MaxLen = Len(ActiveCell.Value)
Here's a variant where we take exponential steps (larger and larger steps whose size increases by a factor stepFactor each time).
Function MaximumNumberOfCharactersACellCanContain(r As Range, _
Optional ByVal stepFactor As Double = 2)
Dim n As Double
Dim nActual As Long
Dim l As Long
n = 1
Do
n = n * stepFactor
nActual = CLng(n)
r.Cells(1, 1).Value = Space$(nActual)
l = Len(r.Cells(1, 1).Value)
If l <> nActual Then
MaximumNumberOfCharactersACellCanContain = l
Exit Function
End If
Loop
End Function
Example usage:
Debug.Print MaximumNumberOfCharactersACellCanContain(Range("A1"), 8)
The choice of stepFactor is a compromise between:
Reducing the number of iterations (larger factor is better), and
Limiting down the cost of the last iteration (the one that fails). If stepFactor is too large, then you're writing a very long string to the cell and this is quite slow.
Making sure the last iteration will never hit the out of memory ceiling (~130 million characters on my system). (Could add error handling do deal with this eventuality.)
stepFactor somewhere between 2 and 8 should be robust and quick.
I'm trying to minimize the value of the sum of the residuals squared by varying the value of De, which is found in F1. I want the values of CFL Calculated to be as close as possible to the values of CFL Measured. The smaller the sum of those residuals squared, the better the fit! After asking stackoverflow for some advice, I decided to use Goal Seek to minimize the sum of the residuals squared to get as close to zero as possible by varying the value of De, which I want to find the most ideal value of.
I got this program to run perfectly, or so I thought... I found out that instead of summing every single residuals using =SUM(D2:D14), I accidentally used =SUM(D2,D14). So I was only summing up the first and last numbers.
Now that I'm trying to sum every residual squared up, I'm getting these crazy errors, and an insane value for De.
I know that the value of De has to be greater than zero, and less than one. how can I use these bounds to keep this goal seek focused within a certain range? The answer for De in this case is about .012, if that helps. I keep getting the error #NUM! in all of the residual cells. Is this because of overflow issues?
If you've concluded that using Goal Seek to minimize these sums by finding the most ideal value of De will not work, how would you go about it? Are there any other solvers I could use?
Here is the code:
Option Explicit
Dim Counter As Long
Dim DeSimpleFinal As Double
Dim simpletime As Variant
Dim Tracker As Double
Dim StepAmount As Double
Dim Volume As Double
Dim SurfArea As Double
Dim pi As Double
Dim FinalTime As Variant
Dim i As Variant
Sub SimpleDeCalculationNEW()
'This is so you can have the data and the table I'm working with!
Counter = 13
Volume = 12.271846
SurfArea = 19.634954
pi = 4 * Atn(1)
Range("A1") = "Time(days)"
Range("B1") = "CFL(measured)"
Range("A2").Value = 0.083
Range("A3").Value = 0.292
Range("A4").Value = 1
Range("A5").Value = 2
Range("A6").Value = 3
Range("A7").Value = 4
Range("A8").Value = 5
Range("A9").Value = 6
Range("A10").Value = 7
Range("A11").Value = 8
Range("A12").Value = 9
Range("A13").Value = 10
Range("A14").Value = 11
Range("B2").Value = 0.0612
Range("B3").Value = 0.119
Range("B4").Value = 0.223
Range("B5").Value = 0.306
Range("B6").Value = 0.361
Range("B7").Value = 0.401
Range("B8").Value = 0.435
Range("B9").Value = 0.459
Range("B10").Value = 0.484
Range("B11").Value = 0.505
Range("B12").Value = 0.523
Range("B13").Value = 0.539
Range("B14").Value = 0.554
Range("H2").Value = Volume
Range("H1").Value = SurfArea
Range("C1") = "CFL Calculated"
Range("D1") = "Residual Squared"
Range("E1") = "De value"
Range("F1").Value = 0.1
'Inserting Equations
Range("C2") = "=((2 * $H$1) / $H$2) * SQRT(($F$1 * A2) / PI())"
Range("C2").Select
Selection.AutoFill Destination:=Range("C2:C" & Counter + 1), Type:=xlFillDefault
Range("D2") = "=((ABS(B2-C2))^2)"
Range("D2").Select
Selection.AutoFill Destination:=Range("D2:D" & Counter + 1), Type:=xlFillDefault
'Summing up the residuals squared
Range("D" & Counter + 2) = "=Sum(D2: D" & Counter + 1 & ")"
'Goal Seek
Range("D" & Counter + 2).GoalSeek Goal:=0, ChangingCell:=Range("F1")
Columns("A:Z").EntireColumn.EntireColumn.AutoFit
DeSimpleFinal = Range("F1")
MsgBox ("The Final Value for DeSimple is: " & DeSimpleFinal)
End Sub
You're getting NUM errors because the value of F1 is going negative in your current solution -- and you are trying to take the square root of F1 in one of your expressions.
Also, goal seek is, in this instance, incredibly sensitive to the particular initial starting "guess" for F1 that you are using. This will be evident if you vary the F1 initial value by a little bit on either side of the 0.1 you are using now. There are, in fact, large regions of instability in the goal seek solution, depending on the F1 value:
As you brought up in your question, you are more likely to get a useable result if you can set constraints on the possible inputs to your solution search. Excel comes with an add-in called Solver that allows that, as well as offers several different search methods. Solver is not loaded automatically when you first start Excel, but loading it is easy, as explained here.
You ask for other solvers. For alternatives and a bit of theory to help understand what's going on, have a look at Numerical Recipes (online books here). Chapter 10 deals with this. It includes ready-made code samples if you want to try something different than GoalSeek or the Solver add-in. Of course the code is in Fortran/C/C++ but these are readily translated into VBA (I've done this many times).
The goalseek function uses a dichotomy algorithm which can be coded like this:
Sub dicho(ByRef target As Range, ByRef modif As Range, ByVal targetvalue As Double, ByVal a As Double, ByVal b As Double)
Dim i As Integer
Dim imax As Integer
Dim eps As Double
eps = 0.01
imax = 10
i = 0
While Abs(target.Value - targetvalue) / Abs(targetvalue) > eps And i < imax
modif.Value = (a + b) / 2
If target.Value - targetvalue > 0 Then
a = (a + b) / 2
Else
b = (a + b) / 2
End If
i = i + 1
Wend
End Sub
Where a and b are you bounds.