Italic font doesn't copied - excel

I have code which joins some strings.
For example:
Before
Now
I want to see
Error:
Easy example
The problem is that the unedited string has italic words, but when I try to join this string, italic words become without this font, how I should edit my code?
Sub MergeText()
Dim strMerged$, r&, j&, i&, uneditedColumn As Long, resultColumn As Long
With ThisWorkbook.Worksheets("Sheet1") 'Change sheet name if needed
uneditedColumn = 1 ' Column number which need to edit !!! uneditedColumn must not be equal resultColumn
resultColumn = 3 ' Column number where need to put edited text
r = 1
Do While True
If Cells(r, uneditedColumn).Characters(1, uneditedColumn).Font.Bold Then
strMerged = "": strMerged = Cells(r, uneditedColumn)
r = r + 1
While (Not Cells(r, uneditedColumn).Characters(1).Font.Bold) And Len(Cells(r, uneditedColumn)) > 0
strMerged = strMerged & " " & Cells(r, uneditedColumn)
r = r + 1
Wend
i = i + 1: Cells(i, resultColumn) = strMerged
Cells(i, resultColumn).Characters(1, InStr(1, strMerged, ".", vbTextCompare)).Font.Bold = True
Else
Exit Do
End If
Loop
End With
End Sub

Ok, that was very fun. Code first, talk later:
Public Sub MergeAndFormat()
Const originalColumn As Long = 1
Const formattedColumn As Long = 3
Dim lastRow As Long
lastRow = Sheet1.Range("A" & Sheet1.Rows.Count).End(xlUp).Row
Dim currentEntry As Long
Dim currentRow As Long
For currentRow = 1 To lastRow
Dim currentCell As Range
Set currentCell = Sheet1.Cells(currentRow, originalColumn)
Dim currentText As String
currentText = currentCell.Value
' ensure we have a space at the end of the line
If Right$(currentText, 1) <> " " Then currentText = currentText & " "
Dim isNewEntry As Boolean 'new entry if first char is bold
isNewEntry = currentCell.Characters(1, 1).Font.Bold
Dim currentCharOffset As Long
Dim currentEntryText As String
If isNewEntry Then
currentEntry = currentEntry + 1
currentEntryText = currentText
currentCharOffset = 1
Else
currentCharOffset = Len(currentEntryText) + 1
currentEntryText = currentEntryText & currentText
End If
Dim entryCell As Range
Set entryCell = Sheet1.Cells(currentEntry, formattedColumn)
If isNewEntry Then entryCell.Value = vbNullString
'append the source characters, without losing formatting in the entryCell
entryCell.Characters(currentCharOffset + 1).Insert currentText
Dim currentIndex As Long
For currentIndex = 1 To currentCell.Characters.Count
entryCell.Characters(currentCharOffset + currentIndex - 1, 1).Font.Bold = currentCell.Characters(currentIndex, 1).Font.Bold
entryCell.Characters(currentCharOffset + currentIndex - 1, 1).Font.Italic = currentCell.Characters(currentIndex, 1).Font.Italic
entryCell.Characters(currentCharOffset + currentIndex - 1, 1).Font.Underline = currentCell.Characters(currentIndex, 1).Font.Underline
entryCell.Characters(currentCharOffset + currentIndex - 1, 1).Font.Strikethrough = currentCell.Characters(currentIndex, 1).Font.Strikethrough
Next
Next
End Sub
The entire loop logic was obscured by single-letter variable names, data types involved were obscured with type hint characters, and the intent of the variables was obscured because the meaning of a variable changed depending on what line of code you were looking at (e.g. uneditedColumn with a value of 1 coincidentally making sense as a Length argument for the Range.Characters property.
So I burned everything to the ground, and rewrote the whole logic.
We know where the "original" text begins, and where it ends - we don't need a near-infinite Do While loop: instead we figure out what the lastRow is, and we use a For...Next loop that starts at the top and finishes at whatever the lastRow is, using currentRow as out counter.
Since we use currentRow for counting where we're at in the original column, we'll use currentCell for the Range object representing that particular "current cell", and currentText will hold the string value of that cell's text.
Then we need to know if we're looking at a "new entry", or if we're continuing the previous one - isNewEntry is True if the first character of the currentCell is bold.
When isNewEntry is True, we increment the currentEntry counter (which is 0 until we first assign it with the first "new entry") so we know what row we're going to be writing to; the currentEntryText will then match the currentText, and the character-formatting offset will be at position 1.
When isNewEntry is False, we don't increment the currentEntry counter (we'll be appending to that cell's text instead), and we compute the new character-formatting offset by adding 1 to the length of the entire text for the current entry - then we update the currentEntryText to append the currentText - not because we need the text itself, but because we'll need it next iteration to compute the new character offset.
At this point we know what to write, and where to write it - only, if we work at the Range level, we're going to overwrite everything we did in the previous iteration, and lose the formatting... and we don't want that, so that's why we track these offsets...
We Insert the currentText at the end of the entryCell's current content, and then we begin iterating the characters in the currentCell, and literally copy the formatting - offsetting the characters by what we've tracked.
The above code preserves Bold, Italic, Underline, and Strikethrough formatting; changing it to also support Subscript and Superscript formats should be trivial.

Related

Why does my search function take much longer after rearranging it to save a few checks?

I wrote a Sub which looks for the Search Terms from one sheet, and captures each line from a big array of strings which contains one of those terms (printing it to another sheet).
Originally I had it written like this:
For each Search Term, look at each line in the file and print each match.
It worked, but it meant that if one line contained multiple Search Terms, it would get printed as many times. I wanted to eliminate that, just printing the line once if it contained at least one Search Term. So, I rearranged the code to work like this:
For each line in the file, look at each Search Term and print the line if it matches.
(and if you do get a match, skip the remaining Search Terms for that line)
If anything, this way should be slightly more efficient than the original, I thought...
And yet, after I rearranged the code, it takes much longer to execute.
I guess this means that, though I feel as though I'm doing the same things in a different order, and even saving some unnecessary checks, I must be somehow compounding the parts that take the longest. Can anyone shed some light and help me understand how I've made the code so much worse?
Here's what the two versions of the relevant code segment look like:
Original way:
searchTermRow = firstSearchTermRow
While ActiveWorkbook.Sheets("SearchTerms").Cells(searchTermRow, 1) <> ""
term = ActiveWorkbook.Sheets("SearchTerms").Cells(searchTermRow, 1)
For j = 1 To UBound(bigStringArray) - 1
counter = counter + 1
If (InStr(bigStringArray(j), term) <> 0) Then
Cells(resultsRow, 1).Value = bigStringArray(j)
resultsRow = resultsRow + 1
End If
Next j
searchTermRow = searchTermRow + 1
Wend
Rearranged:
For j = 1 To UBound(bigStringArray) - 1
searchTermRow = firstSearchTermRow
Do While ActiveWorkbook.Sheets("SearchTerms").Cells(searchTermRow, 1) <> ""
term = ActiveWorkbook.Sheets("SearchTerms").Cells(searchTermRow, 1)
counter = counter + 1
If (InStr(bigStringArray(j), term) <> 0) Then
Cells(resultsRow, 1).Value = bigStringArray(j)
resultsRow = resultsRow + 1
Exit Do
End If
searchTermRow = searchTermRow + 1
Loop
Next j
bigStringArray is a large array of strings which consists of every line from a file.
counter is just there so that I could count how many checks were done.
You can do one pass through the bigStringArray by using a regular expression with a pattern such as term1|term2|term3 etc.
Option Explicit
Sub RegexMatch()
Const firstSearchTermRow = 2
Dim ws As Worksheet
Dim lastRow As Long, resultsRow As Long, j As Long
Dim rngTerms As Range, t0 As Single: t0 = Timer
Set ws = ActiveWorkbook.Sheets("SearchTerms")
lastRow = ws.Cells(Rows.count, 1).End(xlUp).Row
' build regex pattern
Dim sPattern As String
Set rngTerms = ws.Range("A" & firstSearchTermRow & ":A" & lastRow)
sPattern = Join(WorksheetFunction.Transpose(rngTerms), "|")
'Debug.Print sPattern
Dim regex As Object, m As Object
Set regex = CreateObject("vbscript.regexp")
With regex
.Global = False
.MultiLine = False
.IgnoreCase = True
.Pattern = "(" & sPattern & ")"
End With
' test data
Dim bigStringArray
bigStringArray = Array("", "ABCDE", "BCDEF", "CDEFGH", "DEFHIJKL")
resultsRow = 1
For j = 1 To UBound(bigStringArray)
If regex.test(bigStringArray(j)) Then
Set m = regex.Execute(bigStringArray(j)) ' match
Cells(resultsRow, 1).Value = bigStringArray(j)
Cells(resultsRow, 2).Value = m(0) ' matched term
resultsRow = resultsRow + 1
End If
Next
MsgBox j - 1 & " strings checked", vbInformation, Format(Timer - t0, "0.00 secs")
End Sub

Doouble Looopiing

I am trying to print out "OK" value if the statements same value with "NumberPallete" but my code doesn't work right.
I have two conditions to compare from one cell value ["54# / 221"]. The first condition value for "SeriesNumber" is [88] and then the Second condition value for "NumberPallete" is [221#]. I am using looping for "SeriesNumber" and "NumberPallete" to find the value because I have long data in the table sheet.
and then from the different sheets using looping too, I am starting with the First condition checks "SeriesNumber" value if the value is right, then check the second condition from "NumberPallete" value, in this condition, I want a print out "ok" value but "ok" value doesn't print out.
I am sorry, my English is poor. I'm trying my best to explain. Please help me.
Dim NumberPallete As String
Dim SeriesNumber As String
Dim I As Long
Dim j As Long
Dim z As Long
i = Cells(Rows.Count, 15).End(xlUp).Row
For j = 6 To i
'Cells(j, 20).Value = Right(Cells(j, 15).Value, Len(Cells(j, 15)) - InStr(1, Cells(j, 15).Value, "/"))
SeriesNumber = Right(Cells(j, 15).Value, Len(Cells(j, 15)) - InStr(1, Cells(j, 15).Value, "/"))
'Cells(j, 21).Value = Left(Cells(j, 15).Value, Len(Cells(j, 15)) - InStr(1, Cells(j, 15).Value, "/"))
NumberPallete = Left(Cells(j, 15).Value, Len(Cells(j, 15)) - InStr(1, Cells(j, 15).Value, "/"))
If SeriesNumber = 221 Then
For z = 4 To 250
If Worksheets("AAA").Cells(z, 2).Value = NumberPallete Then
Worksheets("AAA").Cells(z, 6).Value = "OK"
End If
Next z
Else
MsgBox ("Not OK")
End If
Next j
I may not have fully understood what you are trying to do but the code below is doing something and, hopefully, it can be fixed to do what you want.
Sub FindPalletNumber()
' 062
' you can find code to enter 2 values with input boxes at this link:-
' https://stackoverflow.com/questions/62651211/vba-excel-search-in-excel-specific-word-and-delete-all-rows-who-does-not-have-t
Dim Snum As Integer ' serial number
Dim Pnum As Integer ' pallet number
Dim Txt As String ' message text
Snum = 221 ' number only
Pnum = 54 ' no # sign, no brackets
If MarkOK(Snum, Pnum) Then
Txt = "Found and marked."
Else
Txt = "No match found."
End If
MsgBox Txt, vbInformation, "Action report"
End Sub
Private Function MarkOK(Snum As Integer, _
Pnum As Integer) As Boolean
' 062
' return True if found and marked
Const Pallet As Long = 0 ' element of array Nums
Const Serial As Long = 1 ' element of array Nums
Dim Nums() As String ' split cell pattern "54# / 221"
Dim Done As Boolean ' True if found
Dim R As Long ' loop counter: Row in ActiveSheet
Dim R2 As Long ' loop counter: Row in Ws("AAA")
For R = 6 To Cells(Rows.Count, 15).End(xlUp).Row
Nums = Split(Cells(R, 15).Value, "/")
' Nums(Pallet) = "54# ", Nums(Serial) = " 221"
If Val(Nums(Serial)) = Snum Then
With Worksheets("AAA")
For R2 = 4 To .Cells(.Rows.Count, 2).End(xlUp).Row
If .Cells(R2, 2).Value = Trim(Nums(Pallet)) Then
.Cells(R2, 6).Value = "OK"
Done = True
Exit For
End If
Next R2
End With
End If
If Done Then Exit For ' stop search if found
Next R
MarkOK = Done
End Function
In the first procedure the Pallet and Serial numbers should be set (Pnum and Snum). Then, when you run that procedure, it will call the other one which reports back whether a match was found or not. I have added a link where you can find code to get the two values from Input boxes, if that is what you need.
The function looks for the serial number in the ActiveSheet. If found, it looks for the pallet number in Sheet("AAA"). This is confusing because it looks for the pallet number found in the ActiveSheet, not the pallet number specified in the search. The pallet number in the search specs ends up not being used at all. Perhaps it's not needed.
Anyway, when the pallet is found the row is marked and the search terminates. If the pallet number isn't found the loop in the ActiveSheet is continued to look for another instance of the serial number. Note that the code is not enabled to find multiple pallets with the same serial number.

Dynamic range based formula results' change every time I switch sheet Excel VBA

I wrote a function that supposed to get a specific part of a specific Column, and then, by comparing each entry of the column to the value of the cell that is left to it, count the times a specific condition is met.
It all works alright, except one problem.. if I use the function on "Sheet1", get a result and then switch to "Sheet2" and use the function on this sheet it changes the result on "Sheet1" for some reason.
Function countStable(rangeObj As Range) 'rangeObj that being passed is a namedRange(Synamic Range)
Application.Volatile
ActiveSheet.Select
Dim entry, preEntryVal, entryVal As Variant
Dim counters(1 To 5, 1 To 1) As Integer
Dim cStable, cIncreased, cDecreased, cAdded, cLost
cStable = 0
cIncreased = 0
cDecreased = 0
cAdded = 0
cLost = 0
Set rangeObj = Intersect(rangeObj, rangeObj.Parent.UsedRange)
For Each entry In rangeObj
If Not IsEmpty(entry.Value) And Not IsEmpty(ActiveSheet.Range("A" & entry.Row)) Then
entryVal = entry.Value
preEntryVal = ActiveSheet.Cells(entry.Row, entry.Column - 1).Value
If entryVal = preEntryVal Then
cStable = cStable + 1
ElseIf InStr(entryVal, "-") And Not (InStr(preEntryVal, "-")) Then
cLost = cLost + 1
ElseIf Not InStr(entryVal, "-") And InStr(preEntryVal, "-") Then
cAdded = cAdded + 1
ElseIf preEntryVal < entryVal Then
cDecreased = cDecreased + 1
ElseIf preEntryVal > entryVal Then
cIncreased = cIncreased + 1
End If
End If
counters(1, 1) = cStable
counters(2, 1) = cIncreased
counters(3, 1) = cDecreased
counters(4, 1) = cAdded
counters(5, 1) = cLost
Next
countStable = counters
End Function
As commented inside the code, rangeObj that is being passed as parameter was defined in the name manager and it is based on an Offset formula.
I know it changes the values on both sheets because of the dynamic range, but not sure why.. I don't want it to be changed.
Help please?
In several places, the code references the ActiveSheet. Wherever the function appears, it will reflect the value of whatever sheet is active. You'll want to use the parent of the supplied range object instead.
Dim currentSheet as Worksheet
Set currentSheet = rangeObj.Parent
Then, search and replace ActiveSheet with currentSheet in the method.

Parsing and comparing a complicated string

I am hoping someone could help me out with a VBA Excel macro.
I have received a worksheet in Excel 2007 which contains product names in one column, and I need to sort this into a logical format so I can use it. However, the list itself is not in any kind of logical order, is 10 000 rows long and I am going to have to do this every month!!
Basically, what I would like to do is search for certain keywords which are common to most of the entries and move them into separate cells in different columns (but in the same row as the original entry).
Regarding keywords: There are 3 different types, two of which I have a complete list of.
Example of keywords: some are measures such as cm (centimetre), mm (millimetre), m (metre) etc.). Then there are other keywords such as % and finally a last set of keywords which is wood, plastic, glass etc.
If this was not complicated enough, the measures (cm for example) are duplicated in some instances and are important details so I cant just separate them but would ideally like them in two adjacent cells.
Fortunately, there is a space after each measure, % sign and item material.
Working from right to left is the easiest way I can think of achieving this as the first description in the string varies wildly between entries and that can stay as is.
So, below is an example string, lets say this is in Cell A1. (Inverted commas are not included in the string and the word "by" appears in only about 100 cases. Usually it is missing...)
"Chair Leg Wood 100% 1m by 20cm"
I would ideally like for the string to be split up into cells as follows
Cell B1 - Chair Leg
Cell C1 - Wood
Cell D1 - 1m
Cell E1 - 2cm
Cell F1 - 100%
Having the % measures in the same column would be extremely helpful
Can anyone please help me with this or the beginnings of a macro which does this and then moves down the list - I have tried using some basic "find" and "len" formulas but really am at my wits end on how to deal with this!
The task boils down to defining a robust definition of the structure of the input data.
Form the info provided a candidate definition might be
<Description, one or more words> <Material, one word> <A value followd by %> <Dimension A> <optional "by"> <Dimension B>
The following macro will process data that conforms this this spec. The definition may need
expanding, eg two word materials (eg Mild Steel)
You will need to add error handling in case any rows don't conform, eg no % in the string, or % character elsewhere in string
Option Explicit
Dim dat As Variant
Sub ProcessData()
Dim r As Range
Dim i As Long
Set r = Intersect(ActiveSheet.UsedRange, ActiveSheet.Columns(1)).Resize(, 5)
dat = r
For i = 1 To UBound(dat, 1)
ParseRow i, CStr(dat(i, 1))
Next
r = dat
ActiveSheet.Columns(5).Style = "Percent"
End Sub
Sub ParseRow(rw As Long, s As String)
'Chair Leg Wood 100% 1m by 20cm
Dim i As Long
Dim sDim As String, sPCnt As String, sMat As String, sDesc As String
Dim sA As String, sB As String
i = InStr(s, "% ")
sDim = Trim(Replace(Mid(s, i + 2), " by ", " ")) ' text to right of %, remove "by"
sA = Trim(Left(sDim, InStr(sDim, " "))) ' split dimension string in two
sB = Trim(Mid(sDim, InStr(sDim, " ")))
s = Left(s, i)
i = InStrRev(s, " ")
sPCnt = Mid(s, i + 1) ' text back to first space before %
s = Trim(Left(s, i))
i = InStrRev(s, " ") ' last word in string
sMat = Mid(s, i + 1)
sDesc = Trim(Left(s, i)) ' whats left
dat(rw, 1) = sDesc
dat(rw, 2) = sMat
dat(rw, 3) = sA
dat(rw, 4) = sB
dat(rw, 5) = sPCnt
End Sub
First, I'd use the Split function to separate the parts into an array, this will avoid most of the string functions and string math:
Dim parts As Variant
parts = Split(A1)
Then, I'd do my comparisons to each part.
Finally, I'd concatenate the parts I didn't breakout, and place all parts on the sheet.
This is based on your example which has spaces inbetween every part, though something similar could work otherwise, you just have to do more work with each part.
Here's my stab at it. We could use about 10 more examples, but this should be a start. To use, select a one column range with your descriptions and run SplitProduct. It will split it out to the right of each cell.
Sub SplitProducts()
Dim rCell As Range
Dim vaSplit As Variant
Dim i As Long
Dim aOutput() As Variant
Dim lCnt As Long
Const lCOLDESC As Long = 1
Const lCOLMAT As Long = 2
Const lCOLPCT As Long = 3
Const lCOLREM As Long = 4
If TypeName(Selection) = "Range" Then
If Selection.Columns.Count = 1 Then
For Each rCell In Selection.Cells
'split into words
vaSplit = Split(rCell.Value, Space(1))
ReDim aOutput(1 To 1, 1 To 1)
'loop through the words
For i = LBound(vaSplit) To UBound(vaSplit)
Select Case True
Case IsPercent(vaSplit(i))
'percents always go in the same column
lCnt = lCOLPCT
If UBound(aOutput, 2) < lCnt Then
ReDim Preserve aOutput(1 To 1, 1 To lCnt)
End If
aOutput(1, lCnt) = vaSplit(i)
Case IsInList(vaSplit(i))
'list items always go in the same column
lCnt = lCOLMAT
ReDim Preserve aOutput(1 To 1, 1 To lCnt)
If UBound(aOutput, 2) < lCnt Then
ReDim Preserve aOutput(1 To 1, 1 To lCnt)
End If
aOutput(1, lCnt) = vaSplit(i)
Case IsMeasure(vaSplit(i))
'measurements go in the last column(s)
If UBound(aOutput, 2) < lCOLREM Then
lCnt = lCOLREM
Else
lCnt = UBound(aOutput, 2) + 1
End If
ReDim Preserve aOutput(1 To 1, 1 To lCnt)
aOutput(1, lCnt) = vaSplit(i)
Case Else
'everything else gets concatentated in the desc column
aOutput(1, lCOLDESC) = aOutput(1, lCOLDESC) & " " & vaSplit(i)
End Select
Next i
'remove any extraneous spaces
aOutput(1, lCOLDESC) = Trim(aOutput(1, lCOLDESC))
'write the values to the left of the input range
rCell.Offset(0, 1).Resize(1, UBound(aOutput, 2)).Value = aOutput
Next rCell
Else
MsgBox "Select a one column range"
End If
End If
End Sub
Function IsPercent(ByVal sInput As String) As Boolean
IsPercent = Right$(sInput, 1) = "%"
End Function
Function IsInList(ByVal sInput As String) As Boolean
Dim vaList As Variant
Dim vaTest As Variant
'add list items as needed
vaList = Array("Wood", "Glass", "Plastic")
vaTest = Filter(vaList, sInput)
IsInList = UBound(vaTest) > -1
End Function
Function IsMeasure(ByVal sInput As String) As Boolean
Dim vaMeas As Variant
Dim i As Long
'add measurements as needed
vaMeas = Array("mm", "cm", "m")
For i = LBound(vaMeas) To UBound(vaMeas)
'any number of characters that end in a number and a measurement
If sInput Like "*#" & vaMeas(i) Then
IsMeasure = True
Exit For
End If
Next i
End Function
No guarantees that this will be speedy on 10k rows.

VBA-Excel and large data sets causes program to crash

First time poster and new to programming in general. I have a project in which i have to build a financial model to mine for data in excel. I have succeeded in building said model on VBA. I have ran tests on 3,000 line dataset and it was successful. I will briefly explain what it does.
I track a given stock on a given day on multiple exchanges. I download the data (roughly 935,000 lines) The first step is copy all the data for a given exchange (roughly 290,000) onto a new sheet (this takes roughly 8min), then I create a new column to log the bid ask spread (12secs), the next step is what Im having trouble with, I basically rank each line of data twice, one column for Bid size and one column for Ask size. I created a function which uses excel Percentile function and ranks based on where the given bid and ask size lands. As of right now, I have been running the Macro for the last 35min and has yet to execute. I cant attempt the other macros since each macro depends on the previous one.
So my basic issue is that since my data set is large, my model keeps crashing.The code seems to be fine when working with the test data, and it doesn't throw any errors when I run the program, but with the larger data set it just crashes. Does anyone have any suggestions? Is this normal with such large amounts of data?
Thanks in advance.
Sham
Here is the sub and function thats giving me the trouble, the sub takes in the required inputs to run the function and then pops into the assigned cell. The code is suppose to repeat the process for three separate sheets. For now, Id like it to work on one sheet, hence used the comments to not include the loop
Sub Bucketting()
Dim firstRow As Long
Dim lastRow As Long
Dim counter As Long
Dim bidRange As Range
Dim offerRange As Range
Dim bidScroll As Range
Dim offerScroll As Range
Dim Ex As String
Dim i As Integer
'For i = 1 To 1 Step 1 'Sheet Selection Process
' If i = 1 Then
' Ex = "Z"
' ElseIf i = 2 Then
' Ex = "P"
' Else
' Ex = "T"
' End If
Sheets("Z").Select 'Sheet selected
With ActiveSheet
firstRow = .UsedRange.Cells(1).Row + 1
lastRow = .UsedRange.Rows.Count
Set bidRange = .Range("F2:F" & lastRow)
Set offerRange = .Range("G2:G" & lastRow)
For counter = lastRow To firstRow Step -1
Set bidScroll = .Range("F" & counter)
Set offerScroll = .Range("G" & counter)
With .Cells(counter, "J")
.Value = DECILE_RANK(bidRange, bidScroll)
End With
With .Cells(counter, "K")
.Value = DECILE_RANK(offerRange, offerScroll)
End With
Next counter
End With
Range("J1").Select
ActiveCell = "Bid Rank"
ActiveCell.Offset(0, 1) = "Offer Rank"
'Next i
End Sub
Function DECILE_RANK(DataRange, RefCell)
'Credit: BJRaid
'DECILE_RANK(The Range of data)
'Declares the function that can be called in the spreadsheet cell - enter '=DECILE_RANK(A5:A50,A5)
'Using the percentile worksheet function calculate where the 10th, 20th etc percentile of the reference range are
DEC1 = Application.WorksheetFunction.Percentile(DataRange, 0.1)
DEC2 = Application.WorksheetFunction.Percentile(DataRange, 0.2)
DEC3 = Application.WorksheetFunction.Percentile(DataRange, 0.3)
DEC4 = Application.WorksheetFunction.Percentile(DataRange, 0.4)
DEC5 = Application.WorksheetFunction.Percentile(DataRange, 0.5)
DEC6 = Application.WorksheetFunction.Percentile(DataRange, 0.6)
DEC7 = Application.WorksheetFunction.Percentile(DataRange, 0.7)
DEC8 = Application.WorksheetFunction.Percentile(DataRange, 0.8)
DEC9 = Application.WorksheetFunction.Percentile(DataRange, 0.9)
' Calculate the Decile rank that the reference cell value sits within
If (RefCell <= DEC1) Then DECILE_RANK = 1
If (RefCell > DEC1) And (RefCell <= DEC2) Then DECILE_RANK = 2
If (RefCell > DEC2) And (RefCell <= DEC3) Then DECILE_RANK = 3
If (RefCell > DEC3) And (RefCell <= DEC4) Then DECILE_RANK = 4
If (RefCell > DEC4) And (RefCell <= DEC5) Then DECILE_RANK = 5
If (RefCell > DEC5) And (RefCell <= DEC6) Then DECILE_RANK = 6
If (RefCell > DEC6) And (RefCell <= DEC7) Then DECILE_RANK = 7
If (RefCell > DEC7) And (RefCell <= DEC8) Then DECILE_RANK = 8
If (RefCell > DEC8) And (RefCell <= DEC9) Then DECILE_RANK = 9
If (RefCell > DEC9) Then DECILE_RANK = 10
End Function
935,000 lines is a lot for excel. Like, really a lot. Barring saying using a real database, If your application is literally putting a =Percentile(...) in each cell, I would recommend Trying to use another tool for that. Perhaps something within VBA itself. More generally, use something outside of a cell - then store the result value in the cell. There is a lot of overhead in maintaining those formulas that are interdependent on 935k rows of data.
The problem is that your looping through each row individually, the Excel way is to try and work with whole ranges at once whenever possible. I would load the ranges into arrays, then modify your DECILE_RANK code to work with the items in the array.
Note that variant arrays that read ranges in are 2-D.
Here is the fully functioning code including my custom VBA array slicer. Note that it was only tested on a small dataset:
Sub Bucketting()
Dim lastRow As Long
Dim bidArray As Variant
Dim offerArray As Variant
Sheets("Sheet1").Select 'Sheet selected
With ActiveSheet
lastRow = .UsedRange.Rows.Count + 1
bidArray = .Range("F2:F" & lastRow)
offerArray = .Range("G2:G" & lastRow)
Range("J2:J" & lastRow).Value = GetArraySlice2D(DECILE_RANK(bidArray), "column", 1, 1, 0)
Range("K2:K" & lastRow).Value = GetArraySlice2D(DECILE_RANK(offerArray), "column", 1, 1, 0)
End With
Range("J1").Select
ActiveCell = "Bid Rank"
ActiveCell.Offset(0, 1) = "Offer Rank"
End Sub
Function DECILE_RANK(DataRange As Variant) As Variant
' Credit: BJRaid
' DECILE_RANK(The Range of data)
' Declares the function that can be called in the spreadsheet cell - enter '=DECILE_RANK(A5:A50,A5)
Dim DEC(0 To 10) As Variant
Dim i As Integer, j As Integer
'Using the percentile worksheet function calculate where the 10th, 20th etc percentile of the reference range are
DEC(0) = 0
For i = 1 To 9
DEC(i) = Application.WorksheetFunction.Percentile(DataRange, 0.1 * i)
Next i
DEC(10) = Application.WorksheetFunction.Max(DataRange)
' Calculate the Decile rank that the reference cell value sits within
For i = 1 To UBound(DataRange, 1)
For j = 1 To 10
If ((DataRange(i, 1) > DEC(j - 1)) And (DataRange(i, 1) <= DEC(j))) Then
DataRange(i, 1) = j
Exit For
End If
Next j
Next i
DECILE_RANK = DataRange
End Function
Public Function GetArraySlice2D(Sarray As Variant, Stype As String, Sindex As Integer, Sstart As Integer, Sfinish As Integer) As Variant
' this function returns a slice of an array, Stype is either row or column
' Sstart is beginning of slice, Sfinish is end of slice (Sfinish = 0 means entire
' row or column is taken), Sindex is the row or column to be sliced (NOTE:
' 1 is always the first row or first column)
' an Sindex value of 0 means that the array is one dimensional 3/20/09 Lance Roberts
Dim vtemp() As Variant
Dim i As Integer
On Err GoTo ErrHandler
Select Case Sindex
Case 0
If Sfinish - Sstart = UBound(Sarray) - LBound(Sarray) Then
vtemp = Sarray
Else
ReDim vtemp(1 To Sfinish - Sstart + 1)
For i = 1 To Sfinish - Sstart + 1
vtemp(i) = Sarray(i + Sstart - 1)
Next i
End If
Case Else
Select Case Stype
Case "row"
If Sfinish = 0 Or (Sstart = LBound(Sarray, 2) And Sfinish = UBound(Sarray, 2)) Then
vtemp = Application.WorksheetFunction.Index(Sarray, Sindex, 0)
Else
ReDim vtemp(1 To Sfinish - Sstart + 1)
For i = 1 To Sfinish - Sstart + 1
vtemp(i) = Sarray(Sindex, i + Sstart - 1)
Next i
End If
Case "column"
If Sfinish = 0 Or (Sstart = LBound(Sarray, 1) And Sfinish = UBound(Sarray, 1)) Then
vtemp = Application.WorksheetFunction.Index(Sarray, 0, Sindex)
Else
ReDim vtemp(1 To Sfinish - Sstart + 1)
For i = 1 To Sfinish - Sstart + 1
vtemp(i) = Sarray(i + Sstart - 1, Sindex)
Next i
End If
End Select
End Select
GetArraySlice2D = vtemp
Exit Function
ErrHandler:
Dim M As Integer
M = MsgBox("Bad Array Input", vbOKOnly, "GetArraySlice2D")
End Function
I'm not sure if this will directly address your problem, but have you considered using Application.ScreenUpdating = False? Don't forget to set it back to true once your data has processed.

Resources