diff --git a/BFR/MainWindow.xaml b/BFR/MainWindow.xaml index e98ffcc..4e63193 100644 --- a/BFR/MainWindow.xaml +++ b/BFR/MainWindow.xaml @@ -280,5 +280,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/BFR/MainWindowUIProperties.cs b/BFR/MainWindowUIProperties.cs index d36b936..8f9e2b3 100644 --- a/BFR/MainWindowUIProperties.cs +++ b/BFR/MainWindowUIProperties.cs @@ -28,6 +28,7 @@ namespace BFR OperationType.Make(), OperationType.Make(), OperationType.Make(), + OperationType.Make(), }; public static readonly AvaloniaProperty IsCommitButtonEnabledProperty = diff --git a/BFR/Operations/Sort.cs b/BFR/Operations/Sort.cs new file mode 100644 index 0000000..e30b7c5 --- /dev/null +++ b/BFR/Operations/Sort.cs @@ -0,0 +1,134 @@ +using System; +using System.Linq; +using System.Text; +using System.Globalization; +using System.Collections.Generic; + +using BFR.Helpers; +using BFR.DataModels; + +using Avalonia; + +namespace BFR.Operations +{ + public class Sort : Operation + { + public override string Name => nameof(Sort); + public override string Description => "Sorts the file names according to how they are when they arrive at this operation."; + + public static readonly AvaloniaProperty ModeProperty = + AvaloniaProperty.Register(nameof(Mode), SortMode.Normal); + public SortMode Mode { get => GetValue(ModeProperty); set => SetValue(ModeProperty, value); } + public SortDirection Direction { get; set; } + + public SortOperationMode[] Modes => SortOperationMode.Modes; + public string[] Directions => Enum.GetNames(typeof(SortDirection)); + + public bool FullName { get; set; } = false; + + private Func GetName => FullName + ? (Func)(x => x.FullName) + : (Func)(x => x.Name); + + protected override void ApplyToInternal(IList files) + { + // Fail conditions: No fail conditions. + // Apply operation + files.ReplaceAll(new List (Mode switch + { + SortMode.Normal => files.OrderBy(GetName, StringComparer.CurrentCulture), + SortMode.Natural => files.OrderBy(GetName, Comparer.Create(CompareNatural)), + SortMode.Ordinal => files.OrderBy(GetName, StringComparer.Ordinal), + SortMode.Length => files.OrderBy(x => GetName(x).Length), + SortMode.Reverse => files.Reverse(), + _ => files + })); + if (Mode != SortMode.Reverse && Direction == SortDirection.Descending) + files.ReplaceAll(new List(files.Reverse())); + } + + // Code taken and slightly modified from https://git.lastassault.de/speatzle/BulkFileRenamer + public static int CompareNatural(string strA, string strB) => + CompareNatural(strA, strB, CultureInfo.CurrentCulture, CompareOptions.IgnoreCase); + + public static int CompareNatural(string strA, string strB, CultureInfo culture, CompareOptions options) + { + var cmp = culture.CompareInfo; + var (iA, iB) = (0, 0); + var softResult = 0; + var softResultWeight = 0; + while (iA < strA.Length && iB < strB.Length) + { + var isDigitA = char.IsDigit(strA[iA]); + var isDigitB = char.IsDigit(strB[iB]); + if (isDigitA != isDigitB) + return cmp.Compare(strA, iA, strB, iB, options); + else if (!isDigitA && !isDigitB) + { + var jA = iA + 1; + var jB = iB + 1; + while (jA < strA.Length && !char.IsDigit(strA[jA])) jA++; + while (jB < strB.Length && !char.IsDigit(strB[jB])) jB++; + var cmpResult = cmp.Compare(strA, iA, jA - iA, strB, iB, jB - iB, options); + if (cmpResult != 0) + { + var (secA, secB) = (strA[iA..jA], strB[iB..jB]); + if (cmp.Compare(secA + "1", secB + "2", options) == + cmp.Compare(secA + "2", secB + "1", options)) + return cmp.Compare(strA, iA, strB, iB, options); + else if (softResultWeight < 1) + { + softResult = cmpResult; + softResultWeight = 1; + } + } + (iA, jA) = (iB, jB); + } + else + { + var zeroA = (char)(strA[iA] - (int)char.GetNumericValue(strA[iA])); + var zeroB = (char)(strB[iB] - (int)char.GetNumericValue(strB[iB])); + var (jA, jB) = (iA, iB); + while (jA < strA.Length && strA[jA] == zeroA) jA++; + while (jB < strB.Length && strB[jB] == zeroB) jB++; + int resultIfSameLength = 0; + do + { + isDigitA = jA < strA.Length && char.IsDigit(strA[jA]); + isDigitB = jB < strB.Length && char.IsDigit(strB[jB]); + var numA = isDigitA ? (int)char.GetNumericValue(strA[jA]) : 0; + var numB = isDigitB ? (int)char.GetNumericValue(strB[jB]) : 0; + if (isDigitA && (char)(strA[jA] - numA) != zeroA) isDigitA = false; + if (isDigitB && (char)(strB[jB] - numB) != zeroB) isDigitB = false; + if (isDigitA && isDigitB) + { + if (numA != numB && resultIfSameLength == 0) + resultIfSameLength = numA < numB ? -1 : 1; + jA++; + jB++; + } + } + while (isDigitA && isDigitB); + if (isDigitA != isDigitB) + return isDigitA ? 1 : -1; + else if (resultIfSameLength != 0) + return resultIfSameLength; + var (lA, lB) = (jA - iA, jB - iB); + if (lA != lB) + return lA > lB ? -1 : 1; + else if (zeroA != zeroB && softResultWeight < 2) + { + softResult = cmp.Compare(strA, iA, 1, strB, iB, 1, options); + softResultWeight = 2; + } + (iA, iB) = (jA, jB); + } + } + if (iA < strA.Length || iB < strB.Length) + return iA < strA.Length ? 1 : -1; + else if (softResult != 0) + return softResult; + return 0; + } + } +} diff --git a/BFR/Operations/SortMode.cs b/BFR/Operations/SortMode.cs new file mode 100644 index 0000000..c8f4997 --- /dev/null +++ b/BFR/Operations/SortMode.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +using BFR.Helpers; + +namespace BFR.Operations +{ + public class SortOperationMode : OperationMode + { + public bool IsAlphanumerical => Index == SortMode.Normal; + public bool IsNatural => Index == SortMode.Natural; + public bool IsLength => Index == SortMode.Length; + public bool IsReverse => Index == SortMode.Reverse; + + public static readonly SortOperationMode[] Modes = All(); + + protected static SortOperationMode[] All() => ((IEnumerable)Enum.GetValues(typeof(SortMode))).Select(x => + new SortOperationMode( + x, + x.GetAttribute().DisplayName, + x.GetAttribute().Description + )).ToArray(); + + public SortOperationMode(SortMode index, string name, string description) : + base(index, name, description) + { } + } + + public enum SortMode + { + [OperationMode(nameof(Normal), "Compares file names based on current culture.")] + Normal, + [OperationMode(nameof(Ordinal), "Compares each successive ordinal character in a string (each character as its ASCII number value).")] + Ordinal, + [OperationMode(nameof(Natural), "Sorts by natural sort order (numeric values grouped).")] + Natural, + [OperationMode(nameof(Length), "Sorts by file name length.")] + Length, + [OperationMode(nameof(Reverse), "Reverses the list order.")] + Reverse, + } + + public enum SortDirection + { + Ascending, + Descending + } +}