Inverse of `contains` operator: check if value is one of a list

I’m looking for a filter operator that, it seems, probably doesn’t exist.

You can think of it as the inverse of a contains operator. contains takes as input a list of items and has a parameter. If any one of the items matches the parameter, then contains succeeds.

I want one that takes a single item a input (likely used in a :filter call) and has a parameter that captures a list of values. It should succeed if that parameter matches any on of the items. Perhaps it might be called contained-in or some such.

Does this operator exist?


Context:

I have a small wiki populated with voter information for my small town. The main point of it is to be able to collect email lists to plug into the “To” box of an email client. I have an <<email>> macro that does this based upon a filter string supplied. For example, these are working queries:

  • All Democrats: <<email "[tag[Person]tag[Democratic]]">>
  • Democratic Women: <<email "[tag[Person]tag[Democratic]gender[Female]]">>
  • Everyone 75 and Above: <<email "[tag[Person]] :filter[get[age]compare:number:gteq[75]]">>
  • Unaffiliated Women on Boston Hill: <<email "[tag[Person]party[Unaffiliated]gender[Female]street[Boston Hill]]">>

They use this macro:

\define email(query)
<$let by-last ="[{!!last-name}] [{!!first-name}] +[join[, ]]">

<$vars txt="""<$list filter="$query$ :filter[has[email]] +[sortsub:string<by-last>]" counter="counter">{{!!title}} <$text text={{{ [[<]] [<currentTiddler>get[email]] [[>]] +[join[]] }}} /><$text text={{{ [<counter-last>!match[yes]then[, ]]}}} /></$list>""">
<$wikify name="cliptxt" output="formattedtext" text=<<txt>>>
<$macrocall $name="copy-to-clipboard" src={{{ [<cliptxt>trim[]] }}}/><br/>
<h4>Found: <$text text={{{ $query$ :filter[has[email]] +[count[]] }}} /></h4>
<$list filter="$query$ :filter[has[email]] +[sortsub:string<by-last>]" counter="counter">
   <$link /> &lt;<a href={{{ [[mailto:]] [<currentTiddler>get[title]] [[<]] [<currentTiddler>get[email]] [[>]] +[join[]] }}}><$text text={{{ [<currentTiddler>get[email]] }}} /></a>&gt;<$text text={{{ [<counter-last>!match[yes]then[, ]]}}} /><br/>
</$list>
</$wikify>

\end

It will yield something output like this:

copy to clipboard

Found: 3

Yogi Bear <yogi@jellystone.gov>,
Fred Flintstone <fred@bedrock.com>,
Scooby Doo <scoob@mysteryinc.org>,

where the copy-to-clipboard link will put this in the system clipboard:

Yogi Bear <yogi@jellystone.gov>, Fred Flintstone <fred@bedrock.com>, Scooby Doo <scoob@mysteryinc.org>.

The goal now is to make a query generator to customize such lists. I think I know how to do most of this, but I’m stuck at trying to offer the choice of multiple parties or multiple streets. I can do the former because party will start my query, and this will work:

  • Democrats and Unaffiliated Under 30: <<email "[tag[Person]tag[Democratic]] [tag[Person]tag[Unaffiliated]] :filter[get[age]compare:number:lt[30]]">>,

by starting with multiple filter expressions for the different (exclusive) tags. But the multiple streets will be a problem. If I had a filter operator like “contained-in”, then I might be able to write something like:

  • All Women on Jurovaty, Townsend or Gilead: <<email "[tag[Person]gender[Female] :filter[get[street]contained-in:Jurovaty,Townsend,Gilead[]]" >>

Am I missing an existing filter? Is this something I will have to write myself? If so, are there other filters with an indefinite number of parameters I could use for a model?

Or at a higher level, are there other suggestions for how to do this? I want users to be able to generate and save queries, but there are probably many ways to do this. Other suggestions?

My first instinct is to want to use the intersection prefix but that would likely be tricky in this context due to the need for creating extra variables.

There is almost certainly a cleaner solution but I am bit rushed and this is the first thing that pops to mind:

[tag[Person]gender[Female]] :filter[[Jurovaty,Townsend,Gilead,NONEXISTENTSTREET]split[,]after{!!street}]

This takes advantage of the fact that if the street matches, there will always be a result due to the placeholder added at the end.

Perhaps you could create a custom operator somewhat along these lines (all the code I have posted here is untested):

\function intersects.with(token) [all[]] :filter[<currentTiddler>match<token>]

[tag[Person]gender[Female]] :filter[[Jurovaty,Townsend,Gilead]intersects.with{!!street}]

Maybe I’m not understanding this correctly, but couldn’t you just use:

:filter[search:street:some[Jurovaty Townsend Gilead]]

I must be misunderstanding the intention of this, so I apologize if that is the case

1 Like

Ahh, I’ve never used after, but the name certainly didn’t make me think about containment. This works perfectly.

No, I think you understand it quite well. This solution also works perfectly.

I have also never used “search”. I’m getting more sophisticated in my use of TW; six months ago, I couldn’t possibly have attempted the more complex data-oriented wikis I’m creating now. But there are huge swaths of TW knowledge I’m still missing. I’m going to have to spend time on all the existing operators. I read several times through the list of operators, and found nothing that made sense. Here we have two very different solutions, both working fine with existing operators!

Perhaps you could create a custom operator somewhat along these lines (all the code I have posted here is untested):

\function intersects.with(token) [all[]] :filter[<currentTiddler>match<token>]

[tag[Person]gender[Female]] :filter[[Jurovaty,Townsend,Gilead]intersects.with{!!street}]

This one did not work for me. I still haven’t spent enough time with the 5.3+ features, so I don’t have the understanding to try to debug why this fails. But I will probably spend the time to understand it and get it working because I can see ways this would make my whole query-builder cleaner and easier to write.

@saqimtiaz & @CasperBooWisdom: Thank you both very much for your help!

1 Like

I forgot to split the comma separated streets so that is one obvious error corrected:

\function intersects.with(token) [all[]] :filter[<currentTiddler>match<token>]

[tag[Person]gender[Female]] :filter[[Jurovaty,Townsend,Gilead]split[,]intersects.with{!!street}]
1 Like

And that fixed it! Thank you again.

If you have a minute could you explain – or point me toward documentation that explains – why exactly the [all[]] is necessary here and what it would resolve to here? And if you don’t have a minute: thank you for all your help already, and I’m sure I’ll eventually figure it out.

Hi Scott, inside the custom operator, all[] returns the input titles provided to the custom operator. In this case, that is the street names that we have converted to a list with split[]. We can then check if any matches the token.

From https://tiddlywiki.com/#all%20Operator:

As a special case, if the parameter is empty, the output is simply a copy of the input.

This simplified version should also work now that I think about it:

[tag[Person]gender[Female]] :filter[[Jurovaty,Townsend,Gilead]split[,]match{!!street}]

Ahh, I should have noticed that before now! I think it explains some other behavior that has confused me over recent months.

It does, and it looks to me to be the easiest one to build.

Again, thanks for all the help in this thread and countless others!

Now I’m off to see if I can create my query builder!

A few notes should it help

I just want to add depending on the outcome you are after sometimes using nested list widgets rather than compound filters is more helpful because you can retain and access the variable names in each level of the nested list widgets.

  • as per your OT we often need to choose between contains and search opperators unless we are only testing titles in which case we can use match, prefix, suffix etc…
  • The v5.3.x functions and custom operators are quite revolutionary allowing more semantic filters and larger compound filters even more than the subfilter operator did.
  • As soon as you start to construct filters that involves multiple steps or nesting you most likely need one of the "filter run prefix"s

Enjoying the journey?

That’s a technique I learned from you and have used successfully in a number of cases. Unfortunately, it won’t work here, as the string representing the filter is supplied to a macro for further interpretation.

I don’t follow this one, maybe because I’ve rarely used either contains or search.

Yes, I’m thinking that these will simplify the filters I actually need to create, which should simplify everything. I’m off to try it today.

Yes, I’m using those a great deal already.

So far, very much, thank you! :smile:

Thank you for you advice. This is very helpful!