Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

To celebrate FabCon Vienna, we are offering 50% off select exams. Ends October 3rd. Request your discount now.

Reply
GotManyProblems
Regular Visitor

Calculating duration of activity for full hours (even though entries are not for full hours)

Dear community,

 

I use Excel's PowerPivot plug-in and often found help by just reading this forum. Now it's time to ask my first question.

 

1) What I want to achieve:

Calculate productive minutes for each full hour. (see example at the bottom)

How could I do this in PowerPivot?

 

2) What data do I have?

My production team enters their production activities in a table. A timestamp is recorded. We don't work over night, only from 6am to 10pm. I cannot force the team to make an extra entry at the end of each full hour (15:00 // 16:00 // 17:00 etc), even though this would make my life much easier. 

 

Example Table of entries with timestamps:

Timestamp

Activity

15:00

Packaging

15:30

Break

16:30

Packaging

18:00

Shift End

 

Now I need to calculate for each full hour (e.g. from 15:00 till 16:00 // 16:00 till 17:00 etc), how much time was spent packaging and how much was break or other activities?

 

Desired Result for this specific example:

 

Packaging

Break

Other activities

15:00-16:00

30min 

(from 15:00-15:30)

30min

0min

16:00-17:00

30min

(from 16:30-17:00)

30min

0min

17:00-18:00

60min

0min

0min

 

Thanks a lot!

Marcel

4 REPLIES 4
GotManyProblems
Regular Visitor

Wow Pete, 

 

that is incredible! Thanks for making it work!

 

Is it possible to adjust the code, so that acitivities that go longer than an hour are also considered?

E.g.: 

 

TimestampActivity

15:00Packaging
15:30Break
15:49Toilet
16:01Packaging
16:42Smoke
17:17Toilet
17:22Packaging
19:00Shift End

 

And that would be the desired result for the last two hours

timeRangePackagingToiletSmoke
17:00-18:0038517
18:00-19:0060nullnull

 

Thanks again!

Marcel

Hi @GotManyProblems ,

 

Try the updated code below. It gives me the following ouput (see new Invoicing activity):

BA_Pete_0-1636363727358.png

 

let
    Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45WMjSyMjJV0lHyzCvLz0zOzEtXitUBippaGZgBRQMSk7MT05FEjQ2Aok5FqYnZMBETS6BISH5mTmoJRMjMysAQU6uZlYkRUDQ4Nz87FSJibmVojqbV3MrICFOrhZUByNbgjMy0EgXXvBSl2FgA", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [Timestamp = _t, Activity = _t]),
    chgTypes = Table.TransformColumnTypes(Source,{{"Timestamp", type time}, {"Activity", type text}}),
    sortChronological = Table.Sort(chgTypes,{{"Timestamp", Order.Ascending}}),
    addIndex1 = Table.AddIndexColumn(sortChronological, "Index1", 1, 1, Int64.Type),
    addIndex0 = Table.AddIndexColumn(addIndex1, "Index0", 0, 1, Int64.Type),
    mergeSelfOnIndex = Table.NestedJoin(addIndex0, {"Index1"}, addIndex0, {"Index0"}, "Added Index0", JoinKind.LeftOuter),
    fixMergeSorting = Table.Sort(mergeSelfOnIndex,{{"Index1", Order.Ascending}}),
    expandSelf = Table.ExpandTableColumn(fixMergeSorting, "Added Index0", {"Timestamp"}, {"endTime"}),
    filterShiftEnd = Table.SelectRows(expandSelf, each ([endTime] <> null)),
    remIndexes = Table.RemoveColumns(filterShiftEnd,{"Index1", "Index0"}),
    addListOfHours = Table.AddColumn(remIndexes, "hourSplit", each List.Transform({Time.Hour([Timestamp])..Time.Hour([endTime])}, each Time.From(Text.Combine({Text.From(_),"00"},":")))),
    expandListOfHours = Table.ExpandListColumn(addListOfHours, "hourSplit"),
    addActivityDurMins = Table.AddColumn(expandListOfHours, "activityDurMins", each if Time.Hour([hourSplit]) <> Time.Hour([Timestamp]) and Time.Hour([hourSplit]) <> Time.Hour([endTime]) then 60
else if [Timestamp] >= [hourSplit] and Time.Hour([Timestamp]) = Time.Hour([endTime]) then Duration.Minutes([endTime] -[Timestamp])
else if [Timestamp] >= [hourSplit] and Time.Hour([Timestamp]) < Time.Hour([endTime]) then Duration.Minutes([hourSplit] - [Timestamp]) + 60
else if [Timestamp] <= [hourSplit] then Duration.Minutes([endTime] -[hourSplit])
else null),
    filterZeroMins = Table.SelectRows(addActivityDurMins, each ([activityDurMins] <> 0)),
    addTimeRange = Table.AddColumn(filterZeroMins, "timeRange", each let
  timeFrom = Text.Combine({Text.From(Time.Hour([hourSplit])), "00"}, ":"),
  timeTo = Text.Combine({Text.From(Time.Hour([hourSplit]) + 1), "00"}, ":")
in
Text.Combine({timeFrom, timeTo}, "-")),
    remOthCols = Table.SelectColumns(addTimeRange,{"timeRange", "Activity", "activityDurMins"}),
    pivotActivities = Table.Pivot(remOthCols, List.Distinct(remOthCols[Activity]), "Activity", "activityDurMins", List.Sum)
in
    pivotActivities

 

Pete



Now accepting Kudos! If my post helped you, why not give it a thumbs-up?

Proud to be a Datanaut!




Hi Pete,

 

that's really impressive! Thank you so much and sorry for the delay in coming back to you.

May I make 2 more requests:

 

1st request:

It works really fine, but there's always a human factor... Sometimes they forget to make the last entry in the shift. E.g. for late shift (see below in the 39th line) we would miss 3 hours of production time, as there is no entry at 10pm and the next entry is from the next day:

 

ActivityActivityDateActivityTime

Verpackung am Arbeiten25/Okt/2119:03:34
Staff Change26/Okt/2106:21:49

The same can happen if we have an early shift, without a following late shift.


Is is possible to insert a "fake" line for the shift end at 2pm and 10pm, if this is the last line of this shift? I think this would need to be done before "addListOfHours".

 

I think to determine where to enter the 10pm shift-end, you can do something like this:

if "ActivityDate" of this line <> "ActivityDate" of the next line

Then add line with shiftend at 10pm

 

For the 2pm "fake" entry, maybe we could 

If "endTime" of next line is >2pm

Then add line with shiftend at 2pm

 

2nd request:

Is it possible to calculate the duration in real time? E.g. if it is now 10:05am and the last entry was at 8:30-packaging to calculate for 8:00-9:00 30mins packaging and for 9:00-10:00 60mins packaging? 

 

I adjusted your code so it fits to my datasource. Please find below how the data source looks like after step "remOthCol" and my complete code (excluding the SQL server's adress for safety reasons)

 

let
    Quelle = Sql.Database("xxx.com", "ProductionDB"),
    dbo_History = Quelle{[Schema="dbo",Item="History"]}[Data],
    #"Gefilterte Zeilen" = Table.SelectRows(dbo_History, each ([JobID] = 19118) and ([JobStepID] = 21)),
    #"Sortierte Zeilen" = Table.Sort(#"Gefilterte Zeilen",{{"JobID", Order.Ascending}, {"JobStepID", Order.Ascending}, {"ActivityDate", Order.Ascending}}),
    remOthCol = Table.SelectColumns(#"Sortierte Zeilen",{"Activity", "ActivityDate"}),
    #"Uhrzeit eingefügt" = Table.AddColumn(remOthCol, "ActivityTime", each DateTime.Time([ActivityDate]), type time),
    #"Extrahiertes Datum" = Table.TransformColumns(#"Uhrzeit eingefügt",{{"ActivityDate", DateTime.Date, type date}}),
    addIndex1 = Table.AddIndexColumn(#"Extrahiertes Datum", "Index1", 1, 1, Int64.Type),
    addIndex0 = Table.AddIndexColumn(addIndex1, "Index0", 0, 1, Int64.Type),
    mergeSelfOnIndex = Table.NestedJoin(addIndex0, {"Index1"}, addIndex0, {"Index0"}, "Added Index0", JoinKind.LeftOuter),
    fixMergeSorting = Table.Sort(mergeSelfOnIndex,{{"Index1", Order.Ascending}}),
    expandSelf = Table.ExpandTableColumn(fixMergeSorting, "Added Index0", {"ActivityTime", "ActivityDate"}, {"endTime","endDate"}),
    filterShiftEnd = Table.SelectRows(expandSelf, each ([endTime] <> null)),
    remIndexes = Table.RemoveColumns(filterShiftEnd,{"Index1", "Index0"}),
    addListOfHours = Table.AddColumn(remIndexes, "hourSplit", each List.Transform({Time.Hour([ActivityTime])..Time.Hour([endTime])}, each Time.From(Text.Combine({Text.From(_),"00"},":")))),
    expandListOfHours = Table.ExpandListColumn(addListOfHours, "hourSplit"),
    addActivityDurMins = Table.AddColumn(expandListOfHours, "activityDurMins", each if Time.Hour([hourSplit]) <> Time.Hour([ActivityTime]) and Time.Hour([hourSplit]) <> Time.Hour([endTime]) then 60
else if [ActivityTime] >= [hourSplit] and Time.Hour([ActivityTime]) = Time.Hour([endTime]) then Duration.Minutes([endTime] -[ActivityTime])
else if [ActivityTime] >= [hourSplit] and Time.Hour([ActivityTime]) < Time.Hour([endTime]) then Duration.Minutes([hourSplit] - [ActivityTime]) + 60
else if [ActivityTime] <= [hourSplit] then Duration.Minutes([endTime] -[hourSplit])
else null),
    filterZeroMins = Table.SelectRows(addActivityDurMins, each ([activityDurMins] <> 0)),
    addTimeRange = Table.AddColumn(filterZeroMins, "timeRange", each let
  timeFrom = Text.Combine({Text.From(Time.Hour([hourSplit])), "00"}, ":"),
  timeTo = Text.Combine({Text.From(Time.Hour([hourSplit]) + 1), "00"}, ":")
in
Text.Combine({timeFrom, timeTo}, "-")),
    remOthCols = Table.SelectColumns(addTimeRange,{"timeRange", "Activity", "activityDurMins"}),
    pivotActivities = Table.Pivot(remOthCols, List.Distinct(remOthCols[Activity]), "Activity", "activityDurMins", List.Sum)
in
    pivotActivities

 

ActivityActivityDateActivityTime

Job Setup21/Okt/2111:27:01
Batch Record verfügbar (BR in Produktion)21/Okt/2111:27:01
Arbeitsvorbereitung (Setup)22/Okt/2115:13:09
Staff Change22/Okt/2115:13:15
Arbeitsvorbereitung Pause22/Okt/2118:14:44
Warten auf GrL25/Okt/2106:22:09
Arbeitsvorbereitung (Setup)25/Okt/2107:02:36
Warten auf QC/QA25/Okt/2107:03:54
Warten auf QC/QA25/Okt/2107:03:59
Auftragsfreigabe (Freigabe QC)25/Okt/2107:18:54
Verpackung am Arbeiten25/Okt/2108:37:16
Staff Change25/Okt/2111:10:22
Staff Change25/Okt/2111:10:26
Verpackung unterbrochen - Pause/Schichtende25/Okt/2111:10:33
Verpackung am Arbeiten25/Okt/2114:01:34
Verpackung unterbrochen - Pause/Schichtende25/Okt/2114:01:41
Verpackung am Arbeiten25/Okt/2116:13:19
Verpackung unterbrochen - Pause/Schichtende25/Okt/2118:17:45
Verpackung am Arbeiten25/Okt/2119:03:34
Staff Change26/Okt/2106:21:49
Verpackung unterbrochen - Pause/Schichtende26/Okt/2106:22:02
Verpackung am Arbeiten26/Okt/2106:22:07
Verpackung unterbrochen - Pause/Schichtende26/Okt/2111:09:19
Verpackung am Arbeiten26/Okt/2111:58:55
Verpackung unterbrochen - Pause/Schichtende26/Okt/2113:51:35
Verpackung am Arbeiten26/Okt/2114:30:53
Warten auf Personal26/Okt/2115:31:05
Verpackung am Arbeiten26/Okt/2115:31:14
Verpackung unterbrochen - Pause/Schichtende26/Okt/2118:21:49
Verpackung am Arbeiten26/Okt/2119:04:16
Verpackung unterbrochen - Pause/Schichtende27/Okt/2106:21:47
Verpackung am Arbeiten27/Okt/2106:21:52
Staff Change27/Okt/2106:22:08
Verpackung unterbrochen - Pause/Schichtende27/Okt/2111:05:34
Verpackung am Arbeiten27/Okt/2111:54:47
Verpackung unterbrochen - Pause/Schichtende27/Okt/2113:49:52
Verpackung am Arbeiten27/Okt/2114:24:37
Verpackung unterbrochen - Pause/Schichtende27/Okt/2118:14:09
Staff Change28/Okt/2106:22:14
Verpackung am Arbeiten28/Okt/2106:22:18
Verpackung unterbrochen - Pause/Schichtende28/Okt/2111:01:42
Verpackung am Arbeiten28/Okt/2111:56:18
Verpackung unterbrochen - Pause/Schichtende28/Okt/2113:46:58
Warten auf Personal28/Okt/2114:30:45
Verpackung am Arbeiten28/Okt/2114:39:02
Verpackung unterbrochen - Pause/Schichtende28/Okt/2118:10:25
Verpackung am Arbeiten28/Okt/2119:00:45
Warten auf Personal28/Okt/2119:44:59
Verpackung am Arbeiten28/Okt/2119:47:48
Staff Change29/Okt/2106:22:50
Verpackung unterbrochen - Pause/Schichtende29/Okt/2106:23:07
Verpackung am Arbeiten29/Okt/2106:23:14
Verpackung unterbrochen - Pause/Schichtende29/Okt/2111:03:18
Verpackung am Arbeiten29/Okt/2111:53:31
Verpackung unterbrochen - Pause/Schichtende29/Okt/2113:51:31
Verpackung am Arbeiten29/Okt/2114:37:39
Verpackung unterbrochen - Pause/Schichtende29/Okt/2118:11:33
Verpackung am Arbeiten29/Okt/2118:57:14
Staff Change30/Okt/2106:22:18
Verpackung unterbrochen - Pause/Schichtende30/Okt/2109:59:24
Verpackung am Arbeiten30/Okt/2110:45:31
Warten auf Technik30/Okt/2111:10:13
Verpackung am Arbeiten30/Okt/2111:15:38
Verpackung unterbrochen - Pause/Schichtende30/Okt/2112:47:48
Staff Change01/Nov/2106:21:59
Verpackung am Arbeiten01/Nov/2106:22:04
Warten auf Druckerei01/Nov/2110:30:24
Staff Change01/Nov/2114:20:28
Verpackung am Arbeiten01/Nov/2115:56:18
Auftragsabschluss (Bilanzierung)01/Nov/2121:02:26
Kabine wird gereinigt01/Nov/2121:02:47
Kabine frei01/Nov/2121:51:01
Step Finished01/Nov/2121:51:01
BA_Pete
Super User
Super User

Hi @GotManyProblems ,

It's a bit of a beast, but the code below turns this:

BA_Pete_0-1636130436844.png

Into this:

BA_Pete_1-1636130457207.png

Create a new blank query in PQ then, in Advanced Editor, paste this over the default code. You will then be able to follow the steps I took:

 

let
    Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45WMjS1MjBQ0lEKSEzOTkzPzEtXitUBixqDRJ2KUhOzYSImlkCRkPzMnNQSiJCZlYEhplYzKxMjoGhwbn52KkTE3MrQHE2ruZWREaZWC4hbgjMy00oUXPNSlGJjAQ==", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [Timestamp = _t, Activity = _t]),
    chgTypes = Table.TransformColumnTypes(Source,{{"Timestamp", type time}, {"Activity", type text}}),
    sortChronological = Table.Sort(chgTypes,{{"Timestamp", Order.Ascending}}),
    addIndex1 = Table.AddIndexColumn(sortChronological, "Index1", 1, 1, Int64.Type),
    addIndex0 = Table.AddIndexColumn(addIndex1, "Index0", 0, 1, Int64.Type),
    mergeSelfOnIndex = Table.NestedJoin(addIndex0, {"Index1"}, addIndex0, {"Index0"}, "Added Index0", JoinKind.LeftOuter),
    fixMergeSorting = Table.Sort(mergeSelfOnIndex,{{"Index1", Order.Ascending}}),
    expandSelf = Table.ExpandTableColumn(fixMergeSorting, "Added Index0", {"Timestamp"}, {"endTime"}),
    filterShiftEnd = Table.SelectRows(expandSelf, each ([endTime] <> null)),
    remIndexes = Table.RemoveColumns(filterShiftEnd,{"Index1", "Index0"}),
    addListOfHours = Table.AddColumn(remIndexes, "hourSplit", each List.Transform({Time.Hour([Timestamp])..Time.Hour([endTime])}, each Time.From(Text.Combine({Text.From(_),"00"},":")))),
    expandListOfHours = Table.ExpandListColumn(addListOfHours, "hourSplit"),
    addActivityDurMins = Table.AddColumn(expandListOfHours, "activityDurMins", each if [Timestamp] >= [hourSplit] and Time.Hour([Timestamp]) = Time.Hour([endTime]) then Duration.Minutes([endTime] -[Timestamp])
else if [Timestamp] >= [hourSplit] and Time.Hour([Timestamp]) < Time.Hour([endTime]) then Duration.Minutes([hourSplit] - [Timestamp]) + 60
else if [Timestamp] <= [hourSplit] then Duration.Minutes([endTime] -[hourSplit])
else null),
    filterZeroMins = Table.SelectRows(addActivityDurMins, each ([activityDurMins] <> 0)),
    addTimeRange = Table.AddColumn(filterZeroMins, "timeRange", each let
  timeFrom = Text.Combine({Text.From(Time.Hour([hourSplit])), "00"}, ":"),
  timeTo = Text.Combine({Text.From(Time.Hour([hourSplit]) + 1), "00"}, ":")
in
Text.Combine({timeFrom, timeTo}, "-")),
    remOthCols = Table.SelectColumns(addTimeRange,{"timeRange", "Activity", "activityDurMins"}),
    pivotActivities = Table.Pivot(remOthCols, List.Distinct(remOthCols[Activity]), "Activity", "activityDurMins", List.Sum)
in
    pivotActivities

 

Summary:

1) Sort the data chronologically on [Timestamp] and add offset indexes.

2) Merge table on itself to get activity start and end time inline.

3) Create list of hours covered by activity to duplicate rows for activities that span before end of hour/after start of hour.

4) Calculate individual activity times splitting by time hour.

5) Calculate the time range in which the activity occurred.

6) Pivot activities with activity duration as aggregate (sum) field.

 

Pete



Now accepting Kudos! If my post helped you, why not give it a thumbs-up?

Proud to be a Datanaut!




Helpful resources

Announcements
September Power BI Update Carousel

Power BI Monthly Update - September 2025

Check out the September 2025 Power BI update to learn about new features.

Top Solution Authors
Top Kudoed Authors