Notifications
Clear all

[Closed] sort array of files by natural number order

as the title suggests is there a way to do this? I’ve solved this problem before in C#, but I hope I don’t have to re-write code in maxscript to solve this problem here because I am not nearly good in maxscript as I am in C#. But for you pros out there I can drop the C# code here.

basically

theFiles = getFiles (path + "\\*_crop.png") --collect all .png files in the dir.

returns
–\Images\0_crop.png”
–\Images\10_crop.png”
–\Images\11_crop.png”
–\Images\12_crop.png”
–\Images\1_crop.png”
–\Images\2_crop.png”
–\Images\3_crop.png”
–\Images\4_crop.png”
–\Images\5_crop.png”
–\Images\6_crop.png”
–\Images\7_crop.png”
–\Images\8_crop.png”
–\Images\9_crop.png”

this is the wrong order… is there a way to fix this?

12 Replies

what is your c# sorting code?

WARNING: C#
USAGE:
string[] files = Directory.GetFiles(path, “*.png”);
NumericComparer ns = new NumericComparer();
Array.Sort(files, ns);


	public class NumericComparer : IComparer
	{
		public NumericComparer()
		{}
		
		public int Compare(object x, object y)
		{
			if((x is string) && (y is string))
			{
				return StringLogicalComparer.Compare((string)x, (string)y);
			}
			return -1;
		}
	}

	// emulates StrCmpLogicalW, but not fully
	public class StringLogicalComparer
	{
		public static int Compare(string s1, string s2)
		{
			//get rid of special cases
			if ((s1 == null) && (s2 == null)) return 0;
			else if (s1 == null) return -1;
			else if (s2 == null) return 1;

			if ((s1.Equals(string.Empty) && (s2.Equals(string.Empty)))) return 0;
			else if (s1.Equals(string.Empty)) return -1;
			else if (s2.Equals(string.Empty)) return -1;

			//WE style, special case
			bool sp1 = Char.IsLetterOrDigit(s1, 0);
			bool sp2 = Char.IsLetterOrDigit(s2, 0);
			if (sp1 && !sp2) return 1;
			if (!sp1 && sp2) return -1;

			int i1 = 0, i2 = 0; //current index
			int r = 0; // temp result
			while (true)
			{
				bool c1 = Char.IsDigit(s1, i1);
				bool c2 = Char.IsDigit(s2, i2);
				if (!c1 && !c2)
				{
					bool letter1 = Char.IsLetter(s1, i1);
					bool letter2 = Char.IsLetter(s2, i2);
					if ((letter1 && letter2) || (!letter1 && !letter2))
					{
						if (letter1 && letter2)
						{
							r = Char.ToLower(s1[i1]).CompareTo(Char.ToLower(s2[i2]));
						}
						else
						{
							r = s1[i1].CompareTo(s2[i2]);
						}
						if (r != 0) return r;
					}
					else if (!letter1 && letter2) return -1;
					else if (letter1 && !letter2) return 1;
				}
				else if (c1 && c2)
				{
					r = CompareNum(s1, ref i1, s2, ref i2);
					if (r != 0) return r;
				}
				else if (c1)
				{
					return -1;
				}
				else if (c2)
				{
					return 1;
				}
				i1++;
				i2++;
				if ((i1 >= s1.Length) && (i2 >= s2.Length))
				{
					return 0;
				}
				else if (i1 >= s1.Length)
				{
					return -1;
				}
				else if (i2 >= s2.Length)
				{
					return -1;
				}
			}
		}

		private static int CompareNum(string s1, ref int i1, string s2, ref int i2)
		{
			int nzStart1 = i1, nzStart2 = i2; // nz = non zero
			int end1 = i1, end2 = i2;

			ScanNumEnd(s1, i1, ref end1, ref nzStart1);
			ScanNumEnd(s2, i2, ref end2, ref nzStart2);
			int start1 = i1; i1 = end1 - 1;
			int start2 = i2; i2 = end2 - 1;

			int nzLength1 = end1 - nzStart1;
			int nzLength2 = end2 - nzStart2;

			if (nzLength1 < nzLength2) return -1;
			else if (nzLength1 > nzLength2) return 1;

			for (int j1 = nzStart1, j2 = nzStart2; j1 <= i1; j1++, j2++)
			{
				int r = s1[j1].CompareTo(s2[j2]);
				if (r != 0) return r;
			}
			// the nz parts are equal
			int length1 = end1 - start1;
			int length2 = end2 - start2;
			if (length1 == length2) return 0;
			if (length1 > length2) return -1;
			return 1;
		}

		//lookahead
		private static void ScanNumEnd(string s, int start, ref int end, ref int nzStart)
		{
			nzStart = start;
			end = start;
			bool countZeros = true;
			while (Char.IsDigit(s, end))
			{
				if (countZeros && s[end].Equals('0'))
				{
					nzStart++;
				}
				else countZeros = false;
				end++;
				if (end >= s.Length) break;
			}
		}

	}

you can use your c# comparer of course but everything can be much easier

fn makeIComparersAssembly = 
(
	source = ""
	source += "using System;
"
	source += "using System.Runtime.InteropServices;
"
	source += "using System.Collections;
"
	source += "namespace IComparers
"
	source += "{
"
	source += "	public sealed class StringLogicalComparer: IComparer
"
	source += "	{
"
	source += "		[DllImport(\"shlwapi.dll\", CharSet = CharSet.Unicode, ExactSpelling = true)]
"
	source += "		static extern int StrCmpLogicalW(String x, String y);
"
	source += "		public int Compare(object x, object y)
"
	source += "		{
"
	source += "			return StrCmpLogicalW((String)x, (String)y);
"
	source += "		}
"
	source += "	}
"
	source += "}
"

	csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
	compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"

	compilerParams.GenerateInMemory = on
	compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)
	compilerResults.CompiledAssembly
)
global StringLogicalComparer = (makeIComparersAssembly()).createInstance "IComparers.StringLogicalComparer"
	
/*-- TEST -- */
(
	a = dotnet.valuetodotnetobject #("test 1", "test 10", "test 2", "1 te1st", "1 te10st", "1 te2st", "10 test", "2 test") (dotnetclass "System.String[]")
	
	a.Sort a StringLogicalComparer
	print (a.Clone())
)

using the native MXS methods we can do it with qsort. any volunteers to make a qsort sorting function?

For this particular case something like this might also work:

(
 
 fn compare f1 f2 =
 (
 	name1 = getFilenameFile f1
 	name2 = getFilenameFile f2
 	
 	n1 = (replace name1 (name1.count-4) 5 "") as integer
 	n2 = (replace name2 (name2.count-4) 5 "") as integer
 
 	case of
 	(
 		(n1 < n2): -1
 		(n1 > n2):  1
 		default: 0
 	)
 )
 
 files = for j = 12 to 0 by -1 collect "C:\Images\\" + (j as string) + "_crop.png"
 qsort files compare
 files
 
 )

[left]PolyTools3D: error here (n1 < n2): -1 /// no “”<“” function for undefined

denisT: this code result in theFiles unable to convert to type String


	if path != undefined do--if the user did not cancel
	(
		theFiles = getFiles (path + "\\*_crop.png") --collect all .png files in the dir.
		a = dotnet.valuetodotnetobject #(theFiles) (dotnetclass "System.String[]")
		a.Sort a StringLogicalComparer
		for f in theFiles do
		(
			print(f)
		)
	)

thankyou so much for the help! Ill keep experimenting…
[/left]

1 Reply
(@polytools3d)
Joined: 11 months ago

Posts: 0

[/left]
The function should work as long as the suffix length is the same, in this case 5 characters “_crop”, as in the example you showed. So, 1ABCDE.png should work but 1ABCDEF.png will not.

I realized that if the suffix is always the same, you could also trim it, do a simple integer sort and then append it again.

Also, if you know the files will have no gaps in the numbering and have the same suffix, you could just collect the numbers and append the suffix for the given amount of files.

In any case, the C# function is much more efficient and works in general cases. I don’t think a pure mxs qsort function can beat C# in this case.

By the way, in the function Denis provided you are putting an array inside another array, that’s what might be giving you the error. Try replacing #(theFiles) by just theFiles.

the Sort method sorts “a” array. why do you expect to get theFiles array sorted?
“a” is a dotnet object copy of the theFiles mxs array.

denisT: sorry I did not know that #() is == to theFiles
PolyTools3D: thank you – I don’t know maxscript that well. That solved the issue. Now I don’t suppose there is a dotnet.dotnetobjecttovalue conversion to get ‘a’ back into theFiles? Maxscript does not allow for loops on ‘a’.

1 Reply
(@denist)
Joined: 11 months ago

Posts: 0

to go back to the string array use (as i showed it above) clone of .net array. max automatically cast it to a mxs string array


 mxsArray = netArray.Clone()
 

you can iterate through .net Array elements using mxs.


 a = dotnet.ValueToDotNetObject #(1,2,3) (dotnetclass "Int32[]")
 for i=0 to a.length-1 do print (a.GetValue i)
 

but it’ will be slower than convert to mxs array at the beginning

you can collect all files using .net, and directly put them into .net array:


a = (dotnetclass "System.IO.Directory").GetFiles (getDir #maxroot) "*.exe" (dotnetclass "System.IO.SearchOption").AllDirectories asdotnetobject:on
a.Sort a
a.Clone()

it might save a memory

Page 1 / 2