Sorting lists in a list using AppleScriptObjC

asobjc

(Vincent Pelletier) #1

Hi,

i know how to sort that listOfLists below by choosen parameters (sort by “p1” then by “p2”), thanks to Shane Stanley:
set listOfLists to {{p1:“a”, p2:“b”}, {p1:“b”, p2:“b”}, {p1:“a”, p2:“a”}, {p1:“b”, p2:“a”}}

But how do i sort that same listOfLists without labeled parameters, just by referencing indexes (sort by items of index 1 then by items of index 2)?
set listOfLists to {{“a”, “b”}, {“b”, “b”}, {“a”, “a”}, {“b”, “a”}}

Thanks for your help!
Vincent


#2

FWIW, this is what I do,
(Composing from a set of reusable generic functions):

use framework "Foundation"
use scripting additions


on run
    set listOfLists to {{"a", "b"}, {"b", "b"}, {"a", "a"}, {"b", "a"}}
    
    sortOn({{fst, true}, {snd, true}}, listOfLists)
    
    -- Or, for descending sort by first items then second items:
    sortOn({{fst, false}, {snd, false}}, listOfLists)
end run


-- REUSABLE GENERIC FUNCTIONS ---------------------------------------------------------

-- comparing :: (a -> b) -> (a -> a -> Ordering)
on comparing(f)
    script
        on |λ|(a, b)
            tell mReturn(f)
                set fa to |λ|(a)
                set fb to |λ|(b)
                if fa < fb then
                    -1
                else if fa > fb then
                    1
                else
                    0
                end if
            end tell
        end |λ|
    end script
end comparing

-- concatMap :: (a -> [b]) -> [a] -> [b]
on concatMap(f, xs)
    set acc to {}
    tell mReturn(f)
        repeat with x in xs
            set acc to acc & |λ|(contents of x)
        end repeat
    end tell
    return acc
end concatMap

-- flatten :: Tree a -> [a]
on flatten(t)
    if class of t is list then
        my concatMap(my flatten, t)
    else
        t
    end if
end flatten

-- foldr :: (a -> b -> b) -> b -> [a] -> b
on foldr(f, startValue, xs)
    tell mReturn(f)
        set v to startValue
        set lng to length of xs
        repeat with i from lng to 1 by -1
            set v to |λ|(item i of xs, v, i, xs)
        end repeat
        return v
    end tell
end foldr

-- fst :: (a, b) -> a
on fst(tpl)
    if class of tpl is record then
        |1| of tpl
    else
        item 1 of tpl
    end if
end fst

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

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

-- SORT ON ANY PROPERTY (VALUES OF RECORD KEYS, 
-- STRING LENGTH, DERIVED PROPERTIES)

-- ARGUMENTS:

--    xs:  List of items to be sorted. 
--          (The items can be records, lists, or simple values).
--
--    f:    A single (a -> b) function (Applescript handler),
--          or a list of such functions.
--          if the argument is a list, any function can 
--          optionally be followed by a bool. 
--          (False -> descending sort)
--
--          (Subgrouping in the list is optional and ignored)
--          Each function (Item -> Value) in the list should 
--          take an item (of the type contained by xs) 
--          as its input and return a simple orderable value 
--          (Number, String, or Date).
--
--          The sequence of key functions and optional 
--          direction bools defines primary to N-ary sort keys.

-- sortOn :: Ord b => ((a -> b) | [((a -> b), Bool)])  -> [a] -> [a]
on sortOn(f, xs)
    script keyBool
        on |λ|(x, a)
            if class of x is boolean then
                {asc:x, lst:lst of a}
            else
                {asc:true, lst:({{x, asc of a}} & lst of a)}
            end if
        end |λ|
    end script
    set {fs, bs} to unzip(lst of foldr(keyBool, {asc:true, lst:{}}, flatten({f})))
    
    set intKeys to length of fs
    set ca to current application
    script dec
        property gs : map(my mReturn, fs)
        on |λ|(x)
            set nsDct to (ca's NSMutableDictionary's ¬
                dictionaryWithDictionary:{val:x})
            repeat with i from 1 to intKeys
                (nsDct's setValue:((item i of gs)'s |λ|(x)) ¬
                    forKey:(character id (96 + i)))
            end repeat
            nsDct as record
        end |λ|
    end script
    
    script descrip
        on |λ|(bool, i)
            ca's NSSortDescriptor's ¬
                sortDescriptorWithKey:(character id (96 + i)) ¬
                    ascending:bool
        end |λ|
    end script
    
    script undec
        on |λ|(x)
            val of x
        end |λ|
    end script
    
    map(undec, ((ca's NSArray's arrayWithArray:map(dec, xs))'s ¬
        sortedArrayUsingDescriptors:map(descrip, bs)) as list)
end sortOn

-- snd :: (a, b) -> b
on snd(tpl)
    if class of tpl is record then
        |2| of tpl
    else
        item 2 of tpl
    end if
end snd

-- unzip :: [(a,b)] -> ([a],[b])
on unzip(xys)
    set xs to {}
    set ys to {}
    repeat with xy in xys
        set {x, y} to xy
        set end of xs to x
        set end of ys to y
    end repeat
    return {xs, ys}
end unzip

(Vincent Pelletier) #3

Wow!
It worked, but i can’t say i understand it all :wink:
I have code analysis to make this week-end!
If i may, how to adapt it so that i could sort lists of, say, 5 items by the second and fourth items?


(Shane Stanley) #4

There’s no standard Objective-C method. Once you start introducing records with list of any size, performance becomes dreadful.

You can use my BridgePlus library:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use theLib : script "BridgePlus" version "1.3.2"

set listOfLists to {{"a", "b"}, {"b", "b"}, {"a", "a"}, {"b", "a"}}
theLib's sublistsIn:listOfLists sortedByIndexes:{1, 2} ascending:{true, true} sortTypes:{"compare:", "compare:"}

Otherwise hunt down one of @NigelGarvey’s sorting handlers.


(Vincent Pelletier) #5

OK Thanks, it’s incredibly useful!
I bought recently your book “Everyday ApplescriptObjC”, reading the first chapters 2 times, playing with your exemples, beginning to understand how to harness the power of it, but i have a long road ahead to master it.
Now, your BridgePlus library is spoiling me as a lot of the hard work is already done.


#6

You just need need to add a function that returns the nth item of a list:

use framework "Foundation"
use scripting additions

-- item4 :: [a] -> Int -> a
on item4(xs)
    item 4 of xs
end item4

on run
    set listOfLists to {{"a", "b", "y", "q"}, {"b", "b", "y", "q"}, {"a", "a", "x", "p"}, {"b", "a", "x", "m"}}
    
    sortOn({{snd, true}, {item4, true}}, listOfLists)
    
    -- Or, for **descending*** sort by second items then fourth items:
    --sortOn({{snd, false}, {item4, false}}, listOfLists)
end run


-- REUSABLE GENERIC FUNCTIONS ---------------------------------------------------------

-- comparing :: (a -> b) -> (a -> a -> Ordering)
on comparing(f)
    script
        on |λ|(a, b)
            tell mReturn(f)
                set fa to |λ|(a)
                set fb to |λ|(b)
                if fa < fb then
                    -1
                else if fa > fb then
                    1
                else
                    0
                end if
            end tell
        end |λ|
    end script
end comparing

-- concatMap :: (a -> [b]) -> [a] -> [b]
on concatMap(f, xs)
    set acc to {}
    tell mReturn(f)
        repeat with x in xs
            set acc to acc & |λ|(contents of x)
        end repeat
    end tell
    return acc
end concatMap

-- flatten :: Tree a -> [a]
on flatten(t)
    if class of t is list then
        my concatMap(my flatten, t)
    else
        t
    end if
end flatten

-- foldr :: (a -> b -> b) -> b -> [a] -> b
on foldr(f, startValue, xs)
    tell mReturn(f)
        set v to startValue
        set lng to length of xs
        repeat with i from lng to 1 by -1
            set v to |λ|(item i of xs, v, i, xs)
        end repeat
        return v
    end tell
end foldr

-- fst :: (a, b) -> a
on fst(tpl)
    if class of tpl is record then
        |1| of tpl
    else
        item 1 of tpl
    end if
end fst

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

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

-- SORT ON ANY PROPERTY (VALUES OF RECORD KEYS, 
-- STRING LENGTH, DERIVED PROPERTIES)

-- ARGUMENTS:

--    xs:  List of items to be sorted. 
--          (The items can be records, lists, or simple values).
--
--    f:    A single (a -> b) function (Applescript handler),
--          or a list of such functions.
--          if the argument is a list, any function can 
--          optionally be followed by a bool. 
--          (False -> descending sort)
--
--          (Subgrouping in the list is optional and ignored)
--          Each function (Item -> Value) in the list should 
--          take an item (of the type contained by xs) 
--          as its input and return a simple orderable value 
--          (Number, String, or Date).
--
--          The sequence of key functions and optional 
--          direction bools defines primary to N-ary sort keys.

-- sortOn :: Ord b => ((a -> b) | [((a -> b), Bool)])  -> [a] -> [a]
on sortOn(f, xs)
    script keyBool
        on |λ|(x, a)
            if class of x is boolean then
                {asc:x, lst:lst of a}
            else
                {asc:true, lst:({{x, asc of a}} & lst of a)}
            end if
        end |λ|
    end script
    set {fs, bs} to unzip(lst of foldr(keyBool, {asc:true, lst:{}}, flatten({f})))
    
    set intKeys to length of fs
    set ca to current application
    script dec
        property gs : map(my mReturn, fs)
        on |λ|(x)
            set nsDct to (ca's NSMutableDictionary's ¬
                dictionaryWithDictionary:{val:x})
            repeat with i from 1 to intKeys
                (nsDct's setValue:((item i of gs)'s |λ|(x)) ¬
                    forKey:(character id (96 + i)))
            end repeat
            nsDct as record
        end |λ|
    end script
    
    script descrip
        on |λ|(bool, i)
            ca's NSSortDescriptor's ¬
                sortDescriptorWithKey:(character id (96 + i)) ¬
                    ascending:bool
        end |λ|
    end script
    
    script undec
        on |λ|(x)
            val of x
        end |λ|
    end script
    
    map(undec, ((ca's NSArray's arrayWithArray:map(dec, xs))'s ¬
        sortedArrayUsingDescriptors:map(descrip, bs)) as list)
end sortOn

-- snd :: (a, b) -> b
on snd(tpl)
    if class of tpl is record then
        |2| of tpl
    else
        item 2 of tpl
    end if
end snd

-- unzip :: [(a,b)] -> ([a],[b])
on unzip(xys)
    set xs to {}
    set ys to {}
    repeat with xy in xys
        set {x, y} to xy
        set end of xs to x
        set end of ys to y
    end repeat
    return {xs, ys}
end unzip

#7

Or, generalising a little for sorting by the nth item:

use framework "Foundation"
use scripting additions

-- nth ::  Int -> [a] -> a
script nth
    on |λ|(i)
        script
            on |λ|(xs)
                item i of xs
            end |λ|
        end script
    end |λ|
end script

on run
    
    set listOfLists to {{"a", "b", "y", "q"}, {"b", "b", "y", "q"}, {"a", "a", "x", "p"}, {"b", "a", "x", "m"}}
    
    sortOn({|λ|(2) of nth, |λ|(4) of nth}, listOfLists)
    
    -- Or, for **descending*** sort by second items then fourth items:
    --sortOn({{|λ|(2) of nth, false}, {|λ|(4) of nth, false}}, listOfLists)
end run


-- REUSABLE GENERIC FUNCTIONS ---------------------------------------------------------

-- comparing :: (a -> b) -> (a -> a -> Ordering)
on comparing(f)
    script
        on |λ|(a, b)
            tell mReturn(f)
                set fa to |λ|(a)
                set fb to |λ|(b)
                if fa < fb then
                    -1
                else if fa > fb then
                    1
                else
                    0
                end if
            end tell
        end |λ|
    end script
end comparing

-- concatMap :: (a -> [b]) -> [a] -> [b]
on concatMap(f, xs)
    set acc to {}
    tell mReturn(f)
        repeat with x in xs
            set acc to acc & |λ|(contents of x)
        end repeat
    end tell
    return acc
end concatMap

-- curry :: ((a, b) -> c) -> a -> b -> c
on curry(f)
    script
        on |λ|(a)
            script
                on |λ|(b)
                    |λ|(a, b) of mReturn(f)
                end |λ|
            end script
        end |λ|
    end script
end curry

-- flatten :: Tree a -> [a]
on flatten(t)
    if class of t is list then
        my concatMap(my flatten, t)
    else
        t
    end if
end flatten

-- foldr :: (a -> b -> b) -> b -> [a] -> b
on foldr(f, startValue, xs)
    tell mReturn(f)
        set v to startValue
        set lng to length of xs
        repeat with i from lng to 1 by -1
            set v to |λ|(item i of xs, v, i, xs)
        end repeat
        return v
    end tell
end foldr

-- fst :: (a, b) -> a
on fst(tpl)
    if class of tpl is record then
        |1| of tpl
    else
        item 1 of tpl
    end if
end fst

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

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

-- SORT ON ANY PROPERTY (VALUES OF RECORD KEYS, 
-- STRING LENGTH, DERIVED PROPERTIES)

-- ARGUMENTS:

--    xs:  List of items to be sorted. 
--          (The items can be records, lists, or simple values).
--
--    f:    A single (a -> b) function (Applescript handler),
--          or a list of such functions.
--          if the argument is a list, any function can 
--          optionally be followed by a bool. 
--          (False -> descending sort)
--
--          (Subgrouping in the list is optional and ignored)
--          Each function (Item -> Value) in the list should 
--          take an item (of the type contained by xs) 
--          as its input and return a simple orderable value 
--          (Number, String, or Date).
--
--          The sequence of key functions and optional 
--          direction bools defines primary to N-ary sort keys.

-- sortOn :: Ord b => ((a -> b) | [((a -> b), Bool)])  -> [a] -> [a]
on sortOn(f, xs)
    script keyBool
        on |λ|(x, a)
            if class of x is boolean then
                {asc:x, lst:lst of a}
            else
                {asc:true, lst:({{x, asc of a}} & lst of a)}
            end if
        end |λ|
    end script
    set {fs, bs} to unzip(lst of foldr(keyBool, {asc:true, lst:{}}, flatten({f})))
    
    set intKeys to length of fs
    set ca to current application
    script dec
        property gs : map(my mReturn, fs)
        on |λ|(x)
            set nsDct to (ca's NSMutableDictionary's ¬
                dictionaryWithDictionary:{val:x})
            repeat with i from 1 to intKeys
                (nsDct's setValue:((item i of gs)'s |λ|(x)) ¬
                    forKey:(character id (96 + i)))
            end repeat
            nsDct as record
        end |λ|
    end script
    
    script descrip
        on |λ|(bool, i)
            ca's NSSortDescriptor's ¬
                sortDescriptorWithKey:(character id (96 + i)) ¬
                    ascending:bool
        end |λ|
    end script
    
    script undec
        on |λ|(x)
            val of x
        end |λ|
    end script
    
    map(undec, ((ca's NSArray's arrayWithArray:map(dec, xs))'s ¬
        sortedArrayUsingDescriptors:map(descrip, bs)) as list)
end sortOn

-- snd :: (a, b) -> b
on snd(tpl)
    if class of tpl is record then
        |2| of tpl
    else
        item 2 of tpl
    end if
end snd

-- unzip :: [(a,b)] -> ([a],[b])
on unzip(xys)
    set xs to {}
    set ys to {}
    repeat with xy in xys
        set {x, y} to xy
        set end of xs to x
        set end of ys to y
    end repeat
    return {xs, ys}
end unzip

#8

Pff :slight_smile:

If the critical element is ever genuinely just performance, or lists of significant length, we probably wouldn’t be using Applescript anyway – doing the automation through JS would work better.


(Shane Stanley) #9

FWIW, I suspect your “we” is rather misplaced hereabouts.


#10

It takes about 3.1 seconds here to create and reverse-sort 1000 records (script below).

Useful for many purposes, I think. A lot of sorting (in the context of desktop scripting) involves much smaller lists.

use framework "Foundation"
use scripting additions

on fromJust(mb)
    |Just| of mb
end fromJust

on run
    
    set listOfLists to map(Just, enumFromToInt(1, 1000))
    
    set xs to sortOn({fromJust, false}, listOfLists)
    
    item 1 of xs
end run


-- REUSABLE GENERIC FUNCTIONS ---------------------------------------------------------

-- Just :: a -> Just a
on Just(x)
    {|type|:"Maybe", Nothing:false, Just:x}
end Just

-- comparing :: (a -> b) -> (a -> a -> Ordering)
on comparing(f)
    script
        on |λ|(a, b)
            tell mReturn(f)
                set fa to |λ|(a)
                set fb to |λ|(b)
                if fa < fb then
                    -1
                else if fa > fb then
                    1
                else
                    0
                end if
            end tell
        end |λ|
    end script
end comparing

-- concatMap :: (a -> [b]) -> [a] -> [b]
on concatMap(f, xs)
    set acc to {}
    tell mReturn(f)
        repeat with x in xs
            set acc to acc & |λ|(contents of x)
        end repeat
    end tell
    return acc
end concatMap

-- curry :: ((a, b) -> c) -> a -> b -> c
on curry(f)
    script
        on |λ|(a)
            script
                on |λ|(b)
                    |λ|(a, b) of mReturn(f)
                end |λ|
            end script
        end |λ|
    end script
end curry

-- enumFromToInt :: Int -> Int -> [Int]
on enumFromToInt(m, n)
    if m ≤ n then
        set lst to {}
        repeat with i from m to n
            set end of lst to i
        end repeat
        return lst
    else
        return {}
    end if
end enumFromToInt

-- flatten :: Tree a -> [a]
on flatten(t)
    if class of t is list then
        my concatMap(my flatten, t)
    else
        t
    end if
end flatten

-- foldr :: (a -> b -> b) -> b -> [a] -> b
on foldr(f, startValue, xs)
    tell mReturn(f)
        set v to startValue
        set lng to length of xs
        repeat with i from lng to 1 by -1
            set v to |λ|(item i of xs, v, i, xs)
        end repeat
        return v
    end tell
end foldr

-- fst :: (a, b) -> a
on fst(tpl)
    if class of tpl is record then
        |1| of tpl
    else
        item 1 of tpl
    end if
end fst

-- id :: a -> a
on |id|(x)
    x
end |id|

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

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

-- SORT ON ANY PROPERTY (VALUES OF RECORD KEYS, 
-- STRING LENGTH, DERIVED PROPERTIES)

-- ARGUMENTS:

--    xs:  List of items to be sorted. 
--          (The items can be records, lists, or simple values).
--
--    f:    A single (a -> b) function (Applescript handler),
--          or a list of such functions.
--          if the argument is a list, any function can 
--          optionally be followed by a bool. 
--          (False -> descending sort)
--
--          (Subgrouping in the list is optional and ignored)
--          Each function (Item -> Value) in the list should 
--          take an item (of the type contained by xs) 
--          as its input and return a simple orderable value 
--          (Number, String, or Date).
--
--          The sequence of key functions and optional 
--          direction bools defines primary to N-ary sort keys.

-- sortOn :: Ord b => ((a -> b) | [((a -> b), Bool)])  -> [a] -> [a]
on sortOn(f, xs)
    script keyBool
        on |λ|(x, a)
            if class of x is boolean then
                {asc:x, lst:lst of a}
            else
                {asc:true, lst:({{x, asc of a}} & lst of a)}
            end if
        end |λ|
    end script
    set {fs, bs} to unzip(lst of foldr(keyBool, {asc:true, lst:{}}, flatten({f})))
    
    set intKeys to length of fs
    set ca to current application
    script dec
        property gs : map(my mReturn, fs)
        on |λ|(x)
            set nsDct to (ca's NSMutableDictionary's ¬
                dictionaryWithDictionary:{val:x})
            repeat with i from 1 to intKeys
                (nsDct's setValue:((item i of gs)'s |λ|(x)) ¬
                    forKey:(character id (96 + i)))
            end repeat
            nsDct as record
        end |λ|
    end script
    
    script descrip
        on |λ|(bool, i)
            ca's NSSortDescriptor's ¬
                sortDescriptorWithKey:(character id (96 + i)) ¬
                    ascending:bool
        end |λ|
    end script
    
    script undec
        on |λ|(x)
            val of x
        end |λ|
    end script
    
    map(undec, ((ca's NSArray's arrayWithArray:map(dec, xs))'s ¬
        sortedArrayUsingDescriptors:map(descrip, bs)) as list)
end sortOn

-- snd :: (a, b) -> b
on snd(tpl)
    if class of tpl is record then
        |2| of tpl
    else
        item 2 of tpl
    end if
end snd

-- unzip :: [(a,b)] -> ([a],[b])
on unzip(xys)
    set xs to {}
    set ys to {}
    repeat with xy in xys
        set {x, y} to xy
        set end of xs to x
        set end of ys to y
    end repeat
    return {xs, ys}
end unzip


(Jim Underwood) #11

@vpell,
I have found Shane’s BP lib to be very fast.

Just ran this test on my iMac-27 Late 2015 model:

Number of items: 20,268
Dual Sort: modDateList, noteLinkList
set {modDateList, noteLinkList} to my sortMultiLists({modDateList, noteLinkList}, {"ASC", "ASC"})
Elapsed Time: 2.122 sec

Each list was a simple list of items:
modDateList: list of dates
noteLinkList: list of strings (URL ~20 characters ea)

Here’s my handler that uses BridgePlus Lib:

on sortMultiLists(pListOfLists, pSortDirList)
  (*
    REQUIRES:
      use framework "Foundation"
      use BPLib : script "BridgePlus"
  *)
  local rowsList, listCount, sortByList, iL, oSort
  
  --- Setup the Sort ---
  set rowsList to BPLib's colsToRowsIn:pListOfLists
  
  set listCount to count of pListOfLists
  set sortByList to {}
  
  --- Sort Order by List as Passed in pListOfLists ---
  
  repeat with iL from 1 to listCount
    set end of sortByList to iL
  end repeat
  
  --- Convert Text Sort Direction to Boolean ---
  --   (true means ascending)
  
  repeat with oSort in pSortDirList
    set contents of oSort to ((oSort as text) starts with "ASC")
  end repeat
  
  --- Do the Sort
  set rowsList to BPLib's sublistsIn:rowsList sortedByIndexes:sortByList ascending:pSortDirList sortTypes:{}
  
  --- Get Sort Results ---
  set pListOfLists to BPLib's colsToRowsIn:rowsList
  
  return pListOfLists
end sortMultiLists

In case anyone is wondering, the source data came from my Evernote account, which had 20,268 Notes. I also used the BPLib’s listByFullyFlattening:pList prior to the sort. It also is very fast.

Total Time: 5 sec (SD7)
Get three properties of 20,268 Notes
Flatten all 3 lists
Sort 2 lists together
Get one Note using noteLink of oldest note.

Many thanks, Shane. Could not have done this without you and your great BridgePlus lib.