Tricks for refactoring ugly long filters?

I have a filter that converts a string by replacing each char that is a digit with a string consisting of dots as long as that number:

<$let str="8x7x6x5x4x3x2x1">
{{{ [<str>
search-replace[8],[........]
search-replace[7],[.......]
search-replace[6],[......]
search-replace[5],[.....]
search-replace[4],[....]
search-replace[3],[...]
search-replace[2],[..]
search-replace[1],[.]
] }}}
</$let>

I’ve split it to multiple lines to show that intuitively there’s potential for refactoring here - this is just a lot of copypasted code. But I have no idea how to approach it: I barely start to understand the idea of list as a loop, while here I see a nested, two level loop.

A second question is about performance. Does my ugly filter above do one additional pass for each search-replace operator? I could probably implement this algorithm in one pass using a Python oneliner.

Edit: not sure it’s one pass, but it’s indeed an oneliner:

>>> ''.join([''.join(['.'] * int(x)) if x in '87654321' else x for x in '8x7x6x5x4x3x2x1'])
'........x.......x......x.....x....x...x..x.'
1 Like

Here’s one way to do it. Whether this is less ugly or not is probably a matter of opinion, but it is at least less visually repetitive.

\function convert.num() [regexp[\D]] :else[range{!!title}search-replace::regexp[\d],[.]]

<$let str="8x7x6x5x4x3x2x1">
{{{ [<str>split[]] :map:flat[convert.num[]] +[join[]] }}}
</$let>

Steps:

  • split between each character
  • The filter with the :map prefix will be run once per input value. Since I needed a two-part filter to make use of the :else condition, I had to move both parts into the function/subfilter convert.num, which becomes the sole content of my :map run.
    • By default, :map runs yield one output per input value. The :flat suffix switches it to a (potentially) one-to-many relationship, which will allow us to turn one digit into many periods.
    • I like functions, personally, but you could also do this with the subfilter operator, if you prefer. You’d need to make the following changes:
      • \function convert.num()\define convert.num()
      • :map:flat[convert.num[]]:map:flat[subfilter<convert.num>]
  • Now, looking at convert.num itself…
    • [regexp[\D]] = search for a non-digit. If this filter run returns any (non-digit) value, the rest of the function will be ignored, and the character is preserved as-is.
    • :else
      • use range to generate a list of numbers beginning with 1 and ending with {!!title} (= the number). For instance, range[8] will give me 1 2 3 4 5 6 7 8.
      • Use search-replace::regexp[\d],[.] to replace each number in the range output with a period.
  • Finally, +[join[]] everything back into a single string.

And my generalized advice for long, ugly filters: if you find yourself reusing the same string of operators with different parameters, move it into a function instead!

Honestly, I might not have ordinarily bothered in a case like this, since your original is far more transparent at a glance and only ~30% longer. But this was a fun challenge, thanks!

4 Likes

Thank you.

I must confess that I find your code less intimidating in this form:

\function convert.num(num) [regexp[\D]] :else[range<num>search-replace::regexp[\d],[.]]

<$let str="8x7x6x5x4x3x2x1">
{{{ [<str>split[]] :map:flat[convert.num<currentTiddler>] +[join[]] }}}
</$let>

Maybe https://tiddlywiki.com/#Functions should include a second example with this {!!title} trick when functions are used with no parameter signature, because it really saves typing for explicitly passing currentTiddler as parameter to the function. The existing example that uses an explicitly declared parameter is forming a habit that feels painful to break.

Oops, the one bit I’d neglected to explain!

This trick actually relies on the use of the :map prefix, not any inherent property of functions. For comparison:
image

You can see how the {!!title}/<currentTiddler> reference in .func changes in the second example vs. the first.

  • Example #2 works because :map remaps <<currentTiddler>> to each input value—a bit like the way <<currentTiddler>> changes when you nest a $list in a $list.
  • In Example #1, {{!!title}} continues to equal Draft of ‘New Tiddler 1’, which was where I took this screenshot.

I agree that it would make intuitive sense if <currentTiddler>/{!!title} in a function did refer to the input value received by that function; this would be more in line with the way the filter and subfilter operators work. But I suspect, for backwards-compatibility reasons, we’re not likely to see that change.

If you do need a current-value “placeholder” in a function (for instance, if you want to use :map itself inside a function), the pass-through operators all[] or is[] will do the trick. I demonstrated all[] in Example #3 above. It’s not technically necessary in this specific function — \function .func2() [lowercase[]] would have the same effect — but you can see the difference vs. .func.

2 Likes