Move item in list - need help with subroutine

Hello!

I’m trying to write a subroutine that will rearrange the order of a list, that is: move one item in the list to a different location in the same list (I can’t believe Applescript don’t have an easy way to do this!). The code below works, but only if you move the selected item to a previous position in the list:

my rearrange_list({“one”, “two”, “three”, “four”, “five”, “six”}, 3, “four”)
→ {“one”, “two”, “four”, “three”, “five”, “six”} = OK

The problem: if I try to move the same item to a later position its inserted one step to early:

my rearrange_list({“one”, “two”, “three”, “four”, “five”, “six”}, 6, “four”)
→ {“one”, “two”, “three”, “five”, “four”, “six”} = Not OK

I suppose this is quite easy to fix, but so far I haven’t been able to figure out how exactly. Any suggestions would be much appreciated. Thank you.


my rearrange_list({"one", "two", "three", "four", "five", "six"}, 2, "four")

on rearrange_list(a_list, item_index, replacement_item)
	set selected_item to item item_index of a_list
	if selected_item is not replacement_item then
		set temp to {}
		repeat until a_list is {}
			set this_item to item 1 of a_list
			if this_item is not replacement_item then
				if this_item is selected_item then
					set a_list to {""} & a_list
					set end of temp to replacement_item
					repeat with i in (rest of a_list)
						set i to i as string
						if i is not replacement_item then
							set end of temp to i
						end if
					end repeat
					exit repeat
				else
					set end of temp to this_item
				end if
			end if
			set a_list to rest of a_list
		end repeat
		copy temp to a_list
	end if
	return a_list
end rearrange_list

Yeah, AS’s lack of built-in basic functionality kinda sucks. Here’s a solution using AppleMods’ List library, which I wrote to address AS’s feature shortage:

property _Loader : run application "LoaderServer"

----------------------------------------------------------------------
-- DEPENDENCIES

property _List : missing value

on __load__(loader)
	set _List to loader's loadLib("List")
end __load__

----------------------------------------------------------------------

__load__(_Loader's makeLoader())


on rearrange_list(lst, newIdx, val)
	set idx to _List's findFirst(lst, val)
	if idx is 0 then error "Not found." number -1728
	set itm to lst's item idx
	set newIdx to newIdx - 1
	if newIdx < idx then
		set lst to _List's deleteItem(lst, idx)
		return _List's insertItem(lst, val, newIdx)
	else
		set lst to _List's insertItem(lst, val, newIdx)
		return _List's deleteItem(lst, idx)
	end if
end rearrange_list

rearrange_list({"one", "two", "three", "four", "five", "six"}, 3, "four")
--> {"one", "two", "four", "three", "five", "six"}

rearrange_list({"one", "two", "three", "four", "five", "six"}, 1, "four")
--> {"four", "one", "two", "three", "five", "six"}

If you’ve not used AppleMods’ libraries before, you’ll need to download and install AppleMods’ Loader system first http://applemods.sourceforge.net/getstarted.html. Run the Loader installer, then download and add the List library to the ASLibraries folder. You can use the LoaderWizard applet to generate the library loading code to paste at the top of your script.

Thank you hhas for the swift reply. I’m not familiar with mods, but if they do require the end user of the compiled script to install the library as well, this won’t cut it. I need a vanilla solution.

If you’re distributing the script to be used in compiled form (e.g. as an applet, Studio app, or .scpt file to be run from a script menu), change:

__load__(_Loader's makeLoader())

to:

property _ : __load__(_Loader's makeLoader())

That will bind the List library into the compiled script at compile time instead of run-time, avoiding the need to install it on the end-user’s machine as well. They’d only need to install it if they needed to recompile the script themselves, e.g. if they were running your script from Script Editor, or had to make any changes to it.

If the script has to be decompilable on the end-user’s machine, you can always extract the relevant code from the List library and paste it into your own script. All the AM libraries are released under the BSD license, so you’re free to do it whichever way you like, as long as you remember to include the original copyright notice for it.

HTH

Oh thats great. The script will only be distributed in compiled form, so I guess the problem is solved. Thanks a lot for your generous help. Much appreciated.

Hm… It seemed to work very well at first, but then I noticed it still returns an erroneous result if newIdx is greater than val’s idx (as you can see in the results below).


property _Loader : run application "LoaderServer"

----------------------------------------------------------------------
-- DEPENDENCIES

property _List : missing value

on __load__(loader)
	set _List to loader's loadLib("List")
end __load__

----------------------------------------------------------------------

__load__(_Loader's makeLoader())


on rearrange_list(lst, newIdx, val)
	set idx to _List's findFirst(lst, val)
	if idx is 0 then error "Not found." number -1728
	set itm to lst's item idx
	set newIdx to newIdx - 1
	if newIdx < idx then
		set lst to _List's deleteItem(lst, idx)
		return _List's insertItem(lst, val, newIdx)
	else
		set lst to _List's insertItem(lst, val, newIdx)
		return _List's deleteItem(lst, idx)
	end if
end rearrange_list

rearrange_list({"one", "two", "three", "four", "five", "six"}, 5, "four")
--> {"one", "two", "three", "four", "five", "six"}

rearrange_list({"one", "two", "three", "four", "five", "six"}, 6, "four")
--> {"one", "two", "three", "five", "four", "six"}

Apparently just a typo. Changing this line

set lst to my insertItem(lst, val, newIdx)

to

set lst to my insertItem(lst, val, newIdx + 1)

takes care of the problem. Thanks again.

Tsk. Second one today. Thought I’d tested it too. I’d like to say I was slipping, but in truth I always was a slob. :slight_smile:

Really glad to see this question and answer - AppleMods is a must and I didn’t have it installed.

Hi knaster,

You just need to temp variable to hold the value of item 2 and switch items. Something like this:

set find_v to “four”
set pos_v to 2
set the_list to {“one”, “two”, “three”, “four”, “five”, “six”}
set c to count the_list
repeat with i from 1 to c
set this_item to item i of the_list
if this_item is find_v then exit repeat
end repeat
set temp_v to item pos_v of the_list
set item pos_v of the_list to find_v
set item i of the_list to temp_v
the_list

Edit: oops. I just reread your post. Disregard this post.

gl,

I couldn’t resist. This interprets “same list” literally:

my rearrange_list({"one", "two", "three", "four", "five", "six"}, 2, "four")

on rearrange_list(a_list, item_index, replacement_item)
	-- Script object for speedy access to list items.
	script o
		property l : a_list
	end script
	
	set list_len to (count a_list)
	if ((a_list does not contain replacement_item) or (item_index > list_len) or (item_index < 1)) then error
	
	-- Find the current location of the item in the list.
	repeat with i from 1 to (count a_list)
		if (item i of o's l is replacement_item) then exit repeat
	end repeat
	-- Does it have to be moved forwards or backwards (or neither)?
	if (item_index > i) then
		set step to 1
	else
		set step to -1
	end if
	-- Move the intervening items up or down by one.
	repeat with i from i to (item_index - step) by step
		set item i of o's l to item (i + step) of o's l
	end repeat
	-- Insert the item into the required location.
	set item item_index of o's l to replacement_item
	
	return a_list -- not really necessary, since it's the same list.
end rearrange_list

Nigel,

thank you. Your script works great, as always. I ended up using a modified variant of hhas mod’s handler, though. Still, I’m curious to know how a property can speed up the script. Properties are saved, right? Isn’t saving always a ticks-count-increaser?

No problem, knaster. I could see you were happy with has’s library, which will no doubt take care of a lot of other stuff for you as well. I was just intrigued enough by the proposition to have a go myself. :slight_smile:

Properties and globals of a script are (usually) saved back into the script’s file when it finishes running. Pedantically, this means if the variables are properties or globals, they and the data they contain are saved back into the file. In my script, the property belongs to a script object that’s held by a local variable. Since locals aren’t saved back into to the script file, the script object and its property aren’t either.

It’s mentioned in ASLG that using a reference to a list variable (instead of just using the variable) considerably speeds up access to the list’s items. This is useful if you’re going to make a large number of such accesses. The technical reason’s something to do with error checking and, possibly, with the way the list is accessed internally.

It’s not possible to set up a reference to a local variable, but it is possible to reference a property of a script object that’s held by a local variable. This makes the script object idea useful inside handlers, where variables are local unless declared otherwise. We can use a reference expression, such as ‘o’s l’, instead of just the variable name (‘l’ or ‘a_list’).

The speed gain only applies when accessing the items in a list. The reference actually slows down very slightly actions on the list itself. That’s why I used ‘count a_list’ and ‘a_list does not contain…’ rather than ‘count o’s l’ and ‘o’s l does not contain…’. The variables ‘a_list’ and ‘l’ contain the same list, of course. They’re just difference sorts of variable.

Thanks for taking the time to explain all this, I really appreciate it. And learned quite a bit.

Yeah, thanks Nigel!