From fa374520988ba32a0737da9ca36d4b9077708087 Mon Sep 17 00:00:00 2001 From: Philipp Kewisch Date: Wed, 21 Feb 2018 13:33:24 +0100 Subject: [PATCH] Bug 1439900 - Move unifinder related functions to calUnifinderUtils.jsm. r=MakeMyDay MozReview-Commit-ID: Hi53o8AJlv2 --- calendar/.eslintrc.js | 1 + calendar/base/content/calendar-task-tree.xml | 13 +- calendar/base/content/calendar-unifinder.js | 9 +- calendar/base/modules/calUnifinderUtils.jsm | 206 ++++++++++++++++++ calendar/base/modules/calUtils.jsm | 207 +------------------ calendar/base/modules/calUtilsCompat.jsm | 7 + calendar/base/modules/moz.build | 1 + calendar/test/unit/head_consts.js | 44 +++- calendar/test/unit/test_unifinder_utils.js | 112 ++++++++++ calendar/test/unit/xpcshell-shared.ini | 1 + 10 files changed, 381 insertions(+), 220 deletions(-) create mode 100644 calendar/base/modules/calUnifinderUtils.jsm create mode 100644 calendar/test/unit/test_unifinder_utils.js diff --git a/calendar/.eslintrc.js b/calendar/.eslintrc.js index b1c5150606..57d505e94a 100644 --- a/calendar/.eslintrc.js +++ b/calendar/.eslintrc.js @@ -485,6 +485,7 @@ module.exports = { files: [ "base/modules/calEmailUtils.jsm", "base/modules/calItipUtils.jsm", + "base/modules/calUnifinderUtils.jsm", ], rules: { "require-jsdoc": [2, { require: { ClassDeclaration: true } }], diff --git a/calendar/base/content/calendar-task-tree.xml b/calendar/base/content/calendar-task-tree.xml index 9beeb8702c..56324fc666 100644 --- a/calendar/base/content/calendar-task-tree.xml +++ b/calendar/base/content/calendar-task-tree.xml @@ -989,18 +989,11 @@ { + let sortvalA = calunifinder.getItemSortKey(a, aSortKey); + let sortvalB = calunifinder.getItemSortKey(b, aSortKey); + return comparer(sortvalA, sortvalB, aModifier); + }); + } +}; + +/** + * Sort compare functions that can be used with Array sort(). The modifier can flip the sort + * direction by passing -1 or 1. + */ +const sortCompare = calunifinder.sortEntryComparer._sortCompare = { + /** + * Compare two things as if they were numbers. + * + * @param {*} a The first thing to compare + * @param {*} b The second thing to compare + * @param {Number} modifier -1 to flip direction, or 1 + * @return {Number} Either -1, 0, or 1 + */ + number: function(a, b, modifier=1) { + return sortCompare.general(Number(a), Number(b), modifier); + }, + + /** + * Compare two things as if they were dates. + * + * @param {*} a The first thing to compare + * @param {*} b The second thing to compare + * @param {Number} modifier -1 to flip direction, or 1 + * @return {Number} Either -1, 0, or 1 + */ + date: function(a, b, modifier=1) { + return sortCompare.general(a, b, modifier); + }, + + /** + * Compare two things generally, using the typical ((a > b) - (a < b)) + * + * @param {*} a The first thing to compare + * @param {*} b The second thing to compare + * @param {Number} modifier -1 to flip direction, or 1 + * @return {Number} Either -1, 0, or 1 + */ + general: function(a, b, modifier=1) { + return ((a > b) - (a < b)) * modifier; + }, + + /** + * Compare two dates, keeping the nativeTime zero date in mind. + * + * @param {*} a The first date to compare + * @param {*} b The second date to compare + * @param {Number} modifier -1 to flip direction, or 1 + * @return {Number} Either -1, 0, or 1 + */ + date_filled: function(a, b, modifier=1) { + const NULL_DATE = -62168601600000000; + + if (a == b) { + return 0; + } else if (a == NULL_DATE) { + return 1; + } else if (b == NULL_DATE) { + return -1; + } else { + return sortCompare.general(a, b, modifier); + } + }, + + /** + * Compare two strings, sorting empty values to the end by default + * + * @param {*} a The first string to compare + * @param {*} b The second string to compare + * @param {Number} modifier -1 to flip direction, or 1 + * @return {Number} Either -1, 0, or 1 + */ + string: function(a, b, modifier=1) { + if (a.length == 0 || b.length == 0) { + // sort empty values to end (so when users first sort by a + // column, they can see and find the desired values in that + // column without scrolling past all the empty values). + return -(a.length - b.length) * modifier; + } + + let collator = cal.createLocaleCollator(); + return collator.compareString(0, a, b) * modifier; + }, + + /** + * Catch-all function to compare two unknown values. Will return 0. + * + * @param {*} a The first thing to compare + * @param {*} b The second thing to compare + * @param {Number} modifier Provided for consistency, but unused + * @return {Number} Will always return 0 + */ + unknown: function(a, b, modifier=1) { + return 0; + } +}; diff --git a/calendar/base/modules/calUtils.jsm b/calendar/base/modules/calUtils.jsm index ca3eacadb1..49906ed7f5 100644 --- a/calendar/base/modules/calUtils.jsm +++ b/calendar/base/modules/calUtils.jsm @@ -50,6 +50,13 @@ var cal = { createRecurrenceInfo: _instance("@mozilla.org/calendar/recurrence-info;1", Components.interfaces.calIRecurrenceInfo, "item"), + + createLocaleCollator: function() { + return Components.classes["@mozilla.org/intl/collation-factory;1"] + .getService(Components.interfaces.nsICollationFactory) + .CreateCollation(); + }, + getCalendarManager: _service("@mozilla.org/calendar/manager;1", Components.interfaces.calICalendarManager), getIcsService: _service("@mozilla.org/calendar/ics-service;1", @@ -186,205 +193,6 @@ var cal = { return gCalThreadingEnabled; }, - // The below functions will move to some different place once the - // unifinder tress are consolidated. - - compareNativeTime: function(a, b) { - if (a < b) { - return -1; - } else if (a > b) { - return 1; - } else { - return 0; - } - }, - - compareNativeTimeFilledAsc: function(a, b) { - if (a == b) { - return 0; - } - - // In this filter, a zero time (not set) is always at the end. - if (a == -62168601600000000) { // value for (0000/00/00 00:00:00) - return 1; - } - if (b == -62168601600000000) { // value for (0000/00/00 00:00:00) - return -1; - } - - return (a < b ? -1 : 1); - }, - - compareNativeTimeFilledDesc: function(a, b) { - if (a == b) { - return 0; - } - - // In this filter, a zero time (not set) is always at the end. - if (a == -62168601600000000) { // value for (0000/00/00 00:00:00) - return 1; - } - if (b == -62168601600000000) { // value for (0000/00/00 00:00:00) - return -1; - } - - return (a < b ? 1 : -1); - }, - - compareNumber: function(a, b) { - a = Number(a); - b = Number(b); - if (a < b) { - return -1; - } else if (a > b) { - return 1; - } else { - return 0; - } - }, - - sortEntryComparer: function(sortType, modifier) { - switch (sortType) { - case "number": - return function(sortEntryA, sortEntryB) { - let nsA = cal.sortEntryKey(sortEntryA); - let nsB = cal.sortEntryKey(sortEntryB); - return cal.compareNumber(nsA, nsB) * modifier; - }; - case "date": - return function(sortEntryA, sortEntryB) { - let nsA = cal.sortEntryKey(sortEntryA); - let nsB = cal.sortEntryKey(sortEntryB); - return cal.compareNativeTime(nsA, nsB) * modifier; - }; - case "date_filled": - return function(sortEntryA, sortEntryB) { - let nsA = cal.sortEntryKey(sortEntryA); - let nsB = cal.sortEntryKey(sortEntryB); - if (modifier == 1) { - return cal.compareNativeTimeFilledAsc(nsA, nsB); - } else { - return cal.compareNativeTimeFilledDesc(nsA, nsB); - } - }; - case "string": - return function(sortEntryA, sortEntryB) { - let seA = cal.sortEntryKey(sortEntryA); - let seB = cal.sortEntryKey(sortEntryB); - if (seA.length == 0 || seB.length == 0) { - // sort empty values to end (so when users first sort by a - // column, they can see and find the desired values in that - // column without scrolling past all the empty values). - return -(seA.length - seB.length) * modifier; - } - let collator = cal.createLocaleCollator(); - let comparison = collator.compareString(0, seA, seB); - return comparison * modifier; - }; - default: - return function(sortEntryA, sortEntryB) { - return 0; - }; - } - }, - - getItemSortKey: function(aItem, aKey, aStartTime) { - function nativeTime(calDateTime) { - if (calDateTime == null) { - return -62168601600000000; // ns value for (0000/00/00 00:00:00) - } - return calDateTime.nativeTime; - } - - switch (aKey) { - case "priority": - return aItem.priority || 5; - - case "title": - return aItem.title || ""; - - case "entryDate": - return nativeTime(aItem.entryDate); - - case "startDate": - return nativeTime(aItem.startDate); - - case "dueDate": - return nativeTime(aItem.dueDate); - - case "endDate": - return nativeTime(aItem.endDate); - - case "completedDate": - return nativeTime(aItem.completedDate); - - case "percentComplete": - return aItem.percentComplete; - - case "categories": - return aItem.getCategories({}).join(", "); - - case "location": - return aItem.getProperty("LOCATION") || ""; - - case "status": - if (cal.item.isToDo(aItem)) { - return ["NEEDS-ACTION", "IN-PROCESS", "COMPLETED", "CANCELLED"].indexOf(aItem.status); - } else { - return ["TENTATIVE", "CONFIRMED", "CANCELLED"].indexOf(aItem.status); - } - case "calendar": - return aItem.calendar.name || ""; - - default: - return null; - } - }, - - getSortTypeForSortKey: function(aSortKey) { - switch (aSortKey) { - case "title": - case "categories": - case "location": - case "calendar": - return "string"; - - // All dates use "date_filled" - case "completedDate": - case "startDate": - case "endDate": - case "dueDate": - case "entryDate": - return "date_filled"; - - case "priority": - case "percentComplete": - case "status": - return "number"; - default: - return "unknown"; - } - }, - - sortEntry: function(aItem) { - let key = cal.getItemSortKey(aItem, this.mSortKey, this.mSortStartedDate); - return { mSortKey: key, mItem: aItem }; - }, - - sortEntryItem: function(sortEntry) { - return sortEntry.mItem; - }, - - sortEntryKey: function(sortEntry) { - return sortEntry.mSortKey; - }, - - createLocaleCollator: function() { - return Components.classes["@mozilla.org/intl/collation-factory;1"] - .getService(Components.interfaces.nsICollationFactory) - .CreateCollation(); - }, - /** * Sort an array of strings according to the current locale. * Modifies aStringArray, returning it sorted. @@ -495,6 +303,7 @@ XPCOMUtils.defineLazyModuleGetter(cal, "dtz", "resource://calendar/modules/calDa XPCOMUtils.defineLazyModuleGetter(cal, "email", "resource://calendar/modules/calEmailUtils.jsm", "calemail"); XPCOMUtils.defineLazyModuleGetter(cal, "item", "resource://calendar/modules/calItemUtils.jsm", "calitem"); XPCOMUtils.defineLazyModuleGetter(cal, "itip", "resource://calendar/modules/calItipUtils.jsm", "calitip"); +XPCOMUtils.defineLazyModuleGetter(cal, "unifinder", "resource://calendar/modules/calUnifinderUtils.jsm", "calunifinder"); XPCOMUtils.defineLazyModuleGetter(cal, "view", "resource://calendar/modules/calViewUtils.jsm", "calview"); XPCOMUtils.defineLazyModuleGetter(cal, "window", "resource://calendar/modules/calWindowUtils.jsm", "calwindow"); diff --git a/calendar/base/modules/calUtilsCompat.jsm b/calendar/base/modules/calUtilsCompat.jsm index ece56b9520..2b802ef537 100644 --- a/calendar/base/modules/calUtilsCompat.jsm +++ b/calendar/base/modules/calUtilsCompat.jsm @@ -83,6 +83,13 @@ var migrations = { getInvitedAttendee: "getInvitedAttendee", getAttendeesBySender: "getAttendeesBySender" }, + unifinder: { + sortEntryComparer: "sortEntryComparer", + getItemSortKey: "getItemSortKey", + // compareNative*, compareNumber, sortEntry, sortEntryItem, sortEntryKey and + // getSortTypeForSortKey are no longer available. There is a new + // cal.unifinder.sortItems though that should do everything necessary. + }, view: { isMouseOverBox: "isMouseOverBox", calRadioGroupSelectItem: "radioGroupSelectItem", diff --git a/calendar/base/modules/moz.build b/calendar/base/modules/moz.build index 4a96048e93..a0991a7733 100644 --- a/calendar/base/modules/moz.build +++ b/calendar/base/modules/moz.build @@ -20,6 +20,7 @@ EXTRA_JS_MODULES += [ 'calPrintUtils.jsm', 'calProviderUtils.jsm', 'calRecurrenceUtils.jsm', + 'calUnifinderUtils.jsm', 'calUtils.jsm', 'calUtilsCompat.jsm', 'calViewUtils.jsm', diff --git a/calendar/test/unit/head_consts.js b/calendar/test/unit/head_consts.js index 600f4f1967..40436f9648 100644 --- a/calendar/test/unit/head_consts.js +++ b/calendar/test/unit/head_consts.js @@ -2,10 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* exported do_calendar_startup, do_load_calmgr, do_load_timezoneservice, - * readJSONFile, ics_unfoldline, compareItemsSpecific, getStorageCal, - * getMemoryCal, createTodoFromIcalString, createEventFromIcalString, - * createDate, Cc, Ci, Cr, Cu +/* exported do_calendar_startup, do_load_calmgr, do_load_timezoneservice, readJSONFile, + * ics_unfoldline, dedent, compareItemsSpecific, getStorageCal, getMemoryCal, + * createTodoFromIcalString, createEventFromIcalString, createDate, Cc, Ci, Cr, Cu */ ChromeUtils.import("resource://gre/modules/Services.jsm"); @@ -190,6 +189,43 @@ function ics_unfoldline(aLine) { return aLine.replace(/\r?\n[ \t]/g, ""); } +/** + * Dedent the template string tagged with this function to make indented data + * easier to read. Usage: + * + * let data = dedent` + * This is indented data it will be unindented so that the first line has + * no leading spaces and the second is indented by two spaces. + * `; + * + * @param strings The string fragments from the template string + * @param ...values The interpolated values + * @return The interpolated, dedented string + */ +function dedent(strings, ...values) { + let parts = []; + + // Perform variable interpolation + for (let [i, string] of strings.entries()) { + parts.push(string); + if (i < values.length) { + parts.push(values[i]); + } + } + let lines = parts.join("").split("\n"); + + // The first and last line is empty as in above example. + lines.shift(); + lines.pop(); + + let minIndent = lines.reduce((min, line) => { + let match = line.match(/^(\s*)\S*/); + return Math.min(min, match[1].length); + }, Infinity); + + return lines.map(line => line.substr(minIndent)).join("\n"); +} + /** * Read a JSON file and return the JS object */ diff --git a/calendar/test/unit/test_unifinder_utils.js b/calendar/test/unit/test_unifinder_utils.js new file mode 100644 index 0000000000..d5f582ed21 --- /dev/null +++ b/calendar/test/unit/test_unifinder_utils.js @@ -0,0 +1,112 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +function run_test() { + test_get_item_sort_key(); + test_sort_items(); +} + + +function test_get_item_sort_key() { + let event = cal.createEvent(dedent` + BEGIN:VEVENT + PRIORITY:8 + SUMMARY:summary + DTSTART:20180102T030405Z + DTEND:20180607T080910Z + CATEGORIES:a,b,c + LOCATION:location + STATUS:CONFIRMED + END:VEVENT + `); + + strictEqual(cal.unifinder.getItemSortKey(event, "nothing"), null); + equal(cal.unifinder.getItemSortKey(event, "priority"), 8); + equal(cal.unifinder.getItemSortKey(event, "title"), "summary"); + equal(cal.unifinder.getItemSortKey(event, "startDate"), 1514862245000000); + equal(cal.unifinder.getItemSortKey(event, "endDate"), 1528358950000000); + equal(cal.unifinder.getItemSortKey(event, "categories"), "a, b, c"); + equal(cal.unifinder.getItemSortKey(event, "location"), "location"); + equal(cal.unifinder.getItemSortKey(event, "status"), 1); + + let task = cal.createTodo(dedent` + BEGIN:VTODO + DTSTART:20180102T030405Z + DUE:20180607T080910Z + PERCENT-COMPLETE:20 + STATUS:COMPLETED + END:VTODO + `); + + equal(cal.unifinder.getItemSortKey(task, "priority"), 5); + strictEqual(cal.unifinder.getItemSortKey(task, "title"), ""); + equal(cal.unifinder.getItemSortKey(task, "entryDate"), 1514862245000000); + equal(cal.unifinder.getItemSortKey(task, "dueDate"), 1528358950000000); + equal(cal.unifinder.getItemSortKey(task, "completedDate"), -62168601600000000); + equal(cal.unifinder.getItemSortKey(task, "percentComplete"), 20); + strictEqual(cal.unifinder.getItemSortKey(task, "categories"), ""); + strictEqual(cal.unifinder.getItemSortKey(task, "location"), ""); + equal(cal.unifinder.getItemSortKey(task, "status"), 2); + + let task2 = cal.createTodo(dedent` + BEGIN:VTODO + STATUS:GETTIN' THERE + END:VTODO + `); + equal(cal.unifinder.getItemSortKey(task2, "percentComplete"), 0); + equal(cal.unifinder.getItemSortKey(task2, "status"), -1); +} + +function test_sort_items() { + // string comparison + let summaries = ["", "a", "b"]; + let items = summaries.map(summary => { + return cal.createEvent(dedent` + BEGIN:VEVENT + SUMMARY:${summary} + END:VEVENT + `); + }); + + cal.unifinder.sortItems(items, "title", 1); + deepEqual(items.map(item => item.title), ["a", "b", null]); + + cal.unifinder.sortItems(items, "title", -1); + deepEqual(items.map(item => item.title), [null, "b", "a"]); + + // date comparison + let dates = ["20180101T000002Z", "20180101T000000Z", "20180101T000001Z"]; + items = dates.map(date => { + return cal.createEvent(dedent` + BEGIN:VEVENT + DTSTART:${date} + END:VEVENT + `); + }); + + cal.unifinder.sortItems(items, "startDate", 1); + deepEqual(items.map(item => item.startDate.icalString), + ["20180101T000000Z", "20180101T000001Z", "20180101T000002Z"]); + + cal.unifinder.sortItems(items, "startDate", -1); + deepEqual(items.map(item => item.startDate.icalString), + ["20180101T000002Z", "20180101T000001Z", "20180101T000000Z"]); + + // number comparison + let percents = [3, 1, 2]; + items = percents.map(percent => { + return cal.createTodo(dedent` + BEGIN:VTODO + PERCENT-COMPLETE:${percent} + END:VTODO + `); + }); + + cal.unifinder.sortItems(items, "percentComplete", 1); + deepEqual(items.map(item => item.percentComplete), [1, 2, 3]); + + cal.unifinder.sortItems(items, "percentComplete", -1); + deepEqual(items.map(item => item.percentComplete), [3, 2, 1]); +} diff --git a/calendar/test/unit/xpcshell-shared.ini b/calendar/test/unit/xpcshell-shared.ini index 670e9b518c..f759cf0559 100644 --- a/calendar/test/unit/xpcshell-shared.ini +++ b/calendar/test/unit/xpcshell-shared.ini @@ -47,6 +47,7 @@ [test_storage.js] [test_timezone.js] [test_timezone_definition.js] +[test_unifinder_utils.js] [test_utils.js] [test_view_utils.js] [test_webcal.js]