I thought I’d have a go at writing a FormulaArray function to complement it, because when I’m building up a complicated formula that uses lots of array manipulation, then I like to document how all the different arrays within it fit together. (I was going to say “come together” there, but after that crack about FORMULATEXT playing with itself, I thought better of it. But now that I’ve said crack, I’m gonna throw caution to the wind and say wind too.)

Currently I document my formula beasts by either either array-enter a sub-part in the sheet with some notes, like this:

…which shows how my ExtractNumber formula works, or I enter the desired formula in one cell with a ShowFormula to the left and a hand-rolled hard-coded array to the right, like in this table where I’m documenting a few ways to dynamically generate consecutive integers:

I get that ResultArray manually, by clicking in the formula bar, pushing F9, copying the resulting evaluated array, then pasting it in another cell. Tedious. Especially when I later make a change to that sub-part, because then I get to do those steps all over.

So I started to roll my own FormulaArray function. I got a bit bogged down in the joining bit, but after about an hour of Googling, I *rediscovered* Nigel Heffernan’s code for joining two dimensional arrays. Which is *very* concerning, because I *discovered* it like just 10 days ago, and even wrote an extensive blog post on it right here. Senility is obviously setting in. If I start saying the same thing over and over like my mother does, just shoot me. If I start saying the same thing over and over like my mother does, just shoot me.

Anyways, Nigel’s function needs a 2D array. You can create an array from formula text by using VBA’s Evaluate method. If the formula returns a Row vector or a 2D vector, then Evaluate nicely turns it into a 2D vector. But here’s the rub: if the formula returns a Column vector, then Evaluate only gives us a 1D vector, which ain’t gonna wash with Nigel’s function:

So what we need to do is TRANSPOSE any formulas that would return Column vectors, because chucking a TRANSPOSE into the mix has the desired effect:

Note that I’m using the square brackets [ ] shortcut for Evaluate. I could just have easily done it like this:

Okay, so we know that * if* our formula string returns a Column vector, we’ve got to transpose it. But how can we tell that ahead of time? I can’t think of a way. So I just do this:

Function FormulaArray(Target As Range) As String

Dim strInput As String

Dim var2 As Variant

Dim lb As Long

strInput = Mid$(Target.Formula, 2)

var2 = ActiveSheet.Evaluate(strInput)

On Error Resume Next

lb = LBound(var2, 2)

If Err.Number <> 0 Then

var2 = Application.Transpose(ActiveSheet.Evaluate(strInput))

FormulaArray = Join2d(var2, ",", ";")

Else:

FormulaArray = Join2d(var2, ";", ",")

End If

End Function

Dim strInput As String

Dim var2 As Variant

Dim lb As Long

strInput = Mid$(Target.Formula, 2)

var2 = ActiveSheet.Evaluate(strInput)

On Error Resume Next

lb = LBound(var2, 2)

If Err.Number <> 0 Then

var2 = Application.Transpose(ActiveSheet.Evaluate(strInput))

FormulaArray = Join2d(var2, ",", ";")

Else:

FormulaArray = Join2d(var2, ";", ",")

End If

End Function

So I evaluate the formula as if it’s a Row vector, then check if I’ve got 2 dimensions as a result. If not, it must have been a column vector, in which case I transpose it, then reevaluate it. Shame about the double evaluation, but I can’t think of a foolproof way to do it differenty, other than perhaps array entering the formula into a 2D range on the worksheet and looking at where the #N/A! errors fall.

Anyway, it seems to work just fine:

..unless you happen to be using Structured Table References, and your arguments happen to use the @ table notation to point at something on the same row:

…or unless you happen to have a formula with the INDIRECT function in it:

With the ThisTableRow thing, I guess I can just replace the @[SomeColumn] bit with the actual address, but I can’t think of easy ways around the INDIRECT thing. Anyone got any ideas?

Edit: Thinking about this some more, all I need to do is substitute the INDIRECT(SomeExpression) with whatever gets returned by RANGE(SomeExpression).value

Sample workbook:

There’s a handy post over at Charles Williams’ site that talks about some other quirks of Evaluate that’s worth checking out:

https://fastexcel.wordpress.com/2011/11/02/evaluate-functions-and-formulas-fun-how-to-make-excels-evaluate-method-twice-as-fast/

Boy, what a battle. I have saved my changed book as an XLTM to just about everywhere I can think of.

- I’ve tried saving it to C:\Users\Samsung\AppData\Roaming\Microsoft\Excel\XLSTART but when I start Excel, I have gridlines.
- I’ve tried creating a new file at C:\xlStart with the template in it, and told Excel via Options>Advanced to open files in that folder but when I start Excel, I have gridlines:
- I’ve saved it to C:\Users\Samsung\Documents\Custom Office Templates but when I start Excel, I have gridlines

I’ve gone from feeling like I’ve mastered to Excel, to feeling like I’m a complete idiot. Anyone care to tell me that I’m not?

How the heck the average *user* is supposed to know how to do this stuff is beyond me. Why isn’t there simply a button on the ribbon or backstage that says:

*Give all future workbooks the settings of this one*.

I’m using Excel 2013 365. But I’m thinking of doing a complete 180.

]]>- The string in question consists of a mixture of numbers, letters and special characters
- The numbers may appear anywhere within that string
- Decimals within the string are to be returned as such
- The desired result is to have all numbers returned to separate cells

That’s a tall order with formulas. Here’s what ExcelXOR came up with:

=0+MID(“α”&s&”α0″,1+SMALL(IF(MMULT(0+(ABS(51.5-CODE(MID(SUBSTITUTE(“α”&s&”α0″,”/”,”α”),ROW(INDIRECT(“1:”&LEN(“α”&s&”α0″)-1))

+{0,1},1)))>6)*{2,1},{1;1})=2,ROW(INDIRECT(“1:”&LEN(“α”&s&”α0″)-1))),e),SUM(SMALL(IF(ISNUMBER(MATCH(MMULT(0+(ABS(51.5-CODE(

MID(SUBSTITUTE(“α”&s&”α0″,”/”,”α”),ROW(INDIRECT(“1:”&LEN(“α”&s&”α0″)-1))+{0,1},1)))>6)*{2,1},{1;1}),{1,2},0)),ROW(INDIRECT(

“1:”&LEN(“α”&s&”α0″)-1))),2*e+{-1,0})*{-1,1}))

+{0,1},1)))>6)*{2,1},{1;1})=2,ROW(INDIRECT(“1:”&LEN(“α”&s&”α0″)-1))),e),SUM(SMALL(IF(ISNUMBER(MATCH(MMULT(0+(ABS(51.5-CODE(

MID(SUBSTITUTE(“α”&s&”α0″,”/”,”α”),ROW(INDIRECT(“1:”&LEN(“α”&s&”α0″)-1))+{0,1},1)))>6)*{2,1},{1;1}),{1,2},0)),ROW(INDIRECT(

“1:”&LEN(“α”&s&”α0″)-1))),2*e+{-1,0})*{-1,1}))

…where s is the string you want to break apart, and e the element you want returned.

That fatboy runs to 415 characters. Which is a *heck* of a lot less than *my* first effort:

=SUM(IFERROR(--REPT(MID(s&"|",ROW(OFFSET($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,LEN(s)))^0,COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(

OFFSET($A$1,,,LEN(s)))^0),(IF((ISNUMBER(-MID(s&"|",ROW(OFFSET($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,LEN(s)))^0,COLUMN(OFFSET(

$A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0))*NOT(ISNUMBER(-MID("|"&s&"|",ROW(OFFSET($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,

LEN(s)))^0,COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0)))*NOT(ISNUMBER(-MID(s&"|",1+ROW(OFFSET($A$1,,,LEN(s)))

*COLUMN(OFFSET($A$1,,,,LEN(s)))^0,COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0))))>0,ROW(OFFSET($A$1,,,LEN(s)))

*COLUMN(OFFSET($A$1,,,,LEN(s)))^0) = SMALL(IF((ISNUMBER(-MID(s&"|",ROW(OFFSET($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,LEN(s)))^0,

COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0))*NOT(ISNUMBER(-MID("|"&s&"|",ROW(OFFSET($A$1,,,LEN(s)))*COLUMN(

OFFSET($A$1,,,,LEN(s)))^0,COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0)))*NOT(ISNUMBER(-MID(s&"|",1+ROW(OFFSET

($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,LEN(s)))^0,COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0))))>0,ROW(OFFSET

($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,LEN(s)))^0),e))),0))

OFFSET($A$1,,,LEN(s)))^0),(IF((ISNUMBER(-MID(s&"|",ROW(OFFSET($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,LEN(s)))^0,COLUMN(OFFSET(

$A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0))*NOT(ISNUMBER(-MID("|"&s&"|",ROW(OFFSET($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,

LEN(s)))^0,COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0)))*NOT(ISNUMBER(-MID(s&"|",1+ROW(OFFSET($A$1,,,LEN(s)))

*COLUMN(OFFSET($A$1,,,,LEN(s)))^0,COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0))))>0,ROW(OFFSET($A$1,,,LEN(s)))

*COLUMN(OFFSET($A$1,,,,LEN(s)))^0) = SMALL(IF((ISNUMBER(-MID(s&"|",ROW(OFFSET($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,LEN(s)))^0,

COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0))*NOT(ISNUMBER(-MID("|"&s&"|",ROW(OFFSET($A$1,,,LEN(s)))*COLUMN(

OFFSET($A$1,,,,LEN(s)))^0,COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0)))*NOT(ISNUMBER(-MID(s&"|",1+ROW(OFFSET

($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,LEN(s)))^0,COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0))))>0,ROW(OFFSET

($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,LEN(s)))^0),e))),0))

…although there is some pretty nifty stuff going on in there, that I’ll bore you with at a later date…including a MID that breaks a string down into ALL possible slices of text:

=MID(s&"|",ROW(OFFSET($A$1,,,LEN(s)))*COLUMN(OFFSET($A$1,,,,LEN(s)))^0,COLUMN(OFFSET($A$1,,,,LEN(s)))*ROW(OFFSET($A$1,,,LEN(s)))^0)

I couldn’t rest with just that. Literally. It was too heavy. So I had another crack. The result: Here’s a generic ExtractNumbers formula I just put together. And by ‘just’ I mean I only just managed it, and it took an entire weekend, I ignored the kids, and forgot to bathe. (That last one is pretty much a given, and I can’t really blame Excel).

=MID(s,SMALL(IF(ISERROR(-MID(TEXT(MID(“||”&s,ROW(OFFSET($A$1,,,LEN(s))),2),”|”),2,1)),FALSE,ROW(OFFSET($A$1,,,LEN(s)))),E)-1,

SUM(SMALL (IF(ISERROR(-MID(TEXT(MID(“|”&s&”|”,ROW(OFFSET($A$1,,,LEN(s)+1)),2),”|”),{1,2},1)),FALSE,ROW(OFFSET($A$1,,,LEN(s)+1

))),{1,2}+(E-1)*2)*{-1,1}))

SUM(SMALL (IF(ISERROR(-MID(TEXT(MID(“|”&s&”|”,ROW(OFFSET($A$1,,,LEN(s)+1)),2),”|”),{1,2},1)),FALSE,ROW(OFFSET($A$1,,,LEN(s)+1

))),{1,2}+(E-1)*2)*{-1,1}))

…again, where:

S = the string you want to break apart

E = the number element you want to return

It will handle numbers with decimal places provided there is a digit to the left of the decimal place e.g. SomeText5.745 and NOT SomeText.745, and it’s a much more svelte 277 characters in length. Isn’t she a beauty? A lot of the inspiration for the approach came from Excel Ninja Sajan, over at the awesome Formula Challenges section of Chandoo’s forum.

In that incarnation, you can use it to extract just one element of a specific number:

Or if you prefer, you can use this version:

=MID($A28,SMALL(IF(ISERROR(-MID(TEXT(MID(“||”&$A28,ROW(OFFSET($A$1,,,LEN($A28))),2),”|”),2,1)),FALSE,ROW(OFFSET($A$1,,,LEN

($A28)))),COLUMNS($B28:B28))-1,SUM(SMALL(IF(ISERROR(-MID(TEXT(MID(“|”&$A28&”|”,ROW(OFFSET($A$1,,,LEN($A28)+1)),2),”|”),{1,2},1)),

FALSE,ROW(OFFSET($A$1,,,LEN($A28)+1))),{1,2}+(COLUMNS($B28:B28)-1)*2)*{-1,1}))

($A28)))),COLUMNS($B28:B28))-1,SUM(SMALL(IF(ISERROR(-MID(TEXT(MID(“|”&$A28&”|”,ROW(OFFSET($A$1,,,LEN($A28)+1)),2),”|”),{1,2},1)),

FALSE,ROW(OFFSET($A$1,,,LEN($A28)+1))),{1,2}+(COLUMNS($B28:B28)-1)*2)*{-1,1}))

…you can use it to extract numbers into separate columns, where $A28 holds the string to be split, and B28 holds the first column that you want to extract a number to. Like so:

I don’t know how either of these perform against a UDF, let alone each other. Anyone got a lean, mean, UDF-based splitting machine that we can test it against?

Here’s a sample file:

ExtractNumber_20141120

—Edit 21 Nov 2014—

It turns out that my above formula fails for a few specific special character & number combinations. Here’s a table, showing in which cases Excel will still treat a number as a number when you pad it out with a non number. For completeness I do three tests:

- Special character before the number
- Special character after the number
- Special character on either side of the number

…but if I wrap a SUM around that INDEX and array enter it, I get the wrong answer:

…meaning I have to return that INDEX function to the grid and *then* point my SUM function at it:

Everybody knows that, right? Well, everybody’s wrong. Everybody but the author over at the new ExcelXOR blog, that is. At this earth-shattering post the Author shows several intriguing ways to get INDEX to play nicely with outer functions:

Far out! That’s the kind of trick I would have sold my soul to the devil to learn.

There’s a lot to learn at that site: go to http://excelxor.com/blog/ and scroll most of the way down until the Archives section, and then get clicking. The earliest post is practically from yesterday: August 2014. But there’s a lifetime of learnings there already…both in the posts, and in the comments. Plus the most wicked formula challenges you will find in one place.

]]>**So I decided to design and build some better ones.**

Here are some of the more-frequently mentioned VLOOKUP INDEX/MATCH problems

- Slow exact match (linear search)
- Approximate sorted match is the wrong default 99.9% of the time and gives the wrong answer without warning
- Cannot do exact match on sorted data (well they can but only if they ignore sorted!)
- Numeric VLOOKUP answer column easy to break
- No built-in error handling for exact match
- VLOOKUP very inflexible
- INDEX/MATCH more flexible but still limited
- …

**MEMLOOKUP ( Lookup_Value, Lookup_Array, Result_Col, Sort_Type, MemType_Name, Vertical_Horizontal )**

The syntax is designed to make it easy to convert a VLOOKUP to MEMLOOKUP, but there are differences!

- Defaults to Exact Match on both unsorted and unsorted data
- Use either column labels or numbers
- Fast exact match on both unsorted and sorted data
- Automatic optimisation of multiple lookups within the same row

It’s always tempting to cram in more function (scope creep is universal), but if the result is too many parameters then it’s a mistake. So instead there is a whole family of these lookup functions that build on the MEMLOOKUP/MEMMATCH technology to provide the ultimate in flexibility and power whilst remaining efficient.

- Lookup using any column
- Lookup using more than one column without slow concatenation
- Lookup the first, last, Nth or all results on both sorted and unsorted data
- Lookup both rows and columns (2-dimensional lookup is built-in)
- Built-in error handling for exact match
- Return multiple answer columns
- Case-sensitive lookup option
- Regex match option

These functions are included in the 90 or so additional Excel functions built into FastExcel V3.

You can download the trial version from here.

I would be delighted to tell the Excel team how I built these functions and the algorithms they use.

By the way they are written as C++ multi-threaded functions in an XLL addin for maximum performance.

]]>

Sumit Bansal’s post VLOOKUP Vs. INDEX/MATCH – The Debate Ends Here! sparked some great discussion on the merits of VLOOKUP vs INDEX/MATCH, including at Oz du Soleil’s lighthearted rebuttal at The Anti-VLOOKUP Crowd Is Out In The Streets Again!

I especially love Peter B’s comment at Sumit’ post:

My opinion is that VLOOKUP and HLOOKUP are simply over-specialised legacy functions and Excel would be all the better for ‘pruning’ them out. I do use VLOOKUP occasionally when I have a 2-D range; the search array happens to be on the left; I only wish to return a single field; I am sure the data is clean and the match will always succeed. Despite that, I think the value they bring to the bloated zoo of Excel functions is not worth their keep.

Of course, NOTHING can ever be cleaned out of Excel, for good reason…otherwise all the millions of complex black-box spreadsheets that continue to function just fine long after the person who constructed them moved on to another task, job, or incarnation will break. Not to mention all those fantasy football spreadsheets. MS has backwards-compatibility issues that are beyond belief really.

At the same time I agree with Bob Phillips’ point at Sumit’s post:

The biggest selling point to me is that VLOOKUP is easy to teach to people, and it sticks, INDEX/MATCH less so.

But I disagree with Bob’s point that VLOOKUP can be/is just as flexible as INDEX/MATCH, merely because we can do stuff like this with it:

=VLOOKUP(“z”,CHOOSE({1,2},$B$1:$B$10,$A$1:$A$10),2,FALSE)

=VLOOKUP(“g”,$D$2:$H$15,MATCH(“Qtr2″,$D$2:$H$2,0),FALSE)

Just as flexible? Maybe, if you bend it double with brute force. Just as fast after you’ve made it just as flexible? Not likely. Any more understandable than the INDEX/MATCH equivalent? Not in my experience.

In fact, I feel a rude joke coming on:

Young analyst with unlit cigarette in mouth, having just consummated his first VLOOKUP:

Has anyone got a match?

Analyst of distinguished years:Yes. Your VLOOKUP and my arse.

If MS were designing Excel from scratch – and I was on the committee that was deciding whether to include a dumbed-down function to do a *subset* of lookups based on hard-coded input parameters and a fixed data layout – then I’d make a case for *not* including it. Not *just* because of those quite reasonable complaints, either. But also because of *evolution*. A user that is forced to learn INDEX and MATCH due to lack of suitable alternatives is be better placed to evolve into a higher Excel lifeform than one that hasn’t looked beyond VLOOKUP.

(I’d make an exception if a major competitor – say Lotus – had a VLOOKUP function in *their* beast. But only in that specific case.)

Formulas remind me a bit like DNA: just by stringing a few different base-pairs together in the right order, you can build a mouse. Or a Human, with a few extra tweaks. Similarly, with a few good formula combinations under your belt, you can conquer most problems you’re likely to come across. INDEX and MATCH are not just formulas in their own right, but are the formula equivalent of DNA basepairs: they give users a peek into other formula ecosystems that they can gradually spread into and colonize. VLOOKUP ain’t one of those base pairs. It’s Neanderthal.

Hey, don’t get me wrong: I’m fine that it’s in the fossil record. I’m happy enough to have one in my spreadsheet, just as I’m happy enough to have an appendix that doesn’t burst.

]]>I Don’t Use That Thing!

To my surprise there were a handful of Excel bloggers I reached out to who don’t use the Quick Access Toolbar at all! This includes the likes of Petros Chatzipantazis (Spreadsheet1.com & RibbonCommander.com), Andy Pope (AndyPope.info), Dick Kusleika (DailyDoseOfExcel.com), and Oscar Cronquist (GetDigitalHelp.com). Jon Peltier (PeltierTech.com) even went as far as to state that he “hate, hate, hates the QAT (it ain’t worth squat!).” I found this extremely intriguing and I hope these guys will share their philosophy on not making use of the QAT in the comments section below.

That’s good enough company for me. I don’t hate the QAT, I’m simply indifferent to it. I was at home when I responded to Chris’ request and when I got to work I noticed that I had added `Speak Cells`

and `Stop Speaking Cells`

, although I’m sure I’ve never used them. If I have used the speaking thing, I hunted for it on the Ribbon oblivious that I had added it to the QAT.

Incidentally (and uninterestingly) I use it extensively in Outlook. There’s no `Application.OnKey`

so I have to have some way to get at those macros.

Where Are The Macros?

One of the biggest surprises for me was that there were not too many people running macros out of there QAT. I was especially surprised that some people who have dedicated blogs for VBA (cough, cough…Jordan Goldmeier….yeah I’m calling you out!) didn’t have one trace of VBA code hanging out in the QAT. I did get feedback from some stating that most of their macro code used on a regular basis was executed via assigned keyboard shortcuts and that does make sense. About 5 mouths ago I started to shy away from using shortcuts with my macros. Here was my reasoning:

Tell us how you use (or don’t use) the QAT in the comments here or at Chris’ site.

]]>My revision can be called from the worksheet, and has the following arguments:

**=JoinText(Array,[Delimiter],[FieldDelimiter],[EndDelimiter],[SkipBlanks],[Transpose])**

Yes, more arguments than at my last social outing. Most are optional and have defaults. Take that how you will. The default delimiter is a comma. The Field Delimiter is a separate Delimiter that gets added if your input array is 2D, and the default is *also* a comma. EndDelimiter puts an extra Delimiter of your choice on the end *if you want one*. Aesthetics only, really. The rest are explained below.

- That orange block is my data.
- Column D shows the result if you point the function at each respective row
- Row 8 shows the result of pointing the function at each respective column
- In rows 12 to 15 you see the result of pointing it at the entire 2D block of data, under different settings.

Those last two results are what happens if the data is laid out by row and then by column, and you’ve incorrectly told the UDF to transpose the input array. If your data happenned to be laid out like this, you wouldn’t need that Transpose argument:

The DelimitEnd argument does something pretty minor, really. If we *include* it, the end of the string gets padded with it – in this case an Exclamation Mark . If we exclude it, the string doesn’t get padded with any extra delimiters:

You might notice it skips blanks. It doesn’t have to, if you don’t want it to:

And it doesn’t need your two arrays to be the same size:

A real-world example where this might be useful is when concatenating lists of names, where some may have more parts than others:

Both the last two screenshots show examples of using three different delimiters…a space between words, a comma between columns, and something different on the end.

Here’s the code and workbook:

Join Function_20141115

Public Function JoinText(target As Range, _

Optional Delimiter As String = ",", _

Optional FieldDelimiter As String = ",", _

Optional EndDelimiter As String = "", _

Optional SkipBlanks As Boolean = False, _

Optional Transpose As Boolean = False) As String

'Based on code from Nigel Heffernan at Excellerando.Blogspot.com

'http://excellerando.blogspot.co.nz/2012/08/join-and-split-functions-for-2.html

' Join up a 2-dimensional array into a string.

' ####################

' # Revision history #

' ####################

' Date (YYYYMMDD) Revised by: Changes:

' 20141114 Jeff Weir Turned into worksheet function, added FinalDelimiter and Transpose options

' 20141115 Jeff Weir Changed FinalDelimiter to EndDelimiter that accepts string, with default of ""

Dim InputArray As Variant

Dim i As Long

Dim j As Long

Dim k As Long

Dim lngNext As Long

Dim i_lBound As Long

Dim i_uBound As Long

Dim j_lBound As Long

Dim j_uBound As Long

Dim arrTemp1() As String

Dim arrTemp2() As String

If target.Rows.Count = 1 Then

If target.Columns.Count = 1 Then

GoTo errhandler 'Target is a single cell

Else

' Selection is a Row Vector

InputArray = Application.Transpose(target)

Transpose = True

End If

Else

If target.Columns.Count = 1 Then

' Selection is a Column Vecton

InputArray = target

Else:

'Selection is 2D range. Transpose it if that's what the user has asked for

If Transpose Then

InputArray = Application.Transpose(target)

Transpose = True

Else: InputArray = target

End If

End If

End If

i_lBound = LBound(InputArray, 1)

i_uBound = UBound(InputArray, 1)

j_lBound = LBound(InputArray, 2)

j_uBound = UBound(InputArray, 2)

ReDim arrTemp1(j_lBound To j_uBound)

ReDim arrTemp2(i_lBound To i_uBound)

lngNext = 1

For i = j_lBound To j_uBound

On Error Resume Next

If SkipBlanks Then

If Transpose Then

ReDim arrTemp2(i_lBound To WorksheetFunction.CountA(target.Rows(i)))

Else

ReDim arrTemp2(i_lBound To WorksheetFunction.CountA(target.Columns(i)))

End If

End If

If Err.Number = 0 Then

k = 1

For j = i_lBound To i_uBound

If SkipBlanks Then

If InputArray(j, i) <> "" Then

arrTemp2(k) = InputArray(j, i)

k = k + 1

End If

Else

arrTemp2(j) = InputArray(j, i)

End If

Next j

arrTemp1(lngNext) = Join(arrTemp2, Delimiter)

lngNext = lngNext + 1

Else:

Err.Clear

End If

Next i

If SkipBlanks Then ReDim Preserve arrTemp1(1 To lngNext - 1)

If lngNext > 2 Then

JoinText = Join(arrTemp1, FieldDelimiter)

Else: JoinText = arrTemp1(1)

End If

If JoinText <> "" Then JoinText = JoinText & EndDelimiter

errhandler:

End Function

Optional Delimiter As String = ",", _

Optional FieldDelimiter As String = ",", _

Optional EndDelimiter As String = "", _

Optional SkipBlanks As Boolean = False, _

Optional Transpose As Boolean = False) As String

'Based on code from Nigel Heffernan at Excellerando.Blogspot.com

'http://excellerando.blogspot.co.nz/2012/08/join-and-split-functions-for-2.html

' Join up a 2-dimensional array into a string.

' ####################

' # Revision history #

' ####################

' Date (YYYYMMDD) Revised by: Changes:

' 20141114 Jeff Weir Turned into worksheet function, added FinalDelimiter and Transpose options

' 20141115 Jeff Weir Changed FinalDelimiter to EndDelimiter that accepts string, with default of ""

Dim InputArray As Variant

Dim i As Long

Dim j As Long

Dim k As Long

Dim lngNext As Long

Dim i_lBound As Long

Dim i_uBound As Long

Dim j_lBound As Long

Dim j_uBound As Long

Dim arrTemp1() As String

Dim arrTemp2() As String

If target.Rows.Count = 1 Then

If target.Columns.Count = 1 Then

GoTo errhandler 'Target is a single cell

Else

' Selection is a Row Vector

InputArray = Application.Transpose(target)

Transpose = True

End If

Else

If target.Columns.Count = 1 Then

' Selection is a Column Vecton

InputArray = target

Else:

'Selection is 2D range. Transpose it if that's what the user has asked for

If Transpose Then

InputArray = Application.Transpose(target)

Transpose = True

Else: InputArray = target

End If

End If

End If

i_lBound = LBound(InputArray, 1)

i_uBound = UBound(InputArray, 1)

j_lBound = LBound(InputArray, 2)

j_uBound = UBound(InputArray, 2)

ReDim arrTemp1(j_lBound To j_uBound)

ReDim arrTemp2(i_lBound To i_uBound)

lngNext = 1

For i = j_lBound To j_uBound

On Error Resume Next

If SkipBlanks Then

If Transpose Then

ReDim arrTemp2(i_lBound To WorksheetFunction.CountA(target.Rows(i)))

Else

ReDim arrTemp2(i_lBound To WorksheetFunction.CountA(target.Columns(i)))

End If

End If

If Err.Number = 0 Then

k = 1

For j = i_lBound To i_uBound

If SkipBlanks Then

If InputArray(j, i) <> "" Then

arrTemp2(k) = InputArray(j, i)

k = k + 1

End If

Else

arrTemp2(j) = InputArray(j, i)

End If

Next j

arrTemp1(lngNext) = Join(arrTemp2, Delimiter)

lngNext = lngNext + 1

Else:

Err.Clear

End If

Next i

If SkipBlanks Then ReDim Preserve arrTemp1(1 To lngNext - 1)

If lngNext > 2 Then

JoinText = Join(arrTemp1, FieldDelimiter)

Else: JoinText = arrTemp1(1)

End If

If JoinText <> "" Then JoinText = JoinText & EndDelimiter

errhandler:

End Function

I like this function. I’m sure I’ll like it even more when you’re all finished polishing it to a bright sheen.

]]>(Aside: If you’re NOT a Ctrl freak, then you can also put Excel into *Add To Selection* mode by pushing Shift + F8, which adds any further cells you click on to the current selection without the need to hold down Ctrl. When you’ve got the cells you want, just push Shift + F8 again.)

I thought I’d try a simpler approach…if the user tries to select something that’s already selected, simply dump it from the current selection. So I came up with this:

Private Sub Workbook_Open()

Set App = Application

End Sub

Private Sub App_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)

Deselect Target

End Sub

Sub Deselect(Target As Range)

Dim lngCount As Long

Dim lngLast As Long

Dim strTarget As String

Dim strOld As String

Dim strNew As String

'This code allows you to deselect cells when CTRL + Clicking

strTarget = Target.Address

lngCount = UBound(Split(strTarget, ","))

If lngCount > 0 Then

strNew = "," & Split(strTarget, ",")(lngCount) & ","

'Need to add the "," as a delimiter so we don't incorrectly identify say $A$1 and $A$10 as the same

strOld = "," & Left(strTarget, Len(strTarget) - Len(strNew) + 1) & ","

If InStr(strOld, strNew) > 0 Then

If strOld <> strNew Then

strOld = Replace(strOld, strNew, ",")

End If

If Right(strOld, 1) = "," Then strOld = Left(strOld, Len(strOld) - 1)

If Left(strOld, 1) = "," Then strOld = Mid(strOld, 2, Len(strOld))

Application.EnableEvents = False

Range(strOld).Select

Range(Split(strOld, ",")(UBound(Split(strOld, ",")))).Activate

Application.EnableEvents = True

End If

End If

End Sub

Set App = Application

End Sub

Private Sub App_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)

Deselect Target

End Sub

Sub Deselect(Target As Range)

Dim lngCount As Long

Dim lngLast As Long

Dim strTarget As String

Dim strOld As String

Dim strNew As String

'This code allows you to deselect cells when CTRL + Clicking

strTarget = Target.Address

lngCount = UBound(Split(strTarget, ","))

If lngCount > 0 Then

strNew = "," & Split(strTarget, ",")(lngCount) & ","

'Need to add the "," as a delimiter so we don't incorrectly identify say $A$1 and $A$10 as the same

strOld = "," & Left(strTarget, Len(strTarget) - Len(strNew) + 1) & ","

If InStr(strOld, strNew) > 0 Then

If strOld <> strNew Then

strOld = Replace(strOld, strNew, ",")

End If

If Right(strOld, 1) = "," Then strOld = Left(strOld, Len(strOld) - 1)

If Left(strOld, 1) = "," Then strOld = Mid(strOld, 2, Len(strOld))

Application.EnableEvents = False

Range(strOld).Select

Range(Split(strOld, ",")(UBound(Split(strOld, ",")))).Activate

Application.EnableEvents = True

End If

End If

End Sub

Here’s an illustration: below is a screenshot where I was trying to select cells in a Checker-board pattern while holding Ctrl, but made a stuff-up a couple of clicks ago:

Without VBA, I’d need to start from scratch, because Excel doesn’t let you deselect particular blocks from your current selection. But with my trusty code, all I need to do is try to select the offending block *again*, and Excel will say *Hey…you’ve already got that in your selection. Oh wait…I guess you’re trying to tell me that you want to dump that particular range from the selection, given it’s already selected.*

And so it does just that:

…which frees me up to try again:

In fact, as long as I keep holding Ctrl down, I can deselect as many ranges as I want:

It works pretty well. See for yourself: Open the below sample file, hold Ctrl down and do some crazy clicking, and occasionally click something you’ve already selected. * ZAP!* It’s removed from the current selection.

Unselect_20141111 v3 (Note: I’ve updated this file with snb’s version of the code listed further below.)

Why this isn’t the native behavior right out of the box is beyond me.

There’s bound to be coding improvements, so let’s have ‘em.

snb has a much smarter approach in the comments that lets users deselect *individual* cells within a particular subs-selection OR deselect a sub-selection in its *entirety*. I’ve amended the sample file accordingly.

His approach goes a little something like so:

Private WithEvents App As Application

Option Explicit

Private Sub Workbook_Open()

Set App = Application

End Sub

Private Sub App_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)

Deselect Target

End Sub

Sub Deselect(Target As Range)

Dim rn As Range

Dim cl As Range

Dim sel As Range

On Error Resume Next

Set rn = Target.Areas(Target.Areas.Count)

If Target.Count > 1 And Target.Areas.Count > 1 Then

If Not Intersect(Range(Replace(Target.Address & "~", "," & rn.Address & "~", "")), rn) Is Nothing Then

For Each cl In Target

If Intersect(cl, rn) Is Nothing Then Set sel = Union(sel, cl)

If Err.Number <> 0 Then Set sel = cl

Err.Clear

Next

sel.Select

End If

End If

End Sub

Option Explicit

Private Sub Workbook_Open()

Set App = Application

End Sub

Private Sub App_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)

Deselect Target

End Sub

Sub Deselect(Target As Range)

Dim rn As Range

Dim cl As Range

Dim sel As Range

On Error Resume Next

Set rn = Target.Areas(Target.Areas.Count)

If Target.Count > 1 And Target.Areas.Count > 1 Then

If Not Intersect(Range(Replace(Target.Address & "~", "," & rn.Address & "~", "")), rn) Is Nothing Then

For Each cl In Target

If Intersect(cl, rn) Is Nothing Then Set sel = Union(sel, cl)

If Err.Number <> 0 Then Set sel = cl

Err.Clear

Next

sel.Select

End If

End If

End Sub

And so with SNB’s code, if I were to select a block:

…and I wanted to ditch the cell in the middle, then I can simply select it while holding Ctrl, and it gets ditched:

Meaning that I can then say apply formatting, to create an in-cell donut:

Much better than my approach. Cheers, snb!

John at Global Electronic Trading has the latest VBA eulogy. He asked several VBA community members (including me) to answer four questions about the future of VBA. Here is my response to what killed VBA

[DK] Time killed it. Nothing last forever. Cobol developers were once in high demand. Now Cobol developers are in very high demand – both of them. Microsoft killed it by not updating the IDE or supporting VBA as a viable development platform. Had they invested in VBA, say by integrating .Net into Office the way they did with VB, then it still may have been a viable platform today. But even if that were true, time would kill it eventually.

The internet killed it by adopting Ajax. A lot of developer resources went to web apps and away from COM based development.Apple killed it by inventing the App Store. None of those developer resources came back to COM, they’re all developing mobile apps now.

So a bunch of stuff killed VBA, but all that means is that evolution killed it. MS evolved their development platform away from VBA just like they evolved away from ANSI C before that.

Go read the rest of the answers. You won’t be surprised by any of the answers, I’ll bet.

I draw two conclusions from this experience:

- I need to proof read my emails before I send them.
- I don’t care if VBA is dead. It still works for me now, I’m very effective with it, and I’m still solving real problems using it every day. If it’s dead, it’s the best damn corpse in the office.