Process dictionary in alphanumeric order

Rather than export values to an organised type from a dictionary I would like to filter on-the-fly in a loop to process the keys in alphanumeric order:

enum Breakfast: String {
    
    case breakfast1
    case breakfast2
    case breakfast3
    case breakfast4
    case breakfast5
}

var breakfastDiary: [String : [Breakfast : Bool]] = [ "Week1" :  [.breakfast1 : true,
                                                                  .breakfast2 : false,
                                                                  .breakfast3 : false,
                                                                  .breakfast4 : false,
                                                                  .breakfast5 : false]
                                                    ]
let organiseMeals = breakfastDiary.sorted { $0.1 < $0.1 }

I tried looking into sort and reduce/map but couldn't figure in a simple way?

What’s the end-result you’re trying to achieve?

Both sides are the same so this will always return false.

I need to get that data somewhere else to create something from it, but the order must be maintained. Before I used an array which made the matter easier, but now I have a need for more control over what data is presented (hence the boolean values), but still need to maintain the order.

What’s the resulting type you aim for, and what order you want to impose on it?
You have 3 layers in that dictionary.
I don’t know if you want to merge any 2 layers, converting types, sort which layers while maintain which layer order. There’s hardly anything I can infer from here.

I’d be nice if you provide the “result” from the example (by hand perhaps?) At least I’d know what it should look like.

I need to represent each result with a sprite, so the false values would be an empty plate sprite, and the true condition with food.

The issue for me is that while I can do this, I cannot maintain the alphanumerical order of the results as they are presented in the order they are processed, of which in a dictionary that could be anything.

func createImage() {
    
    for (key, value) in breakfastDiary {
        if key == "Week1" {
            for (key, value) in value {
                if value == true {
                    let breakfastImage: SKSpriteNode = {
                        let breakfast = SKSpriteNode(imageNamed: key)
                        breakfast.position = CGPoint(x: 0.5, y: 0.5)
                }()
            }
        }
    }
}

I see, you also seem to have some freedom about the type of breakfastDiary. You have a few options:

  • If you don’t call createImage frequently, you can sort it on the fly.

    for (key, values) in breakfastDiary.sorted(by: { $0.key < $1.key }) where key == "Week1" {
      for (key, value) in values.sorted(by: { $0.key.rawValue < $1.key.rawValu }) where value {
        ...
      }
    }
    
  • If you don’t mutate breakfastDiary a lot, you can convert them into big [(String, Breakfast, Bool)] first.

    let newDiary = breakfastDiary.flatMap { arg in
      arg.value.map { value -> (String, Breakfast, Bool) in
        (arg.key, value.key, value.value)
      }
    }.sorted {
      $0.0 < $1.0 || $0.0 == $1.0 && $0.1.rawValue < $1.1.rawValue
    }
    

    Then use it like this

    for (key1, key2, value) in newDiary {
      guard key1 == "Week1" else {
        continue
      }
      ...
    }
    

    Though you’ll need to do it every time you mutate data.

Further the current scenario has 3 states, if the value is true, if the value is false, and if the key(s) is not in the list.

If your data is dense, and all keys are always in the list (only differ by true/false value) you can make them a Set, and iterate through Breakfast instead. See CaseIterable

enum Breakfast: String, CaseIterable, Hashable {
  ...
}

let trueSet = ...

for value in Breakfast.allCases {
  if trueSet.contains(value) {
    // value is true
    ...
  }
}
2 Likes

Wouldn’t creating a custom type that has a dictionary and a sorted array of the keys work, too? Then you could iterate on the array and reference the dictionary entries?

Hi Lantua, apologies for the late reply, I was ill.

Your first approach was perfect for me, the sorted(by: { $0.key.rawValue < $1.key.rawValue } will make for an invaluable codeSnippet in the future, until I can memorise it.

I have not come across the method you used to infer that only true values would be accepted at the end with the where statement, I found it a little too cryptic to understand, that's a little beyond me for now.

Flat Map didn't seem the solution to me before, or even after here with your example, it looks over complicated compared to your first solution. Reading along it seems to allow for an extra argument/parameter (I never see the term "arg" used in Swift, but I have seen it in Perl code, do you have a Perl background?) so I could just rummage through a dictionary as I normally would but it would have been sorted beforehand.

As for using Sets, I never have, I always rely on Arrays or Dictionaries. The enum is part of a larger one, so iterating over it would be clumsy with the way I have one massive Enum, that's why I couldn't use that system, but I am familiar with CaseIterable, it was a good late addition to Swift and I have made good use of it before.

Surely sortedBy would be the preferred option?

Thanks !

So in the end it should have been:

sorted(by: { $0.key.rawValue < $1.key.rawValue }

Here's some explaination.

  1. Dictionary is a collection of key/value pair.

    When you use map/reduce/sort functions on them you'll notice that Dictionary will use (key: Key, value: Value).

  2. sorted(by:) takes a closure that takes 2 parameters, left-hand-side and right-hand-side which are elements you want to compare. You tell the algorithm if lhs should precede rhs.

    That is

    data.sorted(by: { lhs, rhs in /* true if lhs should precede rhs */ })
    

    that's why [1, 2, 4, 5, 3].sorted(by: <) and [1, 2, 4, 5, 3].sorted(by: >) returns ascending and descending collection respectively.

Now to combine both together, you have values which is a collection of (key: Breakfast, value: Bool) and you want to sort them lexicographically by key, so you compare them by their String value of the Breakfast which will be the same as the name of the enum.

If you're to write a full code, it'd look like this

// type of lhs and rhs are the type of collection's *element*
values.sorted { (lhs: (key: Breakfast, value: Bool), rhs: (key: Breakfast, value: Bool)) in
  let leftBreakfast = lhs.key, rightBreakfast = rhs.key
  let leftString = leftBreakfast.rawValue, rightString = rightBreakfast.rawValue
  return leftString < rightString // Compare lexicographically
}

With some key-stroke optimization, it becomes

values.sorted { lhs, rhs in // omit type, swift can infer that
  return lhs.keys.rawValue < rhs.keys.rawValue // get straight to leftString and rightString
}

or better

// Omit argument names, the first one becomes $0, the second one becomes $1
// Also omit "return"
// Since we put it in `for-in`, we need to put closure back inside parentheses.
values.sorted(by: { $0.keys.rawValue < $1.keys.rawValue })

Eventually you can just get straight to the last version. It'd become second nature in to time.

You could also create a special type that handle all this like @toph42 said. Depending of the frequency that you use this stuff that could be a better choice.

Hmm, arg was the name used by a fix-it in ye olde Swift. I guess I sticked with that ever since.

PS

I don't see someone mention ppl like thst often [Lantua](/u/Lantua). I generally see @Lantua. Quite interesting that the forum still notifies me.

1 Like

Thanks for the explanation, I think one of the new changes in Swift is to omit the return when there is only one value to possibly return, but that's new, right?

I just hit the reply button and the forum code did the rest, I didn't specify, but it should notify you even if it's the old way of doing things.

Terms of Service

Privacy Policy

Cookie Policy