From 3ebffad9062e5ba5c9956b32ad744c60876089a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20C=C5=93ur?= Date: Fri, 17 May 2019 00:43:10 +0800 Subject: [PATCH] Sorting like Xcode. Fixes #615 --- CHANGELOG.md | 4 +- lib/xcodeproj/project/object/group.rb | 5 +- .../project/object/helpers/sort_helper.rb | 51 ++++++++++++ .../object/helpers/sort_helper_spec.rb | 81 +++++++++++++++++++ 4 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 lib/xcodeproj/project/object/helpers/sort_helper.rb create mode 100644 spec/project/object/helpers/sort_helper_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 852eb84c8..bb80aa1af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ ##### Enhancements -* None. +* Sort by name like Xcode does. +[Antoine Cœur](https://github.com/Coeur) +[#677](https://github.com/CocoaPods/Xcodeproj/pull/677) ##### Bug Fixes diff --git a/lib/xcodeproj/project/object/group.rb b/lib/xcodeproj/project/object/group.rb index b403080c7..fd4f0e024 100644 --- a/lib/xcodeproj/project/object/group.rb +++ b/lib/xcodeproj/project/object/group.rb @@ -1,5 +1,6 @@ require 'xcodeproj/project/object/helpers/groupable_helper' require 'xcodeproj/project/object/helpers/file_references_factory' +require 'xcodeproj/project/object/helpers/sort_helper' module Xcodeproj class Project @@ -438,9 +439,9 @@ def sort(options = nil) end end - result = File.basename(x.display_name.downcase, '.*') <=> File.basename(y.display_name.downcase, '.*') + result = XcodeSortString.new(File.basename(x.display_name, '.*')) <=> XcodeSortString.new(File.basename(y.display_name, '.*')) if result.zero? - File.extname(x.display_name.downcase) <=> File.extname(y.display_name.downcase) + XcodeSortString.new(File.extname(x.display_name)) <=> XcodeSortString.new(File.extname(y.display_name)) else result end diff --git a/lib/xcodeproj/project/object/helpers/sort_helper.rb b/lib/xcodeproj/project/object/helpers/sort_helper.rb new file mode 100644 index 000000000..2d0aca895 --- /dev/null +++ b/lib/xcodeproj/project/object/helpers/sort_helper.rb @@ -0,0 +1,51 @@ +module Xcodeproj + # Wrapper for a string that sorts by name like Xcode does. + # @example + # arrayOfFilenames.sort_by { |s| XcodeSortString.new(s) } + class XcodeSortString + include Comparable + attr_reader :str_fallback, :ints_and_strings, :ints_and_strings_fallback, :str_pattern + + def initialize(str) + # fallback pass + @str_fallback = str + # first pass: digits are used as integers, symbols are individualized, case is ignored + @ints_and_strings = str.scan(/\d+|\p{L}+|[^\d\p{L}]/).map do |s| + case s + when /\d/ then Integer(s, 10) + else + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.0') + s.unicode_normalize(:nfkd).gsub(/\p{Mn}/, '').downcase + else + s.downcase + end + end + end + # second pass: digits are inverted, case is inverted + @ints_and_strings_fallback = @str_fallback.scan(/\d+|\D+/).map do |s| + case s + when /\d/ then Integer(s.reverse, 10) + else s.swapcase + end + end + # comparing patterns: credit to https://rosettacode.org/wiki/Natural_sorting#Ruby + @str_pattern = @ints_and_strings.map { |el| el.is_a?(Integer) ? :i : :s }.join + end + + def <=>(other) + if str_pattern.start_with?(other.str_pattern) || other.str_pattern.start_with?(str_pattern) + compare = ints_and_strings <=> other.ints_and_strings + if compare != 0 + # we sort naturally (literal ints, symbols individualized, case ignored) + compare + else + # natural equality, we use the fallback sort (int reversed, case swapped) + ints_and_strings_fallback <=> other.ints_and_strings_fallback + end + else + # type mismatch, we sort alphabetically (case ignored) + str_fallback.downcase <=> other.str_fallback.downcase + end + end + end +end diff --git a/spec/project/object/helpers/sort_helper_spec.rb b/spec/project/object/helpers/sort_helper_spec.rb new file mode 100644 index 000000000..e0985be8b --- /dev/null +++ b/spec/project/object/helpers/sort_helper_spec.rb @@ -0,0 +1,81 @@ +require File.expand_path('../../../../spec_helper', __FILE__) + +module ProjectSpecs + describe Xcodeproj::XcodeSortString do + before do + @helper = XcodeSortString + end + + #-------------------------------------------------------------------------# + + describe 'In general' do + it 'sorts names like Xcode' do + # Let's note that Xcode 10.2 on macOS 10.14.5 does not have a strong ordering + # when sorting unicode characters that transliterate to the same ascii characters + # so our unicode tests can stay rudimentary until Xcode is fixed for that. + # [rdar://50854433](http://www.openradar.me/radar?id=5012044621283328) + unsorted_names = [ + # spaces comparison + ' a', ' a', 'a ', 'a b', 'a a', + # dots comparison + '.a', '..a', 'a.', '1.', + # basic mix + '1a', '2 a', 'a1', 'a 2', + # pure integers + '1', '2', '10', '01', + # multi integers + '0.1.1', '0.1.2', '0.1.10', '0.1.01', + # multi equal integers + 'A1B001', 'A01B1', + # case comparison + 'A', 'a' + ] + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.0') + unsorted_names += [ + # unicode + 'ffi ', 'ffh ', 'ffj ' + ] + end + sorted = unsorted_names.sort_by { |s| @helper.new(s) } + # order given by Xcode 10.2 "Sort by Name" on macOS 10.14.5 (ruby 2.3.7p456), English as primary language + should = [ + ' a', + ' a', + '..a', + '.a', + '0.1.1', + '0.1.01', + '0.1.2', + '0.1.10', + '1', + '01', + '1.', + '1a', + '2', + '2 a', + '10', + 'a', + 'A', + 'a ', + 'a b', + 'a 2', + 'a a', + 'a.', + 'a1', + 'A1B001', + 'A01B1', + ] + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.2.0') + should += [ + 'ffh ', + 'ffi ', + 'ffj ', + ] + end + sorted.should == should + end + end + + #-------------------------------------------------------------------------# + end +end