Notifications
Clear all

[Closed] Sorting Filenames

This should be relatively simple. I looked in the help files and here on the forums but I’m a little confused. I modified Bobo’s RealRenderHistory script to display all kinds of images and file names but the problem is they don’t display in order. I want to be able to sort the images both numerically and alphabetically. Right now Bobo used qsort and filtered the string by”” but since I’m displaying all image file names then names that don’t have “” will crash the script. If I delete the qsort code and sort get_image_files using sort then images are displayed alphabetically but then file names like…

Image1
Image3
Image10
Image30

won’t display in order.

I’m thinking I need to filter the “text” from the “#s” in each file but I’m not sure how. Or maybe I need sort the get_image files two times. first alphabetically using “sort” and then sort get_image_files another time with qsort?

I’m trying to display images like this in order:

AAA
BBB
CCC1
CCC2
DD01
DD02

ect


fn update_history_display val =
(
if rrh_scene_only.checked then file_name = (getFileNameFile maxFileName) else file_name = ""
local get_image_files = (getFiles ((Image_Folder_Path1)+"\\"+"*"))
 
--For backwards compatibility, we sort all image numbers using qsort:
--We collect all frame numbers first,
local sortArray = for f in get_image_files collect
(
	local theFileName = getFileNameFile f 
	local theFS = filterString theFileName "_"
	execute (theFS[theFS.count])
)
local indexArray = for f = 1 to get_image_files.count collect f --then collect indices
qsort indexArray compareFN valArray:sortArray --sort the indices by the frame numbers
get_image_files = for i in indexArray collect get_image_files[i] --and collect the file names based on the sorted index array
local image_index = (val*get_image_files.count/100.0 as integer)+1 --then pick the correct frame 
--end backwards compatibility code

13 Replies

Ok this turned out quite a bit longer then anticipated but it does the job…

(
	charClass = dotNetClass "System.Char" --dotnet charclass used for isDigit comparisment

	fn fnSort fName1 fName2 = (
		fNameSuf1 = (for i = fName1.count to 1 by -1 where charClass.IsDigit fName1[i] collect i) --collect all suffix digits from filename 1
		fNameSuf2 = (for i = fName2.count to 1 by -1 where charClass.IsDigit fName2[i] collect i) --collect all suffix digits from filename 2

		split1 = if fNameSuf1.count > 0 then ( --check if digits were found in file name 1
			#(
				(substring fName1 1 (fNameSuf1[fNameSuf1.count] - 1)), --get the text part of the first filename
				execute (substring fName1 fNameSuf1[fNameSuf1.count] (fNameSuf1[1] - (fNameSuf1[fNameSuf1.count]) + 1)) --get the number suffix of the first filename
			)
		) else #(fName1,-1) --else set the array to the filename and number to -1 (allways first when no number prefix)
		split2 = if fNameSuf2.count > 0 then ( --check if digits were found in file name 2
			#(
				(substring fName2 1 (fNameSuf2[fNameSuf2.count] - 1)), --get the text part of the second filename
				execute (substring fName2 fNameSuf2[fNameSuf2.count] (fNameSuf2[1] - (fNameSuf2[fNameSuf2.count]) + 1)) --get the number suffix of the second filename
			)
		) else #(fName2,-1) --else set the array to the filename and number to -1 (allways first when no number prefix)

		--start comparisment (1 for larger, -1 for smaller and 0 for equal)
		case of (
			(split1[1] > split2[1]): 1 --if the text part of filename 1 is larger then that of filename 2
			(split1[1] < split2[1]): -1 --and smaller
			(split1[1] == split2[1]): ( --if its equal compare the number suffixes
				case of (
					(split1[2] > split2[2]): 1 --number from filename 1 is higher then of filename 2
					(split1[2] < split2[2]): -1 --number is smaller
					(split1[2] == split2[2]): 0 --number is equal
				)
			)
		)
	)

	filenames = #("Image","File","Img")
	filenames = for i = 1 to 10 collect (filenames[random 1 filenames.count] + (random 1 1999) as string) --generate dummy filenames in an array

	format "Before:
%

" filenames --output the filenames before being sorted to the listener
	qsort filenames fnSort --sort with custom function
	format "After:
%
" filenames --output the sorted filenames to the listener
)

Alternativly you could use

classof (execute(fName1[i])) == Integer

as well to check for numbers in the string without the dotnet class but I found this easier.

This might not be the quickest way to do it since I dont know all the ropes yet but it does work for how far I can see.

I’m not sure I understood your dilemma,
I tried this code:

a = #("BBB","DD02","DD01","AAA","CCC2","CCC1")
   sort a

and the result was:

#("AAA", "BBB", "CCC1", "CCC2", "DD01", "DD02")

which looks sorted to me…

Edit:
Ok, got it. It is a problem.
If you are trying to sort image1, image2, image10… you get image1, image10, image2…

no need for isDigit, or even the execute method (if you only care about letters vs numbers, you could even try casting to integer, as long as you replace ‘t’, ‘s’, etc. with another character)

First you need to get the numbers from the filename… can use the above function, but if your base filename has a number in it, it will error out:


   charClass = dotNetClass "System.Char" --dotnet charclass used for isDigit comparisment
   dotNetClass:System.Char
   fName1 = "test2_001"
   "test2_001"
   fNameSuf1 = (for i = fName1.count to 1 by -1 where charClass.IsDigit fName1[i] collect i)
   #(9, 8, 7, 5)
   (substring fName1 1 (fNameSuf1[fNameSuf1.count] - 1))
   "test"
   execute (substring fName1 fNameSuf1[fNameSuf1.count] (fNameSuf1[1] - (fNameSuf1[fNameSuf1.count]) + 1))
   -- Error occurred during fileIn in StringStream:"2_001"
   -- Type error: Call needs function or class, got: 2
   

So instead of collecting all numbers, it should collect only the last numbers. Rather than looping, we’ll abuse trimRight(), and get both the base and digits:


   fn getFilesequenceFile f &base &digits = (
   	f = getFilenameFile f
   	base = trimRight f "0123456789"
   	digits = subString f (base.count + 1) -1
   )
   getFilesequenceFile()
   getFilesequenceFile "c:\	est\	est2_10.png" &myBase &myDigits
   "10"
   myBase
   "test2_"
   myDigits
   "10"
   

And once you have that, you can create a sorting function that pads the digits and sorts with that:


   fn pseudoNaturalSort a b = (
   	a = a as string
   	b = b as string
   	getFilesequenceFile a &aBase &aDigits
 	-- hackhackhack.  This pads a number with zeros to 6 digits without using a loop.
 	-- things will fail if there's more digits.. 6 'seems' safe.
   	aDigits = subString ((1000000 + (aDigits as integer)) as string) 2 -1
   	getFilesequenceFile b &bBase &bDigits
   	bDigits = subString ((1000000 + (bDigits as integer)) as string) 2 -1
   	a = aBase + aDigits
   	b = bBase + bDigits
   
   	case of (
   		(a == b): 0
   		(a < b): -1
   		(a > b): 1
   	)
   )
   pseudoNaturalSort()
   

putting it to the test…


   test = #("10","1","3","20","2")
   #("10", "1", "3", "20", "2")
   qsort test pseudoNaturalSort
   OK
   test
   #("1", "2", "3", "10", "20")
   
   test = #("Image1", "Image3", "Image10", "Image30")
   #("Image1", "Image3", "Image10", "Image30")
   qsort test pseudoNaturalSort
   OK
   test
   #("Image1", "Image3", "Image10", "Image30")
   
   -- and from decapitators filename generator (seed 0, 25 filenames)
   -- INPUT:
   #("Image1413", "Image1282", "Image1599", "Image1715", "File406",
     "File63", "File1797", "Image1499", "Img286", "Img1986", 
     "File1104", "File864", "File196", "Img588", "Image443",
     "Img1566", "Img1587", "Image1016", "File263", "Image1787",
     "Img1594", "Image91", "Image785", "Img62", "Image147"
   )
   
   -- OUTPUT:
   #("File63", "File196", "File263", "File406", "File864",
     "File1104", "File1797", "Image91", "Image147", "Image443",
     "Image785", "Image1016", "Image1282", "Image1413", "Image1499",
     "Image1599", "Image1715", "Image1787", "Img62", "Img286",
     "Img588", "Img1566", "Img1587", "Img1594", "Img1986"
   )
   

There’s plenty that can be optimized if you know a little more about the input sequence, in terms of the sorting function used.

There’s more specialized filename sort algorithms floating around on the interwebs (oddly enough I didn’t spot one built-in with the .NET framework… must be overlooking it) that might be good to look at as well.

1 Reply
(@martinb)
Joined: 11 months ago

Posts: 0

Here’s another MAXScript nugget! trimRight looks very useful!
Great, thanks

– MartinB

Thanks everyone for taking the time to write the code

ZeBoxx2 i’m not sure if I understand this part of the first block of code.


getFilesequenceFile()
getFilesequenceFile "c:\	est\	est2_10.png" &myBase &myDigits
"10"
myBase
"test2_"
myDigits
"10"

Does that mean I have to specify the base and the digits for every file that is loaded? The function needs to be dynamic so that it works for all files not just ones I specify the name of, or am I missing something?

EDIT – oops… I think i get what the blue text is… Still got to play around with the code some more.

The ‘getFilesequenceFile’ function takes three parameters… the filename, and two by-reference parameters that the function assigns values to. by-reference parameters allow you to easily work with multiple return values from a function.

So the &myBase and &myDigits aren’t things you have to specify values for at all – they’re just variable names that the function till use to put its output values into.

Essentially, this:


fn getFilesequenceFile f &base &digits = (
	f = getFilenameFile f
	base = trimRight f "0123456789"
	digits = subString f (base.count + 1) -1
)

getFilesequenceFile "c:\	est\	est2_10.png" &myBase &myDigits

Is the same as the following naive alternative… but obviously much shorter to write, easier to deal with and also less memory-consuming (as it doesn’t have to create the array value):


fn getFilesequenceFile f = (
	f = getFilenameFile f
	base = trimRight f "0123456789"
	digits = subString f (base.count + 1) -1
	#(base,digits)
)
myBaseAndDigits = getFilesequenceFile "c:\	est\	est2_10.png"
myBase = myBaseAndDigits[1]
myDigits = myBaseAndDigits[2]

As for the blue lines – they’re the output in the Listener – white’s the actual code.

1 Reply
(@cyclones)
Joined: 11 months ago

Posts: 0

yea, can’t think on 3 hours of sleep lol. Sorry

ok so I had a chance to experiment with your code. There are a few things. First look at the picture below and compare the file ordering in Windows Explorer vs the array order in Maxscript. Explorer displays files in a logical order. The script however is out of order(ex. Image1 is before apple1).

I tried executing your script and it works with a single file path but not if I use getFiles. And which variable do I sort(use) once the getFilesequenceFile function is executed. digits?


 
 
rollout window "window"
(
checkbutton filter_Button "filter" height:20 width:64 pos:[100,100] 
checkbutton sort_button "sort" height:20 width:64 pos:[25,100] 
 
fn getFilesequenceFile f &base &digits =
(
f = getFilenameFile f
base = trimRight f "0123456789"
digits = subString f (base.count + 1) -1
)
 
fn pseudoNaturalSort a b =
(
	 a = a as string
	 b = b as string
	 getFilesequenceFile a &aBase &aDigits
-- hackhackhack. This pads a number with zeros to 6 digits without using a loop.
-- things will fail if there's more digits.. 6 'seems' safe.
	 aDigits = subString ((1000000 + (aDigits as integer)) as string) 2 -1
	 getFilesequenceFile b &bBase &bDigits
	 bDigits = subString ((1000000 + (bDigits as integer)) as string) 2 -1
	 a = aBase + aDigits
	 b = bBase + bDigits
 
	 case of
(
	 (a == b): 0
	 (a < b): -1
	 (a > b): 1
	 )
	)
 
 
on filter_Button changed State do
(
getFilesequenceFile (getFiles "C:\files\\*.txt") &myBase &myDigits
)
on sort_button changed State do
(
local test = #("apple10","apple1","door3","eat20","Image1","Image3","Image10","Image30","1","5","8","20","9","10","0","05","02","08","06","04","year2","cc","hh","dd","aa","00005")
qsort test pseudoNaturalSort 
print test
)
 
)
createDialog window 200 200

This wouldn’t be problem if I stuck with a predefined prefix/suffix but I’m trying to give users the freedom to load any images into the browser. This is the last bit of functionality I want to implement. The rest is just minor stuff. grr I wish I could figure this out myself… seems like it should be easy.

Thanks for the help.

hmm I did some thinking and this seems more complicated then it needs to be. I think the file name needs to be filtered 5+ times? For files named like

Type1 –> 1image – 10image ect <– prefix is a #, suffix is text
Type2 –> image1 – image10 ect <– prefix is text, suffix is a #
Type3 –> 1 – 10 <– only #s
Type4 –> Ape – Zebra <– only text
Type5 –> 1image1 – 10image10 ect <– filename is 3 parts, #,text,#

I think the types could go on for ever ex. 1image1image1image – 10image10imag10image.

So there would need to be some sort of algorithm to check for when the name string changes from numbers to text or text to numbers going from left to right.

And the array would be sorted each time. ex Type#5(1image1) would be first sorted based on “1” then that result would be sorted based on “image” and finally sorted based on the second “1”

The problem is I don’t have enough maxscript/programming knowledged. I hope my logic is correct.

It’s going to depend on which sorting algorithm you’re going to be comparing it to; the sort I posted only considers numerics at the end of the filename, which I think you were asking for
E.g. the “Image” before “apple” is simply because it compares case sensitively – There’s several apps that will do that, Windows by default doesn’t. If you want to compare case insensitive, you’d want to use “a = toLower a” and “b = toLower b” just befor the “case of ()” statement.

If you want a more complete filename sort, then you’ll first want to figure out just how you want things sorted. Windows’s sort is pretty odd at times, imho… here’s an article on the difference between Windows2000 and WindowsXP sorting with two examples:
http://support.microsoft.com/default.aspx?kbid=319827

Personally I prefer the Windows2000 sorting in that case (as I’d want all ‘IE4’ together), but if you apply that same sorting to your image sequence problem, you’re back to the problem you had originally.

I love this quote from the The Old New Thing blog “sorting for humans can result in different results depending on which human you ask”. More technically – it will depend on which Locale is setup in Windows – fun stuff.
( http://blogs.msdn.com/oldnewthing/archive/2005/06/17/430194.aspx )

So… step 1 is to figure out just how you want things sorted… that’s going to be the hard part

If you just want bits and pieces grouped by letters vs numbers, that shouldn’t be too hard – but whether it will give the results you want, especially when throwing in odd characters (e.g. “ä” > “z” – true. whoops.), is another matter entirely.

Edit 1: Here’s an alpha vs numeric separation script:


 fn separateAlphaNumeric str = (
 	if (str == "") then ( #() )
 	else (
 		numeric = undefined
 		result = #()
 		for i = 1 to str.count do (
 			c = str[i]
 			if ((findString "0123456789" c) != undefined) then (
 				if (numeric != true) then ( append result "" )
 				numeric = true
 			)
 			else (
 				if (numeric != false) then ( append result "" )
 				numeric = false
 			)
 			append result[result.count] c
 		)
 	)
 	result
 )
 separateAlphaNumeric()
 separateAlphaNumeric "12this34is56a78string90"
 #("12", "this", "34", "is", "56", "a", "78", "string", "90")
 

Given that, you can do a comparison per-chunk and sort them that way… if the two match, compare the next chunk until running out of chunks in one or the other. Presumably, the one with more chunk would be sorted behind the one with less chunks.

Instead of the findString line, you can use the dotNet method to determine if it’s an integer, or you can use execute. You can’t use “<str> as integer” because several characters – such as “f”, “t” and “s” – will evaluate to a valid integer (zero).

Hi,

Here’s my take on this, it’s part of a struct of useful (to my, anyway) functions.

Usage:


 -- First run the attached file.
 (
 	local files = getFiles @"C:\*"
 	local myLib = s_OsLib()
 	myLib.sortFiles &files
 	print files
 )
 

[Edit]
Forgot to mention that this struct is not well commented (I need to find some time to do this), but here’s the description and options of the sortFiles method:
– sort an array of files.
– modes are: (default #dirsFirst)
#mixed – both dirs. and files sorted together
#filesFirst – files are sorted before dirs.
#dirsFirst – dirs. are sorted before files.
– sortDir options are: #asc for ascending; #desc for descending. default: #asc
– ignoreCase if false case will be conserved when sorting. default:true.
fn sortFiles &items mode: sortDir: ignoreCase: =

[/Edit]

cheers,
o

That did the job thanks.

I agree. Theres no perfect way to sort the names. Just depends on how you look at it.

cool. Looks like it could be useful. I may try to use it later on once I get everything else finished. Thanks

Here is a part of the result I get comparing ZeBoxx2’s script to ofer_z’s. I like ofer_z’s a little more only because the filename “20” is at the bottom of the list, which makes sense because its the largest number in the list. The only thing about ofer’s script is that its way too advanced for me to try reverse-engineer/write from scratch. ZeBoox if you could do something to change your script to also place numbers like ofer’s script that would be great. But no presure


ZeBoxx2's
 
02
04
00005
05
5
06
8
08
9
10
20
01bat
10bat
10test
1bat
1test
2test
3test
aa
apple1
----------------------------------------------------------------------------
ofer_z's
 
0
1
01bat
1bat
1test
02
2test
3test
04
5
00005
05
06
08
8
9
10
10bat
10test
20
aa
apple1

ofer_z thanks! If I decide to, do you mind if I copy your code into my script?

1 Reply
(@ofer_z)
Joined: 11 months ago

Posts: 0

No problem (see the script header :)), you can also just use the entire struct.
Note that if you do, you’ll have to copy some other functions from that struct that are used in the sortFiles function.

I hope I’ll get some time in the near future to comment the code out so it’s more useful to others.

Cheers,
o

Page 1 / 2