Split text into 80 character lines, issue with last line - vba

I'm trying to take a body of text and add line breaks around 80 characters on each line. The issue I'm having is on the last line it's adding an extra line break than would be desired. For instance this string should not have a line break on the second to last line:
Alice was beginning to get very tired of sitting by her sister on the bank, and
of having nothing to do: once or twice she had peeped into the book her sister
was reading, but it had no pictures or conversations in it, and what is the use
of a book, thought Alice without pictures or
conversations?
should look like this (note "conversations" has been moved up):
Alice was beginning to get very tired of sitting by her sister on the bank, and
of having nothing to do: once or twice she had peeped into the book her sister
was reading, but it had no pictures or conversations in it, and what is the use
of a book, thought Alice without pictures or conversations?
Here's the code:
Sub StringChop()
Dim OrigString As String
Dim NewString As String
Dim counter As Long
Dim length As Long
Dim LastSpace As Long
Dim LineBreak As Long
Dim TempString As String
Dim TempNum As Long
OrigString = "Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, and what is the use of a book, thought Alice without pictures or conversations?"
length = Len(OrigString)
counter = 1
Do While counter < length
'Extract next 80 characters from last position
TempString = Mid(OrigString, counter, 80)
'Determine last space in string
LastSpace = InStrRev(TempString, " ")
'Determine first line break in string
LineBreak = InStr(TempString, vbNewLine)
'If line break exists in sentence...
'only count characters up to line break, and set counter to that amount
Select Case LastSpace 'What to do if there are spaces in sentence
Case Is > 0 'There are spaces in sentence
Select Case LineBreak 'What to do if there are line breaks in sentence
Case Is = 0
'From last counter position,
NewString = NewString & Mid(OrigString, counter, LastSpace) & vbNewLine
counter = counter + LastSpace
Case Is <> 0
NewString = NewString & Mid(OrigString, counter, LineBreak)
counter = counter + LineBreak
End Select
Case Is = 0 'There are no more spaces left in remaining sentence
NewString = NewString & Mid(OrigString, counter)
counter = length
End Select
Loop
Debug.Print NewString
End Sub

Word wrapping is an interesting problem. I wrote the following code once as an experiment. You might find it helpful:
Option Explicit
'Implements a dynamic programming approach to word wrap
'assumes fixed-width font
'a word is defined to be a white-space delimited string which contains no
'whitespace
'the cost of a line is the square of the number of blank spaces at the end
'of a line
Const INFINITY As Long = 1000000
Dim optimalCost As Long
Function Cost(words As Variant, i As Long, j As Long, L As Long) As Long
'words is a 0-based array of strings, assumed to have no white spaces
'i, j are indices in range 0,...,n, where n is UBOUND(words)+1
'L is the maximum length of a line
'Cost returns the cost of a line which begins with words(i) and ends with
'words(j-1). It returns INFINITY if the line is too short to hold the words
'or if j <= i
Dim k As Long
Dim sum As Long
If j <= i Or Len(words(i)) > L Then
Cost = INFINITY
Exit Function
End If
sum = Len(words(i))
k = i + 1
Do While k < j And sum <= L
sum = sum + 1 + Len(words(k)) 'for space
k = k + 1
Loop
If sum > L Then
Cost = INFINITY
Else
Cost = (L - sum) ^ 2
End If
End Function
Function WordWrap(words As Variant, L As Long) As String
'returns string consisting of words with spaces and
'line breaks inserted at the appropriate places
Dim v() As Long, d() As Long
Dim n As Long
Dim i As Long, j As Long
Dim candidate As Long
n = UBound(words) + 1
ReDim v(0 To n)
ReDim d(0 To n)
v(0) = 0
d(0) = -1
For j = 1 To n
v(j) = INFINITY 'until something better is found
i = j - 1
Do
candidate = v(i) + Cost(words, i, j, L)
If candidate < v(j) Then
v(j) = candidate
d(j) = i
End If
i = i - 1
Loop While i >= 0 And candidate < INFINITY
If v(j) = INFINITY Then
MsgBox "Some words are too long for the given length"
Exit Function
End If
Next j
optimalCost = v(n)
'at this stage, optimal path has been found
'just need to follow d() backwards, inserting line breaks
i = d(n) 'beginning of current line
WordWrap = words(n - 1)
j = n - 2
Do While i >= 0
Do While j >= i
WordWrap = words(j) & " " & WordWrap
j = j - 1
Loop
If i > 0 Then WordWrap = vbCrLf & WordWrap
i = d(i)
Loop
End Function
The above function expects an array of words. You would have to split a string before using it as input:
Sub test()
Dim OrigString As String
OrigString = "Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, and what is the use of a book, thought Alice without pictures or conversations?"
Debug.Print WordWrap(Split(OrigString), 80)
End Sub
Output:
Alice was beginning to get very tired of sitting by her sister on the bank,
and of having nothing to do: once or twice she had peeped into the book
her sister was reading, but it had no pictures or conversations in it, and
what is the use of a book, thought Alice without pictures or conversations?

Related

Loop to count the number of times a specific letter occurs in a string of letters

This is my first time using VBA and I'm having trouble finding a solution using FOR loops to count how many times a specific letter (entered by user) is in the string (entered by user).
Below I have what I have so I've found other solutions but they don't seem to utilize to FOR loop.
If anyone has any suggestions that'd be awesome.
Sub Week3()
Dim userInput As String
Dim letterSearched As String
Dim counter As Long
Dim occurances As Long
userInput = InputBox("type letters")
letterSearched = InputBox("type letter to be searched")
occurances = 0
For counter = 1 To Len(userInput)
If (InStr(counter, userInput, letterSearched)) > 0 Then
occurances = occurances + 1
Else
occurances = occurances
End If
Next
MsgBox (occurances)
End Sub
No need to use a loop. Is this what you are trying?
Option Explicit
Sub Sample()
Dim Mytext As String
Dim SearchText As String
Mytext = "Sample Text"
SearchText = "e"
MsgBox Len(Mytext) - Len(Replace(Mytext, SearchText, ""))
End Sub
EDIT
Yea I saw that solution but I'm supposed to implement a FOR loop – John Orsa 4 mins ago
Is this what you are trying?
Do not use InStr. Use Mid
For counter = 1 To Len(userInput)
If Mid(userInput, counter, 1) = letterSearched Then occurances = occurances + 1
Next
Note: If you want this not to be case sensitive then try this
For counter = 1 To Len(userInput)
If Mid(UCase(userInput), counter, 1) = UCase(letterSearched) Then occurances = occurances + 1
Next

What is the simplest way to display an aligned table from an Array of Strings in a TextBox using just Tabs (not using Tabstops)

I have an array of strings with 5 entries, where each string consists of 7 fields separated by comma's.
Those fields are of different lengths. I'm using a monospace font, so I can align things with tabs.
I had build a loop that goes through the string array, splitting the strings in the array, to determine which string in each "column" is the longest.
Then another loop, where I assemble a string with fields followed by a vbTab, and if the current field is a multiple of 8 shorter than the maximum length I add extra vbTabs (so if it's 8 shorter, add an extra vbTab, 16 = 2 vbTabs, etc).
But I have trouble getting things properly aligned in some cases.
Code:
Sub WriteTidyBlockH_layoutissues(list As List(Of String))
'Takes a list of strings as argument
'Writes it out as a table
'Assumes first line is a header (will be bolded)
'Go through list, split table fields and determine their lengths
Dim strLineFields As String() 'string array to hold the fields in a single line
Dim strLineField As String 'an individual field in a line
Dim FieldMaxLengths As New List(Of Integer)() 'list that will hold max length of each column
Dim i, j, k, intLines, intColumns As Integer
Dim strLine As String 'string to build a tabbed line in
Dim Tabs As Byte
'determine number of lines
intLines = list.Count - 1
'determine number of columns by counting the commas
intColumns = list(0).Count(Function(c As Char) c = ",")
For i = 0 To intColumns
FieldMaxLengths.Add(0)
Next
Dim strFields(intLines, intColumns) As String '2 dimensional array of strings, containing ALL fields
i = 0
For Each strListElement As String In list
j = 0
strLineFields = Split(strListElement, ",")
For Each strLineField In strLineFields
strFields(i, j) = strLineFields(j)
If strLineFields(j).Length > FieldMaxLengths(j) Then
FieldMaxLengths(j) = strLineFields(j).Length
End If
j += 1
Next
i += 1
Next
For i = 0 To intLines
strLine = ""
For j = 0 To intColumns
If Not j = intColumns Then
strLine += strFields(i, j) + vbTab 'one tab is always needed for every field, except the last
If strFields(i, j).Length <= FieldMaxLengths(j) Then
'strLine += strFields(i, j) + vbTab
'figure out how many additional tabs necessary
Tabs = (FieldMaxLengths(j) - strFields(i, j).Length) \ 8 'Div
For k = 1 To Tabs
strLine += vbTab
Next
End If
Else
'last column, don't add tabs
strLine += strFields(i, j)
End If
Next
If i = 0 Then
WriteOut(strLine, 0, True)
Else
WriteOut(strLine)
End If
Next
End Sub
Output currently looks like this:
Bank Location Capacity Speed Manufacturer Part Number Serial Number
BANK 0 ChannelA-DIMM0 8GB 1600 Kingston 99U5471-066.A00LF 24E8D583
BANK 1 ChannelA-DIMM1 8GB 1600 Kingston 99U5471-054.A00LF 30269BB7
BANK 2 ChannelB-DIMM0 8GB 1600 Kingston 99U5471-058.A00LF 182C9113
BANK 3 ChannelB-DIMM1 8GB 1600 Kingston 99U5471-054.A00LF D63F4C11
N.B. I'm having some trouble with formatting. In the actual output, only Capacity and Serial Number are misaligned (too far left), while Spped, Manufacturer and Part Number are correctly aligned with the data under them.
N.B.: WriteOut is just a sub that writes a single string to the textbox and appends a vbCrLf. The other two arguments are optional (color and bold).

Perform character substitution using Excel VBA

Say you would like to set up a very simple Caesar Cipher, where A --> 1, B --> 2 ... etc.
Say you have a word "Hello" in a cell that you would like to encrypt. You can set up a very simple For Loop to loop through each word:
For i = 1 To Len("Hello")
'perform encryption here
Next i
Is there a quick an easy way to map values from a pre-defined range? I.e. we know that A = 1, or 1 + 26, or 1 + 2*(26) .. etc...
Rather than writing IF statement to check for all letters, I wonder if there is an elegant way of doing this to get: "8 5 12 12 15"
Get the cell's output as a string with Byte array:
Dim brr() As Byte, i As Long, k As String
brr() = StrConv(Cells(1, 3), vbFromUnicode)
Then assess each letter in the new array against a larger array:
dim arr as variant
arr = array("a", "b")
For i = 0 To UBound(brr) 'need to start at 0, lbound applies for std array, not byte
'match brr value to arr value, output arr location
'k will store the final string
k = k + 'didn't look up the output for application.match(arr())
Next i
Edit1: Thanks to JohnColeman, i can add Asc() to the above and it shouldn't need the additional array for A, B, C, etc.
Dim brr() As Byte, i As Long, k As String
brr() = StrConv(Cells(1, 3), vbFromUnicode)
for i = 0 To UBound(brr)
k = k & " " & Asc(brr(i)) 'something like that
next i
Using the Dictionary route, you can build a dictionary which is a list of key, value pairs to hold your cypher. In your case the key of a would have the value 1 and the key of b would have the value 2, and so on. Then you can just bump your word, letter by letter, against the dictionary to build your cipher:
Function caesarCipher(word As String) As String
'create an array of letters in their position for the cipher (a is 1st, b is 2nd)
Dim arrCipher As Variant
arrCipher = Split("a b c d e f g h i j k l m n o p q r s t u v x y z", " ")
'Create a dictionary from the array with the key being the letter and the item being index + 1
Dim dictCipher As Scripting.Dictionary
Set dictCipher = New Dictionary
For i = 0 To UBound(arrCipher)
dictCipher.Add arrCipher(i), i + 1
Next
'Now loop through the word letter by letter
For i = 1 To Len(word)
'and build the cipher output
caesarCipher = caesarCipher & IIf(Len(caesarCipher) = 0, "", " ") & dictCipher(LCase(Mid(word, i, 1)))
Next
End Function
This is a nice way of doing it because you can change your cipher to be whatever you want and you only need monkey with your dictionary. Here I just build a dictionary from an array and use the array's index for the cipher output.
This might get you started:
Function StringToNums(s As String) As Variant
'assumes that s is in the alphabet A, B, ..., Z
Dim nums As Variant
Dim i As Long, n As Long
n = Len(s)
ReDim nums(1 To n)
For i = 1 To n
nums(i) = Asc(Mid(s, i, 1)) - Asc("A") + 1
Next i
StringToNums = nums
End Function
Sub test()
Debug.Print Join(StringToNums("HELLO"), "-") 'prints 8-5-12-12-15
End Sub
All of the answers are good, but this is how you use a dictionary which is simpler and more straight-forward. I defined the dictionary implicitly to make it easier to start, but it is better to define it explicitly by adding runtime scripting from the tools>references in VBE.
Sub Main()
Dim i As Integer
Dim ciphered As String, str As String
Dim dict As Object
Set dict = CreateObject("scripting.Dictionary")
str = "Hello"
For i = 65 To 122
dict.Add Chr(i), i - 64
Next i
For i = 1 To Len(str)
ciphered = ciphered & "-" & dict(Mid(UCase(str), i, 1))
Next i
ciphered = Right(ciphered, Len(ciphered) - 1)
Debug.Print ciphered
End Sub
if you remove ucase when getting the code from the dictionary it will count for the case meaning that uppercase or lowercase will have different codes. You can change this to a function easily, don't forget to remove str = "Hello". Right now it returns:
Output
8-5-12-12-15

Cells containing number that is equal or greater than

what I am currently trying to do is to find and highlight cells that contain simultaneously a certain phrase and (among some other text) a number that is equal or greater than 20 (including numbers with decimals like 25.8332). I tried using FormatConditions, but I wasn't able to make it consider two simultaneous conditions (a phrase and a number). So I decided to use a combination of If and InStr, but I wonder how to fill in the number that is equal or greater than 20?
Select the cells you wish to process and run:
Sub ColorMeYellow()
Dim r As Range, s As String, n As Double
Dim happy As String, CH As String, temp As String
Dim L As Long, i As Long
happy = "happy"
For Each r In Selection
s = r.Value
If InStr(1, s, happy) > 0 Then
L = Len(s)
temp = ""
For i = 1 To L
CH = Mid(s, i, 1)
If CH Like "[0-9]" Or CH = "." Then
temp = temp & CH
End If
Next i
If IsNumeric(temp) Then
If CDbl(temp) > 20 Then
r.Interior.ColorIndex = 6
End If
End If
End If
Next r
End Sub
It will look for cells containing both *"happy" and a number greater than 20.

Extracting text from string between two identical characters using VBA

Let's say I have the following string within a cell:
E. Stark, T. Lannister, A. Martell, P Baelish, B. Dondarrion, and J. Mormont. Increased levels of nudity across Westeros contributes to its sporadic seasonal climate. Nat. Proc. Aca. Sci. (2011) 3: 142-149.
And I want to extract only the title from this. The approach I am considering is to write a script that says "Pull text from this string, but only if it is more than 50 characters long." This way it only returns the title, and not stuff like " Stark, T" and " Martell, P". The code I have so far is:
Sub TitleTest()
Dim txt As String
Dim Output As String
Dim i As Integer
Dim rng As Range
Dim j As Integer
Dim k As Integer
j = 5
Set rng = Range("A" & j) 'text is in cell A5
txt = rng.Value 'txt is string
i = 1
While j <= 10 'there are five references between A5 and A10
k = InStr(i, txt, ".") - InStr(i, txt, ". ") + 1 'k is supposed to be the length of the string returned, but I can't differenciate one "." from the other.
Output = Mid(txt, InStr(i, txt, "."), k)
If Len(Output) < 100 Then
i = i + 1
ElseIf Len(Output) > 10 Then
Output = Mid(txt, InStr(i, txt, "."), InStr(i, txt, ". "))
Range("B5") = Output
j = j + 1
End If
Wend
End Sub
Of course, this would work well if it wasn't two "." I was trying to full information from. Is there a way to write the InStr function in such a way that it won't find the same character twice? Am I going about this in the wrong way?
Thanks in advance,
EDIT: Another approach that might work (if possible), is if I could have one character be " any lower case letter." and ".". Would even this be possible? I can't find any example of how this could be achieved...
Here you go, it works exactly as you wish. Judging from your code I am sure that you can adapt it for your needs quite quickly:
Option Explicit
Sub ExtractTextSub()
Debug.Print ExtractText("E. Stark, T. Lannister, A. Martell, P Baelish, B. Dondarrion, and J. Mormont. Increased levels of nudity across Westeros contributes to its sporadic seasonal climate. Nat. Proc. Aca. Sci. (2011) 3: 142-149.")
End Sub
Public Function ExtractText(str_text As String) As String
Dim arr As Variant
Dim l_counter As Long
arr = Split(str_text, ".")
For l_counter = LBound(arr) To UBound(arr)
If Len(arr(l_counter)) > 50 Then
ExtractText = arr(l_counter)
End If
Next l_counter
End Function
Edit: 5 votes in no time made me improve my code a bit :) This would return the longest string, without thinking of the 50 chars. Furthermore, on Error handlaer and a constant for the point. Plus adding a point to the end of the extract.
Option Explicit
Public Const STR_POINT = "."
Sub ExtractTextSub()
Debug.Print ExtractText("E. Stark, T. Lannister, A. Martell, P Baelish, B. Dondarrion, and J. Mormont. Increased levels of nudity across Westeros contributes to its sporadic seasonal climate. Nat. Proc. Aca. Sci. (2011) 3: 142-149.")
End Sub
Public Function ExtractText(str_text As String) As String
On Error GoTo ExtractText_Error
Dim arr As Variant
Dim l_counter As Long
Dim str_longest As String
arr = Split(str_text, STR_POINT)
For l_counter = LBound(arr) To UBound(arr)
If Len(arr(l_counter)) > Len(ExtractText) Then
ExtractText = arr(l_counter)
End If
Next l_counter
ExtractText = ExtractText & STR_POINT
On Error GoTo 0
Exit Function
ExtractText_Error:
MsgBox "Error " & Err.Number & Err.Description
End Function