[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?
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]
[/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 dont 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, thats 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’.
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