diff --git a/core/modules/filters/compare.js b/core/modules/filters/compare.js index 186dfa27b..fd509a28e 100644 --- a/core/modules/filters/compare.js +++ b/core/modules/filters/compare.js @@ -16,7 +16,7 @@ exports.compare = function(source,operator,options) { var suffixes = operator.suffixes || [], type = (suffixes[0] || [])[0], mode = (suffixes[1] || [])[0], - typeFn = types[type] || types.number, + typeFn = $tw.utils.makeCompareFunction(type,{defaultType: "number"}), modeFn = modes[mode] || modes.eq, invert = operator.prefix === "!", results = []; @@ -28,42 +28,6 @@ exports.compare = function(source,operator,options) { return results; }; -var types = { - "number": function(a,b) { - return compare($tw.utils.parseNumber(a),$tw.utils.parseNumber(b)); - }, - "integer": function(a,b) { - return compare($tw.utils.parseInt(a),$tw.utils.parseInt(b)); - }, - "string": function(a,b) { - return compare("" + a,"" +b); - }, - "date": function(a,b) { - var dateA = $tw.utils.parseDate(a), - dateB = $tw.utils.parseDate(b); - if(!isFinite(dateA)) { - dateA = new Date(0); - } - if(!isFinite(dateB)) { - dateB = new Date(0); - } - return compare(dateA,dateB); - }, - "version": function(a,b) { - return $tw.utils.compareVersions(a,b); - } -}; - -function compare(a,b) { - if(a > b) { - return +1; - } else if(a < b) { - return -1; - } else { - return 0; - } -}; - var modes = { "eq": function(value) {return value === 0;}, "ne": function(value) {return value !== 0;}, diff --git a/core/modules/filters/sortsub.js b/core/modules/filters/sortsub.js new file mode 100644 index 000000000..82de59f8f --- /dev/null +++ b/core/modules/filters/sortsub.js @@ -0,0 +1,44 @@ +/*\ +title: $:/core/modules/filters/sortsub.js +type: application/javascript +module-type: filteroperator + +Filter operator for sorting by a subfilter + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Export our filter function +*/ +exports.sortsub = function(source,operator,options) { + // Collect the input titles + var inputTitles = []; + source(function(tiddler,title) { + inputTitles.push(title); + }); + // Pass them through the subfilter to get the sort keys + var sortKeys = options.wiki.filterTiddlers(operator.operand,options.widget,options.wiki.makeTiddlerIterator(inputTitles)); + // Rather than sorting the titles array, we'll sort the indexes so that we can consult both arrays + var indexes = []; + while(inputTitles.length > indexes.length) { + indexes.push(indexes.length); + } + // Sort the indexes + var compareFn = $tw.utils.makeCompareFunction(operator.suffix,{defaultType: "string",invert: operator.prefix === "!"}); + indexes = indexes.sort(function(a,b) { + return compareFn(sortKeys[a],sortKeys[b]); + }); + // Make the results array in order + var results = []; + $tw.utils.each(indexes,function(index) { + results.push(inputTitles[index]); + }); + return results; +}; + +})(); diff --git a/core/modules/utils/utils.js b/core/modules/utils/utils.js index 7f00f05bb..a5fb24697 100644 --- a/core/modules/utils/utils.js +++ b/core/modules/utils/utils.js @@ -813,4 +813,45 @@ exports.stringifyNumber = function(num) { return num + ""; }; +exports.makeCompareFunction = function(type,options) { + options = options || {}; + var gt = options.invert ? -1 : +1, + lt = options.invert ? +1 : -1, + compare = function(a,b) { + if(a > b) { + return gt ; + } else if(a < b) { + return lt; + } else { + return 0; + } + }, + types = { + "number": function(a,b) { + return compare($tw.utils.parseNumber(a),$tw.utils.parseNumber(b)); + }, + "integer": function(a,b) { + return compare($tw.utils.parseInt(a),$tw.utils.parseInt(b)); + }, + "string": function(a,b) { + return compare("" + a,"" +b); + }, + "date": function(a,b) { + var dateA = $tw.utils.parseDate(a), + dateB = $tw.utils.parseDate(b); + if(!isFinite(dateA)) { + dateA = new Date(0); + } + if(!isFinite(dateB)) { + dateB = new Date(0); + } + return compare(dateA,dateB); + }, + "version": function(a,b) { + return $tw.utils.compareVersions(a,b); + } + }; + return (types[type] || types[options.defaultType] || types.number); +}; + })(); diff --git a/editions/test/tiddlers/tests/test-filters.js b/editions/test/tiddlers/tests/test-filters.js index 929abe93d..0e3ede71b 100644 --- a/editions/test/tiddlers/tests/test-filters.js +++ b/editions/test/tiddlers/tests/test-filters.js @@ -661,6 +661,24 @@ function runTests(wiki) { expect(wiki.filterTiddlers("b a b c +[sortby[b a c b]]").join(",")).toBe("b,a,c"); }); + it("should handle the sortsub operator", function() { + var widget = require("$:/core/modules/widgets/widget.js"); + var rootWidget = new widget.widget({ type:"widget", children:[ {type:"widget", children:[]} ] }, + { wiki:wiki, document:$tw.document}); + rootWidget.makeChildWidgets(); + var anchorWidget = rootWidget.children[0]; + rootWidget.setVariable("sort1","[length[]]"); + rootWidget.setVariable("sort2","[get[text]else[]length[]]"); + expect(wiki.filterTiddlers("[sortsub:number]",anchorWidget).join(",")).toBe("one,hasList,TiddlerOne,has filter,$:/TiddlerTwo,Tiddler Three,$:/ShadowPlugin,a fourth tiddler,filter regexp test"); + expect(wiki.filterTiddlers("[!sortsub:number]",anchorWidget).join(",")).toBe("filter regexp test,a fourth tiddler,$:/ShadowPlugin,$:/TiddlerTwo,Tiddler Three,TiddlerOne,has filter,hasList,one"); + expect(wiki.filterTiddlers("[sortsub:string]",anchorWidget).join(",")).toBe("TiddlerOne,has filter,$:/TiddlerTwo,Tiddler Three,$:/ShadowPlugin,a fourth tiddler,filter regexp test,one,hasList"); + expect(wiki.filterTiddlers("[!sortsub:string]",anchorWidget).join(",")).toBe("hasList,one,filter regexp test,a fourth tiddler,$:/ShadowPlugin,$:/TiddlerTwo,Tiddler Three,TiddlerOne,has filter"); + expect(wiki.filterTiddlers("[sortsub:number]",anchorWidget).join(",")).toBe("one,TiddlerOne,hasList,has filter,a fourth tiddler,Tiddler Three,$:/TiddlerTwo,filter regexp test,$:/ShadowPlugin"); + expect(wiki.filterTiddlers("[!sortsub:number]",anchorWidget).join(",")).toBe("$:/ShadowPlugin,filter regexp test,$:/TiddlerTwo,Tiddler Three,a fourth tiddler,has filter,hasList,TiddlerOne,one"); + expect(wiki.filterTiddlers("[sortsub:string]",anchorWidget).join(",")).toBe("one,TiddlerOne,hasList,has filter,$:/ShadowPlugin,a fourth tiddler,Tiddler Three,$:/TiddlerTwo,filter regexp test"); + expect(wiki.filterTiddlers("[!sortsub:string]",anchorWidget).join(",")).toBe("filter regexp test,$:/TiddlerTwo,Tiddler Three,a fourth tiddler,$:/ShadowPlugin,has filter,hasList,TiddlerOne,one"); + }); + } }); diff --git a/editions/tw5.com/tiddlers/filters/examples/sortsub Operator (Examples).tid b/editions/tw5.com/tiddlers/filters/examples/sortsub Operator (Examples).tid new file mode 100644 index 000000000..475413155 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/examples/sortsub Operator (Examples).tid @@ -0,0 +1,32 @@ +created: 20200425110427700 +modified: 20200425110427700 +tags: [[sortsub Operator]] [[Operator Examples]] +title: sortsub Operator (Examples) +type: text/vnd.tiddlywiki + +\define show-variable(name) +
  • ''$name$'': <$text text=<<$name$>>/>
  • +\end + + +<$vars + compare-by-title-length="[length[]]" + compare-by-text-length="[get[text]else[]length[]]" + compare-by-newest-of-modified-and-created-dates="[get[modified]else[19700101]]" +> + +These examples make use of the following variables: + +
      +<> +<> +<> +
    + +<<.operator-example 1 "[sortsub:numberlimit[10]]">> +<<.operator-example 2 "[!sortsub:numberlimit[10]]">> +<<.operator-example 3 "[sortsub:numberlimit[10]]">> +<<.operator-example 4 "[!sortsub:numberlimit[10]]">> +<<.operator-example 5 "[tag[Field Operators]sortsub:date]">> + + \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/filters/sortsub Operator.tid b/editions/tw5.com/tiddlers/filters/sortsub Operator.tid new file mode 100644 index 000000000..8a4981f44 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/sortsub Operator.tid @@ -0,0 +1,28 @@ +created: 20200424160155182 +modified: 20200424160155182 +tags: [[Filter Operators]] [[Field Operators]] [[Order Operators]] [[Negatable Operators]] +title: sortsub Operator +type: text/vnd.tiddlywiki +caption: sortsub +op-purpose: sort the input by the result of evaluating a subfilter for each item +op-input: a [[selection of titles|Title Selection]] +op-parameter: a subfilter to be evaluated +op-parameter-name: S +op-suffix: the type used for the comparison (string, number, integer, date, version), defaulting to string +op-suffix-name: T +op-output: the input, sorted into ascending order by the result of evaluating subfilter <<.param S>> +op-neg-output: the input, sorted into reverse order by the result of evaluating subfilter <<.param S>> + +The list of input titles are passed to the subfilter. The subfilter transforms the input titles into the form needed for sorting. For example, the subfilter `[length[]]` transforms each input title in the number representing its length, and thus sorts the input titles according to their length. + +The suffix <<.place T>> determines how the items are compared and can be: + +* "string" (the default) +* "number" - invalid numbers are interpreted as zero +* "integer" - invalid integers are interpreted as zero +* "date" - invalid dates are interpreted as 1st January 1970 +* "version" - invalid versions are interpreted as "v0.0.0" + +Note that subfilters should return the same number of items that they are passed. Any missing entries will be treated as zero or the empty string. In particular, when retrieving the value of a field with the [[get Operator]] it is helpful to guard against a missing field value using the [[else Operator]]. For example `[get[myfield]else[default-value]...`. + +<<.operator-examples "sortsub">>