Rearrange order of items in a list?

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

Thanks for any help.

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

Or, indeed:

set newList to text items 1 thru -2 of newString

Cool!
Thanks for the syntax. I like that. Much cleaner.

Many thanks for all of these!

Reply to chrys:

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?

Thanks again to everyone.

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

For those who like variety :cool:

Here is an rb-appscript example.


#!/usr/bin/env ruby -w

require 'appscript'; include Appscript
require 'osax'; include OSAX

list = ['MacScripter', 'Applescript Forums', 'Rules', 'Posting Guidelines', 'User List', 'Search']
exit if list.insert(0, list.delete(osax.choose_from_list(list).to_s))[0] == nil