Forum Discussion
Need help calculating working hours difference between two dates
Hello Team,
I’m currently facing challenges in Power Query while trying to calculate the difference in days between two dates, considering only working days, working hours, and excluding holidays.
Although I have a DAX-based solution that works as expected in terms of logic, it has performance issues. When attempting the same in Power Query, I'm encountering errors, especially for tickets created outside of working hours.
I would appreciate your help in resolving this.
Please find below my DAX and Power Query scripts for reference.
Thank you.
Dax
Days_Difference Exclude weekends =
VAR SubmittedDateTime = 'tbl_Service Request'[SR_CREATED]
VAR ResolvedDateTime = COALESCE( 'tbl_Service Request'[SR_CLOSE_DATE], NOW())
-- Define working hours
VAR WorkingStartTime = TIME(8, 0, 0) -- 8:00 AM
VAR WorkingEndTime = TIME(17, 0, 0) -- 5:00 PM
VAR WorkingHoursPerDay = 9
VAR StartHour = 8
VAR EndHour = 17
-- Adjust start time (if after working hours, move to next day)
VAR AdjustedStartTime =
IF(
TIME(HOUR(SubmittedDateTime), MINUTE(SubmittedDateTime), SECOND(SubmittedDateTime)) > WorkingEndTime,
DATEVALUE(SubmittedDateTime) + 1 + WorkingStartTime,
MAX(SubmittedDateTime, DATEVALUE(SubmittedDateTime) + WorkingStartTime)
)
-- Adjust end time (if after working hours, move to end of working day)
VAR AdjustedEndTime =
IF(
TIME(HOUR(ResolvedDateTime), MINUTE(ResolvedDateTime), SECOND(ResolvedDateTime)) > WorkingEndTime,
DATEVALUE(ResolvedDateTime) + WorkingEndTime,
ResolvedDateTime
)
-- Define start and end dates and times for calculations
VAR StartDate = DATE(YEAR(AdjustedStartTime), MONTH(AdjustedStartTime), DAY(AdjustedStartTime))
VAR StartTime = TIME(HOUR(AdjustedStartTime), MINUTE(AdjustedStartTime), SECOND(AdjustedStartTime))
VAR EndDate = DATE(YEAR(AdjustedEndTime), MONTH(AdjustedEndTime), DAY(AdjustedEndTime))
VAR EndTime = TIME(HOUR(AdjustedEndTime), MINUTE(AdjustedEndTime), SECOND(AdjustedEndTime))
-- If the ticket is within the same working day, calculate the difference based on start and end times
VAR SameDayWorkingHours =
IF(
StartDate = EndDate &&
RELATED('DimDate'[IsWorkingDay]) = TRUE(),
MAX(0, DATEDIFF(AdjustedStartTime, AdjustedEndTime, SECOND)) / 3600,
0
)
-- Calculate the number of seconds for the start day and end day if it's not the same day
VAR StartDaySeconds =
IF(
COUNTROWS(
FILTER(
'DimDate',
'DimDate'[CalendarDate] = StartDate &&
'DimDate'[IsWorkingDay] = TRUE()
)
) > 0,
MAX(0, DATEDIFF(AdjustedStartTime, DATEVALUE(AdjustedStartTime) + WorkingEndTime, SECOND)),
0
)
VAR EndDaySeconds =
IF(
COUNTROWS(
FILTER(
'DimDate',
'DimDate'[CalendarDate] = EndDate &&
'DimDate'[IsWorkingDay] = TRUE()
)
) > 0,
MAX(0, DATEDIFF(DATEVALUE(AdjustedEndTime) + WorkingStartTime, AdjustedEndTime, SECOND)),
0
)
-- Calculate the number of full working days between start and end (exclusive)
VAR FullWorkingDays =
COUNTROWS(
FILTER(
'DimDate',
'DimDate'[CalendarDate] > StartDate &&
'DimDate'[CalendarDate] < EndDate &&
'DimDate'[IsWorkingDay] = TRUE()
)
)
-- Total working hours from full days
VAR FullDaysHours = FullWorkingDays * WorkingHoursPerDay
-- Total working hours
VAR TotalWorkingHours =
IF(SameDayWorkingHours > 0, SameDayWorkingHours, (StartDaySeconds + EndDaySeconds) / 3600 + FullDaysHours)
RETURN
CEILING(TotalWorkingHours/WorkingHoursPerDay,1)
Power Query M Code
let
// Load data
Source = Sql.Database("RV2IVWPBI1P\RAYAMSSQLSERVER", "RayaOperationsData"),
SR = Source{[Schema="dbo", Item="Fact_Siebel_ServiceRequest"]}[Data],
// Remove unwanted columns
RemovedCols = Table.RemoveColumns(SR, {
"SR_TYPE", "SUB_AREA", "PRIORITY", "REQUESTOR_NAME_EN", "SEVERITY",
"SR_DUE_DATE", "TOTAL_TIME", "VEHICLE_MAKE", "VEHICLE_MODEL", "MODEL_YEAR",
"PENDING_PAYMENT_START", "PENDING_PAYMENT_END", "LAST_UPD", "dw_created", "dw_last_upd"
}),
ChangedTypes = Table.TransformColumnTypes(RemovedCols, {
{"SR_CREATED", type datetime},
{"SR_CLOSE_DATE", type datetime}
}),
// Define working hours
WorkingStart = #time(8, 0, 0),
WorkingEnd = #time(17, 0, 0),
// 🟨 Holiday List
HolidayList = {
#date(2025, 3, 30), #date(2025, 3, 31), #date(2025, 4, 1), #date(2025, 4, 2),
#date(2025, 6, 5), #date(2025, 6, 8), #date(2025, 6, 9), #date(2025, 6, 10)
},
// ✅ Adjust Created Date
AdjustedStart = Table.AddColumn(ChangedTypes, "AdjustedStart", each
let
dt = [SR_CREATED],
defaultTime = #time(8, 0, 0),
safeDT = if dt = null then DateTime.LocalNow() else dt,
t = Time.From(safeDT),
d = Date.From(safeDT),
dow = Date.DayOfWeek(d, Day.Sunday),
nextWorkDate =
List.First(
List.Select(
List.Dates(d + 1, 7, #duration(1,0,0,0)),
each Date.DayOfWeek(_, Day.Sunday) <= 4 and not List.Contains(HolidayList, _)
)
),
adjusted =
if dow > 4 or List.Contains(HolidayList, d) then
DateTime.From(nextWorkDate + defaultTime)
else if t < defaultTime then
DateTime.From(d + defaultTime)
else if t >= WorkingEnd then
DateTime.From(nextWorkDate + defaultTime)
else safeDT
in
adjusted
, type datetime),
// ✅ Adjust Closed Date
AdjustedEnd = Table.AddColumn(AdjustedStart, "AdjustedEnd", each
let
dt = if [SR_CLOSE_DATE] = null then DateTime.LocalNow() else [SR_CLOSE_DATE],
t = Time.From(dt),
d = Date.From(dt),
adjusted =
if t < WorkingStart then
DateTime.From(d + WorkingStart)
else if t > WorkingEnd then
DateTime.From(d + WorkingEnd)
else dt
in
adjusted
, type datetime),
// Date Range
AddDateRange = Table.AddColumn(AdjustedEnd, "DateRange", each
let
s = Date.From([AdjustedStart]),
e = Date.From([AdjustedEnd])
in
List.Dates(s, Duration.Days(e - s) + 1, #duration(1, 0, 0, 0))
, type list),
// Working Days (Sunday–Thursday, excluding holidays)
WorkingDates = Table.AddColumn(AddDateRange, "WorkingDates", each
List.Select([DateRange], each Date.DayOfWeek(_, Day.Sunday) <= 4 and not List.Contains(HolidayList, _))
, type list),
// Calculate Working Hours
WorkingHours = Table.AddColumn(WorkingDates, "WorkingHours", each
let
startDT = [AdjustedStart],
endDT = [AdjustedEnd],
startDate = Date.From(startDT),
endDate = Date.From(endDT),
dates = [WorkingDates],
hours = List.Sum(
List.Transform(dates, each
let
currentDate = _,
startTime = if currentDate = startDate then Time.From(startDT) else WorkingStart,
endTime = if currentDate = endDate then Time.From(endDT) else WorkingEnd,
validStart = if startTime < WorkingStart then WorkingStart else startTime,
validEnd = if endTime > WorkingEnd then WorkingEnd else endTime,
duration = Duration.TotalHours(validEnd - validStart)
in
if validEnd > validStart then duration else 0
)
)
in
if startDT = null or endDT = null then null else Number.Round(hours, 2)
, type number),
// Calculate SLA Working Days (9 hours per day rule)
SLA_WorkingDays = Table.AddColumn(WorkingHours, "SLA_WorkingDays", each
let
wh = [WorkingHours]
in
if wh = null then null
else if wh < 9 then 0
else Number.RoundUp(wh / 9)
, type number),
Cleanup = Table.RemoveColumns(SLA_WorkingDays, {"DateRange", "WorkingDates"})
in
Cleanup
The standard process is to use INTERSECT with appropriately sized buckets (hourly in your case, or by the minute if you need). That way you can create a "working hours/minutes" mask that you can overlay over the raw duration.
Please provide sample data that covers your issue or question completely, in a usable format (not as a screenshot).
Do not include sensitive information. Do not include anything that is unrelated to the issue or question.
Please show the expected outcome based on the sample data you provided.
Need help uploading data? https://community.fabric.microsoft.com/t5/Community-Blog/How-to-provide-sample-data-in-the-Power-BI-Forum/ba-p/963216
Want faster answers? https://community.fabric.microsoft.com/t5/Desktop/How-to-Get-Your-Question-Answered-Quickly/m-p/1447523PowerBigginer I did something like this in DAX once if that helps at all: Net Work Duration (Working Hours) - Microsoft Fabric Community
8 Replies
- DataNinja777Super User
Hi PowerBigginer ,
Your Power Query code is likely failing because the logic to adjust the start and end times doesn't correctly handle all scenarios, particularly when a ticket is created or closed on a non-working day (like a weekend or holiday) or outside the 8 AM to 5 PM window. The errors arise when the code cannot find a valid working day or time to "snap" to.
The most effective solution is to use a robust, self-contained custom function that can accurately calculate the duration. This approach fixes the errors and is much cleaner than adding multiple intermediate columns to your table. You can create a new blank query, name it fxCalculateWorkingHours, and paste the following M code into the Advanced Editor. This function encapsulates all the complex logic for adjusting dates and calculating hours.
( StartDateTime as nullable datetime, EndDateTime as nullable datetime, optional Holidays as list, optional StartHour as number, optional EndHour as number ) as nullable number => let // 1. Define Parameters & Handle Nulls HolidaysList = if Holidays = null then {} else Holidays, WorkingStart = #time(if StartHour = null then 8 else StartHour, 0, 0), WorkingEnd = #time(if EndHour = null then 17 else EndHour, 0, 0), WorkingHoursPerDay = Duration.TotalHours(WorkingEnd - WorkingStart), ActualStart = StartDateTime, ActualEnd = if EndDateTime = null then DateTime.LocalNow() else EndDateTime, // Exit if no start date or if end is before start BailOut = ActualStart = null or ActualEnd < ActualStart, // 2. Helper function to find the next valid start time fnAdjustStart = (dt as datetime) => let datePart = Date.From(dt), timePart = Time.From(dt), // Recursive check for the next working day FindNextWorkDate = (checkDate as date) => if Date.DayOfWeek(checkDate, Day.Sunday) > 4 or List.Contains(HolidaysList, checkDate) then @FindNextWorkDate(Date.AddDays(checkDate, 1)) else checkDate, // Determine the adjusted start AdjustedStart = let NextWorkDay = FindNextWorkDate(datePart) in if datePart < NextWorkDay then NextWorkDay + WorkingStart // Start was on a non-working day else if timePart >= WorkingEnd then FindNextWorkDate(Date.AddDays(datePart, 1)) + WorkingStart // Start was after hours else if timePart < WorkingStart then datePart + WorkingStart // Start was before hours else dt // Start was during work hours in AdjustedStart, // 3. Helper function to find the previous valid end time fnAdjustEnd = (dt as datetime) => let datePart = Date.From(dt), timePart = Time.From(dt), // Recursive check for the previous working day FindPrevWorkDate = (checkDate as date) => if Date.DayOfWeek(checkDate, Day.Sunday) > 4 or List.Contains(HolidaysList, checkDate) then @FindPrevWorkDate(Date.AddDays(checkDate, -1)) else checkDate, // Determine the adjusted end AdjustedEnd = let PrevWorkDay = FindPrevWorkDate(datePart) in if datePart > PrevWorkDay then PrevWorkDay + WorkingEnd // End was on a non-working day else if timePart < WorkingStart then FindPrevWorkDate(Date.AddDays(datePart, -1)) + WorkingEnd // End was before hours else if timePart > WorkingEnd then datePart + WorkingEnd // End was after hours else dt // End was during work hours in AdjustedEnd, // 4. Main Calculation TotalHours = if BailOut then 0 else let // Adjust the start and end times using the helper functions AdjStart = fnAdjustStart(ActualStart), AdjEnd = fnAdjustEnd(ActualEnd), // If adjusted end is before adjusted start, duration is zero hours = if AdjEnd <= AdjStart then 0 else let StartDate = Date.From(AdjStart), EndDate = Date.From(AdjEnd), // Generate list of relevant dates DateList = List.Dates(StartDate, Duration.Days(EndDate - StartDate) + 1, #duration(1,0,0,0)), // Filter for working days only (Sunday-Thursday, excluding holidays) WorkingDaysList = List.Select(DateList, each Date.DayOfWeek(_, Day.Sunday) <= 4 and not List.Contains(HolidaysList, _)), // Calculate hours for each working day in the list HoursList = List.Transform(WorkingDaysList, each let currentDate = _, dayStart = if currentDate = StartDate then Time.From(AdjStart) else WorkingStart, dayEnd = if currentDate = EndDate then Time.From(AdjEnd) else WorkingEnd in Duration.TotalHours(dayEnd - dayStart) ) in List.Sum(HoursList) in hours in TotalHours in fxCalculateWorkingHoursAfter creating the function, you can replace your existing table query with the much simpler script below. This code loads your data, defines your holidays, and then calls the fxCalculateWorkingHours function to add a column with the total working hours. A final step calculates the SLA_WorkingDays based on those hours, mirroring the logic from your DAX formula.
let // Load data Source = Sql.Database("RV2IVWPBI1P\RAYAMSSQLSERVER", "RayaOperationsData"), SR = Source{[Schema="dbo", Item="Fact_Siebel_ServiceRequest"]}[Data], // Change required column types ChangedTypes = Table.TransformColumnTypes(SR, { {"SR_CREATED", type datetime}, {"SR_CLOSE_DATE", type datetime} }), // Define your list of holidays HolidayList = { #date(2025, 3, 30), #date(2025, 3, 31), #date(2025, 4, 1), #date(2025, 4, 2), #date(2025, 6, 5), #date(2025, 6, 8), #date(2025, 6, 9), #date(2025, 6, 10) }, // Calculate Working Hours by invoking the custom function AddWorkingHours = Table.AddColumn(ChangedTypes, "WorkingHours", each fxCalculateWorkingHours( [SR_CREATED], [SR_CLOSE_DATE], HolidayList, 8, // Working Start Hour 17 // Working End Hour ), type number ), // Calculate SLA Days (This matches your DAX CEILING logic) AddSLADays = Table.AddColumn(AddWorkingHours, "SLA_WorkingDays", each let wh = [WorkingHours] in if wh = null or wh <= 0 then 0 else Number.RoundUp(wh / 9), type number ), // Final cleanup FinalTable = Table.SelectColumns(AddSLADays, {"SR_NUMBER", "SR_CREATED", "SR_CLOSE_DATE", "WorkingHours", "SLA_WorkingDays"}) in FinalTableThis approach significantly improves your query. The function provides robust date adjustments, correctly finding the nearest valid working time even across long holiday weekends. It successfully handles all edge cases, such as tickets created before, after, or during non-working periods. By encapsulating the complex logic, your main query becomes much simpler and more readable. Finally, the SLA calculation is correct, using Number.RoundUp to properly match the CEILING logic from your working DAX formula.
Best regards,
- PowerBigginerHelper II
DataNinja777 Thanks for you reply
Getting same result as beforeIf you see screenshot ticket closed 5:37 which is non working hour. Result returning with error
- PowerBigginerHelper II
I am creating day difference betweek created and closed the tickets
My requirement is
if ticket got created non working hours I should adjust the time to working hours
ex: if ticket create morning 7 it should adjust same day 8 am (my working hours are 8AM-5PM)
if any ticket created after 5PM should change to next day 8AM
To calculate accurate result
same if any ticket opend in weekends or holidays should refelct to next working day
Below is my Mcode which is not working for the above cases and result returning with errorlet
// Load data
Source = Sql.Database("RV2IVWPBI1P\RAYAMSSQLSERVER", "RayaOperationsData"),
SR = Source{[Schema="dbo", Item="Fact_Siebel_ServiceRequest"]}[Data],// Remove unwanted columns
RemovedCols = Table.RemoveColumns(SR, {
"SR_TYPE", "SUB_AREA", "PRIORITY", "REQUESTOR_NAME_EN", "SEVERITY",
"SR_DUE_DATE", "TOTAL_TIME", "VEHICLE_MAKE", "VEHICLE_MODEL", "MODEL_YEAR",
"PENDING_PAYMENT_START", "PENDING_PAYMENT_END", "LAST_UPD", "dw_created", "dw_last_upd"
}),ChangedTypes = Table.TransformColumnTypes(RemovedCols, {
{"SR_CREATED", type datetime},
{"SR_CLOSE_DATE", type datetime}
}),// Define working hours
WorkingStart = #time(8, 0, 0),
WorkingEnd = #time(17, 0, 0),// 🟨 Holiday List
HolidayList = {
#date(2025, 3, 30), #date(2025, 3, 31), #date(2025, 4, 1), #date(2025, 4, 2),
#date(2025, 6, 5), #date(2025, 6, 8), #date(2025, 6, 9), #date(2025, 6, 10)
},AdjustedStart = Table.AddColumn(ChangedTypes, "AdjustedStart", each
let
dt = [SR_CREATED],
defaultTime = #time(8, 0, 0),
workingEnd = #time(17, 0, 0),
safeDT = if dt = null then DateTime.LocalNow() else dt,
t = Time.From(safeDT),
d = Date.From(safeDT),
dow = Date.DayOfWeek(d, Day.Sunday),
isWeekend = dow = 5 or dow = 6, // Friday = 5, Saturday = 6 (Sunday = 0)
isHoliday = List.Contains(HolidayList, d),nextWorkingDate =
List.First(
List.Select(
List.Dates(d + 1, 7, #duration(1,0,0,0)),
each
let wd = Date.DayOfWeek(_, Day.Sunday)
in (wd <> 5 and wd <> 6) and not List.Contains(HolidayList, _)
)
),adjusted =
if isWeekend or isHoliday then
DateTime.From(nextWorkingDate & defaultTime)
else if t < defaultTime then
DateTime.From(d & defaultTime)
else if t >= workingEnd then
DateTime.From(nextWorkingDate & defaultTime)
else
safeDTin
adjusted,
type datetime
),// ✅ Adjust Closed Date
AdjustedEnd = Table.AddColumn(AdjustedStart, "AdjustedEnd", each
let
dt = if [SR_CLOSE_DATE] = null then DateTime.LocalNow() else [SR_CLOSE_DATE],
t = Time.From(dt),
d = Date.From(dt),
adjusted =
if t < WorkingStart then
DateTime.From(d + WorkingStart)
else if t > WorkingEnd then
DateTime.From(d + WorkingEnd)
else dt
in
adjusted
, type datetime),// Date Range
AddDateRange = Table.AddColumn(AdjustedEnd, "DateRange", each
let
s = Date.From([AdjustedStart]),
e = Date.From([AdjustedEnd])
in
List.Dates(s, Duration.Days(e - s) + 1, #duration(1, 0, 0, 0))
, type list),// Working Days (Sunday–Thursday, excluding holidays)
WorkingDates = Table.AddColumn(AddDateRange, "WorkingDates", each
List.Select([DateRange], each Date.DayOfWeek(_, Day.Sunday) <= 4 and not List.Contains(HolidayList, _))
, type list),// Calculate Working Hours
WorkingHours = Table.AddColumn(WorkingDates, "WorkingHours", each
let
startDT = [AdjustedStart],
endDT = [AdjustedEnd],
startDate = Date.From(startDT),
endDate = Date.From(endDT),
dates = [WorkingDates],
hours = List.Sum(
List.Transform(dates, each
let
currentDate = _,
startTime = if currentDate = startDate then Time.From(startDT) else WorkingStart,
endTime = if currentDate = endDate then Time.From(endDT) else WorkingEnd,
validStart = if startTime < WorkingStart then WorkingStart else startTime,
validEnd = if endTime > WorkingEnd then WorkingEnd else endTime,
duration = Duration.TotalHours(validEnd - validStart)
in
if validEnd > validStart then duration else 0
)
)
in
if startDT = null or endDT = null then null else Number.Round(hours, 2)
, type number),// Calculate SLA Working Days (9 hours per day rule)
SLA_WorkingDays = Table.AddColumn(WorkingHours, "SLA_WorkingDays", each
let
wh = [WorkingHours]
in
if wh = null then null
else if wh < 9 then 0
else Number.RoundUp(wh / 9)
, type number),Cleanup = Table.RemoveColumns(SLA_WorkingDays, {"DateRange", "WorkingDates"})
in
Cleanup- lbendlinSuper User
The standard process is to use INTERSECT with appropriately sized buckets (hourly in your case, or by the minute if you need). That way you can create a "working hours/minutes" mask that you can overlay over the raw duration.
Please provide sample data that covers your issue or question completely, in a usable format (not as a screenshot).
Do not include sensitive information. Do not include anything that is unrelated to the issue or question.
Please show the expected outcome based on the sample data you provided.
Need help uploading data? https://community.fabric.microsoft.com/t5/Community-Blog/How-to-provide-sample-data-in-the-Power-BI-Forum/ba-p/963216
Want faster answers? https://community.fabric.microsoft.com/t5/Desktop/How-to-Get-Your-Question-Answered-Quickly/m-p/1447523 - Greg_DecklerCommunity Champion
PowerBigginer I did something like this in DAX once if that helps at all: Net Work Duration (Working Hours) - Microsoft Fabric Community
- v-menakakotaCommunity Support
Hi PowerBigginer ,
Thanks for reaching out to the Microsoft fabric community forum.
I would also take a moment to thank Greg_Deckler , for actively participating in the community forum and for the solutions you’ve been sharing in the community forum. Your contributions make a real difference.
I hope the above details help you fix the issue. If you still have any questions or need more help, feel free to reach out. We’re always here to support you.
Thank you.- v-menakakotaCommunity Support
Hi PowerBigginer ,
I hope the above details help you fix the issue. If you still have any questions or need more help, feel free to reach out. We’re always here to support you.
Thank you.