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.
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().
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.
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.
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
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]
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.
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?
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.
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).