I’m writing a script in which the user is presented with the contents of a list, in a choose list dialog, and selects one of those items; I want to move the item that the user selects so that it becomes the first item in the list. This seems to be more difficult than I expected. Is there any obvious way to rearrange this list:
MacScripter
Applescript Forums
Rules
Posting Guidelines
User List
Search
so that when the user selects “Rules”, the list is rearranged to look like this:
Rules
MacScripter
Applescript Forums
Posting Guidelines
User List
Search
There is no built-in functionality for that kind of list manipulation (filter references for lists would be really handy, but they are not supported), so you have to do it yourself.
on run
set originalList to {"Rules", "MacScripter", ¬
"Applescript Forums", ¬
"Posting Guidelines", ¬
"User List", ¬
"Search"}
set theList to originalList
set theList to moveToFront("New Item", theList) -- the handler can take a non-list "front" item
repeat while true
set chosen to choose from list theList with multiple selections allowed and empty selection allowed -- whatever your usage requires, the handler can handle empty, single-value and multiple-value "front" lists
if chosen is not false then
set newList to moveToFront(chosen, theList)
else
-- User canceled choose from list
exit repeat
end if
set theList to newList
end repeat
end run
to moveToFront(_subList, _wholeList)
-- Simple version. See below for a version that avoids a speed bug in AppleScript.
(*
return a list that starts with all the items of _subList and also contains all the items of _wholeList that are not in _subList
any items in _subList but not in _wholeList are still present in the return value
any extra duplicates in _wholeList of items in _subList are elimiinated in the return value
any extra duplicates in _wholeList of items NOT in _subList are still present in the return value
*)
set _subList to _subList as list
if _subList is equal to {} then return _wholeList
copy _subList to l
repeat with i in _wholeList
set i to contents of i
if i is not in _subList then
set end of l to i
end if
end repeat
l
end moveToFront
to _moveToFront(_subList, _wholeList)
-- This version should be faster for very long lists. Though for an appreciable difference the lists have to be too long to use with "choose from list".
(*
return a new list that starts with all the items of _subList and also contains all the items of _wholeList that are not in _subList
any items in _subList but not in _wholeList are still present in the return value
any extra duplicates in _wholeList of items in _subList are elimiinated in the return value
any extra duplicates in _wholeList of items NOT in _subList are still present in the return value
*)
script helper
property theList : {}
end script
set _subList to _subList as list
if _subList is equal to {} then return _wholeList
copy _subList to helper's theList
repeat with i in _wholeList
set i to contents of i
if i is not in _subList then
set end of helper's theList to i
end if
end repeat
helper's theList
end _moveToFront
set startList to {"MacScripter", "Applescript Forums", "Rules", "Posting Guidelines", "User List", "Search"}
set newList to startList
set newFirst to (choose from list startList) as text
if newFirst ≠"false" then
set myDelimiter to character id 5
set AppleScript's text item delimiters to {myDelimiter}
set startstring to startList as string
set AppleScript's text item delimiters to {myDelimiter & newFirst & myDelimiter}
set newList to (text items of (myDelimiter & startstring & myDelimiter))
set newString to newFirst & (item 1 of newList) ¬
& myDelimiter & (item 2 of newList)
set AppleScript's text item delimiters to {myDelimiter}
set newList to reverse of (rest of (reverse of (text items of newString)))
end if
set AppleScript's text item delimiters to {return}
display dialog newList as string
Your idea of adding a new item is very useful, because it lets me simplify the flow of this script considerably. One question: I can’t figure out an obvious way to change your solution so that the “new item” comes at the END of the original list, rather than at the beginning. Simply reversing them inside the parentheses didn’t work (the new item appeared as a vertical list of characters, not a single item). Is there something obvious that I’m missing?
Using a bare string for the item to add and swapping the order of the arguments (moveToFront(theList, “New Item”)) adds the individual characters because repeat with i in aString will loop across characters in a string as well as items in a list. So the characters of “New Item” are checked against the contents of theList and then added to the end of the copy of theList. To fix that, you can send an explicit list value for the second argument: moveToFront(theList, {“New Item”}).
A problem with this solution is that if “New Item” is not, in fact, new (it is already present in theList), then it will not be moved to the end of the list. Swapping the order of the arguments means that the initialization and looping roles of the lists will be reversed (required for “move to back”), but so will the testing role (moving to the back would require testing against the second argument instead of the first).
The original code also has a duplicate elimination asymmetry that has been bothering me. Duplicates in the first argument are not eliminated, duplicates in the second argument are not eliminated if they are not also present in the first argument.
So, I rewrote the handlers using different logic. These new ones can process “bare” (non-list) arguments in either position. I think using a non-list argument in the second position may not be terribly useful though. Probably those situations call for the opposite handler and putting the non-list argument as the first one.
on run
set theList to {"AAA", "BBB", "CCC", "AAA", "DDD", "EEE"} -- duplicate "AAA"
set a to moveToFront({"FFF", "BBB", "BBB"}, theList)
set b to moveToBack({"FFF", "BBB", "BBB"}, theList)
set c to moveToFront(theList, {"FFF", "BBB", "BBB"})
set d to moveToBack(theList, {"FFF", "BBB", "BBB"})
return {|short list to front|:a, |short list to back|:b, |long list to front|:c, |long list to back|:d}
end run
to moveToFront(itemsToMove, wholeList)
removeDuplicatesFromEnd((itemsToMove as list) & (wholeList as list))
end moveToFront
to moveToBack(itemsToMove, wholeList)
removeDuplicatesFromBeginning(removeDuplicatesFromEnd(wholeList as list) & removeDuplicatesFromEnd(itemsToMove as list))
end moveToBack
to removeDuplicatesFromEnd(theList)
set newList to {}
repeat with i from 1 to length of theList
tell item i of theList to if it is not in newList then set end of newList to it
end repeat
newList
end removeDuplicatesFromEnd
to removeDuplicatesFromBeginning(theList)
set newList to {}
repeat with i from 1 to length of theList
tell item -i of theList to if it is not in newList then set beginning of newList to it
end repeat
newList
end removeDuplicatesFromBeginning
If you’ll forgive my saying so, Chris, I think you’re being too clever. The original post specifically says:
It’s easy to write a handler that does precisely that. Appending a new item to the end of the list is another process that doesn’t need to be combined with it.
Here (just for interest) is an “in place” move-to-front method:
set originalList to {"MacScripter", "Applescript Forums", "Rules", "Posting Guidelines", "User List", "Search"}
copy originalList to theList
repeat
set chosenItem to (choose from list theList)
if (chosenItem is false) then return
set chosenItem to beginning of chosenItem
moveToFront(theList, chosenItem)
end repeat
-- "In place". The items are rearranged in the passed list.
on moveToFront(theList, theItem)
considering case
repeat with i from (count theList) to 1 by -1
if (item i of theList is theItem) then exit repeat
end repeat
end considering
repeat with i from (i - 1) to 1 by -1
set item (i + 1) of theList to item i of theList
end repeat
set item 1 of theList to theItem
return
end moveToFront