Excel VBA - If value exists > x, add to sum - vba

I have a table with time values. If any value exists in a specified row that is greater than 90 minutes (1:30:00), I need to add the difference (i.e. how much greater it is) to a running total at the end of the row. So, that box could be blank, could have just one cell's value, or could have multiple values added. I already have the For loop to go through each cell in a row. I need the part to sum the values. And ideally, if there was a nested loop to do 6 separate sums for the 6 rows...
'Add break munutes
fp.Activate
Dim rng As Range
For Each rng In Range("B3:F3")
If rng.Value > TimeValue("1:31:00") Then
End If
Next rng

If you want to avoid VBA, this can actually be done by an Excel formula:
=SUMIF($B$3:$B$9,">"&1.5/24)-COUNTIF($B$3:$B$9,">"&1.5/24)*1.5/24
That sums up all values that exceed 90 minutes, and then subtracts off 90 minutes from the total for each value that has been counted.

I would recommend Excel Formula if that is an option, because of the restrictions of VBA solutions.
=SUMPRODUCT((B$3:B9-1/16)*(B$3:B9>1/16))
or a bit shorter with array formula (enter with Ctrl + Shift + Enter) :
=SUMIF(B$3:B9-1/16,">0")

Hard to say if this is fully accurate without more information, but something like this should work for you:
Sub tgr()
Dim aTimes As Variant
Dim dTime As Double
Dim aSumResults() As Double
Dim lResultIndex As Long
Dim i As Long, j As Long
Dim bFirst As Boolean
dTime = TimeValue("01:00:00")
aTimes = ActiveSheet.Range("B3:F9").Value 'Change to the full table range
ReDim aSumResults(1 To UBound(aTimes, 1) - LBound(aTimes, 1) + 1, 1 To 1)
For i = LBound(aTimes, 1) To UBound(aTimes, 1)
'Each i represents a row of the data
'Go through each column and collect the conditional sums
lResultIndex = lResultIndex + 1
bFirst = True 'Use bFirst to ignore first value greater than dTime
For j = LBound(aTimes, 2) To UBound(aTimes, 2)
If aTimes(i, j) > dTime Then
If bFirst Then
'This if the first value found for the row, ignore it
bFirst = False
Else
'Not the first value found, include in sum
aSumResults(lResultIndex, 1) = aSumResults(lResultIndex, 1) + aTimes(i, j) - dTime
End If
End If
Next j
Next i
'Output the results
ActiveSheet.Range("G3").Resize(UBound(aSumResults, 1)).Value = aSumResults
End Sub

You said you wanted the sum of the times in a row but then defined B3:B9, so I assumed you meant the sum of the times in a column.
Try this:
Dim i As Integer
Dim num1 As Date
For i = 3 To 9
If Cells(i, 2).Value > TimeValue("1:30:00") Then
num1 = Cells(10, 2).Value + Cells(i, 2).Value - TimeValue("1:30:00")
Cells(10, 2).Value = num1
End If
Next i
I've defined where the sum is put as cell B10. You could make a similar loop for each column. I tried this out and it worked for me.

Related

VBA, Find highest value in a column (C) and return its value and the adjacent cells value

I'm working on an assignment that requires the solution use VBA, so I can't use indexing or other options on the worksheet page... I've looked all over for an answer, maybe I'm just asking the question incorrectly.
In column K there are ticker symbols, i.e. A, ABM, etc.
In column L there is an number (I've been classifying as Long)
I want to put the highest number in column L in Range("O2") and the tag that is one column to the left in Range("N2").
I've found numerous ways to identify the high number in column L, but can not figure out how to return the adjacent cells value...
Here is the most recent code that I've been trying which is not working. When I remove the tag references the code runs fine, but I need the adjacent value too.
Thanks
Sub attempt38()
Dim sheet As Worksheet
Dim i As Long
Dim firstRow As Integer
Dim columnNumber As Integer
Dim max As Long
Dim tag As Long
firstRow = 2
columnNumber = 12
Set sheet = ActiveSheet
If sheet.UsedRange.Rows.Count <= 1 Then max = 0 Else max = sheet.Cells(2, 12)
For i = firstRow To 300
If sheet.Cells(i, 12) > max Then max = sheet.Cells(i, 12) & (tag = sheet.Cells(i, 11))
Next
sheet.Cells(3, 14) = max
sheet.Cells(4, 14).Value = tag
End Sub
You don't need VBA. You can just use regular excel.
=LARGE(L:L,1)
Will return the largest number.
To get the corresponding just use Index plus match.
=INDEX(K:K,MATCH(LARGE(L:L,1),L:L,FALSE),1)
If you really want to use VBA, adjust your code to be two lines like so:
For i = firstRow To 300
If sheet.Cells(i, 12) > max Then
max = sheet.Cells(i, 12)
tag = sheet.Cells(i, 11)
Endif
Next
Or if you want to look sophisticated:
For i = firstRow To 300
With sheet.Cells(i, 12)
If .Value > max Then
max = .Value
tag = .Offset(0,-1).Value
Endif
End With
Next i
Looping through a range can be time consuming and, in this case, also wasteful.
What if your max value actually exists in the first looped row? You will now loop through 299 rows for nothing.
The below method will be much faster and requires no loops.
Option Explicit
Sub Mad_Max()
Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets("Sheet1")
Dim MyMax As Long, MaxCell As Range
MyMax = Application.WorksheetFunction.Max(ws.Range("L:L"))
Set MaxCell = ws.Range("L:L").Find(MyMax, Lookat:=xlWhole)
ws.Range("N3") = MyMax
ws.Range("N4") = MaxCell.Offset(, -1)
End Sub

Extracting a value for every year at the same date as today

I want to code something on VBA but I have no clue how to:
I have data with one column for the date (everyday from 1998 to today) and one column for the prices.
What I want to do is for every year, on the same day as today, show what was the price, on a different excel sheet.
For example : if today is 28/08/2018, I want to know what was the price on 28/08/2017, 28/08/2016,..., 28/08/1998.
Thanks a lot!
You could read the values into an array and loop that array. Compare if the 5 left characters of column 1 (dates) match the left 5 of today's date (returned with the Date function). Store qualifying values into a dictionary. Write the dictionary out to the a sheet at the end. You could also simply use:
If Format$(arr(i, 1), "dd/mm") = Format$(Date, "dd/mm") Then if dates.
Option Explicit
Public Sub test()
Dim arr(), i As Long, results As Object
Set results = CreateObject("Scripting.Dictionary")
With ThisWorkbook.Worksheets("Sheet1")
arr = .Range("A2:B" & .Cells(.Rows.Count, "A").End(xlUp).Row).Value '< read columns A and B into an array (Dates and Prices)
For i = LBound(arr, 1) To UBound(arr, 1) '< Loop the dates in the array (column 1)
If Left$(arr(i, 1), 5) = Left$(Date, 5) Then 'compare whether left 5 characters match i.e. same dd/mm irrespective of year
results(arr(i, 1)) = arr(i, 2) 'if match add the arr(i,1) date as key to dictionary; add the arr(i,2) price as the value associated with the key
End If
Next
End With
With ThisWorkbook.Worksheets("Sheet2")
.Range("A1").Resize(results.Count, 1) = Application.WorksheetFunction.Transpose(results.keys)
.Range("B1").Resize(results.Count, 1) = Application.WorksheetFunction.Transpose(results.items)
End With
End Sub
Here is an example of the comparison, if it were done in the sheet, showing a qualifying row. This comparison is done for every row which are stored in the array.
The dictionary, results, ends up having the qualifying row column A (date) as key, and column B (Price) as value.
You can access all the .Items or .Keys of the final dictionary producing an array in each case which can be tranposed to write to columns in the sheet.
Your dictionary will end up storing key value pairs of qualiying rows, a sample of which would look like:
Depending on formatting in sheet you might instead need:
If Format$(arr(i, 1), "dd/mm") = Format$(Date, "dd/mm") Then
Test run:
Not ideal but tried and it works, as long as the date and the target day is set to text:
Public Sub Test()
Dim i As Integer
i = 0
For Each c In ThisWorkbook.Sheets("Sheet1").Range("A1:A5")
If Left(c.Value, 6) = ThisWorkbook.Sheets("Sheet1").Range("D1:D1").Value Then
i = i + 1
ThisWorkbook.Sheets("Sheet1").Range("F" & i & ":F" & i).Value = c.Offset(0, 1).Value
End If
Next c
End Sub
For a VBA answer I would suggest something like:
Public Function GetPriceOnEachYear(source As Range, prices As Range) As Object()
Dim rng As Range
Dim answer() As Object
last = 0
For Each rng In source
If Month(rng.Value) = Month(Now) And Day(rng.Value) = Day(Now) Then
rowdiff = rng.Row - source.Row
columndiff = rng.Column - source.Column
ReDim Preserve answer(0 To last)
Set answer(last) = prices(rowdiff + 1, columndiff + 1)
last = last + 1
End If
Next rng
GetPriceOnEachYear = answer
End Function
I didn't do any type safety, and the answer is an array of ranges, if you would like only the value, then instead of Set answer(last) = prices(rowdiff + 1, columndiff + 1) you could use answer(last) = prices(rowdiff + 1, columndiff + 1).

Executing a loop for a range of cells

This is a very basic question and I'm sure it has been answered but I can't seem to find it elsewhere. I have a portion of vba code that works fine for a single cell. However I want to expand it to work for a range of cells (all within the same column).
Basically its a goalseeking loops that changes a value in the "b" column until the value in the "w" column matches (comes within 99%) the value in the "x" column.
What works:
Sub Goalseeker()
Do Until Range("w32").Value / Range("x32").Value > 0.99
Range("b32").Value = Range("b32").Value - 1
Loop
End Sub
I want to extend this to work for rows 32 to 107.
What I've tried:
Edit: I've made adjustment based on the comments received and tweaked and few things before it worked. If anyone is interested in the process:
Option Explicit
Sub Goalseeker()
Dim i As Integer
Dim targetcell As Double
Dim outputcell As Double
Dim variablecell As Range
For i = 32 To 107
targetcell = Cells(i, "x")
outputcell = Cells(i, "w")
Set variablecell = Range("B" & i)
Do Until outputcell / targetcell > 0.99
variablecell = variablecell - 1
targetcell = Cells(i, "x")
outputcell = Cells(i, "w")
Loop
Next
End Sub
The bit I had to tweak was
Do Until outputcell / targetcell > 0.99
variablecell = variablecell - 1
targetcell = Cells(i, "x")
outputcell = Cells(i, "w")
Loop
Redefining (i apologize if that's the wrong term) targetcell and outputcell was necessary to prevent an infinite loop.
Thanks all. I will work on making this function for relative references instead of absolute.
Few problems here. Change your For i loop to the format For i = x to y not For i = x to i = y
You can refer to targetcell and outputcell as double but the variablecell needs to be a range. And if it's a range it needs Set
You should declare ALL your variables, as below.
And finally, you might want to put in a catch to get out of infinite looping (in case the target never reaches above 0.99 ?)
Sub Goalseeker()
Dim i As Integer
Dim targetcell As Double
Dim outputcell As Double
Dim variablecell As Range
For i = 32 To 107
targetcell = Cells(i, "x")
outputcell = Cells(i, "w")
Set variablecell = Range("B" & i)
Do Until outputcell / targetcell > 0.99
variablecell = variablecell - 1
Loop
Next
End Sub
Consider the following example table:
Use the code below to find the correct values in the "B" column (as shown) in order to minimize the error between the result (next column) and the goal (two columns over).
Option Explicit
Public Sub GoalSeekMyValues()
' Call GoalSeek with inputvalues "B2:B16", having the result
' at column offset 1, and the goal in column offset 2.
' Note that Range("B2").Resize(15, 1) = Range("B2:B16"),
' But I prefer the top cell and row count of this syntax.
GoalSeek Range("B2").Resize(15, 1), 1, 2
End Sub
Public Sub GoalSeek(ByVal variables As Range, ByVal result_offset As Long, ByVal goal_offset As Long)
Dim n As Long, i As Long, pct_error As Double, last_error As Double
'x is the input value (variable)
'y is the result
'g is the goal for y
Dim x As Double, y As Double, g As Double
' Read the number of rows in the input values
n = variables.Rows.Count
Dim r As Range
' Loop through the rows
For i = 1 To n
'Set a range reference at the i-th input cell
Set r = variables.Cells(i, 1)
' Read the value, the result and the goal
x = r.Value
y = r.Offset(0, result_offset).Value
g = r.Offset(0, goal_offset).Value
pct_error = Abs(y / g - 1)
Do
'Set the next value
r.Value = x - 1
' Read the result (assume goal doesn't change)
y = r.Offset(0, result_offset).Value
' Keep last error, and calculate new one
last_error = pct_error
pct_error = Abs(y / g - 1)
' If new error is more than last then exit the loop
' and keep the previous value (with less error).
If pct_error > last_error Then
' Keep last value
r.Value = x
Exit Do
End If
' read the input value
x = r.Value
' Assume inputs must be positive so end the loop
' on zero on negative numbers
Loop Until x <= 0
Next i
End Sub
Your code has lots of points of failure.
Your code might not ever reach a solution and excel will hang (until Ctrl-Break is pressed). I have a hard break when the inputs become zero or negative. Other problems require other ways to tell that there isn't a solution.
The first time the result comes within 1% of the solution might not produce the least error. I solve this by tracking the absolute value of the relative error. Only when the error starts increasing I terminate the loop. This assumes that decreasing the input by one unit will improve the solution (at least initially). If this is not the case the code will fail.
You use absolute referencing (like reading the 12th cell down and 4th across) and that is not very reusable style of programming. I always try to use relative referencing. I start from the top left referenced cell (in this case B2) and move down and right from there using the following methods:
Range("B2").Cells(5,1) - Reference 5th row and 1st column from B2.
Range("B2").Resize(15, 1) - Expand the range to include 15 rows and one column.
Range("B2).Cells(i,1).Offset(0, 3) - From the i-th row use column offset of 3 (meaning the 4th column in the table).
I suggest to use one of the common goal seeking methods (like bisection), or better yet, use the built-in goal seek function
example:
Range("B2").Cells(i,2).GoalSeek Goal:=Range("B2").Cells(i,3).Value, ChangingCell:=Range("B2").Cells(i,1)

Summing through filtered rows in Excel

Hi so I'm currently trying to sum visible values of a filtered row in Excel using VBA aka using SUBTOTAL. My problem is that the number of rows change depending on the filter. I tried to write a code that loops through the rows until it hits a blank cell, and subtotals the visible rows. Any help would be appreciated
Sub Test1()
Dim x As Integer
NumRows = Range("K15", Range("K15").End(xlDown)).Rows.Count
Range("K15").Select
For x = 1 To NumRows
H14 = SUBTOTAL(9,"K15" : "NumRows")
ActiveCell.Offset(1, 0).Select
Next
End Sub
Something like this? You should check start row (15 here), and cell where sum is written (Cells(14,8) in this case, id est, H8). Also is usually helpful to define worksheet (Data in this case).
Sub Test1()
Dim x As Long
Dim lMySum As Long
Dim NumRows As Long
lMySum = 0
NumRows = Worksheets("Data").Cells(15, 11).End(xlDown).Row
For x = 15 To NumRows
If Not (Worksheets("Data").Rows(x).EntireRow.Hidden) Then
lMySum = lMySum + Worksheets("Data").Cells(x, 11).Value
End If
Next
Worksheets("Data").Cells(14, 8) = lMySum
End Sub

Create a row with subtotals above data to be added with a Loop through an array in VBA

I need to programmatically format an excel table creating two different subtotals base on changes of values in two specified column (the first and second of the spreadsheet).
I managed to code the routine for the first subtotals but I’m now stuck trying to figure out the second part of the code.
In the second routine I need to create the subtotal row above the data that will be added. I couldn’t manage to insert the calculation in the right row and there is something wrong when I do the loop through the columns included in the array.
Anyone that can see what I’m doing wrong?
Thank you
Public Sub SubtotalTable()
Dim RowNumber As Long
Dim RangePointer As Range
Dim RangeTopRow As Range
'Pointing the column to check
Set RangePointer = ActiveSheet.Range("B1")
'Assigning the first row to a range
Set RangeTopRow = Rows(2)
'Assigning to long a variable the number of row from which begin checking
RowNumber = 3
Do
If RangePointer.Offset(RowNumber).Value <> RangePointer.Offset(RowNumber - 1).Value Then
Set RangeTopRow = Rows(RowNumber)
'Call the function to insert the row
Call InsertRowTotalsAbove(RangePointer.Offset(RowNumber), RangeTopRow, RowNumber)
Else
RowNumber = RowNumber + 1
End If
RowNumber = RowNumber + 1
Loop
End Sub
Public Function InsertRowTotalsAbove(RangePointer As Range, lastRow As Range, RowNumber As Long)
Dim ArrayColumns() As Variant
Dim ArrayElement As Variant
Dim newRange As Range
'Assigning number of columns to an array
ArrayColumns = Array("D", "E", "F", "G")
Do
If RangePointer.Offset(RowNumber).Value = RangePointer.Offset(RowNumber - 1).Value Then
RowNumber = RowNumber - 1
Else
ActiveSheet.Cells(RowNumber + 1, 2).Select
Set newRange = Range(ActiveCell, ActiveCell.Offset(0, 0))
Rows(newRange.Offset(RowNumber + 1).Row).Insert shift:=xlDown
newRange.Offset(RowNumber + 1, 0).Value = "Totale" & " " & newRange.Offset(RowNumber + 2, 0)
For Each ArrayElement In ArrayColumns
newRange.Cells(RowNumber, ArrayElement).Value = Application.WorksheetFunction.Sum(Range(lastRow.Cells(-1, ArrayElement).Address & ":" & RangePointer.Cells(0, ArrayElement).Address))
Next ArrayElement
End If
Loop
End Function
I think this is the cause of your first problem (the shift). The second problem (the wrong values) will hopefully be clear once you cleaned up the code.
ActiveSheet.Cells(RowNumber + 1, 2).Select
Set newRange = Range(ActiveCell, ActiveCell.Offset(0, 0))
First, as a general best practice never use select as it's difficult to debug and can cause significant performance issues. However in this case that's not causing your problem.
Your newRange is now the cell Cells(RowNumber + 1, 2), i.e. in column B. (This is way too complicated btw, why not do it in one line Set newRange = ActiveSheet.Cells(RowNumber + 1, 2)?)
Anyway, when you later have
newRange.Cells(RowNumber, ArrayElement).Value
the column is relative to newRange. Since newRange is a cell in column B, newRange.Cells(RowNumber, "A") is going to be in column B and so on.
My advice would be to use column indices instead and adjust for the shift of 1 by substracting 1 from the column index. However, you should try to clean up your code by using less indirect references. For example using less Offsets if you know the actual row number. Rows(newRange.Offset(RowNumber + 1).Row would simply be Rows(2 * RowNumber + 1) etc.