want to split a time range into 1 hour intervals
split the given time range into 1 hour intervals from cell A2 and A3, the time range will be changed a serval time and on a (Macro) click it should split the given time range into 1 hour intervals.
Create an Hourly Sequence
Sub CreateHourlySequence()
Dim ws As Worksheet: Set ws = ActiveSheet ' improve!
Dim dt1: dt1 = ws.Range("A2").Value
Dim dt2: dt2 = ws.Range("A3").Value
Dim dfCell As Range: Set dfCell = ws.Range("C2")
dfCell.Resize(ws.Rows.Count - dfCell.Row + 1).ClearContents
Select Case False
Case IsDate(dt1), IsDate(dt2): Exit Sub
End Select
Dim dtDiff As Long: dtDiff = DateDiff("h", dt1, dt2)
Dim dtStart As Date, dStep As Long
Select Case dtDiff
Case Is > 0: dtStart = dt1: dStep = 1
Case Is < 0: dtStart = dt2: dStep = -1
End Select
Dim rCount As Long: rCount = Abs(dtDiff) + 1
Dim Data() As Date: ReDim Data(1 To rCount, 1 To 1)
Dim d As Long, r As Long
If dStep = 0 Then
Data(1, 1) = dtStart
Else
For d = 0 To dtDiff Step dStep
r = r + 1
Data(r, 1) = DateAdd("h", d, dtStart)
Next d
End If
dfCell.Resize(rCount).Value = Data
End Sub
If you are ok with a non-VBA solution, then you have some options.
Option 1: SEQUENCE
For the Excel version listed here, you could use the SEQUENCE function as suggested by chris neilsen.
Example:
Let's assume that your data starts at A1 like this:
Then, in C2, you could have :
=SEQUENCE((A3-A2)/VALUE("01:00:00")+1,1,A2,VALUE("01:00:00"))
Note that VALUE("01:00:00") represents 1 hour (but you could also use TIME(1,,) as suggested by Mayukh Bhattacharya).
Option 2: Dynamic Array Formula
You have an Excel version listed here, you can use a dynamic array formula .
Example:
Making the same assumptions as option 1, for where the data is, you could use a formula like this one:
=(ROW(INDIRECT("1:"&(A3-A2)/VALUE("01:00:00")+1))-1)*VALUE("01:00:00")+A2
Explanations:
Using the INDIRECT function inside the ROW function is a neat trick to get an array with consecutive values. For instance, INDIRECT("1:9") return the array containing rows 1 to 9 and passing it to ROW will return the array as a column like this {1;2;3;4;5;6;7;8;9} (we get only one element per row).
Since we don't know in advance how many steps we will take we calculate the number of elements using (A3-A2)/VALUE("01:00:00")+1 and concatenate it to "1:" to get the range of size that we need.
When we have the sequential array, we just need to make sure it starts by zero, which is why we remove 1 to all elements of the array like this:
ROW(INDIRECT("1:"&(A3-A2)/VALUE("01:00:00")+1))-1
Finally, we multiply each element of the array by the value corresponding to 1 hour and add the starting point in A2.
Option 3: Old array formula
Same idea as option 3 but using the old array formula explained here. Basically, you'll have to use Ctrl+Shift+Enter.
A Simple Solution given your example (to clear cells it is your job :-)
It would be better to write to an array but as example it should be ok.
Option Explicit
Sub TimeToHour()
Dim startTime As Double, endTime As Double, i As Double, z As Double
startTime = Range("a2")
endTime = Range("a3")
Columns(3).NumberFormat = Range("a2").NumberFormat ' Column C
z = 2
For i = startTime To endTime Step 1 / 24
Cells(z, 3) = i ' write to column c starting in row 2
z = z + 1
Next
End Sub
Related
I have a data set where I need to compare the first number in each transect against each other.
For example, in the below data set I need to compare cells D2, D7, D12 and D17 and assign a value based on which one is the smallest number, the next smallest and so on. This will be used to assign the transect numbers in column A.
My issue is that the number of sections (in this example 4) and the number of transects (also 4 in this example) will vary. So the cells I need to compare will change.
I have written the code that calculates the number of transects, which is:
Dim tlength As Worksheet
Dim tb As Long *'tb=transect break*
Sub tlength_start_stop_coords()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("sheet1")
Set tlength = ThisWorkbook.Worksheets("transect lengths") *' assigns the sheet to a variable
for efficient coding*
tb = 0 *'counter to calculate the number of transects*
j = 2 *'counter for row*
Lastrow = Lastrow + 1 *'add a row to last row so that the last row includes a blank line.*
*'the following for loop is used to calculate the number of transects*
For i = 2 To Lastrow
If tlength.Range("D" & i) = vbNullString Then
If tlength.Range("D" & i - 1) <> vbNullString Then
tb = tb + 1 *'updates the counter*
End If
End If
Next i
tbtotal = tb *'stores the total counter in variable tbtotal for later use*
I think I may need to use a loop. But I am stuck trying to figure out how to manage the unknown number of comparisons in changing cell locations.
The desired result is in the below screenshot of the expected outcome, with results in column A. To begin with, I only need to get the number for the first row of each transect. Once I have that, I can copy using xldown.
Expected outcome:
Another possible data set and outcome expected might be:
enter image description here
with an expected outcome of:
enter image description here
Worked for me using your second set of sample data:
Sub Tester()
Dim tlength As Worksheet, i As Long, tNum As Long, v, vPrev, arr
Dim col As New Collection, e, c As Range, rng As Range
Set tlength = ThisWorkbook.Worksheets("transect lengths")
'collect all the Section 1 Latitudes and row numbers
For i = 2 To tlength.Cells(Rows.Count, "B").End(xlUp).Row
If tlength.Cells(i, "B") = 1 Then
col.Add Array(i, tlength.Cells(i, "D").Value) 'store start row and first Latitude
End If
Next i
SortCollection col, 2 'sort collection by second element in each array
tNum = 0
'loop over the sorted collection and assign the order of the transects
For Each e In col
tNum = tNum + 1
Set c = tlength.Cells(e(0), "B")
'following assumes all transects have at least 2 sections...
tlength.Range(c, c.End(xlDown)).Offset(0, -1).Value = tNum
Next e
End Sub
'Sort a collection of 0-based arrays in ascending order by the n'th element of each array
'Adapted from https://stackoverflow.com/a/3588073/478884
Sub SortCollection(col As Collection, n As Long)
Dim i As Long, j As Long, vTemp As Variant
For i = 1 To col.Count - 1 'Two loops to bubble sort
For j = i + 1 To col.Count
If col(i)(n - 1) < col(j)(n - 1) Then 'change to > for ascending sort
vTemp = col(j) 'store the lesser item
col.Remove j 'remove the lesser item
col.Add Item:=vTemp, before:=i 're-add the lesser item before the greater Item
End If
Next j
Next i
End Sub
I am trying to put together a formula that gives me the average total for an order if the order contains a specific item in it, how can I compute that?
For example if the table looks like
Order# | Item | Total for Entire order
a Apple 50
a Juice 50
a Chicken 50
a Bread 50
b Bread 23
b fish 23
c Chicken 43
c Wine 43
c rice 43
I want get the avg total of all orders that contain Chicken in them at least once? but dont want to count the total of once order twice in my average calculation - Thanks for looking
If one has the new Dynamic Array formulas:
=AVERAGE(INDEX(UNIQUE(FILTER(A2:C10,B2:B10="Chicken")),,3))
If not:
=SUMPRODUCT(((B2:B10="Chicken")*(C2:C10))/(COUNTIFS(A2:A10,A2:A10,B2:B10,"Chicken")+(B2:B10<>"Chicken")))/SUMPRODUCT((B2:B10="Chicken")/(COUNTIFS(A2:A10,A2:A10,B2:B10,"Chicken")+(B2:B10<>"Chicken")))
If you're not opposed to using VBA this function will do what you want.
=Average_Orders_Subset(range, criteria)
I changed this from my original answer to improve the functionality and added comments so you can see what it's doing. With the function you can add as many orders as you like. I have also tested it and it will work if the order letters are changed to numbers i.e. a = 1, b= 2 ect... Hope this helps. Don't hesitate to ask if you have any questions.
If your data starts in cell A1 it works like this:
Example: =Average_Orders_Subset(A1:C13,"Chicken")
With this version you can average for Chicken, rice, bread, anything you want.
In case you don't know here's how to add the code to the workbook.
If you want I can send you a copy of a workbook with it already built in.
Option Explicit
' User Defined Function to Average totals for only orders that contain specific string
' Pass the range and string you are looking for to the function
' Note the criteria string is case senstive so "chicken" is not the same as "Chicken"
'
' Example: =Average_Orders_Subset(A1:C13,"Chicken")
'
Public Function Average_Orders_Subset(rng As Range, criteria As String) As Double
Dim ws As Worksheet
Dim arrData() As Variant
Dim arrOrder() As Variant
Dim i As Long
Dim j As Long
Dim cnt As Long
Dim sum As Double
Dim avg As Double
' The worksheet with data
Set ws = ThisWorkbook.ActiveSheet
' Counter and array to keep track of order letters
cnt = 0
ReDim arrOrder(cnt)
With ws
' Create an array with all the values
arrData = rng.Value2
' Iterate through the array looking for orders with the criteria e.g., "Chicken"
For i = 2 To UBound(arrData)
' If criteria is found
If arrData(i, 2) = criteria Then
If cnt > 0 Then
' If the array of order letters is less than 0
If arrData(i, 1) <> arrOrder(cnt - 1) Then
' Checking if the order letter is already in the array so orders with two Chicken
' or multipe of any criteria don't get double counted
' Add them to the order letter array
arrOrder(cnt) = arrData(i, 1)
cnt = cnt + 1
ReDim Preserve arrOrder(cnt)
End If
ElseIf cnt = 0 Then
' This is to add the first occurence of the critera to the order array
arrOrder(cnt) = arrData(i, 1)
cnt = cnt + 1
ReDim Preserve arrOrder(cnt)
End If
End If
Next i
' Remove the last empty value in the order array
' this is a result of the count expanding the array after the value is added
ReDim Preserve arrOrder(UBound(arrOrder) - 1)
' Reset counter
cnt = 0
sum = 0
' For all the values in the order array
For i = LBound(arrOrder) To UBound(arrOrder)
' For all the values in the range
For j = 2 To UBound(arrData)
' If a row in the range matches the order letter add that value to the sum
If arrData(j, 1) = arrOrder(i) Then
sum = sum + arrData(j, 3)
' Keep track of the number of summed values for an average
cnt = cnt + 1
End If
Next j
Next i
' Calculte the average
avg = (sum / cnt)
' Pass the average back to the formula
Average_Orders_Subset = avg
End With
Set rng = Nothing
Set ws = Nothing
End Function
I have a large workbook with 9 sheets, each containing about 30-40 columns. I need to loop through each column individually and find values that are out of range or outliers and color them.
The only problem is that each column has unique values and I would need individual if statements for each. Is there a way to accomplish this without creating a bunch of individual for statements? I want to avoid having as many variables as possible and prevent it from taking an extremely long time to process. I have some example code of what I would need to do for each column. At the moment, all I would know how to do is repeat this code over and over for each column.
Dim rngData As Range, cellData As Range
Set rngData = Range("A2:A" & (numRows + 1))
For Each cellData In rngData
If cellData.Value < 0 Or cellData.Value > 200 Then
cellData.Interior.Color = RGB(126, 206, 124)
End If
Next cellData
You can use arrays to store outliners and column specification like this. It is not easy to fill in the arrays but one should do it :)
Dim oHI(100) As Long, oLOW(100) As Long, oCnt(100) As Long
Dim oCOL(100) As Long ' column numbers
Dim iColCnt As Long ' number of columns to process
oCOL(0) = 1 ' number of column #0
oHI(0) = 1000 : oLOW(0) = 200 ' high-low pairs for column #0
oCnt(0) = 44 ' count of valid elements in column #0
oCOL(1) = 5 ' number of column #1
oHI(1) = 100 : oLOW(1) = 0
oCnt(1) = 60
...
iColCnt = <count of columns to be processed, 0 relativ>
For i = 0 to iColCnt
Set rngData = Range(Cells(2, oCOL(i), Cells(oCnt(i) + 1, oCOL(i))
For Each cellData In rngData
If cellData.Value < oLOW(i) Or cellData.Value > oHI(i) Then
cellData.Interior.Color = RGB(126, 206, 124)
End If
Next cellData
Supposing all outliners are integers.
If the min and max changes by each column you'll have to identify them somewhere. I would put these in Excel somewhere to make it easy. However, you could also code it into VBA and have it dynamic using an array. In the below example I set up the limit for column 1. You'd have to include the min and max for all columns (which doesn't sound fun), but unless there's a dynamic method to the limits... this might be your best option.
Sub LoopCOLO()
Dim rngData As Range, cellData As Range, c As Long
Dim cMIN(1 To 40) As Double, cMAX(1 To 40) As Double
'Enter your min/maxex here based on column number)
cMIN(1) = 0
cMAX(1) = 200
cMIN(2) = 100
cMAX(2) = 300
'if you get confused on column nubmers you could do
cMIN(Range("C1").Column) = 200
cMAX(Range("C11").Column) = 300
'Loop your columns
For c = 1 To UBound(cMAX) 'however many columns
Set rngData = Range(Cells(1, c), Cells(numrows + 1, c))
'check values in each cell
For Each cellData In rngData.Cells
If cellData.Value < cMIN(c) Or cellData.Value > cMAX(c) Then
cellData.Interior.Color = RGB(126, 206, 124)
End If
Next cellData
Next c
End Sub
I am currently using this formula to calculate the unique number of dates for each user in column R
=SUM(IF(Q1=$J$2:$J$39115, 1/(COUNTIFS($J$2:$J$39115, Q1, $H$2:$H$39115, $H$2:$H$39115)), 0))
This formula works, but takes forever to calculate due to the large number of rows it has to work through. I am sure there must be a quicker way using a macro or a vba.
Column J contains the list of users (users appears multiple times) and column H contains the dates.
I need to calculate the number of days worked by each user in column Q.(column Q contains the unique list of users .
Add the following into a VBA module:
Public Function count_distinct(user As Range, userlist As Range, datelist As Range) As Long
Dim i As Long, j As Long, dates() As Variant, found As Boolean, this_date As Variant
Dim varUL As Variant, varDL As Variant, unique_count As Long
varUL = userlist.Value
varDL = datelist.Value
ReDim dates(0 To 0)
For i = 1 To UBound(varUL, 1)
If varUL(i, 1) = user Then
found = False
this_date = varDL(i, 1)
For j = 1 To unique_count
If this_date = dates(j) Then
found = True
Exit For
End If
Next j
If Not found Then
unique_count = unique_count + 1
ReDim Preserve dates(0 To unique_count)
dates(unique_count) = this_date
End If
End If
Next i
count_distinct = unique_count
End Function
Then replace your formula in column R with: =count_distinct(Q1,$J$2:$J$39115,$H$2:$H$39115)
It's not the quickest and I am sure it could be improved, but it may give you back a few minutes!!
I am currently in the process of analysing data from Excel, and would like to make comparisons between data in Column A and Column B, identifying duplicate data. I am using the following code:
Sub Find_Matches()
Dim CompareRange As Variant, x As Variant, y As Variant
Set CompareRange = Range("c2", Range("c2").End(xlUp))
For Each x In Selection
For Each y In CompareRange
If x = y Then x.Offset(0, 1) = x
Next y
Next x
End Sub
This code has been taken from MSDN, so if it finds a match in Column C against Column A, it will display the matched number in Column B. For the most part it does what I need. However I am looking to modify this code so it only matches a number in the list once.
Example of what the code currently does:
A2 B2 C2
1 1 1
1 1 2
1 1 3
So essentially, because the number 1 appears once in Column C, Column A keeps finding a match.
What I would like it to do is:
A2 B2 C2
1 1 1
1 2
1 3
So because the number 1 only appears in Column C once, it should only be matched once against the numbers in Column A.
I'm assuming this is probably something simple, but I can't seem to determine the logic. Could someone point me in the right direction please?
Testing for duplicates can be simple or complicated depending on how fast you want your procedure to be and how large the data sets are.
I personally favour the Collection object because it has a unique key and testing for the existence of that key is very fast, especially if the dataset is large. The unique test is done by seeing if the code throws an error when you interrogate the Collection for a particular key. Some are philosophically opposed to testing for errors - I have to say that I'm one, so I actually prefer the Dictionary object but for a task this mundane, I won't go through the steps to reference that.
You'll also see that the code below works with arrays rather than cells on the worksheet itself - again, just a matter of personal taste because it's quicker.
Const SOURCE_COL As String = "A"
Const SOURCE_START_ROW As Long = 2
Const COMPARE_COL As String = "C"
Const COMPARE_START_ROW As Long = 2
Const OUTPUT_COL As String = "B"
Dim ws As Worksheet
Dim sourceValues As Variant
Dim compareValues As Variant
Dim outputValues() As Variant
Dim sourceIndex As Long
Dim compareIndex As Long
Dim uniques As Collection
Dim val As Variant
Dim key As String
Dim exists As Variant
Set ws = ThisWorkbook.Worksheets("Sheet1")
sourceValues = ws.Range(ws.Cells(SOURCE_START_ROW, SOURCE_COL), _
ws.Cells(Rows.Count, SOURCE_COL).End(xlUp)).Value2
compareValues = ws.Range(ws.Cells(COMPARE_START_ROW, COMPARE_COL), _
ws.Cells(Rows.Count, COMPARE_COL).End(xlUp)).Value2
Set uniques = New Collection
ReDim outputValues(1 To UBound(sourceValues, 1), 1 To 1)
For sourceIndex = 1 To UBound(sourceValues, 1)
val = sourceValues(sourceIndex, 1)
key = CStr(val)
exists = Empty
On Error Resume Next
exists = uniques(key)
On Error GoTo 0
If IsEmpty(exists) Then
For compareIndex = 1 To UBound(compareValues, 1)
If val = compareValues(compareIndex, 1) Then
outputValues(sourceIndex, 1) = val
uniques.Add val, key
Exit For
End If
Next
End If
Next
ws.Cells(SOURCE_START_ROW, OUTPUT_COL).Resize(UBound(outputValues, 1)).Value = outputValues