Importing Time Entries and Projects from a Toggl Export
Please follow the steps below “Exporting time entries” on this Toggl knowledge bbase page to generate a Timing-compatible report, then run the script below.
Simply copy the following script(s) into a new "Script Editor" document and press run.
After pasting them, consider saving the scripts to disk for future re-use. You could even save them to iCloud Drive (or e.g. Dropbox or Google Drive) to have them available on all your Macs.
Feel free to customize them for your use cases; if you need details on how a particular method works, please refer
to our AppleScript reference.
Note: We do not take responsibility for any data loss incurred by running these scripts.
Make sure to back up your data (e.g. by copying the directories mentioned here to a different location) before running these scripts.
-- Copyright (c) 2024 Timing Software GmbH. All rights reserved. -- This script is licensed only to extend the functionality of Timing. Redistribution and any other uses are not allowed without prior permission from us. tell application "TimingHelper" if not advanced scripting support available then error "This script requires a Timing Connect subscription. Please contact support via https://timingapp.com/contact to upgrade." end if end tell -- AppleScript to import Toggl time entries (and the corresponding projects) into Timing. -- This script requires a CSV export from Toggl's "Detailed Report" screen. -- Please see https://support.toggl.com/detailed-reports-toggl-new/#export on how to generate that report. set csvFile to ((choose file with prompt "Please select a Toggl Export to process:") as string) -- If you want to specify a file path directly, uncomment this line: --set csvFile to ":Users:daniel:Documents:AppleScript:Timing:Toggl_time_entries_2017-06-05_to_2017-06-11.csv" -- Start CSV to list script by Nigel Garvey. -- Source: http://macscripter.net/viewtopic.php?pid=125444#p125444 (* Assumes that the CSV text adheres to the convention: Records are delimited by LFs or CRLFs (but CRs are also allowed here). The last record in the text may or may not be followed by an LF or CRLF (or CR). Fields in the same record are separated by commas (unless specified differently by parameter). The last field in a record must not be followed by a comma. Trailing or leading spaces in unquoted fields are not ignored (unless so specified by parameter). Fields containing quoted text are quoted in their entirety, any space outside them being ignored. Fields enclosed in double-quotes are to be taken verbatim, except for any included double-quote pairs, which are to be translated as double-quote characters. No other variations are currently supported. *) on csvToList(csvText, implementation) -- The 'implementation' parameter must be a record. Leave it empty ({}) for the default assumptions: ie. comma separator, leading and trailing spaces in unquoted fields not to be trimmed. Otherwise it can have a 'separator' property with a text value (eg. {separator:tab}) and/or a 'trimming' property with a boolean value ({trimming:true}). set {separator:separator, trimming:trimming} to (implementation & {separator:",", trimming:false}) script o -- Lists for fast access. property qdti : getTextItems(csvText, "\"") property currentRecord : {} property possibleFields : missing value property recordList : {} end script -- o's qdti is a list of the CSV's text items, as delimited by double-quotes. -- Assuming the convention mentioned above, the number of items is always odd. -- Even-numbered items (if any) are quoted field values and don't need parsing. -- Odd-numbered items are everything else. Empty strings in odd-numbered slots -- (except at the beginning and end) indicate escaped quotes in quoted fields. set astid to AppleScript's text item delimiters set qdtiCount to (count o's qdti) set quoteInProgress to false considering case repeat with i from 1 to qdtiCount by 2 -- Parse odd-numbered items only. set thisBit to item i of o's qdti if ((count thisBit) > 0) or (i is qdtiCount) then -- This is either a non-empty string or the last item in the list, so it doesn't -- represent a quoted quote. Check if we've just been dealing with any. if (quoteInProgress) then -- All the parts of a quoted field containing quoted quotes have now been -- passed over. Coerce them together using a quote delimiter. set AppleScript's text item delimiters to "\"" set thisField to (items a thru (i - 1) of o's qdti) as string -- Replace the reconstituted quoted quotes with literal quotes. set AppleScript's text item delimiters to "\"\"" set thisField to thisField's text items set AppleScript's text item delimiters to "\"" -- Store the field in the "current record" list and cancel the "quote in progress" flag. set end of o's currentRecord to thisField as string set quoteInProgress to false else if (i > 1) then -- The preceding, even-numbered item is a complete quoted field. Store it. set end of o's currentRecord to item (i - 1) of o's qdti end if -- Now parse this item's field-separator-delimited text items, which are either non-quoted fields or stumps from the removal of quoted fields. Any that contain line breaks must be further split to end one record and start another. These could include multiple single-field records without field separators. set o's possibleFields to getTextItems(thisBit, separator) set possibleFieldCount to (count o's possibleFields) repeat with j from 1 to possibleFieldCount set thisField to item j of o's possibleFields if ((count thisField each paragraph) > 1) then -- This "field" contains one or more line endings. Split it at those points. set theseFields to thisField's paragraphs -- With each of these end-of-record fields except the last, complete the field list for the current record and initialise another. Omit the first "field" if it's just the stub from a preceding quoted field. repeat with k from 1 to (count theseFields) - 1 set thisField to item k of theseFields if ((k > 1) or (j > 1) or (i is 1) or ((count trim(thisField, true)) > 0)) then set end of o's currentRecord to trim(thisField, trimming) set end of o's recordList to o's currentRecord set o's currentRecord to {} end repeat -- With the last end-of-record "field", just complete the current field list if the field's not the stub from a following quoted field. set thisField to end of theseFields if ((j < possibleFieldCount) or ((count thisField) > 0)) then set end of o's currentRecord to trim(thisField, trimming) else -- This is a "field" not containing a line break. Insert it into the current field list if it's not just a stub from a preceding or following quoted field. if (((j > 1) and ((j < possibleFieldCount) or (i is qdtiCount))) or ((j is 1) and (i is 1)) or ((count trim(thisField, true)) > 0)) then set end of o's currentRecord to trim(thisField, trimming) end if end repeat -- Otherwise, this item IS an empty text representing a quoted quote. else if (quoteInProgress) then -- It's another quote in a field already identified as having one. Do nothing for now. else if (i > 1) then -- It's the first quoted quote in a quoted field. Note the index of the -- preceding even-numbered item (the first part of the field) and flag "quote in -- progress" so that the repeat idles past the remaining part(s) of the field. set a to i - 1 set quoteInProgress to true end if end repeat end considering -- At the end of the repeat, store any remaining "current record". if (o's currentRecord is not {}) then set end of o's recordList to o's currentRecord set AppleScript's text item delimiters to astid return o's recordList end csvToList -- Get the possibly more than 4000 text items from a text. on getTextItems(txt, delim) set astid to AppleScript's text item delimiters set AppleScript's text item delimiters to delim set tiCount to (count txt's text items) set textItems to {} repeat with i from 1 to tiCount by 4000 set j to i + 3999 if (j > tiCount) then set j to tiCount set textItems to textItems & text items i thru j of txt end repeat set AppleScript's text item delimiters to astid return textItems end getTextItems -- Trim any leading or trailing spaces from a string. on trim(txt, trimming) if (trimming) then repeat with i from 1 to (count txt) - 1 if (txt begins with space) then set txt to text 2 thru -1 of txt else exit repeat end if end repeat repeat with i from 1 to (count txt) - 1 if (txt ends with space) then set txt to text 1 thru -2 of txt else exit repeat end if end repeat if (txt is space) then set txt to "" end if return txt end trim -- End CSV to list script. -- Start string to date script by Richard Hyde. -- Source: https://gist.github.com/RichardHyde/3386ac57b55455b71140 to convertDate(textDate) set resultDate to the current date set the year of resultDate to (text 1 thru 4 of textDate) set the month of resultDate to (text 6 thru 7 of textDate) set the day of resultDate to (text 9 thru 10 of textDate) set the time of resultDate to 0 if (length of textDate) > 10 then set the hours of resultDate to (text 12 thru 13 of textDate) set the minutes of resultDate to (text 15 thru 16 of textDate) if (length of textDate) > 16 then set the seconds of resultDate to (text 18 thru 19 of textDate) end if end if return resultDate end convertDate -- End string to date script. set alertResult to display alert "Please back up your data!" message "Before proceeding, please make sure to back up your Timing database.\n\nSee https://timingapp.com/help/faq#data on which folders you need to back up." buttons {"Cancel", "I Have A Backup"} if button returned of alertResult is "Cancel" then error number -128 end if on getOrCreateProject(projectName, parentProject) if projectName = "" then return missing value end if tell application "TimingHelper" if parentProject is missing value then set results to (projects whose name is projectName) if results = {} then return (create project name projectName) else return (first item of results) end if else set results to (projects of parentProject whose name is projectName) if results = {} then return (create project name projectName parent project parentProject) else return (first item of results) end if end if end tell end getOrCreateProject set csvData to csvToList(read file csvFile as «class utf8», {}) set csvData to items 2 thru -1 of csvData repeat with row in csvData set client to item 3 of row set project to item 4 of row set task to item 5 of row set itemDescription to item 6 of row set startDay to item 8 of row set startTime to item 9 of row set endDay to item 10 of row set endTime to item 11 of row set startDate to convertDate(startDay & " " & startTime) set endDate to convertDate(endDay & " " & endTime) set timingProject to getOrCreateProject(project, getOrCreateProject(client, missing value)) set taskDescription to "" if task is not equal to "" and itemDescription is not equal to "" then set taskDescription to task & " " & itemDescription else if task is not equal to "" then set taskDescription to task else if itemDescription is not equal to "" then set taskDescription to itemDescription end if if startDate is greater than endDate then -- swap start and end date if neeeded set tmp to startDate set startDate to endDate set endDate to tmp end if if endDate is greater than startDate then -- ignore zero-duration time entries tell application "TimingHelper" add time entry from startDate to endDate with title taskDescription project timingProject end tell end if end repeat