How to calculate my work hours

-–
name: Daily-20230220-Mon
-–

• 9:30 am #starttime
• log 1
• log 2
• 12:30 pm #endtime
• break
• 1:30 pm #starttime
• log 3
• log 4
• 7:00 pm #endtime
• break
• 10:00 pm #starttime
• log 5
• log 6
• 1:00 am #starttime

How can I use dataview to calculate the hours work for the day.

The hours that bleed into the next day should also count.

Any recommendation on changing the tags, structure to make the query easier.

Here’s what I do, maybe there’s something useful here.

I track my time in a section in my daily note, in this format. I use Templater to insert the timestamps for me:

``````## 🕔 Time tracking
- 07:30-10:56 Some stuff
- 10:56-11:23 Another task, did some things
- 12:34-13:30 Third project
``````

Then I have a master time tracking note which sums them all up for me using Dataview, in hours-per-week/day:

``````const tracked = {}
dv.pages('"Daily/Notes"').file.lists
.where(x => x.section.subpath === "🕔 Time tracking").array()
.forEach(x => {
// Find the start/end times for each bullet point
const times = x.text.match(/^(\d{2}:\d{2})-(\d{2}:\d{2})/)
if (times) {
const start = moment(times[1], 'HH:mm')
const end = moment(times[2], 'HH:mm')
const minutes = moment.duration(end.diff(start)).asMinutes()
const date = x.path.match(/(\d{4}-\d{2}-\d{2})/)[1]
const week = moment(date).format('YYYY, [Week] WW')
if (!tracked[week]) tracked[week] = {}
if (tracked[week][date]) {
tracked[week][date].minutes += minutes
} else {
tracked[week][date] = {
path: x.path,
minutes: minutes
}
}
}
})

const hours = minutes => (minutes / 60).toFixed(1)

const table = []
Object.keys(tracked).sort((a, b) => b.localeCompare(a))
.forEach(weekDate => {
// Push weekly value
const week = tracked[weekDate]
const weekTime = Object.values(week).reduce((prev, curr) => prev + curr.minutes, 0)
table.push([weekDate, '**' + hours(weekTime) + '**'])

// Push daily values
Object.keys(week).sort((a, b) => b.localeCompare(a))
.forEach(date => {
const link = `– [[\${week[date].path}#🕔 Time tracking|\${moment(date).format('dddd D MMMM')}]]`
})
})

dv.table(['Date', 'Hours'], table)
``````

The summary table looks like this, with links back to each day:

1 Like

In your code, how would you change the code to use the current date and only display the days for the week?

How does the template to insert the timestamps look like?

In my case the time entries are in my daily Periodic Notes journals. The script looks like this:

``````    // Open the latest journal file
const latest = this.journal[0]
await this.goToFile(latest.file.path)
// Find the end of the time tracking section
const contents = await this.getContents(latest.file.path)
const match = contents.match(/^.*?\n## 🕔 Time tracking.*?\n---/s)
if (match) {
const lines = match[0].split('\n').slice(0, -2)
// Move cursor to end of time tracking section
await this.setEditMode(true)
await this.sleep(20)
this.view.editor.focus()
this.view.editor.setCursor({ line: lines.length - 1, ch: lines[lines.length - 1].length })
await this.sleep(50)
return moment().format('HH:mm') + '-'
}
return ''
``````

The template part is really just:

``````moment().format('HH:mm') + '-'
``````

It’s fascinating to see the different methods to access the files, and I’ve not seen a script actually switching modes and editing at this level before. It’s very educational for me. .

Is this part of a bigger setup since you also seem to have a part ending the previous section, and starting a new with the same time? Or is that different templates/commands?

Yes, this is referencing a couple of other files:

My journal class:

``````class main {
constructor() {
const tp = app.plugins.plugins['templater-obsidian'].templater.current_functions_object
tp.user._boilerplate(this)
// Journal is sorted by date descending, so most recent is index 0
this.journal = this.dv.pages('"Journal/Daily"').sort(x => x.file.path, 'desc').array()
}

async changeIndex(amount) {
let index = this.journal.findIndex(x => x.file.path === this.file.path)
index = Math.max(0, Math.min(index + amount, this.journal.length - 1))
await this.goToFile(this.journal[index].file.path)
}

async randomEntry() {
const random = this.journal[Math.floor(Math.random() * this.journal.length)]
await this.goToFile(random.file.path)
await this.setEditMode(false) // change to Preview mode
return ''
}

async goToLatestEntry() {
await this.goToFile(this.journal[0].file.path)
return ''
}

async previousEntry() {
await this.changeIndex(1)
return ''
}

async nextEntry() {
await this.changeIndex(-1)
return ''
}
}
module.exports = main
``````

And that file is referencing this general functions class:

``````function main(target) {
const dv = app.plugins.plugins.dataview.api
const tp = app.plugins.plugins['templater-obsidian'].templater.current_functions_object

target.app = app
target.dv = dv
target.tp = tp
target.page = dv.page(tp.file.path(true))
target.file = target.page.file
target.view = app.workspace.activeLeaf.view

target.sleep = async function (ms) {
await new Promise(resolve => setTimeout(resolve, ms))
}

/**
* Create a new file from a template, and return the link or open the file
* @param {string} templatePath
* @param {string} newNoteName
* @param {string} destinationFolder - Path to the destination folder. Defaults to current folder
* @param {boolean} openNewNote - Open the note in a new window, or return a link
* @returns
*/
target.createFromTemplate = async function (templatePath, newNoteName, destinationFolder, openNewNote) {
destinationFolder = destinationFolder || tp.file.folder(true)
await tp.file.create_new(tp.file.find_tfile(templatePath), newNoteName, openNewNote, app.vault.getAbstractFileByPath(destinationFolder))
return openNewNote ? '' : `[[\${newNoteName}]] `
}

target.setEditMode = function (canEdit) {
const curr = app.workspace.activeLeaf.getViewState()
curr.state.mode = canEdit ? 'source' : 'preview'
app.workspace.activeLeaf.setViewState(curr)
if (canEdit) {
target.view.editor?.focus()
}
}

/**
* Get the text contents of a file, specified by string path
* @param {string} path
*/
target.getContents = async function (path) {
const file = app.vault.getAbstractFileByPath(path)
}

target.goToFile = async function (path) {
const file = app.vault.getAbstractFileByPath(path)
if (path !== target.file.path) {
await app.workspace.getLeaf(false).openFile(file)
}
}
}
module.exports = main
``````

First, Thank you AlanG.

I made the following changes to only display the current week and handle roll-over time into the next day. (For those that work late hours)

``````const tracked = {}
dv.pages('"daily/2023"').file.lists
.where(x => x.section.subpath === "Worklog").array()
.forEach(x => {
// /(\d{4}\d{2}\d{2})/ - Works too
const date = x.path.match(/(\d{8})/)[1]
const week = moment(date).format('YYYY, [Week] WW')
if (week == moment().format('YYYY, [Week] WW')) {

// Find the start/end times for each bullet point
const times = x.text.match(/^(\d{2}:\d{2}) - (\d{2}:\d{2})/)
if (times) {
const start = moment(date + times[1], 'YYYYMMDDHH:mm')
var end = moment(date + times[2], 'YYYYMMDDHH:mm')
if ( start > end) {
end = moment(date + times[2], 'YYYYMMDDHH:mm').add(1, 'days')
}

const minutes = moment.duration(end.diff(start)).asMinutes()

// Setup the week
if (!tracked[week]) tracked[week] = {}

if (tracked[week][date]) {
tracked[week][date].minutes += minutes
} else {
tracked[week][date] = {
path: x.path,
minutes: minutes
}
}
}
}
})

const hours = minutes => (minutes / 60).toFixed(1)

const table = []
Object.keys(tracked).sort((a, b) => b.localeCompare(a))
.forEach(weekDate => {
// Push weekly value
const week = tracked[weekDate]
const weekTime = Object.values(week).reduce((prev, curr) => prev + curr.minutes, 0)
table.push([weekDate, '**' + hours(weekTime) + '**'])

// Push daily values
Object.keys(week).sort((a, b) => b.localeCompare(a))
.forEach(date => {
const link = `– [[\${week[date].path}#Worklog|\${moment(date).format('dddd, D MMMM')}]]`