From f60d0ef10945c9a10cda02d70f612ffc61cb602f Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 11 Dec 2020 17:07:52 +0700 Subject: [PATCH] reduce and :reduce handle empty input identically (#5255) Fixes #5246. Now the reduce operator and :reduce filter run prefix will both return empty output when their input is empty, so that both can be chained together with the else operator or :else prefix. --- core/modules/filters/reduce.js | 6 ++- .../tiddlers/tests/test-prefixes-filter.js | 7 +++- .../examples/reduce Operator (Examples).tid | 37 ++++--------------- editions/tw5.com/tiddlers/filters/reduce.tid | 17 ++++++--- .../syntax/Filter Run Prefix (Examples).tid | 4 +- 5 files changed, 32 insertions(+), 39 deletions(-) diff --git a/core/modules/filters/reduce.js b/core/modules/filters/reduce.js index fd5ccf568..206936887 100644 --- a/core/modules/filters/reduce.js +++ b/core/modules/filters/reduce.js @@ -48,7 +48,11 @@ exports.reduce = function(source,operator,options) { accumulator = "" + list[0]; } } - return [accumulator]; + if(results.length > 0) { + return [accumulator]; + } else { + return []; + } }; })(); diff --git a/editions/test/tiddlers/tests/test-prefixes-filter.js b/editions/test/tiddlers/tests/test-prefixes-filter.js index e84e86cf4..62f51263f 100644 --- a/editions/test/tiddlers/tests/test-prefixes-filter.js +++ b/editions/test/tiddlers/tests/test-prefixes-filter.js @@ -77,6 +77,8 @@ describe("'reduce' and 'intersection' filter prefix tests", function() { expect(wiki.filterTiddlers("[tag[shopping]] :reduce[get[quantity]add]").join(",")).toBe("22"); expect(wiki.filterTiddlers("[tag[shopping]] :reduce[get[price]multiply{!!quantity}add]").join(",")).toBe("27.75"); expect(wiki.filterTiddlers("[tag[shopping]] :reduce[compare:number:gt[0]thenaddsuffix[, ]addsuffixelse]").join(",")).toBe("Brownies, Chick Peas, Milk, Rice Pudding"); + // Empty input should become empty output + expect(wiki.filterTiddlers("[tag[non-existent]] :reduce[get[price]multiply{!!quantity}add]").length).toBe(0); expect(wiki.filterTiddlers("[tag[non-existent]] :reduce[get[price]multiply{!!quantity}add] :else[[0]]").join(",")).toBe("0"); }); @@ -93,7 +95,10 @@ describe("'reduce' and 'intersection' filter prefix tests", function() { expect(wiki.filterTiddlers("[tag[shopping]reduce]",anchorWidget).join(",")).toBe("22"); expect(wiki.filterTiddlers("[tag[shopping]reduce]",anchorWidget).join(",")).toBe("27.75"); expect(wiki.filterTiddlers("[tag[shopping]reduce]",anchorWidget).join(",")).toBe("Brownies, Chick Peas, Milk, Rice Pudding"); - expect(wiki.filterTiddlers("[tag[non-existent]reduce,[0]]",anchorWidget).join(",")).toBe("0"); + // Empty input should become empty output + expect(wiki.filterTiddlers("[tag[non-existent]reduce,[0]]",anchorWidget).join(",")).not.toBe("0"); + expect(wiki.filterTiddlers("[tag[non-existent]reduce,[0]]",anchorWidget).length).toBe(0); + expect(wiki.filterTiddlers("[tag[non-existent]reduceelse[0]]",anchorWidget).join(",")).toBe("0"); }); it("should handle the :intersection prefix", function() { diff --git a/editions/tw5.com/tiddlers/filters/examples/reduce Operator (Examples).tid b/editions/tw5.com/tiddlers/filters/examples/reduce Operator (Examples).tid index 1c619c506..94ad021fe 100644 --- a/editions/tw5.com/tiddlers/filters/examples/reduce Operator (Examples).tid +++ b/editions/tw5.com/tiddlers/filters/examples/reduce Operator (Examples).tid @@ -7,18 +7,17 @@ type: text/vnd.tiddlywiki \define add-price() [get[price]multiply{!!quantity}add] \define num-items() [get[quantity]add] \define join-with-commas() [compare:number:gt[0]thenaddsuffix[, ]addsuffixelse] +\define multiply-input() [multiply] \define display-variable(name) ''<$text text=<<__name__>>/>'': <$text text={{{ [<__name__>getvariable[]] }}}/> \end -\define reduce-tip() -Remember that <<.op reduce>> always produces output, so <<.op else>> will never trigger after <<.op reduce>>. -\end These examples use the following predefined variables: * <> * <> * <> +* <> They also use the following data tiddlers: @@ -30,32 +29,10 @@ They also use the following data tiddlers: -Number of items: - <<.operator-example 1 "[tag[shopping]reduce]">> - -Total price: - <<.operator-example 2 "[tag[shopping]reduce]">> - -Using `` to act differently on the first item than the rest: - -<<.operator-example 3 "[tag[shopping]reduce]">> - -Empty input, no second parameter: - -<<.operator-example 4 "[tag[non-existent]reduce]">> - -Note how the output contains a single item with no text. This is not "empty output" for the purposes of the <<.op else>> operator. - -<$macrocall $name=".tip" _=<> /> - -Empty input, no second parameter, followed by <<.op else>>: - -<<.operator-example 5 "[tag[non-existent]reduceelse[0]]">> - -Note how the output still contains a single item with no text: <<.op else>> did not trigger. If you want the value to be 0 when <<.op reduce>> has no items to process, you need to specify 0 as the initial value by passing it as a second parameter to <<.op reduce>>. - -Empty input, second parameter provided: - -<<.operator-example 6 "[tag[non-existent]reduce,[0]]">> +<<.operator-example 3 "[tag[shopping]reduce]" "Uses `` to act differently on the first item than the rest">> +<<.operator-example 4 "[tag[non-existent]reduce]" "Empty input produces empty output">> +<<.operator-example 5 "[tag[non-existent]reduceelse[0]]" "Use `else` to ensure output if input was empty">> +<<.operator-example 6 "=1 =2 =3 +[reduce]" "Empty initial value is treated as 0 by mathematical operators">> +<<.operator-example 7 "=1 =2 =3 +[reduce,[1]]" "Setting initial value is sometimes necessary for correct results">> diff --git a/editions/tw5.com/tiddlers/filters/reduce.tid b/editions/tw5.com/tiddlers/filters/reduce.tid index 153fe463c..d9893346c 100644 --- a/editions/tw5.com/tiddlers/filters/reduce.tid +++ b/editions/tw5.com/tiddlers/filters/reduce.tid @@ -10,10 +10,6 @@ tags: [[Filter Operators]] title: reduce Operator type: text/vnd.tiddlywiki -\define reduce-tip() -The <<.op reduce>> operator will always produce output, even if its input was empty. If its input is empty, the output of <<.op reduce>> will be the initial value of the accumulator, i.e. the value of the second parameter. One result of this fact is that the <<.op else>> operator will never be triggered if it follows a <<.op reduce>>. The "Empty input" examples show what happens when <<.op reduce>> receives no input. -\end - <<.from-version "5.1.23">> The <<.op reduce>> operator runs a subfilter for each input title, passing the result of the previous subfilter run as a variable. The initial value of the accumulator can optionally be specified. It returns the result of the final subfilter run. The <<.op reduce>> operator is used to flatten a list of items down to a single item by repeatedly applying a formula. A typical use is to add up the values in a given field of a list of tiddlers. @@ -26,10 +22,17 @@ The following variables are available within the subfilter: * ''revIndex'' - the reverse numeric index of the current list item (with zero being the last item in the list) * ''length'' - the total length of the input list -<$macrocall $name=".tip" _=<> /> +If the <<.op reduce>> operator receives no input, its output will be empty. The [[else Operator]] can be useful in such cases. +<<.tip "Literal filter operands cannot contain square brackets but you can work around the issue by using a variable:">> -<<.tip "Compare with the analagous named filter run prefix `:reduce`">> +``` +<$set name="sum-input" value="[add]"> +{{{ =1 =2 =3 +[reduce] }}} + +``` + +<<.tip "Compare with the analagous named [[filter run prefix|Filter Expression]] `:reduce`">> ``` \define num-items() [get[quantity]add] @@ -43,4 +46,6 @@ is equivalent to: [tag[shopping]] :reduce[get[quantity]add] ``` +<$macrocall $name=".tip" _="""If the optional second parameter is not given, the initial accumulator value will be empty. Numerical operators treat empty input as if it was the number 0. See the multiply-input examples for how this can affect the result of <<.op reduce>> in some cases."""/> + <<.operator-examples "reduce">> diff --git a/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix (Examples).tid b/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix (Examples).tid index f40cc7356..fde0a1557 100644 --- a/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix (Examples).tid +++ b/editions/tw5.com/tiddlers/filters/syntax/Filter Run Prefix (Examples).tid @@ -39,6 +39,8 @@ is equivalent to: [tag[shopping]reduce] ``` -Specifying a default value: +Specifying a default value when input is empty: `[tag[non-existent]] :reduce[get[price]multiply{!!quantity}add] :else[[0]]` + +<$macrocall $name=".tip" _="""Unlike the [[reduce Operator]], the `:reduce` prefix cannot specify an initial value for the accumulator, so its initial value will always be empty (which is treated as 0 by mathematical operators). So `=1 =2 =3 :reduce[multiply]` will produce 0, not 6. If you need to specify an initial accumulator value, use the [[reduce Operator]]."""/>