I have the following Excel table used to calculate the overtime hours worked.
I want the last two columns to be auto-generated, preferably with formulas, so that the user can see on which dates the employees have worked OT.
The example output for Bob would be:
Dates worked Normal OT : "2nd, 3rd, 4th, 5th, 10th & 12th"
Dates worked Double OT : "6th, 7th & 13th"
Please note that I don't have Excel 2016, and therefore can't use TEXTJOIN(). Also note that the dates for Week 1 and Week 2 are stored in number format, not date format, so the use of WEEKDAY() is also not possible.
P.S. I have already tried a TextJoin UDF but it doesn't seem to work since I have a lot of criteria within the formula.
The working formula for TEXTJOIN in Excel 2016 is this:
=TEXTJOIN(", ",TRUE,IF(WEEKDAY($B$3:$O$3,2)<6,IF($B5:$O5>0,TEXT($B$3:$O$3,"dd/mm/yyyy"),""),""))
This is using date formats. The UDF doesn't seem to work with these parameters.
I am ignoring everything in the PS at the end of your question as Jeeped is right in that it is contradictory to the main body and screenshot.
Without using a UDF, the formula would be much too complicated. Don't worry, I have supplied my own basic TEXTJOIN() UDF that definitely works.
All the following formulae need to be "array-entered" (by pressed Ctrl+Shift+Enter) in a single cell, and then copied/filled down. (Remember not to copy the starting { and ending }.)
The only difference in the two formulae is that one uses the comparison <5 whilst the other uses >=5, and each one refers to the appropriate "No. of Days" cell.
This first formula needs to be array-entered into cell T5 (Bob's "Dates Worked Normal OT") and then filled down:
{=SUBSTITUTE(TEXTJOIN(", ", TRUE, IF((MOD(COLUMN($B5:$O5)-COLUMN($B5), 7)<5)*($B5:$O5>0), $B$3:$O$3 & CHOOSE(IF($B$3:$O$3<4, $B$3:$O$3, IF($B$3:$O$3<21, 4, IF($B$3:$O$3<24, $B$3:$O$3-20, IF($B$3:$O$3<31, 4, 1)))), "st", "nd", "rd", "th"), "")), ", ", " & ", MAX(1, S5-1))}
The expanded, easier to read, version of the above formula (it will also work if you copy-paste it):
{=
SUBSTITUTE(
TEXTJOIN(
", ",
TRUE,
IF(
(MOD(COLUMN($B5:$O5)-COLUMN($B5), 7)<5)*($B5:$O5>0),
$B$3:$O$3
&
CHOOSE(
IF($B$3:$O$3<4, $B$3:$O$3, IF($B$3:$O$3<21, 4, IF($B$3:$O$3<24, $B$3:$O$3-20, IF($B$3:$O$3<31, 4, 1)))),
"st", "nd", "rd", "th"
),
""
)
),
", ",
" & ",
MAX(1, R5-1)
)}
This second formula needs to be array-entered into cell U5 (Bob's "Dates Worked Double OT") and then filled down:
{=SUBSTITUTE(TEXTJOIN(", ", TRUE, IF((MOD(COLUMN($B5:$O5)-COLUMN($B5), 7)>=5)*($B5:$O5>0), $B$3:$O$3 & CHOOSE(IF($B$3:$O$3<4, $B$3:$O$3, IF($B$3:$O$3<21, 4, IF($B$3:$O$3<24, $B$3:$O$3-20, IF($B$3:$O$3<31, 4, 1)))), "st", "nd", "rd", "th"), "")), ", ", " & ", MAX(1, S5-1))}
The expanded version of the above formula is:
{=
SUBSTITUTE(
TEXTJOIN(
", ",
TRUE,
IF(
(MOD(COLUMN($B5:$O5)-COLUMN($B5), 7)>=5)*($B5:$O5>0),
$B$3:$O$3
&
CHOOSE(
IF($B$3:$O$3<4, $B$3:$O$3, IF($B$3:$O$3<21, 4, IF($B$3:$O$3<24, $B$3:$O$3-20, IF($B$3:$O$3<31, 4, 1)))),
"st", "nd", "rd", "th"
),
""
)
),
", ",
" & ",
MAX(1, S5-1)
)}
Notes:
As mention above, the formulae rely on the availability/accuracy of the "No. of Days" cells in order to work correctly.
These formulae are pretty straightforward:
(MOD(COLUMN($B5:$O5)-COLUMN($B5), 7)>=5)*($B5:$O5>0) is just an array-formula friendly way of writing AND((…), (…));
The only sneaky thing is the use of the CHOOSE() function with four nested IF()s to select the ordinal indicator.
Don't forget to not include the { at the start, and the } at the end of the formulae when copy-pasting. These are just used to show that a formula needs to be array-entered.
My version of the TEXTJOIN UDF:
'============================================================================================
' Module : <any standard module>
' Version : 0.1.0
' Part : 1 of 1
' References : Optional - Microsoft VBScript Regular Expressions 5.5 [VBScript_RegExp_55]
' Source : https://stackoverflow.com/a/49218794/1961728
'============================================================================================
Public Function TEXTJOIN( _
ByRef delimiter As String, _
ByRef ignore_empty As Boolean, _
ByRef text1 As Variant _
) _
As String
Dim ƒ As Excel.WorksheetFunction: Set ƒ = Excel.WorksheetFunction
Const DELIMITER_ As String = "#"
Const PATTERN_ As String = "^(?:#)+|(?:#)+$|(#){2,}"
Static rexDelimiterEscaper As Object ' VBScript_RegExp_55.RegExp ' ## Object
Static rexEmptyIgnorer As Object ' VBScript_RegExp_55.RegExp ' ## Object
If rexEmptyIgnorer Is Nothing _
Then
Set rexEmptyIgnorer = CreateObject("VBScript.RegExp") ' New VBScript_RegExp_55.RegExp ' ## CreateObject("VBScript.RegExp")
With rexEmptyIgnorer
.Global = True
.Pattern = PATTERN_ ' Replacement = "$1"
End With
Set rexDelimiterEscaper = CreateObject("VBScript.RegExp") ' New VBScript_RegExp_55.RegExp ' ## CreateObject("VBScript.RegExp")
With rexDelimiterEscaper
.Global = True
.Pattern = "(.)" ' Replacement = "\$1"
End With
End If
Dim varText1 As Variant
Select Case TypeName(text1)
Case "Range":
varText1 = ƒ.Transpose(text1.Value2)
If text1.Rows.Count = 1 Then
varText1 = ƒ.Transpose(varText1)
If text1.Columns.Count = 1 Then varText1 = Array(varText1)
End If
Case "Variant()":
varText1 = text1
Case Else:
varText1 = Array(text1)
End Select
If ignore_empty _
Then
With rexEmptyIgnorer
.Pattern = Replace(PATTERN_, DELIMITER_, rexDelimiterEscaper.Replace(delimiter, "\$1"))
TEXTJOIN = .Replace(Join(varText1, delimiter), "$1")
End With
Else
TEXTJOIN = Join(varText1, delimiter)
End If
End Function
Notes:
This is not a proper poly-fill:
The first two arguments are not optional;
If you no not wish to use a delimiter, you must pass an empty string as the first parameter.
There is only one other (also required) argument allowed.
You can pass in anything for the third argument, except a multi-dimension array/range. Doing so will result in a #VALUE! error.
It should be very fast, especially for large inputs, as it doesn't use any loops. If you aren't ignoring empty values, it will be lightning fast. Ignoring them will be slower as a couple of regexes and an extra string manipulation have to be used as well.
Related
All - I'm stuck and need some assistance please. I have a string which is a free-text field to the end user. I need to extract a specific string of text from within this field.
Text is in an array and I have confirmed location of necessary text with InStr and I know it is typically surrounded by at least one space on either side.
I'm looking for a way to extract it based on the location using InStr and Split but I'm not sure how to nest these. There could be any number of spaces in field before or after the string I need because some people like excess spaces. String length is typically 12 BUT could be more or less bc it IS a free text field.
I'm open to any solution that gets the string containing "PO" extracted.
Example String: "V000012345 SAPO22-12345 additional information blah blah"
If InStr(1, Arr2(j, 10), "PO", 1) > 0 Then
Arr3(i, 18) = Split(Arr2(j, 10), " ")(??)
End if
You may try to Filter() the array after Split(). Alternatively, use a regular expression:
Sub Test()
Dim str As String: str = "V000012345 SAPO22-12345 additional information blah blah"
'Option 1: Via Filter() and Split():
Debug.Print Filter(Split(str), "PO")(0)
'Option 2: Via Regular Expressions:
With CreateObject("vbscript.regexp")
.Pattern = ".*?\s?(\S*PO\S*).*"
Debug.Print .Replace(str, "$1")
End With
End Sub
It's case-sensitive and the above would return the 1st match.
This would give you the first element of a SPLIT, that contains "PO":
PONumber = Split(arr2(j, 10), " ")(Len(Left(arr2(j, 10), InStr(1, arr2(j, 10), "PO"))) - Len(Replace(Left(arr2(j, 10), InStr(1, arr2(j, 10), "PO")), " ", "")))
This works by counting the number of spaces before the PO and using that as the index of the SPLIT.
I concede however, the FILTER function offered by JvdV saves you all this hassle - I've not seen it used that way before and it's very efficient.
Can someone help me fix the following code. I am trying to modify the first cell from the user's selection, so that it is absolute to the column only. (i.e. $A$1 to become $A1).
When using the below Replace function, it replaces both $ signs rather than just the 2nd...
var_address = Selection.Address
first_cell = Range(var_address).Cells(1, 1).Address
first_cell_new = Replace(first_cell, "$", "", Start:=2, Count:=1)
The Replace(..., Start:=n, ...) function removes the first n characters regardless what the function searches for.
Example:
Debug.Assert Replace("ABCDEF", "KLM", "XYZ", Start:=4) = "DEF"
Debug.Assert Replace("$AB$CD$EF", "$", "", Start:=4, Count:=1) = "CD$EF"
Debug.Assert Replace("$AB$CD$EF", "$", "", Start:=4, Count:=2) = "CDEF"
For your purpose, you need to combine Left and Replace functions:
var_address = "$A$1"
Debug.Print Left(var_address, 2) & Replace(var_address, "$", "", Start:=3) ' Count:=1 is useless here
See also Microsoft VBA documentation.
https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/replace-function
Well, after you clarification in a comment under my previous answer, it's now more clearer. You actually want to change absolute addresses (e.g. $A$1) relative one (e.g. $A1, A$1 or A1).
To do this, simply use the Address function and its parameters
var_address = Selection.Address
first_cell = Range(var_address).Cells(1, 1).Address(False, True)
Background:
I have an Excel sheet with four columns (title, URL, status, type).
The table is a mix of text from various users who use different separators in the column title:
hyphen (surface-air-temperature)
underscore (latent_heat_flux)
plus and blank (+land surface elevation+)
point and blank (,Total cloud cover)
Problem:
The in-built customized sorting works up to a certain point. Unfortunately it does not target the ending of each entry (examples above).
Desired solution:
I would like to select a specific text as criteria for the sorting.
Before:
After:
Possible approach:
I have been thinking about splitting the rows. Unfortunately I cannot specify/customize the splitting. Would it be possible to write a script that allows "cut" the text at a specified point (e.g. that -mean-sea-level-pressure is separated from the rest and displayed in another column)?
CLIM-PRO-CMIP-SINGLE-XXXX-M-HISTORICAL-mean-sea-level-pressure
Another obstacle might be that different separators have been used.
Before splitting:
After splitting:
Try this function
Function EXTRACT_SORT_KEYWORDS(INPUT_TEXT As String) As String
Application.Volatile
Dim vSeparators As Variant
Dim vKeywords As Variant
Dim v As Variant
EXTRACT_SORT_KEYWORDS = ""
If Len(INPUT_TEXT) = 0 Then Exit Function
' Add more separators here
vSeparators = Array(" ", ";", ",", "-", "_")
' Add more keywords here. Note the line continuations
vKeywords = Array("mean sea level pressure", _
"surface air temperature", _
"latent heat flux", _
"land surface elevation", _
"land surface elevation", _
"total cloud cover", _
"cloud cover", _
"mean precipitation flux" _
)
For Each v In vSeparators
If v <> " " And InStr(1, INPUT_TEXT, v, vbTextCompare) > 0 Then
INPUT_TEXT = Replace(INPUT_TEXT, v, " ")
End If
Next v
INPUT_TEXT = Trim$(INPUT_TEXT)
For Each v In vKeywords
If InStr(1, INPUT_TEXT, v, vbTextCompare) > 0 Then
EXTRACT_SORT_KEYWORDS = v
Exit Function
End If
Next v
End Function
As your data grows, it is not a good idea to have many user defined functions in you sheets as they will slow down your application considerably. A better solution is to have code that generates this column dynamically, performs the sort and then delete the column.
Hope this helps.
I am attempting to parse out clean target DATES from cells populated with free form TEXT STRINGS.
ie: TEXT STRING: "ETA: 11/22 (Spring 4.5)" or "ETA 10/30/2019 EOD"
As you can see, there is no clear standard for the position of the date in the string, rendering LEFT or RIGHT formulas futile.
I tried leveraging a VBA function that I found which essentially breaks up the string into parts based on spaces in the string; however it has not been working.
Public Function GetDate(ResNotes As String) As Date
Dim TarDate As Variant
Dim part As Variant
TarDate = Split(ResNotes, " ")
For Each part In ResNotes
If IsDate(part) = True Then
GetDate = part
Exit Function
End If
Next
GetDate = "1/1/2001"
End Function
I'm referring to the cells with text strings as "ResNotes", short for "Resolution Notes" which is the title of the column
"TarDate" refers to the "Target Date" that I am trying to parse out
The result of the custom GETDATE function in Excel gives me a #NAME? error.
I expected the result to give me something along the lines of "10/30/2019"
Unless you need VBA for some other part of your project, this can also be done using worksheet formulas:
=AGGREGATE(15,6,DATEVALUE(MID(SUBSTITUTE(A1," ",REPT(" ",99)),seq_99,99)),1)
where seq_99 is a named formula and refers to:
=IF(ROW($A$1:INDEX($A:$A,255,1))=1,1,(ROW($A$1:INDEX($A:$A,255,1))-1)*99)
*seq_99 generates an array of numbers {1;99;198;297;396;495;...
Format the cell with the formula as a Date of some type.
If there are no dates, it will return an error which you can either leave, or wrap the function in an IFERROR(your_formula,your_error_message)
Algorithm
Split the cell on the spaces
Replace each space with 99 spaces
Using the MID function, return an array of substrings 99 characters long
Apply the DATEVALUE function which will return either an error (if the substring is not a date) or a date serial number.
Since dates in Excel are serial numbers since 1/1/1900, we can use the AGGREGATE function to pick out a value, and ignore errors.
If you are getting #NAME then the code is not stored in a general module. It should NOT be in a worksheet module or ThisWorkbook module.
Also there are few errors in the code. Split returns a String Array. And since IsDate returns TRUE/FALSE the = True is not needed.
As per #MathieuGuindon we can change the string to a date in the code if found and return an error if not. For that we need to allow the return to be a variant.
Public Function GetDate(ResNotes As String)
Dim TarDate() As String
Dim part As Variant
TarDate = Split(ResNotes, " ")
For Each part In TarDate
If IsDate(part) Then
GetDate = CDate(part)
Exit Function
End If
Next
GetDate = "1/1/2001"
'Instead of a hard coded date, one can return an error, just use the next line instead
'GetDate =CVErr(xlErrValue)
End Function
Approach isolating the date string via Filter function
Just for fun another approach demonstrating the use of the Filter function in combination with Split to isolate the date string and split it into date tokens in a second step; finally these tokens are transformed to date using DateSerial:
Function getDat(rng As Range, Optional ByVal tmp = " ") As Variant
If rng.Cells.count > 1 Then Set rng = rng.Cells(1, 1) ' allow only one cell ranges
If Len(rng.value) = 0 Then getDat = vbNullString: Exit Function ' escape empty cells
' [1] analyze cell value; omitted year tokens default to current year
' (valid date strings must include at least one slash, "11/" would be interpreted as Nov 1st)
tmp = Filter(Split(rng.Value2, " "), Match:="/", include:=True) ' isolate Date string
tmp = Split(Join(tmp, "") & "/" & Year(Now), "/") ' split Date tokens
' [2] return date
Const M% = 0, D% = 1, Y& = 2 ' order of date tokens
getDat = VBA.DateSerial(Val(tmp(Y)), Val(tmp(M)), _
IIf(tmp(D) = vbNullString, 1, Val(tmp(D))))
End Function
I have URL formatted content, usually I just translate manually 1-by-1m but this time there are thousand of entry, eg:-
%E5%B7%B2%E4%BB%8E%E5%B8%90%E6%88%B7zh*****%40outlook.com%E5%88%A0%E9%99%A48618650533*%E3%80%82%E4%B8%8D%E6%98%AF%E4%BD%A0%EF%BC%9F
%E7%AE%A1%E7%90%86%E9%A2%84%E8%AE%A2%0A
https%3A%2F%2Faccount.live.com%2Fa
OTO+GLOBAL+Certification+No%3A%5B6198%5D
Deluxe+Room+-1+%E9%97%B4%0A
Ihre+Agoda+Buchung+Nr.+77083713+ist+bes %C3%A4tigt%21+Verwalten+Sie+Ihre+B
%E6%82%A8%E7%9A%84Agoda%E8%AE%A2%E5%8D%95%2877083753%29%E5%B7%B2%E7%A%AE%E8%AE%A4%EF%BC%81+%E4%BD%BF%E7%94%A8%E6%88%91%E4%BB%AC%E7%9A%84%E5%85%8D%E8%B4%B9%E5%AE%A2%E6%88%B7%E7%AB%AFhttp%3A%2F%2Fapp-agoda.com%2FGetTheApp%EF%BC%8C%E8%BD%BB%E6%9D%BE
Is there any way to convert all of this content to plain English text in Microsoft Excel?
Regards
There isn't a built-in function to handle this, but it's possible with a custom function, installing a third-party add-in, or using the substitute command:
Using a custom VBA function
Source: http://www.freevbcode.com/ShowCode.asp?ID=1512
Public Function URLDecode(StringToDecode As String) As String
Dim TempAns As String
Dim CurChr As Integer
CurChr = 1
Do Until CurChr - 1 = Len(StringToDecode)
Select Case Mid(StringToDecode, CurChr, 1)
Case "+"
TempAns = TempAns & " "
Case "%"
TempAns = TempAns & Chr(Val("&h" & _
Mid(StringToDecode, CurChr + 1, 2)))
CurChr = CurChr + 2
Case Else
TempAns = TempAns & Mid(StringToDecode, CurChr, 1)
End Select
CurChr = CurChr + 1
Loop
URLDecode = TempAns
End Function
With third-party add-in
Source: SeoTools (needs installation)
=UrlDecode(your_string_here)
With substitute command
Source: https://searchmarketingcorner.wordpress.com/2013/03/27/creating-an-excel-formula-to-encode-or-unencode-urls/
Paste the formula below to the right of your cell in order to URL decode the contents of that cell
=SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(CELL_TO_BE_DECODED,"%3F","?"),"%20"," "),"%25", "%"),"%26","&"),"%3D","="),"%7B","{"),"%7D","}"),"%5B","["),"%5D","]")
Or for working with GUIDs, add one more SUBSTITUTE for the dashes.
=SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(CELL_TO_BE_DECODED,"%3F","?"),"%20"," "),"%25", "%"),"%26","&"),"%3D","="),"%7B","{"),"%7D","}"),"%5B","["),"%5D","]"),"%2D","-")
For completeness, here is the reverse formula for URL encode. This is the same as the URL encode formula but positions of new_text and old_text swapped around.
=SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(CELL_TO_BE_ENCODED,"?","%3F")," ","%20"),"%","%25"),"&","%26"),"=","%3D"),"{","%7B"),"}","%7D"),"[","%5B"),"]","%5D")
Or for working with GUIDs, add one more SUBSTITUTE for the dashes.
=SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(CELL_TO_BE_ENCODED,"?","%3F")," ","%20"),"%","%25"),"&","%26"),"=","%3D"),"{","%7B"),"}","%7D"),"[","%5B"),"]","%5D"),"-","%2D")
Here is a User Defined Function (UDF) that actually works.
In a standard code module, place this routine:
Public Function URLDecode(url$) As String
With CreateObject("ScriptControl")
.Language = "JavaScript"
URLDecode = .Eval("unescape(""" & url & """)")
End With
End Function
Now you can call it from the worksheet, just like a built-in Excel function.
For example, if your encoded URL text were in cell A1, you could enter the following formula in cell B1:
=URLDecode(A1)
That's it. The fully decoded URL is now in cell B1.
Note that this is the real deal. It is not an attempt to replace a couple of characters. This uses the full power of JavaScript by way of the Microsoft Script Control to completely decode the URL.
The following adds decoding for commas in URLs. Just an additional SUBSTITUTE for %2C. This just adds to Carlos's post from 4 years ago.
=SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(CELL_TO_BE_DECODED,"%3F","?"),"%20"," "),"%25", "%"),"%26","&"),"%3D","="),"%7B","{"),"%7D","}"),"%5B","["),"%5D","]"),"%2D","-"),"%2C",",")
You can do it without VBA using array formulas. If A1 is the cell to be decoded, enter this formula, and then press Ctrl-Shift-Enter:
=TEXTJOIN("", FALSE,
MID(A1,
FIND("*",
SUBSTITUTE("%DD"&A1,"%","*", ROW(INDIRECT("1:"&LEN(A1)-LEN(SUBSTITUTE(A1,"%",""))+1)) )
),
FIND("*",
SUBSTITUTE(A1&"%","%","*", ROW(INDIRECT("1:"&LEN(A1)-LEN(SUBSTITUTE(A1,"%",""))+1)) )
) - FIND("*",
SUBSTITUTE("%EE"&A1,"%","*", ROW(INDIRECT("1:"&LEN(A1)-LEN(SUBSTITUTE(A1,"%",""))+1)) )
)
) & IFERROR(CHAR(HEX2DEC(MID(A1,
FIND("*",
SUBSTITUTE(A1&"%","%","*", ROW(INDIRECT("1:"&LEN(A1)-LEN(SUBSTITUTE(A1,"%",""))+1)) )
)+1,
2
))),"")
)
It won't work if you don't Ctrl-Shift-Enter.