Combine work days and calendar days for a calculator - excel

Im trying to create a calculator for detention of containers.
Each provider has different rules that have a breakdown between work days and calendar days.
for example:
the first 5 working days (excl Saturday and Sunday) are free of cost
After that the next 3 calendar days are at a cost of 135
After the above, the next 5 calendar days are at a cost of 160
After that 180 onward.
is this possible to do in excel? My idea is to have a difference between 2 dates: date of arrival versus date of return. and based on the 4 rules below use "IF" to give me a cost.
what would be a more efficient way to do this?

HardCoded option:
With Dynamic array formula SEQUENCE()
=SUM(IF(SEQUENCE(B1-A1+1,,A1)<WORKDAY(A1,5),0,IF(SEQUENCE(B1-A1+1,,A1)<WORKDAY(A1,5)+3,135,IF(SEQUENCE(B1-A1+1,,A1)<WORKDAY(A1,5)+8,160,180))))
Older version:
=SUM(IF(ROW(INDEX($ZZ:$ZZ,A1):INDEX($ZZ:$ZZ,B1))<WORKDAY(A1,5),0,IF(ROW(INDEX($ZZ:$ZZ,A1):INDEX($ZZ:$ZZ,B1))<WORKDAY(A1,5)+3,135,IF(ROW(INDEX($ZZ:$ZZ,A1):INDEX($ZZ:$ZZ,B1))<WORKDAY(A1,5)+8,160,180))))
As an array formula, using Ctrl-Shift-Enter to confirm instead of Enter when exiting edit mode.
Using a lookup table.
Put 0 in the first cell in the lookup table. The second cell would get the formula =WORKDAY(A1,5) the next below: =F2+3 and the last =F3+5 and put the corresponding values:
Then use SUMPRODUCT and LOOKUP:
=SUMPRODUCT(LOOKUP(SEQUENCE(B1-A1+1,,A1),F1:F4,G1:G4))
older version:
=SUMPRODUCT(LOOKUP(ROW(INDEX($ZZ:$ZZ,A1):INDEX($ZZ:$ZZ,B1)),F1:F4,G1:G4))

Below is a VBA function that accepts three arguments - the start date, the end date, and the name of the carrier, and returns a cost. Only costing for one carrier is included, but it can be expanded for other carriers.
Function fContainerCost(dtmStart As Date, dtmEnd As Date, strCarrier As String) As Currency
Dim lngDays As Long
Select Case strCarrier
Case "CarrierA"
dtmStart = WorksheetFunction.WorkDay(dtmStart, 5)
If dtmStart > dtmEnd Then
fContainerCost = 0
Else
lngDays = DateDiff("d", dtmStart, dtmEnd) + 1 ' need to get the actual days, so get the difference in dates and add 1.
If lngDays <= 3 Then
fContainerCost = fContainerCost + (135 * lngDays)
Else
fContainerCost = fContainerCost + (135 * 3)
lngDays = lngDays - 3
If lngDays <= 5 Then
fContainerCost = fContainerCost + (160 * lngDays)
Else
fContainerCost = fContainerCost + (160 * 5)
lngDays = lngDays - 5
fContainerCost = fContainerCost + (180 * lngDays)
End If
End If
End If
Case "CarrierB"
End Select
End Function
To use it, just treat it as a regular Excel function:
=fContainerCost(A1,B1,C1)
Regards,

Related

Placing arrays in cells with Excel VBA

I use the following Excel custom function to put a value into cell based on a start and end date, and a frequency. The spreadsheet is for projecting costs into the future, so I may allocate $10k every 5 years starting from 2022 and ending in 2072 (eg 2022, 2027, 2032....2067, 2072). I also index it also sometimes.
Current Macro
Public Function spend(FY, start, finish, frequency, amount, index, base)
'Check if FY is within range if not return nothing and exit
If FY < start Or FY > finish Then
spend = ""
Exit Function
End If
'Check frequency to see if valid year
If FY = start Then
spend = amount * (1 + index) ^ (FY - base)
ElseIf (FY - start) Mod frequency = 0 Then
spend = amount * (1 + index) ^ (FY - base)
Else
spend = ""
End If
End Function
Current Spreadsheet
For some larger projects, the costs may spread over multiple years (eg Year 1:$100k, Year 2:$500k, Year 3:$600). Is there a way to put a series of numbers into "Amount" cell. For example if this project repeats every 10 years from 2022 simliar to below:

Why does DateDiff return a date and not the number of minutes?

I need to find how many minutes exist between two string.
h1 = TimeValue("06:00:00")
h2 = TimeValue("22:00:00")
res = DateDiff("n", h1, h2)
However, res = 17/08/1902 whereas the expected result is 960.
Sub calcul(hours As Variant, Optional n As Integer = 0)
i = 3
Do While (Cells(i, 0) <> "")
Dim res As Date
Dim h2 As Date
Dim h1 As Date
Dim h As Integer
If (n = 0) Then
h = 0
Else
h = Cells(i, 7).Value - 1
End If
h1 = TimeValue(hours(h)("h1"))
h2 = TimeValue(hours(h)("h2"))
res = DateDiff("n", h1, h2)
...
The problem here is how you you've defined res.
Dates and time values are numbers. Even if you see it as 30/09/2019 or 12:00:00, actually, for Excel, both cases are numbers.
First date Excel can recognize properly is 01/01/1900 which integer numeric value is 1. Number 2 would be 02/01/1900 and so on. Actually, today is 43738.
For times is the same, but the decimal parts are the hours, minutes and second. 0,5 means 12:00:00. So, actually, 43738,5 means 30/09/2019 12:00:00.
Anyways, in your case, you are obtaining time difference between 2 times in minutes. The result is 960, but you are asigning this value to a date type, so 960 is getting converted to 17/08/1902.
Dim h1 As Date
Dim h2 As Date
Dim res As Single
h1 = TimeValue("06:00:00")
h2 = TimeValue("22:00:00")
res = DateDiff("n", h1, h2)
Debug.Print res
The code above will return 960 properly. Adapt it to your needs.
UPDATE: Because DateDiff returns a Long, defining res as Single is not worth it at all. I did it because working with times, in many cases, needs decimals, but if you are using just DateDiff, then you can perfectly do res as Long or res as Integer.
Note the difference between DateDiff and a normal substraction with a simple code:
Dim time1 As Date
Dim time2 As Date
Dim res1 As Integer
Dim res2 As Single 'or double if you wish
time1 = "06:00:00"
time2 = "06:30:30"
'time difference between these 2 values are 30 minutes and 30 seconds (30,5 minutes in decimal)
res1 = DateDiff("n", time1, time2)
res2 = (time2 - time1) * 1440 '1440 is the number of minutes in a whole day
Debug.Print "With DateDiff:" & res1, "Normal: " & res2
The output of this code is:
With DateDiff:30 Normal: 30,5
Using DateDiff sometimes is not worth it. Depending on how accurate you need the result, DateDiff may compensate or not. I would suggest you to avoid it if you can (this is jut my opinion)
Hope this helps
UPDATE 2: About the code above, yes, a solution would be using DateDiff("s", time1, time2) / 60 to get the seconds transformed into minutes, but this value, because of decimals, should be assigned to a data type that allows it.

Run user defined function for each row matching criteria of array formula if statement

I made a custom function in Excel VBA:
Option Explicit
Function Units(budgetYear As Integer, birthday As Date, serviceStart As Date, serviceEnd As Date, isMarried As Boolean, isServingAbroad As Boolean) As Double
' in this function all dates are converted to the first day of the date's month to make calculations easier
Dim units As Double: units = 0
' service end date cannot be earlier than service start
If serviceEnd < serviceStart Then
Units = units
Exit Function
End If
Dim firstMonthOfYear As Date: firstMonthOfYear = DateSerial(budgetYear, 1, 1)
Dim lastMonthOfYear As Date: lastMonthOfYear = DateSerial(budgetYear, 12, 1)
Dim newServiceStart As Date: newServiceStart = FirstOfTheMonth(serviceStart)
Dim newServiceEnd As Date: newServiceEnd = FirstOfTheMonth(serviceEnd)
Dim eighteenthBirthday As Date: eighteenthBirthday = DateSerial(year(birthday) + 18, Month(birthday), 1)
Dim thisYearServiceStart As Date: thisYearServiceStart = WorksheetFunction.Max(newServiceStart, firstMonthOfYear)
Dim thisYearServiceEnd As Date: thisYearServiceEnd = WorksheetFunction.Min(newServiceEnd, lastMonthOfYear)
Dim serviceMonthsThisYear As Integer: serviceMonthsThisYear = (Month(thisYearServiceEnd) - Month(thisYearServiceStart)) + 1
' unit multipliers
Dim serviceTypeMultiplier As Integer: serviceTypeMultiplier = 1
If isServingAbroad Then serviceTypeMultiplier = 2
' service must fall in the budget year supplied
If newServiceStart > lastMonthOfYear Or newServiceEnd < firstMonthOfYear Then
Units = units
Exit Function
End If
If isMarried Then
units = serviceMonthsThisYear
ElseIf eighteenthBirthday < thisYearServiceStart Then
' this person is already eighteen in the supplied year
units = serviceMonthsThisYear * 0.5
ElseIf eighteenthBirthday > thisYearServiceEnd Then
' this person is under eighteen in the supplied year
units = serviceMonthsThisYear * 0.25
Else
' this person turns eighteen during the supplied year
Dim underEighteenMonths As Integer: underEighteenMonths = (Month(eighteenthBirthday) - Month(thisYearServiceStart))
Dim overEighteenMonths As Integer: overEighteenMonths = serviceMonthsThisYear - underEighteenMonths
units = (underEighteenMonths * 0.25) + (overEighteenMonths * 0.5)
End If
' multiply units by servingAbroad multiplier
units = units * serviceTypeMultiplier
Units = units
End Function
Private Function FirstOfTheMonth(dateToConvert As Date) As Date
FirstOfTheMonth = DateSerial(year(dateToConvert), Month(dateToConvert), 1)
End Function
I have a lookup table (named Table1):
I want to run the function above on each of the rows that match a certain criteria in the lookup table via an array formula.
I've tried this:
=SUM(IF(Table1[Payee]="Bill",Units(G1,"1/2/2003",Table1[Start],Table1[End],Table1[Status],Table1[Location])))
This just gives me #VALUE.
I want it to do like the SUM function. If I had another column in the lookup table called value, I could do this:
=SUM(IF(Table1[Payee]="Bill",Table1[value]))
And it would sum the value column just for the rows that match the criteria ("Bill").
How can I get the same to work with my custom function? I would prefer not to have to modify the custom function. The array formula should run the custom function for each row that matches the criteria. Am I misunderstanding how array functions work in Excel?
Edit:
Just in case it makes a difference, I'm using the latest version of Excel from Office 365 on a Mac.
Edit 2:
Added the full code from the function. Basically the function takes the months served in the budgetYear and applies a weight to them. Months under eighteen get 0.25 weight, months eighteen or older get 0.5 weight, and married gets 1 weight. If isServingAbroad is TRUE, the result gets multiplied by 2.

Get day number within calendar week for a specific date

I have a data set which includes dates.
I need to split this out by week number for reporting purposes.
What I have so far is:
startDate variable containing 03/01/2015 (populated from data in spreadsheet)
startDay = Day(startDate)
startMonth = Month(startDate)
startYear = Year(startDate)
startWeek = Application.WorksheetFunction.WeekNum(DateSerial(startYear, startMonth, startDay))
which gives me week 1 in startWeek
However I know need to know how far into week 1 the date is.
So for this example, as the date is the 3rd of January, it includes 3 days of week 1
Meaning the reporting I'm putting together will only report on 3 days (as opposed to the full week)
The only way I've figured to do this so far is to calculate which day of the year the date is and the use a MOD calculation (basically divide by 7 and the remainder is how far into the week it is)
dayNumber = DateDiff("d", DateSerial(startYear, 1, 1), DateSerial(startYear, startMonth, startDay)) + 1
dayOfWeek = dayNumber Mod 7
This does work, but I was wondering if there was a nicer solution than this.
You could use a loop to determine how many days before startDate the week number changed:
Public Sub FindDaysInWeekNo()
Dim startDate As Date
startDate = DateSerial(2015, 1, 3)
Dim startWeek As Integer
startWeek = Application.WorksheetFunction.WeekNum(startDate)
Dim i As Integer
Do While startWeek = Application.WorksheetFunction.WeekNum(DateAdd("d", -i, startDate))
i = i + 1
Loop
Debug.Print i '= 3rd day in this week number
End Sub
The following table shows my comparison to the other suggested formulas and why I think that (refered to =WEEKNUM) my calculation is correct.
Note that if you assume 1st to 7th January will be week 1 (days 1 to 7) you cannot use the WeekNum function because this will give you a different result (see table above and note that the first week has only 6 days according to the WeekNum function). Also you cannot name this week number (as what everybody calls week number is defined as https://en.wikipedia.org/wiki/Week#Week_numbering).
Instead you will need to use …
Public Function AlternativeWeekNum(startDate As Date) As Integer
AlternativeWeekNum = WorksheetFunction.Days(startDate, DateSerial(Year(startDate), 1, 1)) \ 7 + 1 'note that this no normal division but a integer division and uses a backslash instead
End Function
to calculate the week number your alternative way, and …
Public Function AlternativeWeekNumDay(startDate As Date) As Integer
AlternativeWeekNumDay = WorksheetFunction.Days(startDate, DateSerial(Year(startDate), 1, 1)) Mod 7 + 1
End Function
to calculate the day in the alternative week.
You can use the Weekday() function for this:
=WEEKDAY(B4;2)
The second parameter mentions how you want your days to be counted (starting from Sunday or Monday, counting starting from 0 or from 1, ...).
dayOfWeek = (8 + Weekday(startDate) - Weekday(DateSerial(startYear, 1, 1))) mod 7
Just take the positive mod 7 of the difference between the current Day-Of-Week and the Day-Of-Week for the 1st January of whatever the year is

Write VBA function to find period no. and week no. for 13 periods

Overview: I basically want to write a version of the MONTH function to work for a 13 period work year...
(Disclaimer: I'm new to VBA) What is the best way to reformat a date dd/mm/yyyy to be in terms of Period Number (1-13) and Week Number (1-4)? I've used the WEEKNUM function to sort of figure it out, but I can't quite seem to get it. I think the problem is that the start of the new year (Period 1, Week 1) is on 12/30/18 and in excel, it counts this date as being the 5th or even 6th week by how it's measured.
Weeks start on Sundays and every period of the year (13) has 4 weeks. So far I have tried:
this basic portion of a self-written function to just get the week number portion to work and it just gives me #value, but no errors in msgbox (if it did work I may be able to figure out the rest):
Function PeriodNum(serial_num As Date, number_periods As Integer, start_date As Date)
Dim first_week As Integer
Dim second_week As Integer
Dim third_week As Integer
Dim fourth_week As Integer
Dim day As Integer
first_week = 1
second_week = 2
third_week = 3
fourth_week = 4
day = serial_num
If day >= start_date Or day <= start_date + 7 Then
Selection.Value = first_week
ElseIf day > start_date + 7 Or day <= start_date + 14 Then
Selection.Value = second_week
ElseIf day > start_date + 14 Or day <= start_date + 21 Then
Selection.Value = third_week
ElseIf day > start_date + 21 Or day <= start_date + 28 Then
Selection.Value = fourth_week
Else
Selection.Value = "error"
End If
End Function
-I have also tried using the WEEKNUM function but I don't know how to get it to give me numbers 1-4 for each perspective period
Thanks so much for your help! Much appreciated
1) There is one problem according to code rules of VBA:
to each
Function PeriodNum(serial_num As Date, number_periods As Integer, start_date As Date)
definition you should also use row:
PeriodNum = ...
by this way you are returning a value from a function (similar to return x; in other languages)
2) Using a Selection object is unpredictable and can change any cell you have active in Excel at time of calling of method. If you are using method for computation you should use similar way:
Function weekCompute(date1)
'...
weekCompute = "val1"
End Function
With this function you can use this cell formula:
=weekCompute(A1)
In Excel, Functions whether built-in (i.e. SUM, COUNT) or user-defined like your PeriodNum, usually return a value to the cell they are entered in. They don't modify workbook objects such as ranges which Selection is a member of. Procedures Subs modify objects but don't have return values.
As #VitezslavSimon alluded in his answer, you need to assign a value to the function name somewhere in your code - PeriodNum = 1 or PeriodNum ="error".
It's also a good idea to explicitly add a return Type to your functions. In your case you could use String if you aren't doing any arithmetic calculations on the return value or Variant if you want to use the return in further calculations. Your function definition would then be:
Function PeriodNum(serial_num As Date, number_periods As Integer, start_date As Date) As Variant
Iterative approach to function errors
I copied your code into a standard VBA module and put a break-point on line first_week = 1 so i could step through the function after entering it in a cell. To test it I put your start_date in cell C2 and serial_num in C3. I called the function by entering =PeriodNum(C3,13,$C$2) in D3.
When stepping through your code, the line day = serial_num caused the function to end and return #VALUE! error in D3.
#VALUE! is the only error returned by UDFs that have error in them. You don't get the luxury of run-time errors in UDFs so the only way to investigate is to step through the code and see where it falls over.
The date I was testing was 12/30/18 which has a Serial No of 43464. You defined day As Integer which must be from -32,768 to 32,767. So the underlying error is Overflow because 43464 is outside the allowable values for Integer.
2nd. Iteration
Changing Integer to Long overcomes that error but then
If day >= start_date Or day <= start_date + 7 Then
PeriodNum = first_week
Is always going to return 1 for serial_num >= start_date or "error" for serial_num < start_date. The second part of your test Or day <= start_date + 7 doesn't really matter because if it's FALSE, the other test is TRUE. I tested this by extending dates down to C381 to take them into 2020. So back to the drawing board!
3rd Iteration
Changing all the Or to And ensured the second part would be evaluated.
If day >= start_date And day <= start_date + 7 Then
PeriodNum = first_week
ElseIf day > start_date + 7 And day <= start_date + 14 Then
PeriodNum = second_week
ElseIf day > start_date + 14 And day <= start_date + 21 Then
PeriodNum = third_week
ElseIf day > start_date + 21 And day <= start_date + 28 Then
PeriodNum = fourth_week
Else
PeriodNum = "error"
End If
Back in Excel I notice that 1/6/19 which should be the 1st day of period 2 is showing as 1. So something is wrong with the test. Changing day <= start_date + 7 to day < start_date + 7 should fix it but it returns "error". This is because the subsequent tests don't allow for day = start_date + 7.
4th. iteration
If day >= start_date And day < start_date + 7 Then
PeriodNum = first_week
ElseIf day >= start_date + 7 And day < start_date + 14 Then
PeriodNum = second_week
ElseIf day >= start_date + 14 And day < start_date + 21 Then
PeriodNum = third_week
ElseIf day >= start_date + 21 And day < start_date + 28 Then
PeriodNum = fourth_week
Else
PeriodNum = "error"
End If
Back to Excel and it looks good until scrolling down. 1/27/19 and everything below returns "error". day obviously can't be more than start_date + 28.
5th. iteration
Change day = serial_num to day = (serial_num - start_date) Mod 28 and the tests to look at that number in relation to 0, 7, 14, 21 and 28.
day = (serial_num - start_date) Mod 28
If day >= 0 And day < 7 Then
PeriodNum = first_week
ElseIf day >= 7 And day < 14 Then
PeriodNum = second_week
ElseIf day >= 14 And day < 21 Then
PeriodNum = third_week
ElseIf day >= 21 And day < 28 Then
PeriodNum = fourth_week
Else
PeriodNum = "error"
End If
Back in Excel and it all looks good.
Final iteration - polish it
There are still some improvements:
number_periods As Integer isn't used so delete it
the function PeriodNum name is deceptive. It isn't returning a period but a week in a period soWeekInPeriod`
It isn't good practice to use reserved words or VBA functions as variable names - -
day is a VBA function (returns the day in the month). I changed it to DayInPeriod28. Descriptive variable names are easier for the user to parse.
I also changed serial_num to MyDate.
Instead of nested Ifs, Select Case is more compact.
Don't declare and define variable you are only going to use once. It's a waste of space and time.
Function WeekInPeriod(MyDate As Date, start_date As Date) As Variant
Dim DayInPeriod28 As Long
DayInPeriod28 = (MyDate - start_date) Mod 28
Select Case DayInPeriod28
Case Is < 7
WeekInPeriod = 1
Case Is < 14
WeekInPeriod = 2
Case Is < 21
WeekInPeriod = 3
Case Is < 28
WeekInPeriod = 4
Case Else
WeekInPeriod = "error"
End Select
End Function
You could just calculate the week:
LngPeriod = int((day - start_date +1)/7)
If lngPeriod >4 then
PeriodNum = “error”
Else
PeriodNum = lngPeriod
End if
Where The PeriodNum function returns a variant as suggested.

Resources