14

I have files in directory like that

0-0.jpeg
0-1.jpeg
0-5.jpeg
0-9.jpeg
0-10.jpeg
0-12.jpeg

....

when i loading files:

FileInfo[] files = di.GetFiles();

They getting in wrong order (they should go like above):

0-0.jpeg
0-1.jpeg
0-10.jpeg
0-12.jpeg
0-5.jpeg
0-9.jpeg

How to fix that?

I was trying to sort them but no way:

1) Array.Sort(files, (f1, f2) => f1.Name.CompareTo(f2.Name));

2) Array.Sort(files, (x, y) => StringComparer.OrdinalIgnoreCase.Compare(x.Name, y.Name)); 
casperOne
  • 73,706
  • 19
  • 184
  • 253
angularrocks.com
  • 26,767
  • 13
  • 87
  • 104
  • 1
    may take a look at this http://stackoverflow.com/questions/52842/sorting-directory-getfiles – Gonzalo.- Aug 22 '12 at 16:09
  • 3
    "Wrong order" according to who? `0-12.jpeg` comes before `0-5.jpeg` because `0-1` comes before `0-5`. The sorting method looks at characters, not number values. – dlras2 Aug 22 '12 at 16:11
  • 6
    Isn't the problem that file names are sorted as strings, not numerics. Therefore, 12 always comes before 9. You could either make sure the file names are 0-09.jpeg or implement a comparer to convert the filename to a number and compare that. – Mark Sherretta Aug 22 '12 at 16:11
  • @Mark Sherretta i guess you right – angularrocks.com Aug 22 '12 at 16:12

7 Answers7

28

Time to get creative.

FileInfo[] files = di.GetFiles().OrderBy(file =>
    Regex.Replace(file.Name, @"\d+", match => match.Value.PadLeft(4, '0'))
);

Using Regex replace in the OrderBy Clause:

Regex.Replace(file.Name, @"\d+", match => match.Value.PadLeft(4, '0'))

So what this does is it pads each match of numeric values in the file name with a 0 character to a length of 4 characters:

0-0.jpeg     ->   0000-0000.jpeg
0-1.jpeg     ->   0000-0001.jpeg
0-5.jpeg     ->   0000-0005.jpeg
0-9.jpeg     ->   0000-0009.jpeg
0-10.jpeg    ->   0000-0010.jpeg
0-12.jpeg    ->   0000-0012.jpeg

This only happens in the OrderBy clause, it does not alter the original file name.
You will end up with the order you are looking for.

If your file names have dates in them, it would still work:

pic_20230124_1528.jpg -> pic_00020000000200030000000100020004_0001000500020008.jpg
pic_20230124_1601.jpg -> pic_00020000000200030000000100020004_0001000600000001.jpg
pic_20230305_0951.jpg -> pic_00020000000200030000000300000005_0000000900050001.jpg
Pierre
  • 8,397
  • 4
  • 64
  • 80
  • 3
    The simplest solution of all shown here. I bumped the padding up to 9 characters just in case. – InteXX Mar 16 '19 at 23:58
  • 2
    Awesome solution! :-) – Valvestino Jul 31 '20 at 08:49
  • worth mentioning that this uses [Linq `OrderBy`](https://learn.microsoft.com/dotnet/api/system.linq.enumerable.orderby) and you need `using System.Linq;` in your script file ;) – derHugo Dec 03 '21 at 09:35
  • @InteXX have in mind though that this will add some unnecessary overhead (I'm kinda sure you won't have like 1000000000 numbered files in most cases ^^) .. you should keep the padding as low as possible but as high as relevant in your specific use case ;) – derHugo Dec 03 '21 at 09:37
  • I had to change this to `FileInfo[] files = di.GetFiles().OrderBy(n => Regex.Replace(n.Name, @"\d+", x => x.Value.PadLeft(4, '0'))).ToArray();` after running into various compilation errors, e.g. `Cannot implicitly convert type 'System.Linq.IOrderedEnumerable' to 'System.IO.FileInfo[]' (CS0029)` or `A local variable named 'n' cannot be declared in this scope because it would give a different meaning to 'n', which is already used in a 'parent or current' scope to denote something else (CS0136)` – abulhol Jan 04 '22 at 10:56
9

Alphabetically, the "wrong" order is in fact correct. If you want it sorted numerically then you'll need to either:

  1. convert the filenames to a list of numeric numbers and sort them
  2. name the files in such a way that alphabetic and numeric sorting are the same (0-001.jpeg and 0-030.jpg)
  3. rely on the file creation time to sort (presuming the files were created in order).

See the answer to Sorting Directory.GetFiles() for an example of #3.

Community
  • 1
  • 1
rein
  • 32,967
  • 23
  • 82
  • 106
  • 1
    This link was helpful to me where I could perform numeric ordering by reading only numerical part of file name https://stackoverflow.com/a/9080636/1503372. This helped me to apply ordering without applying any changes to file naming rules. – Jackson Sep 01 '21 at 14:29
7

See the "CustomSort" function here.

List<string> list = new List<string>() { 
                    "0-5.jpeg",
                    "0-9.jpeg",
                    "0-0.jpeg",
                    "0-1.jpeg",
                    "0-10.jpeg",
                    "0-12.jpeg"};
list.CustomSort().ToList().ForEach(x => Console.WriteLine(x));

Its output:

0-0.jpeg
0-1.jpeg
0-5.jpeg
0-9.jpeg
0-10.jpeg
0-12.jpeg
InteXX
  • 6,135
  • 6
  • 43
  • 80
L.B
  • 114,136
  • 19
  • 178
  • 224
7

For solving this problem you can use StrCmpLogicalW windows API.

For more details see This Artice.

sma6871
  • 3,198
  • 3
  • 38
  • 52
1

Here my solution. I first parsed the file path, then defined the order rules.

For the following code you need the namespace System.Text.RegularExpressions.

Regex parseFileNameForNaturalSortingRegex = new Regex(
    @"
        ^
        (?<DirectoryName>.*)
        (?<FullFileName>
            (?<FileNameBasePart>[^/\\]*)
            (?<FileNameNumericPart>\d+)?
            (?>
            \.
            (?<FileNameExtension>[^./\\]+)
            )?
        )
        $
    ",
    RegexOptions.IgnorePatternWhitespace | RegexOptions.RightToLeft
);

FileInfo[] filesOrdered = System.IO.Directory.GetFiles(@"C:\my\source\directory")
    .Select(fi =>
    {
        Match match = parseFileNameForNaturalSortingRegex.Match(fi.FullName);
        return new
        {
            FileInfo = fi,
            DirectoryName = match.Groups["DirectoryName"].Value,
            FullFileName = match.Groups["FullFileName"].Value,
            BasePart = match.Groups["FileNameBasePart"].Value,
            NumericPart = match.Groups["FileNameNumericPart"].Success ? int.Parse(match.Groups["FileNameNumericPart"].Value) : -1,
            HasFileNameExtension = match.Groups["FileNameExtension"].Success,
            FileNameExtension = match.Groups["FileNameExtension"].Value
        };
    })
    .OrderBy(r => r.DirectoryName)
    .ThenBy(r => r.BasePart)
    .ThenBy(r => r.NumericPart)
    .ThenBy(r => r.HasFileNameExtension)
    .ThenBy(r => r.FileNameExtension)
    .Select(r => r.FileInfo)
    .ToArray();
rittergig
  • 715
  • 1
  • 5
  • 16
0

You filenames appear to be structured. If you just sort them, they sort as ordinary strings. You need to:

  1. Parse the file name into its constituent component parts.
  2. Convert the numeric segments to a numeric value.
  3. Compare that structure in the desired order to get the intended collation sequence.

Personally, I'd create a class that represented the structure implicit in the filename. Perhaps it should wrap the FileInfo. The constructor for that class should parse the filename into its constituent parts and instantiate the properties of the class appropriately.

The class should implement IComparable/IComparable<T> (or you could create an implementation of Comparer).

Sort your objects and they should then come out in the collating sequence you desire.

If looks like your file names are composed of 3 parts:

  • a high-order numeric value (let's call it 'hi'),
  • a low-order numeric value (let's call it 'lo'),
  • and an extension (let's call it 'ext')

So your class might look something like

public class MyFileInfoWrapper : IComparable<MyFileInfoWrapper>,IComparable
{
  public MyFileInfoWrapper( FileInfo fi )
  {
    // your implementation here
    throw new NotImplementedException() ;
  }

  public int    Hi         { get ; private set ; }
  public int    Lo         { get ; private set ; }
  public string Extension  { get ; private set ; }

  public FileInfo FileInfo { get ; private set ; }

  public int CompareTo( MyFileInfoWrapper other )
  {
    int cc ;
    if      ( other   == null     ) cc = -1 ;
    else if ( this.Hi <  other.Hi ) cc = -1 ;
    else if ( this.Hi >  other.Hi ) cc = +1 ;
    else if ( this.Lo <  other.Lo ) cc = -1 ;
    else if ( this.Lo >  other.Lo ) cc = +1 ;
    else                            cc = string.Compare( this.Extension , other.Extension , StringComparison.InvariantCultureIgnoreCase ) ;
    return cc ;
  }

  public int CompareTo( object obj )
  {
    int cc ;
    if      ( obj == null              ) cc = -1 ;
    else if ( obj is MyFileInfoWrapper ) cc = CompareTo( (MyFileInfoWrapper) obj ) ;
    else throw new ArgumentException("'obj' is not a 'MyFileInfoWrapper' type.", "obj") ;
    return cc ;
  }

}
Nicholas Carey
  • 71,308
  • 16
  • 93
  • 135
0

Implement Comparison method for your specific case and use it in Array.Sort.

private int CompareByNumericName(FileInfo firstFile, FileInfo secondFile)
{
    /* First remove '0-' and '.jpeg' from both filenames then... */

    int firstFileNumericName = Int32.Parse(firstFile.Name);
    int secondFileNumericName = Int32.Parse(secondFile.Name);

    return firstFileNumericName.CompareTo(secondFileNumericName);
}

 

FileInfo[] files = di.GetFiles();
Array.Sort<FileInfo>(files, CompareByNumericName);
Almir
  • 1,158
  • 8
  • 9