4

Swift 4

I have an array that will have random days of the week text in it. For example

var daysOfWeek: [String] = [] // ["Tuesday", "Thursday" , "Sunday", "Friday"]

I want to be able to sort them like: Sunday, Monday, Tuesday, ect...

Im not sure if this is the right approach, but I tried this..

     let dateFormatter = DateFormatter()

    for element in daysOfWeek {

        print(dateFormatter.weekdaySymbols[element])
    }

Which throws an error:

value of optional type '[String]?' must be unwrapped to refer to member 'subscript' of wrapped base type

Im fairly new to Xcode and Swift

Is this the right way to do it? if so, how can I fix the error?

If this isn't the right way to do it, then what is? I appreciate any help

5 Answers 5

10

You can create a dictionary like this, that corresponds each string to a numerical value:

let weekDayNumbers = [
    "Sunday": 0,
    "Monday": 1,
    "Tuesday": 2,
    "Wednesday": 3,
    "Thursday": 4,
    "Friday": 5,
    "Saturday": 6,
]

And then you can just sort by this:

weekdays.sort(by: { (weekDayNumbers[$0] ?? 7) < (weekDayNumbers[$1] ?? 7) })

This will order non-weekday strings at the end.

Also note that different areas of the world have a different start of week. They might order things differently.

Sign up to request clarification or add additional context in comments.

2 Comments

Clever solution. (voted) I was thinking of using a date formatter's spelled out weekday symbols to convert the week names to integers, but yours is simpler. (On the other hand if you want the solution to work for different local languages then the DateFormatter approach would be a better choice.)
@DuncanC It might be worth submitting your solution as well, as you raise a good point about locales. And it's always good to have a number of different solutions for a problem, as we can always pick out elements of an answer that are useful in some other context.
6

Here is the way to do it:

let week = DateFormatter().weekdaySymbols!
print(week)  //["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]

This way, the day names and the first day of the week will be set automatically based on the current locale and system settings. For example:

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "fr-FR")
let week = formatter.weekdaySymbols!
print(week)  //["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]

To sort an array of some day names:

let week = DateFormatter().weekdaySymbols!
var daysOfWeek: [String] = ["Tuesday", "Thursday" , "Sunday", "Friday"]
daysOfWeek.sort { week.firstIndex(of: $0)! < week.firstIndex(of: $1)!}
print(daysOfWeek) //["Sunday", "Tuesday", "Thursday", "Friday"]

I am force-unwrapping here just for brevity. You could check that all strings in daysOfWeek are valid using:

var daysOfWeek: [String] = ["Tuesday", "Thursday" , "Sunday", "Friday"]
let week = DateFormatter().weekdaySymbols!
guard Set(daysOfWeek).isSubset(of: week) else {
    fatalError("The elements of the array must all be day names with the first letter capitalized")
}

To make the above solution quicker, as suggested by Mr Duncan, here is an alternative approach:

let week = DateFormatter().weekdaySymbols!
var dayDictionary: [String: Int] = [:]
for i in 0...6 {
    dayDictionary[week[i]] = i
}
var daysOfWeek: [String] = ["Tuesday", "Thursday" , "Sunday", "Friday"]
daysOfWeek.sort { (dayDictionary[$0] ?? 7) < (dayDictionary[$1] ?? 7)}
print(daysOfWeek) //["Sunday", "Tuesday", "Thursday", "Friday"]

Using strings as day name identifiers is prone to error. A safer approach uses enums:

enum WeekDay: String {
    case first      = "Sunday"
    case second     = "Monday"
    case third      = "Tuesday"
    case fourth     = "Wednesday"
    case fifth      = "Thursday"
    case sixth      = "Friday"
    case seventh    = "Saturday"
}

let week: [WeekDay] = [.first, .second, .third, .fourth, .fifth, .sixth, .seventh]
var dayDictionary: [WeekDay : Int] = [:]
for i in 0...6 {
    dayDictionary[week[i]] = i
}
var daysOfWeek: [WeekDay] = [.third, .fifth , .first, .sixth]
daysOfWeek.sort { (dayDictionary[$0] ?? 7) < (dayDictionary[$1] ?? 7)}
print(daysOfWeek.map {$0.rawValue}) //["Sunday", "Tuesday", "Thursday", "Friday"]

11 Comments

If you don't need the date formatter, just use Calendar.current.weekdaySymbols.
Im trying to sort the array for those days.
Carpsen, your solution is using Array.firstIndex(of:) in the body of the sort. That method is slow, and has O(n) time performance, so if you're sorting a large array of week names it will be slow.
I tried a benchmark of your original array-based approach and my dictionary-based approach, and mine was about 5x faster. However, when I turned on speed optimization in the compiler your array-based version was almost 3x faster! Who would've thought? Dictionaries use hashes for lookup, and arrays use linear searching. I have no idea how the array-based version could be faster!
@thibautnoah It depends on the locale
|
3

Here's a solution that doesn't require creating a dictionary of week days and their indexes.

func sortWeekDays(_ weekDays: [String]) -> [String]? {
    guard let correctOrder = DateFormatter().weekdaySymbols else {
        return nil
    }
    let result = weekDays.sorted {
        guard let firstItemIndex = correctOrder.firstIndex(of: $0),
            let secondItemIndex = correctOrder.firstIndex(of: $1) else {
                return false
        }
        return firstItemIndex < secondItemIndex
    }
    return result
}

Comments

2

My solution is a variant of @sweeper's solution:

var inputDaysOfWeek: [String] = ["Tuesday", "Thursday" , "Sunday", "Friday", "Foo", "Bar"]
print("inputDaysOfWeek = \(inputDaysOfWeek)")

//Build a dictionary of days of the week using the current calendar,
//which will use the user's current language
//This step only needs to be done once, at startup.
var weekdaysDict = [String: Int]()
let weekdays = Calendar.current.weekdaySymbols.enumerated()
weekdays.forEach { weekdaysDict[$0.1]  = $0.0 }
//-----------------

//If a weekday name doesn't match the array of names, use a value of -1, 
//which will cause it to sort at the beginning of the sorted array.
inputDaysOfWeek.sort {weekdaysDict[$0] ?? -1 < weekdaysDict[$1] ?? -1 }
print("sorted inputDaysOfWeek = \(inputDaysOfWeek)")

My code builds a dictionary of weekday names and their index values, like Sweeper's answer. Carpsen's approach of using firstIndex(of:) will work, but it will be slower, and for large arrays of strings might be quite a bit slower. I'm using the weekday symbols from the current calendar, which will be in the user's locale/language. If you want to force the array of weekday names to a specific language/locale, you could instead use a DateFormatter created for that language/locale, as in Carpsen's answer.

Note that if the upper-lower case value of the input weekday name strings is unpredictable then you might want to change the code above slightly to lowerCase the dictionary of weekday names and also lower-case the array of strings to be sorted so that weekday names with different case still match.


EDIT:

I wrote a test command line tool that uses both array-based matching of items (as per @Carpsen90's first solution) and my/Sweeper's dictionary-based matching, and found that the array-based version takes about 5x as long on a 1,000,000 element array of weekday names. A factor of 5x on 1M items isn't bad, truth be told. That suggests that the two approaches have the same time complexity.

HOWEVER, when I turn on "optimize for speed" in the compiler, however, the dictionary based approach then takes about 2.8x longer!

Below is all the test code:

//Build a dictionary of days of the week using the current calendar,
//which will use the user's current language
var weekdaysDict = [String: Int]()
let weekdays = Calendar.current.weekdaySymbols
let weekdayTuples = weekdays.enumerated()
weekdayTuples.forEach { weekdaysDict[$0.1]  = $0.0 }
//--------------- --

func sortWeekDaysUsingDict(array: [String]) -> [String] {
    let result = array.sorted { weekdaysDict[$0] ?? -1 < weekdaysDict[$1] ?? -1 }
    return result
}

func sortWeekDaysUsingArray(array: [String]) ->  [String] {
    let result = array.sorted { weekdays.firstIndex(of: $0)! < weekdays.firstIndex(of: $1)! }
    return result
}

/*This function times a sorting function
 It takes an array to sort, a function name (for logging) and a function pointer to the sort function.
 It calculates the amount of time the sort function takes, logs it, and returns it as the function result.
 */
func sortArray(array: [String],
               functionName: String,
               function: ([String]) -> [String]) -> TimeInterval {
    let start = Date().timeIntervalSinceReferenceDate
    let _ = function(array)
    let elapsed = Date().timeIntervalSinceReferenceDate - start
    print("\(functionName) for \(array.count) items took " + String(format: "%.3f", elapsed) + " seconds")
    return elapsed
}

//Build a large array of random day-of-week strings:
var randomWeekdayNames = [String]()
for _ in 1 ... 1_000_000 {
    randomWeekdayNames.append(weekdays.randomElement()!)
}

let time1 = sortArray(array: randomWeekdayNames,
                      functionName: "sortWeekDaysUsingDict(array:)",
                      function: sortWeekDaysUsingDict(array:))
let time2 = sortArray(array: randomWeekdayNames,
                      functionName: "sortWeekDaysUsingArray(array:)",
                      function:  sortWeekDaysUsingArray(array:))

if time1 > time2 {
    print("dict-based sorting took " + String(format:"%0.2f", time1/time2) + "x longer")
} else {
    print("array-based sorting took " + String(format:"%0.2f", time2/time1) + "x longer")
}

With optimization turned off (the debug default) The result is:

sortWeekDaysUsingDict(array:) for 1000000 items took 9.976 seconds
sortWeekDaysUsingArray(array:) for 1000000 items took 59.134 seconds
array-based sorting took 5.93x longer

But with "optimize for speed" selected, the results are quite different:

sortWeekDaysUsingDict(array:) for 1000000 items took 3.314 seconds
sortWeekDaysUsingArray(array:) for 1000000 items took 1.160 seconds
dict-based sorting took 2.86x longer

That is quite surprising, and I don't know how to explain it.

EDIT #2:

Ok, I figured it out. The fact there are only 7 possible values in the array/dict of keys we're matching skews the results.

I did another test where instead of days of the week, I was sorting spelled-out numbers from "one" to "one thousand" In that case, the dict-based approach is a LOT faster, as I expected:

Optimized for time performance, and using 1,000 unique words:

sortWeekDaysUsingDict(array:) for 100000 items took 0.520 seconds
sortWeekDaysUsingArray(array:) for 100000 items took 85.162 seconds
array-based sorting took 163.64x longer

(Dealing with 1000 unique words, the array-based approach is too slow with 1,000,000 random words to sort. I had to lower the number of random words to 100,000 for the second set of tests.)

3 Comments

An enum would be a better way to avoid lower case and random strings in day names
Explain your enum idea?
Something like enum WeekDay { case sunday; case monday; case tuesday; case wednesday; case thursday; case friday; case saturday }; let week: [WeekDay] = [.sunday, .monday, .tuesday, .wednesday, .thursday, .friday, .saturday].
0

Firstly convert the array to String and check the
selected days = ["Friday","Thursday","Monday"]
let stringRepresentation = selectedDays.joined(separator: ",")

    var sequenceDays = [String]()
    if stringRepresentation.contains("Monday") {
        sequenceDays.append("Monday")
    }
    if stringRepresentation.contains("Tuesday") {
        sequenceDays.append("Tuesday")
    }
    if stringRepresentation.contains("Wednesday") {
        sequenceDays.append("Wednesday")
    }
    if stringRepresentation.contains("Thursday") {
        sequenceDays.append("Thrusday")
    }
    if stringRepresentation.contains("Friday") {
        sequenceDays.append("Friday")
    }
    if stringRepresentation.contains("Saturday") {
        sequenceDays.append("Saturday")
    }
    if stringRepresentation.contains("Sunday") {
        sequenceDays.append("Sunday")
    }

    print(sequenceDays)//["Monday","Thursday","Friday"]

// let Days = sequenceDays.joined(separator: ",") print(Days) let parsedDays = Days.replacingOccurrences(of: "day", with: "") print(parsedDays)

1 Comment

Hi Hafeez, welcome to Stack Overflow. Can you more specifically address this user's question? Your answer builds a new array, which I think is different from their question of sorting the array.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.