1

I want to sort my data by the title of media item in my table view but I want to sort it by the first "actual" letter, without the common articles like "the" or "an"

  • A Ballad
  • Ballad of Red Beard Pirate
  • The Ballad of Black Beard Pirate
  • Balls of steel

I tried a solution below:

  extension String {

    func firstLetter() -> Character{
            var tmp = self.lowercased()
            if tmp.hasPrefix("the "){
                tmp = String(tmp.characters.dropFirst(4))
            }else if tmp.hasPrefix("a "){
                tmp = String(tmp.characters.dropFirst(2))
            }else if tmp.hasPrefix("an "){
                tmp = String(tmp.characters.dropFirst(3))
            }
            let hmm = "aąbcćdeęfghijklmnoópqrsśtuvwxyzżź0123456789"
            let letters = Array(hmm.characters)
            for index in characters.indices{
                if letters.contains(tmp[index]){
                    return tmp[index]
                }
            }
            return "_"
        }

        func firstSpecial() -> Bool {
            let characterset = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
            if prefix(1).rangeOfCharacter(from: characterset.inverted) != nil {
                return true
            }else{
                return false
            }
        }

        func firstNumber() -> Bool {
            if lowercased().hasPrefix("the "){
                return Int(dropFirst(4).prefix(1)) != nil
            }else if lowercased().hasPrefix("a "){
                return Int(dropFirst(2).prefix(1)) != nil
            }else if lowercased().hasPrefix("an "){
                return Int(dropFirst(3).prefix(1)) != nil
            }else{
                return Int(prefix(1)) != nil
            }
        }
    }

songs = [MPMediaItem]()
result = [String: [MPMediaItem]]()
indexes = [String]()

    func setup(){
            var numbers = false
            var special = false
            songs = musicQuery.shared.songs
            for song in songs {
                var key = ""
                if song.title!.firstNumber() {
                    print(song.title)
                    key = "#"
                    if result[key] != nil {
                        result[key]?.append(song)
                    }else{
                        result[key] = []
                        result[key]?.append(song)
                        numbers = true
                    }
                }else if !(song.title?.firstSpecial())! {
                    key = String(describing: song.title?.firstLetter()).uppercased()
                    if result[key] != nil {
                        result[key]?.append(song)
                    }else{
                        result.updateValue([song], forKey: key)
                        indexes.append(key)
                    }
                }else{
                    print(song.title)
                    key = "?"
                    if result[key] != nil {
                        result[key]?.append(song)
                    }else{
                        result[key] = []
                        result[key]?.append(song)
                        special = true
                    }
                }
            }
            indexes = indexes.sorted { $0.compare($1) == ComparisonResult.orderedAscending }
            if numbers {
                indexes.append("#")
            }
            if special {
                indexes.append("?")
            }
        }

But it is pretty far from being optimal, takes very long time to complete and omits some entries

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Adam
  • 1,776
  • 1
  • 17
  • 28

4 Answers4

1

Simplified solution, just replace those articles which you do not want to include while sorting.

    let unsortedList = ["Ballad of El Red Beard Pirate","A Ballad","Ballad of An Red Beard Pirate","The Ballad of Black Beard Pirate","Balls of steel"];

    func removeLeadingArticle(string: String) -> String {
        let articles = ["The ", "A ", "of "," An"];
        var changedStr = string;
        for (_,article) in articles.enumerated() {
            changedStr = changedStr.replacingOccurrences(of: article, with: "");
        }
        print("changed string \(changedStr)");
        return changedStr;
    }

    let sortedList = unsortedList.sorted { (firstStr, secondStr) -> Bool in
        let title1 = removeLeadingArticle(string: firstStr);
        let title2 = removeLeadingArticle(string: secondStr);
        return title1.localizedCaseInsensitiveCompare(title2) == ComparisonResult.orderedAscending
    }

    print("sorted list \(sortedList)");

output------------

changed string Ballad
changed string Ballad El Red Beard Pirate
changed string Ballad Red Beard Pirate
changed string Ballad El Red Beard Pirate
changed string Ballad Black Beard Pirate
changed string Ballad Red Beard Pirate
changed string Ballad Black Beard Pirate
changed string Ballad El Red Beard Pirate
changed string Ballad Black Beard Pirate
changed string Ballad
changed string Balls steel
changed string Ballad Red Beard Pirate
sorted list ["A Ballad", "The Ballad of Black Beard Pirate", "Ballad of El Red Beard Pirate", "Ballad of An Red Beard Pirate", "Balls of steel"]

Getting dictionary with leading character as key ignoring "A","An","The".

    let unsortedList = ["A Ballad","Ballad of An Red Beard Pirate","The Ballad of Black Beard Pirate","All Balls of steel","Red Riding Hood","The Earth"];

    let articles = ["The","A","An"];

    var dictionary:Dictionary = Dictionary<String,String>();

    for objStr in unsortedList {

        let article = objStr.components(separatedBy: " ").first!;

        print("article: \(article)");

        if articles.contains(article) {

            if objStr.components(separatedBy: " ").count > 1 {
                let secondStr = objStr.components(separatedBy: " ")[1];
                dictionary["\(secondStr.first!)"] = objStr;
            }
        }else {
            dictionary["\(article.first!)"] = objStr;
        }
    }


    print("dictionary:- \(dictionary)");

Output --------

article: A article: Ballad article: The article: All article: Red article: The

dictionary:- ["R": "Red Riding Hood", "B": "The Ballad of Black Beard Pirate", "A": "All Balls of steel", "E": "The Earth"]
Prince Kumar Sharma
  • 12,591
  • 4
  • 59
  • 90
  • your remove leading article function removes all articles including in the middle of the string and results in incorrect sorting. You cannot remove ALL of the articles only the leading ones. – Josh Homann Nov 23 '17 at 16:35
  • Hi @JoshHomann, as question given, it clearly says he want to sort by ignoring common articles like "an","the","a" etc. So I did that. If he wanna ignore first article then he should use your answer otherwise mine, we are here to provide options to him. – Prince Kumar Sharma Nov 24 '17 at 05:43
1

In order to do that, you can try with this utility function that removes any prefixes passed as argument. It uses a first match replacement: https://stackoverflow.com/a/40863622/8236481 e

extension String {
  func removingPrefixes(_ prefixes: [String]) -> String {
    var resultString = self
    prefixes.map {
      if resultString.hasPrefix($0) {
        resultString = resultString.dropFirst($0.count).description
      }
    }
    return resultString
  }
}

Using this one, you can now sort an array of strings using this function:

extension Array where Element == String {
  func sorted(ignoring: [String]) -> [String] {
    let filteredData = self.map { $0.lowercased().removingPrefixes(ignoring) }
    let sortedData = filteredData.enumerated().sorted { $0.element < $1.element }
    return sortedData.map { self[$0.offset] }
  }
}
  1. Remove any unwanted articles from the input data.
  2. Sort that array
  3. Return original array using sorted array indices

Hope it helped you!

Oxthor
  • 512
  • 4
  • 15
  • stringByRemovingAll doesn't work for this case; you need to strip the leading articles, not every article from the string. The ones in the middle of the string are still valid and this is goign to cause your function to sort incorrectly for some cases. – Josh Homann Nov 23 '17 at 16:37
  • that's true, just edited my answer with just replacing the first occurrence if it was a prefix. – Oxthor Nov 23 '17 at 16:53
  • Its still wrong. You should not replace the first occurance as in "Beauty and The Beast". You should only replace a prefix as in "The Great Gatsby". – Josh Homann Nov 23 '17 at 16:56
  • I am only replacing the first occurrence if it has the article as prefix – Oxthor Nov 23 '17 at 17:05
0

You can use the built in sort (mutating) or sorted (copy) functions on array to sort in any order you wish. In this case all you need to do is strip off any of the prefixes you don't want before you compare the two strings. This isn't the most efficient solution since you could be caching the substitution calculations (ie add a field to your model that has the precomputed sort string or make a dictionary with the sort strings precomputed), but it does work.

import PlaygroundSupport
import UIKit

func strippingArticles(from: String) ->  String {
    let articles = ["The ", "A "]
    var target = from
    for article in articles {
       if target.hasPrefix(article) {
           target = String(target.dropFirst(article.count))
       }
    }
    return target
}
let unsortedTitle = ["Title", "The sample", "A word"]
let sortedTitles = unsortedTitle.sorted { (lhs: String, rhs: String) -> Bool in
    let left = strippingArticles(from: lhs)
    let right = strippingArticles(from: rhs)
    return left.compare(right) == .orderedAscending
}
print(sortedTitles)
Josh Homann
  • 15,933
  • 3
  • 30
  • 33
  • hi @joshhomann, why you have deleted you first answer, you have option to edit it. You could have edited your first answer instead of writing another one. – Prince Kumar Sharma Nov 24 '17 at 06:12
-1

Don't try to figure out what the articles are; use the intelligence already built-in to iOS. Use NSLinguisticTagger to tokenize the text into words, and thus break it down into parts of speech. Now you know what initial text to ignore as you sort.

After that, it's just a matter of cosorting. And that's something that has been dealt with many times on Stack Overflow.

matt
  • 515,959
  • 87
  • 875
  • 1,141