diff --git a/boot/boot.js b/boot/boot.js index d2ad69db9..6b472cb61 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -409,10 +409,10 @@ $tw.utils.resolvePath = function(sourcepath,rootpath) { }; /* -Parse a semantic version string into its constituent parts +Parse a semantic version string into its constituent parts -- see https://semver.org */ $tw.utils.parseVersion = function(version) { - var match = /^((\d+)\.(\d+)\.(\d+))(?:-([\dA-Za-z\-]+(?:\.[\dA-Za-z\-]+)*))?(?:\+([\dA-Za-z\-]+(?:\.[\dA-Za-z\-]+)*))?$/.exec(version); + var match = /^v?((\d+)\.(\d+)\.(\d+))(?:-([\dA-Za-z\-]+(?:\.[\dA-Za-z\-]+)*))?(?:\+([\dA-Za-z\-]+(?:\.[\dA-Za-z\-]+)*))?$/.exec(version); if(match) { return { version: match[1], @@ -427,25 +427,37 @@ $tw.utils.parseVersion = function(version) { } }; +/* +Returns +1 if the version string A is greater than the version string B, 0 if they are the same, and +1 if B is greater than A. +Missing or malformed version strings are parsed as 0.0.0 +*/ +$tw.utils.compareVersions = function(versionStringA,versionStringB) { + var defaultVersion = { + major: 0, + minor: 0, + patch: 0 + }, + versionA = $tw.utils.parseVersion(versionStringA) || defaultVersion, + versionB = $tw.utils.parseVersion(versionStringB) || defaultVersion, + diff = [ + versionA.major - versionB.major, + versionA.minor - versionB.minor, + versionA.patch - versionB.patch + ]; + if((diff[0] > 0) || (diff[0] === 0 && diff[1] > 0) || (diff[0] === 0 & diff[1] === 0 & diff[2] > 0)) { + return +1; + } else if((diff[0] < 0) || (diff[0] === 0 && diff[1] < 0) || (diff[0] === 0 & diff[1] === 0 & diff[2] < 0)) { + return -1; + } else { + return 0; + } +}; + /* Returns true if the version string A is greater than the version string B. Returns true if the versions are the same */ $tw.utils.checkVersions = function(versionStringA,versionStringB) { - var defaultVersion = { - major: 0, - minor: 0, - patch: 0 - }, - versionA = $tw.utils.parseVersion(versionStringA) || defaultVersion, - versionB = $tw.utils.parseVersion(versionStringB) || defaultVersion, - diff = [ - versionA.major - versionB.major, - versionA.minor - versionB.minor, - versionA.patch - versionB.patch - ]; - return (diff[0] > 0) || - (diff[0] === 0 && diff[1] > 0) || - (diff[0] === 0 && diff[1] === 0 && diff[2] >= 0); + return $tw.utils.compareVersions(versionStringA,versionStringB) !== -1; }; /* diff --git a/core/modules/filters/compare.js b/core/modules/filters/compare.js new file mode 100644 index 000000000..186dfa27b --- /dev/null +++ b/core/modules/filters/compare.js @@ -0,0 +1,76 @@ +/*\ +title: $:/core/modules/filters/compare.js +type: application/javascript +module-type: filteroperator + +General purpose comparison operator + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.compare = function(source,operator,options) { + var suffixes = operator.suffixes || [], + type = (suffixes[0] || [])[0], + mode = (suffixes[1] || [])[0], + typeFn = types[type] || types.number, + modeFn = modes[mode] || modes.eq, + invert = operator.prefix === "!", + results = []; + source(function(tiddler,title) { + if(modeFn(typeFn(title,operator.operand)) !== invert) { + results.push(title); + } + }); + 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;}, + "gteq": function(value) {return value >= 0;}, + "gt": function(value) {return value > 0;}, + "lteq": function(value) {return value <= 0;}, + "lt": function(value) {return value < 0;} +} + +})(); diff --git a/core/modules/filters/math.js b/core/modules/filters/math.js index c3f6d3b45..ac2b40117 100644 --- a/core/modules/filters/math.js +++ b/core/modules/filters/math.js @@ -114,9 +114,9 @@ exports.minall = makeNumericReducingOperator( function makeNumericBinaryOperator(fnCalc) { return function(source,operator,options) { var result = [], - numOperand = parseNumber(operator.operand); + numOperand = $tw.utils.parseNumber(operator.operand); source(function(tiddler,title) { - result.push(stringifyNumber(fnCalc(parseNumber(title),numOperand))); + result.push($tw.utils.stringifyNumber(fnCalc($tw.utils.parseNumber(title),numOperand))); }); return result; }; @@ -129,18 +129,10 @@ function makeNumericReducingOperator(fnCalc,initialValue) { source(function(tiddler,title) { result.push(title); }); - return [stringifyNumber(result.reduce(function(accumulator,currentValue) { - return fnCalc(accumulator,parseNumber(currentValue)); + return [$tw.utils.stringifyNumber(result.reduce(function(accumulator,currentValue) { + return fnCalc(accumulator,$tw.utils.parseNumber(currentValue)); },initialValue))]; }; } -function parseNumber(str) { - return parseFloat(str) || 0; -} - -function stringifyNumber(num) { - return num + ""; -} - })(); diff --git a/core/modules/utils/utils.js b/core/modules/utils/utils.js index 8d8c294d9..dfb70aa0c 100644 --- a/core/modules/utils/utils.js +++ b/core/modules/utils/utils.js @@ -801,4 +801,16 @@ exports.getSystemInfo = function(str,ending,position) { return results.join("\n"); }; +exports.parseNumber = function(str) { + return parseFloat(str) || 0; +}; + +exports.parseInt = function(str) { + return parseInt(str) || 0; +}; + +exports.stringifyNumber = function(num) { + return num + ""; +}; + })(); diff --git a/editions/test/tiddlers/tests/test-compare-filter.js b/editions/test/tiddlers/tests/test-compare-filter.js new file mode 100644 index 000000000..b146fcc18 --- /dev/null +++ b/editions/test/tiddlers/tests/test-compare-filter.js @@ -0,0 +1,84 @@ +/*\ +title: test-compare-filters.js +type: application/javascript +tags: [[$:/tags/test-spec]] + +Tests the compare filter. + +\*/ +(function(){ + +/* jslint node: true, browser: true */ +/* eslint-env node, browser, jasmine */ +/* eslint no-mixed-spaces-and-tabs: ["error", "smart-tabs"]*/ +/* global $tw, require */ +"use strict"; + +describe("'compare' filter tests", function() { + + var wiki = new $tw.Wiki(); + + it("should compare numerical equality", function() { + expect(wiki.filterTiddlers("[[2]compare:number:eq[0003]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]compare:number:ne[000003]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]compare:number:eq[3]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]compare:number:ne[3]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]compare:number:eq[2]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]compare:number:ne[2]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]compare:number:eq[x]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]compare:number:ne[x]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]!compare:number:eq[3]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]!compare:number:ne[3]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]!compare:number:eq[2]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]!compare:number:ne[2]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]!compare:number:eq[x]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]!compare:number:ne[x]]").join(",")).toBe(""); + }); + + it("should compare numerical magnitude", function() { + expect(wiki.filterTiddlers("[[2]compare:number:gt[3]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]compare:number:lt[3]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]compare:number:gt[2]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]compare:number:lt[2]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]compare:number:gt[x]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]compare:number:lt[x]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]!compare:number:gt[3]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]!compare:number:lt[3]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]!compare:number:gt[2]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]!compare:number:lt[2]]").join(",")).toBe("2"); + expect(wiki.filterTiddlers("[[2]!compare:number:gt[x]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[2]!compare:number:lt[x]]").join(",")).toBe("2"); + }); + + it("should compare string", function() { + expect(wiki.filterTiddlers("[[Monday]compare:string:lt[M]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[Monday]compare:string:lt[W]]").join(",")).toBe("Monday"); + expect(wiki.filterTiddlers("Monday Tuesday Wednesday Thursday Friday Saturday Sunday +[compare:string:gt[M]sort[]]").join(",")).toBe("Monday,Saturday,Sunday,Thursday,Tuesday,Wednesday"); + expect(wiki.filterTiddlers("Monday Tuesday Wednesday Thursday Friday Saturday Sunday +[compare:string:gt[M]compare:string:lt[W]sort[]]").join(",")).toBe("Monday,Saturday,Sunday,Thursday,Tuesday"); + }); + + it("should compare dates", function() { + expect(wiki.filterTiddlers("[[20200101]compare:date:gt[201912311852]]").join(",")).toBe("20200101"); + }); + + it("should compare version numbers", function() { + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:eq[v1.1.0]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:eq[v1.2.2]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:eq[v1.2.3]]").join(",")).toBe("v1.2.3"); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:eq[v1.2.4]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:eq[v2.0.0]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:gt[v1.1.0]]").join(",")).toBe("v1.2.3"); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:gt[v1.2.2]]").join(",")).toBe("v1.2.3"); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:gt[v1.2.3]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:gt[v1.2.4]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:gt[v2.0.0]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:lt[v1.1.0]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:lt[v1.2.2]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:lt[v1.2.3]]").join(",")).toBe(""); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:lt[v1.2.4]]").join(",")).toBe("v1.2.3"); + expect(wiki.filterTiddlers("[[v1.2.3]compare:version:lt[v2.0.0]]").join(",")).toBe("v1.2.3"); + }); + +}); + +})(); diff --git a/editions/test/tiddlers/tests/test-utils.js b/editions/test/tiddlers/tests/test-utils.js index 860cc4d51..a79c6ccee 100644 --- a/editions/test/tiddlers/tests/test-utils.js +++ b/editions/test/tiddlers/tests/test-utils.js @@ -107,6 +107,30 @@ describe("Utility tests", function() { }); + it("should compare versions", function() { + var cv = $tw.utils.compareVersions; + expect(cv("v0.0.0","v0.0.0")).toEqual(0); + expect(cv("0.0.0","v0.0.0")).toEqual(0); + expect(cv("v0.0.0","0.0.0")).toEqual(0); + expect(cv("v0.0.0","not a version")).toEqual(0); + expect(cv("v0.0.0",undefined)).toEqual(0); + expect(cv("not a version","v0.0.0")).toEqual(0); + expect(cv(undefined,"v0.0.0")).toEqual(0); + expect(cv("v1.0.0","v1.0.0")).toEqual(0); + expect(cv("v1.0.0","1.0.0")).toEqual(0); + + expect(cv("v1.0.1",undefined)).toEqual(+1); + expect(cv("v1.0.1","v1.0.0")).toEqual(+1); + expect(cv("v1.1.1","v1.1.0")).toEqual(+1); + expect(cv("v1.1.2","v1.1.1")).toEqual(+1); + expect(cv("1.1.2","v1.1.1")).toEqual(+1); + + expect(cv("v1.0.0","v1.0.1")).toEqual(-1); + expect(cv("v1.1.0","v1.1.1")).toEqual(-1); + expect(cv("v1.1.1","v1.1.2")).toEqual(-1); + expect(cv("1.1.1","1.1.2")).toEqual(-1); + }); + }); })(); diff --git a/editions/tw5.com/tiddlers/filters/compare Operator.tid b/editions/tw5.com/tiddlers/filters/compare Operator.tid new file mode 100644 index 000000000..a882914ae --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/compare Operator.tid @@ -0,0 +1,51 @@ +created: 20200412181551706 +modified: 20200412181551706 +tags: [[Filter Operators]] [[Mathematics Operators]] [[String Operators]] [[Negatable Operators]] +title: compare Operator +type: text/vnd.tiddlywiki +caption: compare +op-purpose: filter the input by comparing each item against the operand +op-input: a [[selection of titles|Title Selection]] +op-suffix: the <<.op compare>> operator uses a rich suffix, see below for details +op-parameter: the value to compare +op-output: those input titles matching the specified comparison +op-neg-output: those input titles <<.em not>> matching the specified comparison + +<<.from-version "5.1.22">>The <<.op compare>> filter allows numerical, string and date comparisons to be performed. + +The <<.op compare>> operator uses an extended syntax to specify all the options: + +``` +[compare::[]] +``` + +The ''type'' can be: + +* "number" - invalid numbers are interpreted as zero +* "integer" - invalid integers are interpreted as zero +* "string" +* "date" - invalid dates are interpreted as 1st January 1970 +* "version" - invalid versions are interpreted as "v0.0.0" + +The ''mode'' can be: + +* "eq" - equal to +* "ne" - not equal ot +* "gteq" - greater than or equal to +* "gt" - greater than +* "lteq" - less than or equal to +* "lt" - less than + +The operator compares each item in the selection against the value of the parameter, retaining only those items that pass the specified condition. + +For example: + +``` +[[2]compare:number:eq[3]] returns nothing +[[2]compare:number:lt[3]] returns "2" +[[2]compare:number:eq[2]] returns "2" +``` + +Note that several of the variants of the <<.op compare>> operator are synonyms for existing operators, and are provided in the interests of consistency. For example, `compare:string:eq[x]` is a synonym for `match[x]`. + +<<.operator-examples "compare">> diff --git a/editions/tw5.com/tiddlers/filters/examples/compare Operator (Examples).tid b/editions/tw5.com/tiddlers/filters/examples/compare Operator (Examples).tid new file mode 100644 index 000000000..f729dc5d2 --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/examples/compare Operator (Examples).tid @@ -0,0 +1,11 @@ +created: 20200412212935849 +modified: 20200412212935849 +tags: [[compare Operator]] [[Operator Examples]] +title: compare Operator (Examples) +type: text/vnd.tiddlywiki + +<<.operator-example 1 "[[20200101]compare:date:gt[201912311852]]" "compares two partial dates">> +<<.operator-example 2 "[[202001011852]compare:integer:gt[20191231]]" "compares the same two strings as integers">> +<<.operator-example 3 "[list[Days of the Week]compare:string:gt[M]compare:string:lt[W]]">> +<<.operator-example 4 "[[v5.1.23-prerelease]compare:version:gt[v5.1.22]]">> +<<.operator-example 5 "[[1]compare:number:gt[2]then[yes]else[no]]">>