Sorting anything with Execute AppleScript actions

Well, perhaps worth trying, though Applescript memory allocations are quite constrained. AppleScript makes a serviceable penknife for a picnic or lunch-break, but after a few hundred records I would personally be reaching for a different instrument.

A modified function below with slightly different arguments, ( and one additional generic helper function –unzip )

sortOn(f, xs) has two arguments – reading from right to left:

  1. xs a list of items to be sorted. (The items can be records, lists, or simple values).
  2. f a single function – an Applescript handler of type (Item -> simple orderable value),
    or a list of such functions.
    • If the f argument is a list, any function in it can optionally be followed by a Bool.
      (false means descending sort).
    • Any subgrouping {sub-bracketing} in the list is optional and ignored.
    • The sequence of key functions and optional direction bools defines primary to N-ary sort keys.

The examples below are the same as before with the exception of the last.

The last example sorts by primary, secondary and tertiary keys:

sortOn({country, {capital, false}, {population, false}}, cityData())
  1. The primary key (ascending String) is the name of the country, so Bangladesh comes first, followed by three records in which the country is China
  2. the secondary key is a boolean value for capital (descending Bool, true before false) so Beijing, which is the capital of China, comes before Shanghai and Guangzhou
  3. the tertiary key is population (descending Real), so Shanghai, with a larger population, comes before Guangzhou.
use framework "Foundation"
use scripting additions

-- Ver 0.2 Rob Trew 2017

-- 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, fbs:fbs of a}
            else
                {asc:true, fbs:({{x, asc of a}} & fbs of a)}
            end if
        end |λ|
    end script
    set {fs, bs} to unzip(fbs of foldr(keyBool, ¬
        {asc:true, fbs:{}}, 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


-- TESTS ------------------------------------------------------------------
-- cityData :: () -> [Record]
on cityData()
    {{city:"Shanghai", pop:24.3, country:"China", capital:false}, ¬
        {city:"Beijing", pop:21.5, country:"China", capital:true}, ¬
        {city:"Delhi", pop:11.0, country:"India", capital:true}, ¬
        {city:"Lagos", pop:16.0, country:"Nigeria", capital:true}, ¬
        {city:"Karachi", pop:14.9, country:"Pakistan", capital:false}, ¬
        {city:"Dhaka", pop:14.5, country:"Bangladesh", capital:true}, ¬
        {city:"Guangzhou", pop:14.0, country:"China", capital:false}, ¬
        {city:"Istanbul", pop:14.0, country:"Turkey", capital:false}, ¬
        {city:"Tokyo", pop:13.5, country:"Japan", capital:true}}
end cityData

-- population :: Record -> Real
on population(x)
    pop of x
end population

-- country :: Record -> String
on country(x)
    country of x
end country

-- capital :: Record -> Bool
on capital(x)
    capital of x
end capital

-- greekAlphabet :: () -> [String]
on greekAlphabet()
    ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", ¬
        "eta", "theta", "iota", "kappa", "lambda", "mu"]
end greekAlphabet

-- stringLength :: String -> Int
on stringLength(s)
    length of s
end stringLength

-- romanAlpha :: String -> String
on romanAlpha(x)
    x
end romanAlpha

on run
    
    sortOn(stringLength, greekAlphabet())
    
    --> {"mu", "eta", "beta", "zeta", "iota", "alpha", "gamma", 
    --  "delta", "theta", "kappa", "lambda", "epsilon"}
    
    sortOn({stringLength, false}, greekAlphabet())
    
    --> {"epsilon", "lambda", "alpha", "gamma", "delta", "theta", 
    --   "kappa", "beta", "zeta", "iota", "eta", "mu"}
    
    sortOn(romanAlpha, greekAlphabet())
    
    --> {"alpha", "beta", "delta", "epsilon", "eta", "gamma", 
    --   "iota", "kappa", "lambda", "mu", "theta", "zeta"}
    
    sortOn({romanAlpha, false}, greekAlphabet())
    
    --> {"zeta", "theta", "mu", "lambda", "kappa", "iota", 
    --   "gamma", "eta", "epsilon", "delta", "beta", "alpha"}
    
    sortOn(population, cityData())
    
    --> {{capital:true, country:"India", city:"Delhi", pop:11.0}, 
    -- {capital:true, country:"Japan", city:"Tokyo", pop:13.5}, 
    -- {capital:false, country:"China", city:"Guangzhou", pop:14.0}, 
    -- {capital:false, country:"Turkey", city:"Istanbul", pop:14.0}, 
    -- {capital:true, country:"Bangladesh", city:"Dhaka", pop:14.5}, 
    -- {capital:false, country:"Pakistan", city:"Karachi", pop:14.9}, 
    -- {capital:true, country:"Nigeria", city:"Lagos", pop:16.0}, 
    -- {capital:true, country:"China", city:"Beijing", pop:21.5}, 
    -- {capital:false, country:"China", city:"Shanghai", pop:24.3}}
    
    sortOn({population, false}, cityData())
    
    --> {{capital:false, country:"China", city:"Shanghai", pop:24.3}, 
    -- {capital:true, country:"China", city:"Beijing", pop:21.5}, 
    -- {capital:true, country:"Nigeria", city:"Lagos", pop:16.0}, 
    -- {capital:false, country:"Pakistan", city:"Karachi", pop:14.9}, 
    -- {capital:true, country:"Bangladesh", city:"Dhaka", pop:14.5}, 
    -- {capital:false, country:"China", city:"Guangzhou", pop:14.0}, 
    -- {capital:false, country:"Turkey", city:"Istanbul", pop:14.0}, 
    -- {capital:true, country:"Japan", city:"Tokyo", pop:13.5}, 
    -- {capital:true, country:"India", city:"Delhi", pop:11.0}}
    
    sortOn(country, cityData())
    
    --> {{capital:true, country:"Bangladesh", city:"Dhaka", pop:14.5}, 
    -- {capital:false, country:"China", city:"Shanghai", pop:24.3}, 
    -- {capital:true, country:"China", city:"Beijing", pop:21.5}, 
    -- {capital:false, country:"China", city:"Guangzhou", pop:14.0}, 
    -- {capital:true, country:"India", city:"Delhi", pop:11.0}, 
    -- {capital:true, country:"Japan", city:"Tokyo", pop:13.5}, 
    -- {capital:true, country:"Nigeria", city:"Lagos", pop:16.0}, 
    -- {capital:false, country:"Pakistan", city:"Karachi", pop:14.9}, 
    -- {capital:false, country:"Turkey", city:"Istanbul", pop:14.0}}
    
    sortOn({country, {capital, false}, {population, false}}, cityData())
    
    --> {{capital:true, country:"Bangladesh", city:"Dhaka", pop:14.5}, 
    -- {capital:true, country:"China", city:"Beijing", pop:21.5}, 
    -- {capital:false, country:"China", city:"Shanghai", pop:24.3}, 
    -- {capital:false, country:"China", city:"Guangzhou", pop:14.0}, 
    -- {capital:true, country:"India", city:"Delhi", pop:11.0}, 
    -- {capital:true, country:"Japan", city:"Tokyo", pop:13.5}, 
    -- {capital:true, country:"Nigeria", city:"Lagos", pop:16.0}, 
    -- {capital:false, country:"Pakistan", city:"Karachi", pop:14.9}, 
    -- {capital:false, country:"Turkey", city:"Istanbul", pop:14.0}}
end run


-- GENERIC FUNCTIONS ---------------------------------------------------------

-- 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

-- 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

-- 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
2 Likes