"Syncing" Projects and Tasks via iCloud Drive

Timing now offers a proper built-in sync solution, so these scripts have been deprecated. For more information, see this article.

Simply copy one of the following script into a new "Script Editor" document, select "JavaScript" as the script's language, and press run (see this article for more detailed instructions).
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 the scripts for your use cases.

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.

How to use these Scripts

To transfer your projects and tasks from Mac A to Mac B, simply perform the following steps:

  1. Download our prepared versions of these scripts and extract them on both Macs.
  2. Double-click the export script on Mac A to open it in Script Editor. If this returns an "unidentified developer" error, instead right-click the script and click "Open".
  3. Click the 'Run' button in Script Editor.
  4. Wait for the files created by that script to be synced to Mac B via iCloud Drive.
  5. Run the import script on Mac B.

If you later also want to transfer projects and tasks back from Mac B to A, similarly run the export script on Mac B, then the import script on Mac A.
We recommend saving these scripts to iCloud Drive as well to make sure that they are available on all your Macs.

Also, note that these scripts only create or update Projects and Tasks.
They will not delete them. (Although creating a new task in the place of an existing one will overwrite the old one.)

Exporting All Timing Projects and Tasks in the Past 14 Days to iCloud Drive

This script will:

  1. Create a "Timing" directory in iCloud drive.
  2. Write a "TimingProjects.json" file containing your project hierarchy to that directory.
  3. Write a "TimingTasks.csv" file containing the last 14 days' tasks to that directory.
var endDate = new Date();
var startDate = new Date(endDate);
startDate.setDate(startDate.getDate() - 14 /* days */);

// Copyright (c) 2017 timingapp.com / Daniel Alm. 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.
var helper = Application("TimingHelper");
var app = Application.currentApplication();
app.includeStandardAdditions = true;

var projectHierarchyPath = "~/Library/Mobile Documents/com~apple~CloudDocs/Timing/TimingProjects.json";
var taskListPath = "~/Library/Mobile Documents/com~apple~CloudDocs/Timing/TimingTasks.csv";

$.NSFileManager.defaultManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError($(projectHierarchyPath).stringByDeletingLastPathComponent.stringByStandardizingPath, true, $(), $());

function enumerateProjects(projects) {
	return projects.map(function(project) { return { name: project.name(), color: project.color(), productivityRating: project.productivityRating(), ruleData: project.ruleData(), children: enumerateProjects(project.projects()) }; });
}

var fullProjectHierarchy = enumerateProjects(helper.rootProjects());

var str = $(JSON.stringify(fullProjectHierarchy, null, 4));
str.writeToFileAtomicallyEncodingError($(projectHierarchyPath).stringByStandardizingPath, true, $.NSUTF8StringEncoding, $());

$.NSFileManager.defaultManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError($(taskListPath).stringByDeletingLastPathComponent.stringByStandardizingPath, true, $(), $());

var reportSettings = helper.ReportSettings().make();
var exportSettings = helper.ExportSettings().make();

reportSettings.firstGroupingMode = "raw";
reportSettings.tasksIncluded = true;
reportSettings.appUsageIncluded = false;

exportSettings.fileFormat = "CSV";
exportSettings.durationFormat = "seconds";
exportSettings.shortEntriesIncluded = true;

var app = Application.currentApplication();
app.includeStandardAdditions = true;

helper.saveReport({ withReportSettings: reportSettings, exportSettings: exportSettings, between: startDate, and: endDate, to: Path($(taskListPath).stringByStandardizingPath.js) });

helper.delete(reportSettings);
helper.delete(exportSettings);

Importing Projects and Tasks from iCloud Drive into Timing

This script will:

  1. Read the "TimingProjects.json" file created with the script above and create new or update existing projects accordingly.
    The script will import project names, colors, productivity ratings, and rules. The only thing that is not imported is the order in which project rules are applied.
  2. Read the "TimingTasks.csv" file created with the script above and create new or update existing tasks accordingly.
var importRules = true;
var overwriteExistingRules = true;

// Copyright (c) 2017 timingapp.com / Daniel Alm. 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.
var helper = Application("TimingHelper");
var app = Application.currentApplication();
app.includeStandardAdditions = true;

app.displayDialog("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.");

var projectHierarchyPath = "~/Library/Mobile Documents/com~apple~CloudDocs/Timing/TimingProjects.json";
var taskListPath = "~/Library/Mobile Documents/com~apple~CloudDocs/Timing/TimingTasks.csv";

function importProjects(projects, parent) {
	var existingChildren = parent ? parent.projects() : helper.rootProjects()
	for (var project of projects) {
		if (!project.name) continue;
		
		var newProject = existingChildren.filter(function(childProject) { return childProject.name() == project.name; })[0];
		var args = { name: project.name };
		if (project.color) { args.color = project.color; }
		if (project.productivityRating) { args.productivityRating = project.productivityRating; }
		if (importRules
			&& project.ruleData
			&& (overwriteExistingRules || !newProject || !newProject.ruleData())) { args.ruleData = project.ruleData; }
		if (!newProject) {
			if (parent) {
				args.parentProject = parent;
			}
			newProject = helper.createProject(args);
		} else {
			helper.updateProject(newProject, args);
		}
		
		importProjects(project.children, newProject);
	}
}

var fullProjectHierarchy = $.NSString.stringWithContentsOfFileEncodingError(
               $(projectHierarchyPath).stringByStandardizingPath,
               $.NSUTF8StringEncoding,
               $()
          ).js;

importProjects(JSON.parse(fullProjectHierarchy), null);

// Minimal ES6 CSV parser.
// Taken from: https://lowrey.me/parsing-a-csv-file-in-es6-javascript/
var Csv = null;
Csv = class {
  parseLine(text) {
    const regex =
    /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g;
    let arr = [];
    text.replace(regex, (m0, m1, m2, m3) => {
      if (m1 !== undefined) {
        arr.push(m1.replace(/\\'/g, "'"));
      } else if (m2 !== undefined) {
        arr.push(m2.replace(/\\"/g, "\""));
      } else if (m3 !== undefined) {
        arr.push(m3);
      }
      return "";
    });
    if (/,\s*$/.test(text)) {
      arr.push("");
    }
    return arr;
  }

  zipObject(props, values) {
    return props.reduce((prev, prop, i) => {
      prev[prop] = values[i];
      return prev;
    }, {});
  }

  parse(csv) {
    let [properties, ...data] = csv.split("\n").map(this.parseLine);
    return data.map((line) => this.zipObject(properties, line))
  };

  serialize(obj) {
    let fields = Object.keys(obj[0]);
    let csv = obj.map(row => fields.map((fieldName) => JSON.stringify(row[fieldName] || "")));
    return [fields, ...csv].join("\n");
  }; 
}

function createProjectWithNameChainElements(projectNameChainElements) {
	var currentProject = null;
	var currentSearchSpace = helper.rootProjects();
	for (var projectName of projectNameChainElements) {
		if (!projectName) continue;
		
		var newProject = currentSearchSpace.filter(function(project) { return project.name() == projectName; })[0];
		if (!newProject) {
			var arguments = { name: projectName };
			if (currentProject) {
				arguments["parentProject"] = currentProject;
			}
			newProject = helper.createProject(arguments);
		}
		currentProject = newProject;
		currentSearchSpace = currentProject.projects();
	}
	return currentProject;
}

function createProjectWithNameChain(projectNameChain) {
	return createProjectWithNameChainElements(projectNameChain.split(" β–Έ "));
}

var csvLines = $.NSString.stringWithContentsOfFileEncodingError(
               $(taskListPath).stringByStandardizingPath,
               $.NSUTF8StringEncoding,
               $()
          ).js.split("\n");

csvLines.shift();
var csv = new Csv();
for (var csvLine of csvLines) {
	if (!csvLine) continue;
	
	var csvFields = csv.parseLine(csvLine);
	var taskStartDate = new Date(csvFields[2]);
	var taskEndDate = new Date(csvFields[3]);
	var taskProjectNameChain = csvFields[4];
	var taskDescription = csvFields[5];
	var taskNotes = csvFields[6];
	
	if (!taskStartDate || !taskEndDate || (taskEndDate.getTime() - taskStartDate.getTime()) < 1000) continue;
	
	var arguments = { from: taskStartDate, to: taskEndDate };
	if (taskDescription) arguments["withTitle"] = taskDescription;
	if (taskProjectNameChain) {
		var taskProject = createProjectWithNameChain(taskProjectNameChain);
		if (taskProject) arguments["project"] = taskProject;
	}
	if (taskNotes) arguments["notes"] = taskNotes;
	helper.addTask(arguments);
}