From 08358ee775e4542127d73cf615115b41ca07e485 Mon Sep 17 00:00:00 2001 From: Kevin Kaland Date: Fri, 12 Sep 2014 16:19:00 +0200 Subject: [PATCH] Support partial string matching. --- .meteor/packages | 1 + .meteor/versions | 1 + README.md | 30 ++++++++++++++---------- todoist-sorter.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 77 insertions(+), 13 deletions(-) diff --git a/.meteor/packages b/.meteor/packages index e8ea33a..1357200 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -15,4 +15,5 @@ ejson wizonesolutions:todoist http meteorhacks:async +wizonesolutions:underscore-string diff --git a/.meteor/versions b/.meteor/versions index 939b7d9..bd240e0 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -26,3 +26,4 @@ retry@1.0.0 tracker@1.0.2 underscore@1.0.0 wizonesolutions:todoist@1.0.1 +wizonesolutions:underscore-string@1.0.0 diff --git a/README.md b/README.md index 4dfab68..caba5b4 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,23 @@ breaks your train of thought if you process your tasks GTD style. **This app fixes that.** +## Usage + When you create tasks in Todoist, simply add `##` (e.g. `##work`) anywhere in your task name (much like you would use `@label`s). +You can also use partial project names, such as `##w`. That will match `work` +as long as it is the shortest project name beginning with `w`. You can use partial +names from anywhere in the project name, so `##proc` will match `work/process-improvement`. + +You cannot use spaces, and matches are still case-sensitive (it's on the roadmap to make them case-insensitive). + + Every 5 minutes (by default), this app will use the Todoist API to move those tasks into the desired projects. You can also stop and start the app to do it immediately. -# Installation (command line required) +## Installation (command line required) It is written in [Meteor](http://meteor.com), so you have to install that to use it. It's super-easy. Just copy and paste one line. @@ -21,7 +30,7 @@ use it. It's super-easy. Just copy and paste one line. Then clone this app from GitHub and run it with `meteor --settings=settings.json` AFTER you configure it.. -# Configuration +## Configuration Copy `settings.json.example` to `settings.json` and replace the parameters with your Todoist credentials and desired update frequency (you can actually delete @@ -29,7 +38,7 @@ the line with update frequency if you want; the default is 5 minutes). There are a few API calls each time, so I wouldn't update too often or Todoist might think it's abuse. I haven't had any issues with a 5-minute interval so far. -## Privacy/security +### Privacy/security These are used by the app to talk directly to the Todoist API and are not sent anywhere else. I store the Todoist token in a local @@ -37,11 +46,11 @@ Mongo database using Meteor's APIs so that you don't have to log in for every check (your credentials are transferred with HTTPS, but this minimizes how often they have to be). -# Running +## Running `meteor --settings=settings.json` -# Reporting bugs, requesting features, asking questions +## Reporting bugs, requesting features, asking questions First see known issues below. @@ -50,11 +59,11 @@ Use the [issue list](https://github.com/wizonesolutions/todoist-sorter/issues) o If there is no GitHub issue for the known issue, you are welcome to open one. -# Stopping +## Stopping You can stop Meteor apps by pressing `Ctrl + C`. -# Known issues +## Known issues - Won't work if the project name has spaces. - I don't know if Todoist login tokens expire. If they do, you'll start getting @@ -63,17 +72,14 @@ again. This is safe since the only thing that is stored is your user info from Todoist (by the [todoist](https://github.com/wizonesolutions/meteor-todoist) package). -# Roadmap +## Roadmap - Support spaces in project name, maybe with dashes or something like `#project# instead of ##project?` -- Support partial project name matching, e.g. `#impo` instead of -`##work/important-project`, assuming that `work/important-project` is the only -project with `impo` in its name. - Case-insensitive project name matching. - Maybe label tasks that are moved by this app (for premium users)? -# Author +## Author This Meteor package was written by [WizOne Solutions](http://www.wizonesolutions.com), a Meteor and Drupal CMS developer. diff --git a/todoist-sorter.js b/todoist-sorter.js index f090170..a299779 100644 --- a/todoist-sorter.js +++ b/todoist-sorter.js @@ -63,6 +63,10 @@ checkForNewTasks = function () { data2 = projectsRes.result; } + // @todo: Lowercase all names in data2 so the later findWhere calls can match + // case-insensitively. Store todoistSorterOriginalName with the original case for when + // we display messages later. + // What word actually matched? projectName = matches[2]; console.log("Project name: " + projectName) @@ -70,10 +74,62 @@ checkForNewTasks = function () { // OK great, now try to find a project with this name. var project = _.findWhere(data2, { name: projectName }); + if (_.isEmpty(project)) { + // See if we have a partial match with exactly one project. + var projectNames = _.pluck(data2, 'name'); + var matchingProjects = _.filter(projectNames, function (value) { + return _s.include(value, projectName); + }); + + console.log('Might mean: '); + console.log(matchingProjects); + + if (matchingProjects.length == 1) { + project = _.findWhere(data2, { name: _.first(matchingProjects) }); + } + else { + console.log('Using closest match...'); + // Compile array of Levenshtein distances + var matchDistances = []; + _.each(matchingProjects, function (match) { + matchDistances.push({ name: match, distance: _.levenshtein(projectName, match), length: match.length }); + }); + + // console.log(matchDistances); + + // Use the name from the object having the lowest Levenshtein distance. + var sortedMatches = _.sortBy(matchDistances, 'distance'); + // console.log(sortedMatches); + var firstMatch = _.first(sortedMatches); + // console.log(firstMatch); + + // Are there other elements with the same distance? + var contenders = _.where(sortedMatches, { distance: firstMatch.distance }); + + if (contenders.length == 1) { + project = _.findWhere(data2, { name: firstMatch.name }); + } + else { + console.log("Still multiple matches, going with the one sorted higher in Todoist...") + // If we have multiple matches, go with the shorter name. + // If they are the same length, _.min() will return the first one. + // We'll sort by project name length to make sure. + var shortest = _.min(_.sortBy(contenders, 'length'), function (contender) { + return contender.length; + }); + + project = _.findWhere(data2, { name: shortest.name }); + } + + // Partial matching ain't easy. + } + } + if (project) { var projectId = project.id; - console.log("Project ID: " + projectId) + console.log("Matched project name: " + project.name); + // console.log("Project ID: " + projectId) // Finally, move the item to this project. Then we are done. var projectMapping = {};