Join us at FabCon Atlanta from March 16 - 20, 2026, for the ultimate Fabric, Power BI, AI and SQL community-led event. Save $200 with code FABCOMM.
Register now!The Power BI Data Visualization World Championships is back! Get ahead of the game and start preparing now! Learn more
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
Solved! Go to Solution.
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-...
Want faster answers? https://community.fabric.microsoft.com/t5/Desktop/How-to-Get-Your-Question-Answered-Quickly/m-p/1447...
@PowerBigginer I did something like this in DAX once if that helps at all: Net Work Duration (Working Hours) - Microsoft Fabric Community
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 error
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)
},
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
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
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.
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.
Hi @PowerBigginer ,
Can you please confirm whether the issue has been resolved. If you have any additional questions or concerns, please do not hesitate to contact us. We are here to support you and will be happy to help with any further assistance you may need.
Thank you.
@PowerBigginer I did something like this in DAX once if that helps at all: Net Work Duration (Working Hours) - Microsoft Fabric Community
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-...
Want faster answers? https://community.fabric.microsoft.com/t5/Desktop/How-to-Get-Your-Question-Answered-Quickly/m-p/1447...
@DataNinja777 Thanks for you reply
Getting same result as before
If you see screenshot ticket closed 5:37 which is non working hour. Result returning with error
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
fxCalculateWorkingHours
After 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
FinalTable
This 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,
The Power BI Data Visualization World Championships is back! Get ahead of the game and start preparing now!
| User | Count |
|---|---|
| 39 | |
| 37 | |
| 33 | |
| 32 | |
| 29 |
| User | Count |
|---|---|
| 133 | |
| 88 | |
| 85 | |
| 68 | |
| 64 |