Skip to content

Commit

Permalink
Sorting like Xcode. Fixes #615
Browse files Browse the repository at this point in the history
  • Loading branch information
Coeur committed May 16, 2019
1 parent 3be1684 commit 3ebffad
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 3 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions lib/xcodeproj/project/object/group.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions lib/xcodeproj/project/object/helpers/sort_helper.rb
Original file line number Diff line number Diff line change
@@ -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
81 changes: 81 additions & 0 deletions spec/project/object/helpers/sort_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 3ebffad

Please sign in to comment.