Tinderbox Forum

Help with an agent to collect notes containing sets of tags

If I want to create an agent that collects all notes containing a set of specific tags, how would I do that?

I tried this: $Tags.contains(“tag1”) & (“tag2”) but that query appears to collect all notes containing tag1, whether or not the note also contains tag2.

I’m pretty sure this would work:

$Tags.contains(“tag1”) & $Tags.contains(“tag2”)

However, I do wonder if there is a more compact version of that. Querying sets is a trickier process than querying strings. I bet @mwra has a better answer / explanation.

I’m with @derekvan’s answer here, you need to apply the same query operator each time.

Frustratingly, due to [too long to explain] you can’t use == or != against a list-based attribute (i.e. List or Set). Suffice it to say it’s a feature request at this point.

So, to match a list value of Value you must use $SomeAttr.contains("Value") or $SomeAttr.icontains("Value").

And, as you’ve now seem you need to do this for each value you wish to test. Plus, queries don’t let you use .each().

HTH :grinning:

This solves my problem. Thank you!

One possible way to make this easier depending on what you’re doing is to use multiple agents.

Let’s say you have 10 tags. If you mainly want to see matches where tag1 and some other tag also appear (e.g., tag1 & tag2; tag1 & tag3; tag1 & tag 4; …), you could make 1 agent the the Tag 1 agent:

$Tags.contains("tag1")

Name that agent something like Tag 1 Agent

Then, make a subsequent agent with this query:

inside("Tag 1 Agent") & $Tags.contains(agent)

Then, set the $Tags of that agent to the second tag you want to find (this makes it easy to change on the fly). This second agent will now have the matches you’re looking for.

(more on that “agent” designator here: https://www.acrobatfaq.com/atbref8/index/AgentQueries/Usingtheagentdesignator.html)

I’m not sure what the details of the current feature request are, but I wonder whether it looks at all feasible to implement the following pair of boolean functions:

  • all(test, list)
  • any(test, list)

Where all returns true if every item in the list satisfies the test,

and any returns true as long as (at least) one item in the list satisfies the test.

i.e. in JS and AS terms, something like the following pair(s)

JavaScript

// True if all elements of the list 
// satisfy the predicate.

// all :: (a -> Bool) -> [a] -> Bool
const all = (p, xs) => xs.every(p);

// True if any contained element satisfies the predicate.

// any :: (a -> Bool) -> [a] -> Bool
const any = (p, xs) => xs.some(p);

AppleScript

-- all :: (a -> Bool) -> [a] -> Bool
on all(p, xs)
    -- True if p holds for every value in xs
    tell mReturn(p)
        set lng to length of xs
        repeat with i from 1 to lng
            if not |λ|(item i of xs, i, xs) then return false
        end repeat
        true
    end tell
end all

-- Applied to a predicate and a list, 
-- |any| returns true if at least one element of the 
-- list satisfies the predicate.
-- any :: (a -> Bool) -> [a] -> Bool
on |any|(f, xs)
    tell mReturn(f)
        set lng to length of xs
        repeat with i from 1 to lng
            if |λ|(item i of xs) then return true
        end repeat
        false
    end tell
end |any|

-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

I meant in the sense that if someone wants this they should make their case by writing to the developer. This being a user-to-user forum, stating/asking something here isn’t making a request. Sometimes, ideas may get taken up from here, but my point is that as Eastgate is a small shop, people who take the trouble to explain their need are probably more likely to see a positive outcome. As it happens, I think this idea (per my last post) is ‘on the spike’ already having been realised some years ago.

Thanks for the code examples. In passing, the (unintended!) opacity of the code show why for non-coder users having this wrapped into the Action code would be a boon. An alternative might be if Action code had wrapper call to run the scripts without needing ScriptEditor or such. runCommand() might work - too busy thesis writing to test that (anyone?).

I guess one issue for the dev is also whether this just leads users into more CPU-sucking code use. Still, I just had a user ask me (off forum) if their query was OK … it included 3 scoping elements followed by ten .contains() (i.e. regex). All because the just wanted to test for an age, which had been stored in a Let as “age_N” and they wanted N within a certain range (because each object much relate to > person and twins would bot work with a Set-type). Lest you ask “why not use a number”, the default Number is 0 (yes, some know we can set a different doc default) and some ages were things like “four months” and the idea of using 0.33 (rough 3/12) to store the number had been overlooked.

So there’s a chick and egg here. Some of these requests arrive after one of us users has boxed ourselves in with data structure choices that now prove sub-par. Ironically, for an app that facilitates incremental formalisation and restructuring data, I regularly see people unwilling to do the latter on the justification of it being ‘more work’ despite the time wasted getting to a structural dead-end … and meanwhile the output is being requested by client/boss/spouse/whoever.

</rant> :roll_eyes:

Sure – I take that as read, I and I should have explained that it had also been copied to the developer.

wrapped into the Action code

Absolutely - that is what I am suggesting.

1 Like

point taken

In pseudo-code (or Haskell) the pattern might something like:

(given, for example xs = [55, 65, 75, 85])

59

or in JS

const xs = [55, 65, 75, 85];

    all(x => x > 50, xs)

    // -> true

    all(x => x > 80, xs)

    // -> false

    any(x => x > 80, xs)

    // -> true

    any(x => x < 40, xs)

    // -> false

and AS either:

set xs to [55, 65, 75, 85]

script below50
    on |λ|(x)
        x < 50
    end |λ|
end script

script above50
    on |λ|(x)
        x > 50
    end |λ|
end script


all(below50, xs) --> false
all(above50, xs) --> true

any(below50, xs) --> false
any(above50, xs) --> true

or:

set xs to [55, 65, 75, 85]

on below50(x)
    x < 50
end below50

on above50(x)
    x > 50
end above50


all(below50, xs) --> false
all(above50, xs) --> true

any(below50, xs) --> false
any(above50, xs) --> true

1 Like

Don’t forget intersect, which computes the intersection of two lists or sets.

Query: intersect($Tags,$MySet(agent)).size=$MySet(agent).size

This agent finds all notes that have each of the tags in the agent’s $MySet.

Side note: It seems to me that

$MyList==$Tags(agent)

$MySet == $Tags(agent)

should work fine. Does they not?

This implies that interesect is a dot operator.

intersect($Tags,$MySet(agent)).size=$MySet(agent).size

doesn’t seem to do anything. (Could be fat fingering over here, of course.)

In Attribute Browser’s query this works

$Tags.intersect("culture;clipping")

and this provides the inverse

!$Tags.intersect("culture.clipping")

No, an agent (with a value in $MySet) where all target notes have multiple $MySet values and with query:

$MySet==$MySet(agent)

finds one note… the agent. If I make a note have one $MySet value, the same as the agents, then that note is also found. IOW, == is testing the whole stored list as a string, not discrete values. Indeed that is the documented status quo as documented.

From a users standpoint, ISTM at least that == and != would test every discrete value against the test string provided. In the less likely event of wanting to test the list (for Set, in current sort order) matched a string it would make sense to convert the list to string literal and test all values. IOW, the reverse of the current logic were the equality operators match the whole list and not against individual value(s). I do realise that for large list thats a search loop per queried item and might be slow(er) in a big file but it is a task often needed and so worth waiting for rather than running a regex instead with scope for error if discrete value strings are close in characters.

[edit: FWIW, I did test the above rather than work from memory]

Suppose that note A has

MySet: important; Wisconsin

The agent agent also has

MySet: important;Wisconsin
AgentQuery: $MySet==$MySet(agent)

This agent finds one note — A. What am I missing?


Note that *list* comparison, unlike set comparison, takes order into account. Lists preserve order, and so two lists might be considered different even though they have the same elements.
$Reps="Clark;Kennedy III;Lynch;Keating…"
$SortedReps=$Reps.sort()

It seems sensible that $SortedReps!=$Reps

I have a question that is somewhat related to this.

I have created a user attribute named KeyWords that does what its name suggests. Many of my notes have associated multiple keywords to them via this attribute so I want to use the new feature ‘filtered outlines’ to locate notes containing specific combinations of keywords.

What would be the query that I should enter to filter my outline so that only the notes that contain a combination of two or more keywords show up?

I tried the following and it didn’t work:

$KeyWords.contains(“blah”) & $KeyWords.contains(“blih”)

Thanks in advance for your help. I haven’t been able to find how to do this in the available tutorial or in the aTbRef documentation.

JM

Assuming your code sample had the quote type changed when posting, your code as above works for me, so there must be something else in the mix.

Could you post a small file (or link to one) that shows the problem, i.e. minimal content enough to show the issue. In the file please add a note explaining what you expected to see and what you actually saw - please don’t assume that difference is self-evident.

If, when you make the small test file, it doesn’t show the problem then it is a good sign that there are other factors (so far unstated) in the mix.

Respectfully, this isn’t the test being discussed. The original point is about being able to use a match to a single value in a multi-value attribute. At present that is not possible. To modify your case, if the agent has the the $MySet value Wisconsin;something" and the agent “Winsconsin” A in not matched. A contains or incontains must be used and as we know contains are regex-based so use more resource, IOW at scale they slow things down.

I think the hope is that if == or != are applied to a multi-value attribute, then the for each note polled the attribute is iterated for matches. Of course, if that is as/more consuming of resource that a regex test the idea is moot. But the presumption at present is that it is not.

Put another way this isn’t about matching the exact multi-value string but matching (case-sensitively) one of possibly N values in the target list.

I hope I don’t seem to labour a point - I’m simply trying to get us all on the same hymn sheet.

I’m still not quite singing from the same hymnal, I think.

$MySet.contains(“foo”) is true if one of the elements of $MySet is “foo”

 "foo; bar" ▸ true
 "food; bar" ▸ false

A linked issue arises is I have a list (List or Set) on numbers. For example the ages of children separated from their parents. Why a list? Because the family may have several children. Now let’s assume we want to find all the children under 9 years old. With a Number attribute, we would test something like:

$Age>0 & $Age<9

For a list-based attribute we have to use 8 AND-joined .contains queries.

I accept that if one can see the problem far enough ahead their may be alternative ways to store the data, e.g. $AgeChild1, $AgeChild2, etc. - though this too may not scale well.

Which leads around to saying that being able to easily query list-based attribute with == and != operators would be a boon (offer a different operator that achieves the same effect).