Excel VBA Vlookup second most recent data for a value - vba

Currently working on an Excel worksheet with Vlookup. My reference table in Sheet 1 has a list of data along with its date, its name defined as ProjectEntry. For example:
-----------------------------------
| Project No | ID | Service Date |
|------------|----|---------------|
| 01 | A1 | 10/12/17 |
| 02 | B2 | 13/12/17 |
| 01 | A1 | 14/12/17 |
| 03 | C3 | 14/12/17 |
| 01 | A1 | 16/12/17 |
-----------------------------------
Now my Vlookup in Sheet2 wants to lookup the second most recent date based on the ID to get the last service date. For example when I select ID = 01 , Vlookup = 14/12/17.
For the Vlookup formula I managed to get the first entered Service Date (10/12/17):
=VLOOKUP(I7,ProjectEntry[[#All],[ID]:[Service Date]],2,FALSE)
But I'm not sure how to get the 2nd most recent date for A1. What should I add to the formula to make it work?

Instead of seeking the "second-to-last match" we could instead look for the "second highest match", in which case an array formula using the LARGE function will return what you need.
If your example data is arranged in A2:C6 like this:
then you could use this array formula to return the "2nd highest date" where the ID = A1 :
=LARGE(IF($B$2:$B$6="A1",$C$2:$C$6),2)
Your question said you need to look for ID = 01 but those are two different columns. If you instead need to look for `Project No = 01" then your array formula would be :
=LARGE(IF($A$2:$A$6="01",$C$2:$C$6),2)
...that's assuming that Project No is stored as text. If it's actually a number (formatted with a leading zero) then you would use :
=LARGE(IF($A$2:$A$6=1,$C$2:$C$6),2).
☆ Remember that since these are all ARRAY FORMULAS you need to specify that when entering the formulas; instead of using ENTER, finish entering the formula with:
CTRL + SHIFT + ENTER
EDIT:
To clarify whether this method will work, if the data is arranged like this:
...which value should be returned for ID = A1 (or Project No = 1)?
second-to-last (2016-12-17)
second-most-recent? (2014-12-17)

Related

Creating an Excel Lookup Table Sheet from a Comma Delimited and ID column

We exported a customer's table who was using AirTable to keep track of their client's information and locations in an attempt to import into a SQL database. Because of the way AirTable exports, the references to other tables in their "AirTable Base" are not via ID's, but exported in a single column as basically power labels for lack of a better explanation.
There's about 4,000 client rows in this table. Clients can have one or more locations. Excluding many of the other columns it looks like:
| Client_ID | Client_Name | ... | Locations
| 3456 | Acme Grocery | ... | "Memphis, TN","Orlando, FL","Philadelphia, PA"
| 3457 | Addition Financial | ... | "Miami, FL","Plano, TX","New York, NY"
| 3458 | Barros Pizza | ... | "Queen Creek, AZ"
We are trying to get the data ready for import into SQL, so we are attempting to find a formula/method which could take the Client_ID and then insert that into rows in a new data sheet made from the comma-delimited column. Using the above example the new data should look like the following:
| ClientInLocation_ID | Client_ID | Location |
| 10000 | 3456 | Memphis, TN |
| 10001 | 3456 | Orlando, FL |
| 10002 | 3456 | Philadelphia, PA |
| 10003 | 3457 | Miami, FL |
| 10004 | 3457 | Plano, TX |
| 10005 | 3457 | New York, NY |
| 10006 | 3458 | Queen Creek, AZ |
Doing so will allow us to then grab the unique locations, assign ID's to them and then replace the Location text with a Location_ID field.
I was thinking pivot tables, text to rows, etc. but perhaps I'm not experienced enough with them to pull this off. Also, any solutions can obviously exclude the ClientInLocation_ID auto increment as we could always have that autofilled once the other two fields are populated. Any help greatly appreciated.
There are many ways to tackle this problem. You can use PowerQuery (PQ) to do some of the lifting if you have an appropriate version of Excel. PQ is built into recently released Excel versions and is a free add-on for Excel 2013 and 2010 but is not available for anything older than Excel 2010. If you see a Power Query tab on the ribbon then you're good to go.
Use your data as the source for a new query and split the location column by delimiter "," To clarify, you are using three characters as the delimiter: the last quote of a location, the comma delimiting two locations, and the first quote of the second location. This puts one location in a cell with subsequent locations in columns to the right.
Every cell in the first column well have a quote in front of the text and the cell holding the final location for that row will have a quote at the end of the text. This is easily cleared in PQ but we're done here so it's probably faster to click Save & Load to close the editor and use Ctrl+H in Excel to clear them.
Your data will automatically be converted into a table that is connected to your source data. That means that refreshing the table does two things: it wipes any edits you've made and it updates the table with any changes in your source data. So either delete the query (if this is a one and done project) or copy the table to a new sheet (if you want to rapidly rebuild with new source data)
From there, I'd turn to VBA and use three nested For loops. The outer loop iterates every row in your data from the bottom up (Step -1). The middle loop iterates the columns to add new rows. The inner loop populates the rows.
This is quick, dirty, makes several assumptions and is in no way tested because it was written on my phone:
Option Explicit
Sub TransformTable ()
Dim ws As Worksheet
Dim myTable As ListObject
Dim rng As Range
Dim j As Long
Dim k As Long
Dim l as Long
Set ws = ActiveSheet
Set myTable = ws.ListObjects(1)
Application.ScreenUpdating = False
For j = myTable.ListRows.Count to 2 Step -1
For k = 1 to Application.WorksheetFunction.CountA(ws.Range(ws.Cells(j,1),ws.Cells(j,myTable.ListColumns.Count) - 3
Set rng = ws.Cells(j,1)
myTable.ListRows.Add j+k
For l = 0 to 1
rng.Offset(k,l) = rng.Offset(0,l)
Next l
rng.Offset(k,3) = rng.Offset(0,3+k)
rng.Offset(0,3+k).Cells.Clear
Next k
Next l
Application.ScreenUpdating = True
End Sub

Excel VBA- if cell contains text copy paste to another row (uneven spacing)

im relatively New to VBA and ive been asked to come up with a Macro(or in the terms given to me Automate) to filter all the rows in my workbook that contain "Total" & "Average" over a month long period and adjust the positions of 2 cells to be in line with another row.Now i can normally Filter what i need filtered but the problem lies in how i need to format it Exported. i need it looking from this
e.g
COL1 | COL2 | COL3 | COL4
Report Date:| xx/xx/xx | (BLANK) | (BLANK)
-------------
Service Group:| Repayments |
-------------
Total xx | xx | xx
-----------
Average | xx | xx
To this.
COL1 | COL2 | COL3 | COL4 |
Report Date:| xx/xx/xx | Service Group:| Repayments
--------
Total | xx | xx | xx
--------
Average | xx | xx | xx
the problem im having (besides being a completely new to VBA) is how to have it be "Automated" to which I had the idea of assigning a Macro to a Button which when pressed would cut Paste (then remove the blank space)every instance of (Service Group:) & (Repayments) up 1 row and over 3 columns
so for another Example: after the filter the rows would be Numbered variously so Starting on Position: B6 & C6 MOVE Up one and Over three to Position E5 & F5
Sorry for the Long winded convoluted explanation I tried to TLDR but i couldnt think of a way to better describe it. edit 1. Also the data is nonsequential

SSRS: Create chart

I want to create a chart in SSRS where the user can input a time range and maybe selects the wanted TrackingIDs for display.
I have a table which has three main columns:
ID,
TrackingID and
TrackingTime
For each ID are TrackingIDs saved with their corresponding TrackingTime. That means that there are multiple rows with the same ID but different TrackingIDs.
| ID | TrackingID | TrackingTime |....
|--------|----------------|-----------------------|
| 001 | 10 |2017-03-08 10:12:20.240|
| 003 | 50 |2017-03-08 12:30:23.240|
| 001 | 10 |2017-03-03 09:10:23.240|
| 002 | 10 |2017-03-06 10:12:23.240|
| 001 | 15 |2017-03-05 10:12:23.240|
| 001 | 20 |2017-03-08 17:12:23.240|
| 002 | 15 |2017-03-04 00:12:23.240|
| 003 | 10 |2017-03-06 01:18:23.240|
....
The user than receives a chart where the sum of each TrackingID is displayed over time. Out of this chart the user can read the change of TrackingIDs over time.
Important is that only the last given TrackingID for an ID is added to the sum of one kind of TrackingID.
You can add two text box parameters to your report that allow user input. I will assume in this case that we are just going to filter between date ranges and not time.
Then go to your table properties and the Filters tab. Add a new Filter.
Expression: Press the fx button to add an expression.
Try this:
CDate(Fields!TrackingTime.Value)
This should convert your DataSet date/time into a VBnet CDate
Operator: > or < depending on which parameter we are comparing to (add one for each)
Value: Press the fx button to add an expression.
Try this:
CDate(Parameters!(YOUR PARAMETER NAME).Value)
This will convert the user input to a VBnet CDate
Your filter should then be able to successfully compare two CDates and determine if they are greater than or less than. Add one for start and end date and you will get your filtered range.
If CDate doesn't work give this function a try:
FormatDateTime(Fields!TrackingTime.Value, DateFormat.ShortDate)
EDIT: If you are doing this for a chart you can add this as a filter in your data element.

Excel/VBA/Conditional Formatting: Dictionary of Dictionaries

I've got an Excel workbook that obtains data from an MS SQL database. One of the sheets is used to check the data against requirements and to highlight faults. In order to do that, I've got a requirements sheet where the requirement is in a named range; after updating the data I copy the conditional formatting of the table header to all data rows. That works pretty nicely so far. The problem comes when I have more than one set of requirements:
An (agreeably silly) example could be car racing, where requirements may exist for driver's license and min/max horsepower. When looking at the example, please imagine there are a few thousand rows and 71 columns presently...
+-----+--------+----------------+------------+---------+
| Car | Race | RequirementSet | Horsepower | License |
+-----+--------+----------------+------------+---------+
| 1 | Monaco | 2 | 200 | A |
+-----+--------+----------------+------------+---------+
| 2 | Monaco | 2 | 400 | B |
+-----+--------+----------------+------------+---------+
| 3 | Japan | 3 | 200 | C |
+-----+--------+----------------+------------+---------+
| 4 | Japan | 3 | 300 | A |
+-----+--------+----------------+------------+---------+
| 5 | Japan | 3 | 350 | B |
+-----+--------+----------------+------------+---------+
| 6 | Mexico | 1 | 200 | A |
+-----+--------+----------------+------------+---------+
The individual data now needs to be checked against the requirements set in another sheet:
+-------------+---------------+---------------+---------+
| Requirement | MinHorsepower | MaxHorsepower | License |
+-------------+---------------+---------------+---------+
| 1 | 200 | 250 | A |
+-------------+---------------+---------------+---------+
| 2 | 250 | 500 | B |
+-------------+---------------+---------------+---------+
| 3 | 250 | 400 | A |
+-------------+---------------+---------------+---------+
In order to relate back to my present situation, I am only looking at either the Monaco, Japan or Mexico Race, and there is only 1 record in the requirements sheet, where the value in e.g. Cell B2 is always the MinHorsepower and the value in C2 is always the MaxHorsepower. So these cells are a named range that I can access in my data sheet.
Now however I would like to obtain all races at once, and refer conditional formatting formulas to the particular requirement.
Focussing on "Horsepower" in Monaco (requirement set 2), I can now find out that the min Horsepower is 250 and the max is 500 - so I will colour that column for car 1 as red and for car 2 as green.
The formula is programatically copied from the header row (the first conditional format rule is if row(D1) = 1 then do nothing)
I can't decide what the best approach to the problem is. Ideally, the formula is readable, something like `AND(D2 >= MinHorsepower; D2 <= MaxHorsepower) - I cannot imagine it to be maintainable if I had to use Vlookup combined with Indirect and Match to match a column header in requirements for that particular requirement - especially when it comes to combining criteria like in the HP example with min and max above.
I am wondering if I should read the requirements table into a dictionary or something in VBA, and then use a function like
public function check(requirementId as int, requirement$)
which then in Excel I could use like =D2 >= check(c2, "MinHorsepower")
Playing around with this a little bit it appears to be pretty slow as opposed to the previous system where I could only have one requirement. It would be fantastic if you could help me out with a fresh approach to this problem. I'll update this question as I go along; I'm not sure if I managed to illustrate the example really well but the actual data wouldn't mean anything to you.
In any case, thanks for hanging in until here!
Edit 29 October 2016
I have found a solution as basis for mine. Using the following code I can add my whole requirements table to a dictionary, and access the requirement.
Using a class clsRangeToDictionary (based on Tim Williams clsMatrix)
Option Explicit
Private m_array As Variant
Private dictRows As Object
Private dictColumns As Object
Public Sub Init(vArray As Variant)
Dim i As Long
Set dictRows = CreateObject("Scripting.Dictionary")
Set dictColumns = CreateObject("Scripting.Dictionary")
'add the row keys and positions. Skip the first row as it contains the column key
For i = LBound(vArray, 1) + 1 To UBound(vArray, 1)
dictRows.Add vArray(i, 1), i
Next i
'add the column keys and positions, skipping the first column
For i = LBound(vArray, 2) + 1 To UBound(vArray, 2)
dictColumns.Add vArray(1, i), i
Next i
' store the array for future use
m_array = vArray
End Sub
Public Function GetValue(rowKey, colKey) As Variant
If dictRows.Exists(rowKey) And dictColumns.Exists(colKey) Then
GetValue = m_array(dictRows(rowKey), dictColumns(colKey))
Else
Err.Raise 1000, "clsRangeToDictionary:GetValue", "The requested row key " & CStr(rowKey) & " or column Key " & CStr(colKey) & " does not exist"
End If
End Function
' return a zero-based array of RowKeys
Public Function RowKeys() As Variant
RowKeys = dictRows.Keys
End Function
' return a zero-based array of ColumnKeys
Public Function ColumnKeys() As Variant
ColumnKeys = dictColumns.Keys
End Function
I can now read the whole RequirementSet table into a dictionary and write a helper to obtain the particular requirement roughly so:
myDictionaryObject.GetValue(table1's RequirementSet, "MinHorsePower")
If someone could help me figure out how to put this into an answer giving the credit due to Tim Williams that'd be great.

VB.net | Save data from DataGridView to text file (Line per line)

The application I designed so far has a DataGridView which loads data from a text file line per line.
All I want is for the code to save the (first row, first column) on the first line as a string, then (first row, second column) on the second line, etc.
Here is an example of what my table looks like:
|-------------------------------------------------------|
| ID | Date | Height | Weight | BMI | Units |
|-------------------------------------------------------|
| 01 | 16/06 | 1.74 | 64 | 20.9 | Metric |
| 02 | 17/06 | 1.74 | 63 | 20.6 | Metric |
|-------------------------------------------------------|
So from this example, after the data has been saved to the text file it should look exactly like this:
01
16/06
1.74
64
20.9
Metric
02
17/06
1.74
63
20.6
Metric
I came across some excellent code which does this with tabs, instead of a next line, here it is:
dgvMeasures.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText
dgvMeasures.SelectAll()
IO.File.WriteAllText(fileName, dgvMeasures.GetClipboardContent.GetText.TrimEnd)
dgvMeasures.ClearSelection()
NOTE: The DataGridView is called dgvMeasures
Also please note that I cannot provide anything that I have already tried since there is nothing I can do, I have no idea what to do.
So if there is anyone who could help, it would be greatly appreciated
To do this, you just need to use a writer, and go through it in the way you want.
Using writer As New System.IO.StreamWriter(filePath)
For row As Integer = 0 To dgvMeasures.RowCount - 1
For col As Integer = 0 To dgvMeasures.ColumnCount - 1
writer.WriteLine(dgvMeasures.Rows(row).Cells(col).Value.ToString)
Next
Next
End Using
This will go through each column for each row (as you describe), and then go to the next row.
I am sure you have a reason for writing the text file like this, but if you want to read it back in at some point, I would really recommend using a tab-delimited (or similar) format.