Created
October 7, 2010 04:08
-
-
Save gvvaughan/614540 to your computer and use it in GitHub Desktop.
TaskPaper Submit Timesheet.scpt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(* script version: 2010-10-08 | |
Changes: 2010-10-09 | |
- work in all locales, by coercing date strings to US or EU internal formats as necessary | |
*) | |
property ratePerHour : 51 | |
property roundMinutes : 15 | |
property theBoss : "Name of your Boss" | |
property theBossEmail : "[email protected]" | |
property theSender : "My Name" | |
property theSenderEmail : "[email protected]" | |
property theSignature : "Cheers, | |
-- | |
Generated by Submit Timesheet.scpt (v.2010-10-08)" | |
-- CONSTANTS -- | |
property firstDayOfTheWeek : "Mon" -- FIXME: isoWeek calculations currently hardcode Mon as day 1 | |
(* assuming the following format: | |
CLOCK: [YYYY-MM-DD ddd hh:mm]--[ZZZZ-NN-EE eee ii:nn] => hh:mm | |
*) | |
property clockString : "CLOCK: " | |
property d1Beg : 9 | |
property d1End : 18 | |
property d2Beg : 33 | |
property d2End : 42 | |
property hBeg : 58 | |
property hEnd : 62 | |
property defaultEndDateString : "" -- set to "yyyy-mm-dd" to override `current date' | |
on dateToDateObject(theDay, theMonth, theYear) | |
local maybe, dateString | |
try | |
set maybe to date ("31/12/2000" as string) | |
set dateString to (theDay as string) & "/" & (theMonth as string) | |
on error | |
set dateString to (theMonth as string) & "/" & (theDay as string) | |
end try | |
return date (dateString & "/" & (theYear as string)) | |
end dateToDateObject | |
-- calculate the date of the beginning of ISO week number 1 of the given year | |
on isoWeek1(year) | |
set jan4 to dateToDateObject(4, 1, year) | |
copy weekday of jan4 as integer to dayNumber | |
set dayNumber to dayNumber - 1 | |
if dayNumber is equal to 0 then | |
set dayNumber to 7 | |
end if | |
return jan4 + ((1 - dayNumber) * days) | |
end isoWeek1 | |
-- calculate the iso week number of theDate | |
-- returns a string of format "YYYY-Wnn" | |
on isoWeek(theDate) | |
set {year:isoYear} to theDate | |
if theDate ≥ dateToDateObject(29, 12, isoYear) then | |
set week1 to isoWeek1(isoYear + 1) | |
if theDate < week1 then | |
set week1 to isoWeek1(isoYear) | |
else | |
set isoYear to isoYear + 1 | |
end if | |
else | |
set week1 to isoWeek1(isoYear) | |
if theDate < week1 then | |
set isoYear to isoYear - 1 | |
set week1 to isoWeek1(isoYear) | |
end if | |
end if | |
return (isoYear as string) & "-W" & (1 + ((theDate - week1) div days div 7) as string) | |
end isoWeek | |
-- calculate the date of the first day of the first isoWeek of theMonth and theYear | |
on isoMonthBegin1(theMonth, theYear) | |
set day4 to dateToDateObject(4, theMonth, theYear) | |
copy weekday of day4 as integer to dayNumber | |
set dayNumber to dayNumber - 1 | |
if dayNumber is equal to 0 then | |
set dayNumber to 7 | |
end if | |
return day4 + ((1 - dayNumber) * days) | |
end isoMonthBegin1 | |
-- calculate the date of the first day of the first isoWeek of the month | |
-- containing |theDate| | |
on isoMonthBegin(theDate) | |
set firstDay to isoMonthBegin1(theDate's month, theDate's year) | |
if theDate < firstDay then | |
set theYear to theDate's year | |
set theMonth to (theDate's month as integer) - 1 | |
if theMonth < 1 then | |
set theYear to theYear - 1 | |
set theMonth to 12 | |
end if | |
set firstDay to isoMonthBegin1(theMonth, theYear) | |
end if | |
set nextDay to isoMonthBegin1((theDate's month as integer) + 1, theDate's year) | |
if theDate ≥ nextDay then | |
set firstDay to nextDay | |
end if | |
return firstDay | |
end isoMonthBegin | |
-- return a date object from a timestamp string in the format "YYYY-MM-DD" or | |
-- "YYYY-MM-DD ddd hh:mm" | |
on timestampToDate(theTimestamp) | |
set theDate to date ("01/01/1000" as string) -- just a placeholder; no need to call dateToDateObject | |
tell theTimestamp | |
set theDate's year to text 1 thru 4 | |
set theDate's month to text 6 thru 7 | |
set theDate's day to text 9 thru 10 | |
if length of theTimestamp > 10 then | |
set theDate's time to (text 16 thru 17) * hours + (text 19 thru 20) * minutes | |
end if | |
end tell | |
return theDate | |
end timestampToDate | |
-- return an integer counting the number of minutes in a string in the format "hh:mm" | |
on timestringToMinutes(theTimestring) | |
set numMinutes to 0 | |
tell theTimestring | |
if item 1 is not equal to " " then | |
set numMinutes to numMinutes + (600 * (item 1) as integer) | |
end if | |
set numMinutes to numMinutes + (60 * (item 2) as integer) + (10 * (item 4) as integer) + (item 5) as integer | |
end tell | |
return numMinutes | |
end timestringToMinutes | |
-- return a time string from a number of minutes | |
-- returns a string of format "hh:mm" | |
on minutesToTimeString(numMinutes) | |
if numMinutes > 6000 then | |
set timeString to numMinutes div 6000 as string | |
set numMinutes to numMinutes mod 6000 | |
else | |
set timeString to " " | |
end if | |
if numMinutes > 600 then | |
set timeString to timeString & numMinutes div 600 as string | |
set numMinutes to numMinutes mod 600 | |
else | |
set timeString to timeString & " " | |
end if | |
set timeString to timeString & (numMinutes div 60 as string) & "." | |
set numMinutes to numMinutes mod 60 | |
set timeString to timeString & (numMinutes div 6 as string) | |
set numMinutes to numMinutes mod 6 | |
set timeString to timeString & (numMinutes * 10 div 6 as string) | |
return timeString | |
end minutesToTimeString | |
-- return a string showing the payment due for |numMinutes| effort according to ratePerHour | |
-- returns a left-padded string of format "$xxx.xx" | |
on minutesToRateString(numMinutes) | |
set totalPay to 100 * numMinutes / 60 * ratePerHour as integer | |
set rateString to "$" & (totalPay div 100 as string) & "." & (totalPay mod 100 as string) | |
if totalPay mod 100 is equal to 0 then | |
set rateString to rateString & "0" | |
end if | |
repeat while length of rateString < 9 | |
set rateString to " " & rateString | |
end repeat | |
return rateString | |
end minutesToRateString | |
-- return a date string from a date object | |
on dateToDateString(theDate) | |
set {day:d, year:y} to theDate | |
-- Calculate the month number. | |
copy theDate to b | |
set b's month to January | |
set m to (b - 2500000 - theDate) div -2500000 | |
-- Date string in "yyyy-mm-dd" format. | |
tell (y * 10000 + m * 100 + d) as string | |
set dateString to text 1 thru 4 & "-" & text 5 thru 6 & "-" & text 7 thru 8 | |
end tell | |
return dateString | |
end dateToDateString | |
-- calculate the daily row leader of |theDate| | |
-- returns a string of format "ddd YYYY-Wnn [YYYY-MM-DD]" | |
on dailyRowLeader(theDate) | |
return (text 1 thru 3 of (theDate's weekday as string)) & " " & isoWeek(theDate) & " [" & my dateToDateString(theDate) & "]" | |
end dailyRowLeader | |
-- calculate the monthly row leader of |theDate| | |
-- returns a string of format "YYYY-Wnn [w/e YYYY-MM-DD]" | |
on monthlyRowLeader(theDate) | |
return isoWeek(theDate) & " [w/e " & my dateToDateString(theDate) & "]" | |
end monthlyRowLeader | |
-- return the number of complete days between two date objects | |
on deltaDays(beginDate, endDate) | |
return (endDate - beginDate) / days as integer | |
end deltaDays | |
-- after the minutes for each day have been totalled, they are rounded | |
-- according to this function | |
on dailyRounding(numMinutes) | |
return (numMinutes + 7) div 15 * 15 | |
end dailyRounding | |
-- select all the "CLOCK: " notes from TaskPaper, returning a list of records | |
-- by date with total minutes clocked on that date | |
on fetchClocksFromTaskPaper(beginDate, endDate) | |
-- create a list of zeros for the number of minutes in each day, | |
-- we can increment the minutes for a given day by indexing into | |
-- this list by the date offset from |begin| | |
set minutesList to {} | |
repeat my deltaDays(beginDate, endDate) times | |
set minutesList to minutesList & {0} | |
end repeat | |
tell front document of application "TaskPaper" | |
set clocks to search with query (clockString & " and type = note") | |
repeat with each in clocks | |
set thisClockString to text content of each | |
if entry type of each is note type and thisClockString begins with clockString and thisClockString contains "]--[" then | |
-- add the minutes from each clock entry to the appropriate entry in | |
-- |minutesList| as offset from |beginDate| in days | |
set thisDate to my timestampToDate(rich text d1Beg thru d1End of thisClockString) | |
if thisDate ≥ beginDate and thisDate < endDate then | |
set itemNo to 1 + (my deltaDays(beginDate, thisDate)) | |
set item itemNo of minutesList to (item itemNo of minutesList) + (my timestringToMinutes(rich text hBeg thru hEnd of thisClockString)) | |
end if | |
end if | |
end repeat | |
end tell | |
-- CLOCK: items are usually out of sequence, so we can't perform any | |
-- rounding until after all the minutes have been totalled (above) | |
repeat with itemNo from 1 to length of minutesList | |
set item itemNo of minutesList to my dailyRounding(item itemNo of minutesList) | |
end repeat | |
return {begin:beginDate, |minutesList|:minutesList} | |
end fetchClocksFromTaskPaper | |
(* For the purposes of the next few functions, we build and render "Tables": | |
A table is a stylized list as follows: | |
1. the first entry is a list with pairs of entries {width, "HEADER"} | |
2. the remaining entries are lists with one element per column in each list | |
3. the last entry is separated from the body, assumed to contain totals *) | |
-- returns a table of weekly data for the month beginning at |beginDate| using |minutesList|, | |
-- each entry in minutesList is assumed to be the number of minutes for 1 entire day starting | |
-- from |beginDate| | |
on buildMonthTable(beginDate, minutesList) | |
set numWeeks to (((length of minutesList) - 1) div 7) + 1 | |
set weeklyTable to {{25, "PERIOD", 6, "TIME", 9, "RATE"}} | |
set endOfLastWeek to 0 | |
set minutesThisMonth to 0 | |
repeat numWeeks times | |
set minutesThisWeek to 0 | |
set dayNo to 1 | |
repeat while dayNo ≤ 7 and endOfLastWeek + dayNo ≤ length of minutesList | |
set itemNo to endOfLastWeek + dayNo | |
set minutesThisWeek to minutesThisWeek + (item itemNo of minutesList) | |
set dayNo to dayNo + 1 | |
end repeat | |
set minutesThisMonth to minutesThisMonth + minutesThisWeek | |
set weekEndDate to beginDate + (endOfLastWeek * days) + (6 * days) | |
set thisWeek to {my monthlyRowLeader(weekEndDate), my minutesToTimeString(minutesThisWeek), my minutesToRateString(minutesThisWeek)} | |
set weeklyTable to weeklyTable & {thisWeek} | |
set endOfLastWeek to endOfLastWeek + 7 | |
end repeat | |
set totalTime to {" *Total time*", my minutesToTimeString(minutesThisMonth), my minutesToRateString(minutesThisMonth)} | |
set weeklyTable to weeklyTable & {totalTime} | |
return weeklyTable | |
end buildMonthTable | |
-- returns a table of daily data for the week beginning at |beginDate| using |minutesList|, | |
-- each entry in minutesList is assumed to be the number of minutes for 1 entire day starting | |
-- from |beginDate| | |
on buildWeekTable(beginDate, minutesList) | |
set dailyTotalsList to {{25, "DATE", 6, "TIME"}} | |
set minutesThisWeek to 0 | |
repeat with itemNo from 1 to length of minutesList | |
set thisDate to beginDate + (itemNo - 1) * days | |
set thisDay to {my dailyRowLeader(thisDate), my minutesToTimeString(item itemNo of minutesList)} | |
if item itemNo of minutesList > 0 then | |
set dailyTotalsList to dailyTotalsList & {thisDay} | |
set minutesThisWeek to minutesThisWeek + (item itemNo of minutesList) | |
end if | |
end repeat | |
set totalTime to {" *Total time*", my minutesToTimeString(minutesThisWeek)} | |
set dailyTotalsList to dailyTotalsList & {totalTime} | |
return dailyTotalsList | |
end buildWeekTable | |
on formatTableDivider(headerList, divideChar) | |
set tableString to "|" & divideChar | |
repeat with headerOffset from 0 to (length of headerList) div 2 - 1 | |
set theWidth to item (headerOffset * 2 + 1) of headerList | |
repeat theWidth times | |
set tableString to tableString & divideChar | |
end repeat | |
if headerOffset * 2 + 3 < length of headerList then | |
set tableString to tableString & divideChar & "+" & divideChar | |
else | |
set tableString to tableString & divideChar & "|" | |
end if | |
end repeat | |
return tableString | |
end formatTableDivider | |
on formatTableRow(rowEntries) | |
set tableString to "| " | |
repeat with itemNo from 1 to length of rowEntries | |
set tableString to tableString & item itemNo of rowEntries & " |" | |
if itemNo < length of rowEntries then | |
set tableString to tableString & " " | |
end if | |
end repeat | |
return tableString | |
end formatTableRow | |
on formatTable(theTable) | |
set tableString to "| " | |
set headerList to item 1 of theTable | |
repeat with headerOffset from 0 to (length of headerList) div 2 - 1 | |
set theWidth to item (headerOffset * 2 + 1) of headerList | |
set theHeader to item (headerOffset * 2 + 2) of headerList | |
repeat while (length of theHeader) + 2 ≤ theWidth | |
set theHeader to " " & theHeader & " " | |
end repeat | |
if length of theHeader < theWidth then | |
set theHeader to " " & theHeader | |
end if | |
set tableString to tableString & theHeader & " |" | |
if headerOffset * 2 + 3 < length of headerList then | |
set tableString to tableString & " " | |
end if | |
end repeat | |
set tableString to tableString & " | |
" & formatTableDivider(item 1 of theTable, "-") | |
repeat with rowNo from 2 to (length of theTable) - 1 | |
set tableString to tableString & " | |
" & formatTableRow(item rowNo of theTable) | |
end repeat | |
set tableString to tableString & " | |
" & formatTableDivider(item 1 of theTable, "-") | |
set tableString to tableString & " | |
" & formatTableRow(last item of theTable) | |
return tableString | |
end formatTable | |
-- Prompt for the final date to be processed | |
set defaultEndDateString to my dateToDateString(current date) | |
set endDateString to the text returned of (display dialog "End Date?" default answer defaultEndDateString) | |
set endDate to my timestampToDate(endDateString) | |
-- Calculate a nominal beginning of week date, 1 week earlier | |
set beginWeekDate to endDate - (7 * days) | |
-- Then, ensure we are starting the week on the correct day | |
repeat while text 1 thru 3 of (beginWeekDate's weekday as string) is not equal to firstDayOfTheWeek | |
set beginWeekDate to beginWeekDate + (1 * days) | |
end repeat | |
set beginMonthDate to (beginWeekDate) | |
set beginLastMonthDate to isoMonthBegin(beginMonthDate - days) | |
set lastMonthClocks to my fetchClocksFromTaskPaper(beginLastMonthDate, beginMonthDate) | |
set thisWeekClocks to my fetchClocksFromTaskPaper(beginWeekDate, endDate + days) | |
set thisMonthClocks to my fetchClocksFromTaskPaper(beginMonthDate, endDate + days) | |
set lastMonthTable to buildMonthTable(begin of lastMonthClocks, |minutesList| of lastMonthClocks) | |
set thisWeekTable to buildWeekTable(begin of thisWeekClocks, |minutesList| of thisWeekClocks) | |
set thisMonthTable to buildMonthTable(begin of thisMonthClocks, |minutesList| of thisMonthClocks) | |
set theContent to (("Hi " & (word 1 of theBoss) & ", | |
Here are the final hours for last month, pending payment: | |
* " & month of ((begin of lastMonthClocks) + 7 * days) as string) & " Timesheet Summary | |
" & formatTable(lastMonthTable) & " | |
And here are the hours I logged last week: | |
* Timesheet " & isoWeek(beginWeekDate) & " | |
" & formatTable(thisWeekTable) & " | |
And here is the running total of hours for this month: | |
* " & month of ((begin of thisMonthClocks) + 7 * days) as string) & " Timesheet Summary | |
" & formatTable(thisMonthTable) & " | |
" & theSignature | |
-- submit the timesheet | |
tell application "Mail" | |
-- "2010-W22 [w/e 2010-06-06]" | |
set theSubject to my isoWeek(endDate) & " [w/e " & endDateString & "]" | |
set theMessage to make new outgoing message with properties {visible:true, sender:theSender & " <" & theSenderEmail & ">", subject:theSubject, content:theContent} | |
tell theMessage | |
make new to recipient at end of to recipients with properties {name:theBoss, address:theBossEmail} | |
end tell | |
end tell | |
-- convert to plain text (doesn't seem to work tho') | |
tell application "Mail" to activate | |
tell application "System Events" to tell process "Mail" | |
delay 1 | |
keystroke "t" using {shift down, command down} | |
end tell | |
(* | |
Copyright (c) 2010 Gary V. Vaughan <[email protected]> | |
This script is free software: you can redistribute it and/or modify | |
it under the terms of the GNU General Public License as published by | |
the Free Software Foundation, either version 2 of the License, or | |
(at your option) any later version. | |
This script is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License for more details. | |
*) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Turns out the original assumed EU date formats in your system settings, which causes AppleScript to barf if you try to coerce an EU format date string to a date object. This version first tries to coerce an EU format date, and if that fails tries again with a US format date... so it should work in any locale now.