TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

Sorting and Editing Arrays of Substring from String.split(separator: " ")

Forums > SwiftUI

Hi there, I'm slowly learning SwiftUi online and I've gotten stuck! I'm trying to sort some messy data that I get from an outside source (and I don't have control over) to make it usable in my app.

this is what is looks like when I get it

| 559a| 649a| 705a| 943a|1234p| 321p| 629p| 857p|1001p| 658a| 827a|1108a|156p| 456p| 748p|1201x|

Each number in the string represents a time on a schedule with 'a' for AM, 'p' for PM, and 'x' for AM the next day

i want to be able to past the above into a text feild in the app (this part I know how to do), then remove the '|' symbol and use the string to create an array based on them

I then want to sort them in the array chronologically (sorted both by the number itself then a/p/x)

I don't need to be able to access them as actual dates/times, just strings to feed into a ForEach list

Output would look like this:

sortedSchedule = ["559a", "649a", "658a", "705a", "827a", "943a", "1108a", "1234p", "156p", "321p", "456p", "629p", "748p", "857p", "1001p", "1201x"]

I've tried using string.split(separator: "|") and can get that sybmbol out, but i'm really struggling to figure out the sorting part.

here's what I've been testing with


struct ContentView: View {
    var providedSchedule: String = "| 559a| 649a| 705a| 943a|1234p| 321p| 629p| 857p|1001p| 658a| 827a|1108a|156p| 456p| 748p|"

    var splitSchedule: [String] {
     let times = providedSchedule.split(separator: "|").map(String.init)
        return times
    }

    var body: some View {
        List {
            ForEach(splitSchedule, id: \.self) { time in 
                Text(time)
            }
        }
    }

}

I'm not sure if there's a better way to do this, but any direction would be helpful!

2      

Here's what I came up with. Noon and midnight proved a wee bit tricky, but it wasn't that bad.

import Foundation

let providedSchedule: String = "| 559a| 649a| 705a| 943a|1234p| 321p| 629p| 857p|1001p| 658a| 827a|1108a|156p| 456p| 748p|"

let times = providedSchedule.split(separator: "|").map({$0.trimmingCharacters(in: .whitespaces)})

let sortedTimes = times.sorted { str1, str2 in
    //standardize all times to 4 digits and a letter
    var s1 = "0\(str1)".suffix(5), s2 = "0\(str2)".suffix(5)

    //are the times in the same part of the day?
    //if not, AM always comes before PM
    if s1.last! != s2.last! { return s1.last! == "a" }

    //special cases for noon and midnight
    //they should always sort at the start of their respective
    //parts of the day, yet 12 would normally come last
    //so we manually adjust before we compare
    s1 = s1.prefix(2) == "12" ? "00" + s1.suffix(3) : s1
    s2 = s2.prefix(2) == "12" ? "00" + s2.suffix(3) : s2

    //and just compare our two strings
    return s1 < s2
}
print(sortedTimes)
//["559a", "649a", "658a", "705a", "827a", "943a", "1108a", "1234p", "156p", "321p", "456p", "629p", "748p", "857p", "1001p"]

2      

@rooster provides an excellent example.

But to boost your Swift skills, take a moment to consider one of Swift's latest powertools: Regular Expressions. Someone might suggest that regular expressions are overkill in this situation, probably true. But consider this an opportunity to learn a ninja skill for your programming arsenal.

Regular Expressions

Searching, extracting, and manipulating data is a core skill for most software engineers. Regex, or regular expressions, has been the go to tool since ancient times.

By the way, the "G" in Regex is pronouced EXACTLY the same way as the "G" in gif! 😜

A famous quote goes "I tried to solve a problem with Regex. Now I have two problems."

Yeah, Regex used to be terrifying. But the boffins at Apple have greatly simplified regular expressions in a recent (5.7?) release of Swift. My crack at solving this is below.

Structure Your Data

In the spirit of taking a big problem and breaking it down into smaller, solvable problems, the first thing you might consider is the structure of the data you want to end up with. In the code below, I created an EventTime structure to capture the hours, minutes, and meridien of each bit you extract.

See -> Eating an Elephant

Find the Pattern. Define it as a Regular Expression

Look at the data you provided. Then look closely for a pattern that you can recognize.

I won't attempt to show you what the old school regular expression might look like 🤮. Instead, I used the more expressive Regex Builder available in Swift. This allows you to express, in common terms, the patterns you're looking for in your input string. Defining the patterns that you want to find allows you to easily skip malformed records, partial records, or just incorrect data.

// Try this in Swift Playgrounds
import RegexBuilder  // <-- New Import
import Foundation

// Basic EventTime Structure
struct EventTime {
    // Evaluate the data. Find the pattern.
    // Event Time has these three properties
    var hour      : Int
    var minute    : Int
    var meridiem  : Meridiem
}

// This extension provides extras for your basic EventTime structure
extension EventTime:  CustomStringConvertible{
    // Convience method for printing and debugging
    var description: String {
        let hour   = self.hour   < 10 ? "0" + String(self.hour)   : String(self.hour)
        let minute = self.minute < 10 ? "0" + String(self.minute) : String(self.minute)
        return "Event: \(hour):\(minute) \(meridiem.rawValue)"
    }

    // Convenience initializer to turn Regex results
    // into an EventTime object
    init(hour: String, minute: String, meridiem: String) {
        self.hour     = Int(hour)!
        self.minute   = Int(minute)!
        self.meridiem = Meridiem(rawValue: String)
    }
    // These are the only valid meridiem options
    enum Meridiem: String {
        case am, pm, xm, error
        init(rawValue: String) {
            switch rawValue {
              case "a": self = .am
              case "p": self = .pm
              case "x": self = .xm
              default:  self = .error
            }
        }
    }
}

// ====== TEST DATA ============
// NOTE!
// Neither 42, nor Obelix match the pattern! They are ignored
// Notice 923g matches the pattern, but has (potentially) bad data
var testSchedule: String = "| 559a| 649a| 42| 943a|1234p| 321p| 629p| 857p|1001p| Obelix| 827a|1108x|156p| 923g| 748p|"

// This is a regular expression pattern describing the minute digits
let minuteDigits = Regex {
    Repeat(count: 2) {
        One(.digit)  // <- Look for two digits next to each other
    }
}

// This is a pattern describing the hour digits
let hourDigits = Regex {
    Repeat(1...2) {
        One(.digit)  // <- look for one, or possibly two digits
    }
}

// This is the entire pattern you want to find in the string
//  1234a| 923p|1132x| 0702a|
let timePattern = Regex {
    ZeroOrMore(.whitespace)   // <- Ignore
    Capture { hourDigits   }  // <- Capture the 1 or 2 digits
    Capture { minuteDigits }  // <- Capture the digits
    Capture { One(.word)   }  // <- Capture the meridiem
    "|"                       // <- Ignore
}

// Find all matching patterns in your string
// Push them into an array
let allMatches = testSchedule.matches(of: timePattern) // ignore if parts dont match this pattern

// Find all matching patterns
// turn them into EventTime objects
var allTimes: [EventTime] = [EventTime]()  // empty array
allTimes = allMatches.map {
    // Pull the captured parts of the match
    // Push into the EventTime initializer
    // NOTE: These are the three parts that were Captured
    EventTime(hour: String($0.1), minute: String($0.2), meridiem: String($0.3))
}

// Print all the EventTime objects
for event in allTimes {
    print(event)  // <- Uses the description var because it conforms to CustomStringConvertable
}

Results

If you run this code in Playgrounds it shows:

Event: 05:59 am
Event: 06:49 am
Event: 09:43 am
Event: 12:34 pm
Event: 03:21 pm
Event: 06:29 pm
Event: 08:57 pm
Event: 10:01 pm
Event: 08:27 am
Event: 11:08 xm
Event: 01:56 pm
Event: 09:23 error
Event: 07:48 pm

Because the parsed parts are in a struct, you can improve the struct with whatever functionality your application requires. For example, as @rooster demonstrated, you can add unique sorting rules to sort EventTime objects.

Keep Coding

2      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Wow!

@roosterboy @Obelix, thank you so much. One thing I'm continously learning on my learning journey is how much more there is to learn. It is definitley daughting at times, but has also been very empowering and exciting. I really do appreciate you both taking the time to help workshop this.

@roosterboy

This is perfect! I continue to find sorting hard to wrap my head around, so I appreciate you breaking it down. This along with @Obelix's Regex/struct suggestions are amazing.

Some thoughts in response to your answer, @Obelix

Firstly, thank you for breaking things down so clearly and providing so much context.

Regular Expressions

I'm new to coding as a whole and never really delt with Regex! Now that I've seen an example of how powerful it can be I'll definitely be doing some digging. The app I'm trying to build is related to my job and I have a ton of ugly data, like what I provided in my example, so Regex will likely be an immensly useful tool.

Also thank you for the context of how challenging Regex can be - one thing I've really appreciated over the past year of learning Swift/SwiftUI is how much effort has clearly gone into simplifying and making coding more accessible (effort that clearly continues - yay SwiftData!!!!!!!).

Structuring your data

These times will become the very last piece of data in my overall data structure so I hadn't thought to break this down even further into a struct. You're example clearly highlights the value of doing so though!

I've always been a bit nervous to post in the forums, so I cannot express enough how thankful I am to you both for your help and direction. You've both given me lots to consider and dig into over the next few days.

Happy coding!

2      

Follow Up

After posting, I bounced a few more ideas around.

You may want to capture malformed data ( 0812g|1022m| ) so you can highlight this to fix in your authoritative datastore.

On the otherhand, you may just want to ignore these, but the current pattern includes them.

Evaluate the Pattern

This goes back to the step where you need to document the pattern and turn it into a regular expression.

The current pattern is defined as:

// == TEST DATA ========================
//  1234a| 923p|1132x| 0702a|1122m|

// This is the entire pattern you want to find in the string
let timePattern = Regex {
    ZeroOrMore(.whitespace)   // <- Ignore
    Capture { hourDigits   }  // <- Capture the 1 or 2 digits
    Capture { minuteDigits }  // <- Capture the minute digits
    Capture { One(.word)   }  // <- Capture the meridiem
    "|"                       // <- Ignore
}

Notice the pattern indicates capturing one word, which means any collection of letters followed by some word boundary.

In your data, you only want to find patterns that have a very particular ending, namely a, m, or x.

Improved Pattern

Your data field may contain thousands of event times, or more. But you are only interested in patterns that end with a, m, or x. Redefine the regular expression pattern to indicate you're only interested in this particular pattern. Ignore all other patterns.

For example 1122m|. and 023W| seem to have times and meridiems, but these fail the pattern test and are ignored.

// == TEST DATA ========================
//  1234a| 923p|1132x| 0702a|1122m| 42|  023W|obelix3|         1001p| 0942ap|

// This is the entire pattern you want to find in the string
let timePattern = Regex {
    ZeroOrMore(.whitespace)            // <- Ignore
    Capture { hourDigits   }           // <- Capture the 1 or 2 digits
    Capture { minuteDigits }           // <- Capture the minute digits
    Capture { One(.anyOf("apx") )   }  // <- Capture the meridiem, but only if a, p, or x
    "|"                                // <- Ignore
}

2      

As cool as regular expressions can be, they are really overkill for this situation. All you need is some simple string manipulation.

// Basic EventTime Structure
struct EventTime {
    // Evaluate the data. Find the pattern.
    // Event Time has these three properties
    var hour      : Int
    var minute    : Int
    var meridiem  : Meridiem
}

// This extension provides extras for your basic EventTime structure
extension EventTime:  CustomStringConvertible{
    // Convience method for printing and debugging
    var description: String {
        let hour   = self.hour   < 10 ? "0" + String(self.hour)   : String(self.hour)
        let minute = self.minute < 10 ? "0" + String(self.minute) : String(self.minute)
        return "Event: \(hour):\(minute) \(meridiem.rawValue)"
    }

    // These are the only valid meridiem options
    enum Meridiem: String {
        case am, pm, xm, error

        init(rawValue: String) {
            switch rawValue {
            case "a": self = .am
            case "p": self = .pm
            case "x": self = .xm
            default: self = .error
            }
        }
    }
}

extension EventTime {
    init(_ using: String) {
        var initString = "0\(using)".suffix(5)
        self.meridiem = Meridiem(rawValue: String(initString.popLast()!))
        self.minute = Int(initString.suffix(2))!
        self.hour = Int(initString.prefix(2))!
    }
}

let eventTimes = times.map(EventTime.init)
print(eventTimes)
//[Event: 05:59 am, Event: 06:49 am, Event: 07:05 am, Event: 09:43 am, Event: 12:34 pm, Event: 03:21 pm, Event: 06:29 pm, Event: 08:57 pm, Event: 10:01 pm, Event: 06:58 am, Event: 08:27 am, Event: 11:08 am, Event: 01:56 pm, Event: 04:56 pm, Event: 07:48 pm]

I won't attempt to show you what the old school regular expression might look like 🤮.

Aw, it's not that bad:

^(\d{1,2})(\d{2})([apx])$

2      

Maybe you're helping me make my point?

Aw, it's not that bad:
^(\d{1,2})(\d{2})([apx])$

There are two nice Regex parsers online, swiftregex.com and regex101.com

Your pattern only works (?) if the input string has one, and only one, entry and the entry doesn't end with the pipe character.

^(\d{1,2})(\d{2})([apx])$

1234p        <- this works
912a         <- so does this
1142x|       <- this fails
1234p| 912a| <- this also fails

I think the caret charater (^) locks the pattern to the start of the string. And the $ locks it to the end of the string. So this Regex pattern only finds time events with 3 or 4 digits, the letter a, p, or x that is the only pattern in the input string. Add more data, and pipe separaters, and the expression fails.

We want to find this pattern anywhere in the long string. So, don't lock the pattern to the start of the string. Nor lock it to the end of the string. The modified pattern below seems to work in both websites with the sample data below:

(\d{1,2})(\d{2})([apx])[|]   // <- remove both the ^ and the $. Add the pipe.

924p|3232apx| 559a| 649a| 42| 943a| 645g|1234p| 321p| 99999x| 629p| 857p|1001p| Roosterboy222x| 912p|

Both online parsers show 12 matches with these adjustments.

SwiftRegex.com Parser

What's nice about the swiftregex.com parser is how it creates the dynamic Swift Regex builder code from the old school Regex code.

So you can see that this -> (\d{1,2})(\d{2})([apx])[|]

becomes this:

// Auto generated Regex Builder code
Regex {
  Capture {
    Repeat(1...2) { One(.digit) }
  }
  Capture {
    Repeat(count: 2) { One(.digit) }
  }
  Capture { One(.anyOf("apx")) }
  One(.anyOf("|"))
}
.anchorsMatchLineEndings()

Sweet! I prefer Swift's more declarative approach!

Keep Coding!

2      

That pattern isn't for checking the entire string but for the pieces that have previously been split on the | character, which is what the OP specified. So I didn't bother with multiple matches or the delimiter character. Another reason why regular expressions aren't really a good solution for this problem.

It is a nice exercise, though. The new Regex stuff is very intriguing.

2      

I agree with you.

Applying regular expression pattern matching to (potentially) thousands of pieces previously parsed with string manipulation doesn't make sense. Instead, I suggested applying the pattern to the entire string to harvest the pieces.

In this approach, I suggested the old school regular expression (with carets, parentheses, brackets, and braces) was barf-worthy. I think you proved this point providing an expression that didn't work on the entire string, but on a single extraction. If you have (potentially) thousands of individual pieces, I agree it's overkill to apply a regular expression thousands of times.

OP followed up stating

The app I'm trying to build is related to my job and I have a ton of ugly data

He said it himself. He has a ton of ugly data to deal with. Applying one pattern to tons of data, that might be a different story.

2      

I'm loving this discussion around what is most effective!

It's very eye opening and I think both have their place in my app.

The initial example string I provided is from a PDF and copy/pasting it into the app to extract and display the specific data for a single 'shift' was the limit of my thinking, based on my limited knowledge, when I made the post. That is to say, if the pdf has 100 shifts, I'd be copy/pasting each shift individually.

However, the information you shared @Orbelix on Regex really opens up possibilities - I can expand it to parse through multiple days at a time by building out my model structure and the regex. Easier said than done obviously, but a new avenue to explore that I wouldn't have considered otherwise.

And yeah, so much other ugly data, this example was the least ugly amongst the heap 🤢

@roosterboys' code would likely cover me for the present version of my app, but the combo of both is a game changer for future interations as I learn more!

happy Coding!

p.s. this is what a full 'shift' looks like copy/pasted with no editing or clipping on my part

RUN | |RO | | 7| 7| 7| 7| 5| 5| 1| 7| | | | | | | | | |TOTAL |DOWN | 627a| 712a| 918a|1133a| 148p| 402p| 622p| 827p|1022p|1209x| 149x| | | | | | | | | 1 | UP | | 815a|1025a|1240p| 252p| 512p| 728p| 927p|1115p| 102x| 152x| 235x| | | | | | | |20:08 | | | 1| 6| 6| 3| 3| 2| 6| 6| 6| |RO | |

2      

@ErinJulius posts

this is what a full 'shift' looks like copy/pasted with no editing or clipping on my part

// Shift data
RUN | |RO | | 7| 7| 7| 7| 5| 5| 1| 7| | | | | | | | | |TOTAL |DOWN | 627a| 712a| 918a|1133a| 148p| 402p| 622p| 827p|1022p|1209x| 149x| | | | | | | | | 1 | UP | | 815a|1025a|1240p| 252p| 512p| 728p| 927p|1115p| 102x| 152x| 235x| | | | | | | |20:08 | | | 1| 6| 6| 3| 3| 2| 6| 6| 6| |RO | 

I pasted this shift data into the Playgrounds variable testSchedule in the code posted above, without editing.

The regular expression parsed this hodge-podge of codes and data and found 22 matching patterns, ignoring all the other data.

Results:

Event: 06:27 am
Event: 07:12 am
Event: 09:18 am
Event: 11:33 am
Event: 01:48 pm
Event: 04:02 pm
Event: 06:22 pm
Event: 08:27 pm
Event: 10:22 pm
Event: 12:09 xm
Event: 01:49 xm
Event: 08:15 am
Event: 10:25 am
Event: 12:40 pm
Event: 02:52 pm
Event: 05:12 pm
Event: 07:28 pm
Event: 09:27 pm
Event: 11:15 pm
Event: 01:02 xm
Event: 01:52 xm
Event: 02:35 xm

2      

RUN | |RO | | 7| 7| 7| 7| 5| 5| 1| 7| | | | | | | | | |TOTAL |DOWN | 627a| 712a| 918a|1133a| 148p| 402p| 622p| 827p|1022p|1209x| 149x| | | | | | | | | 1 | UP | | 815a|1025a|1240p| 252p| 512p| 728p| 927p|1115p| 102x| 152x| 235x| | | | | | | |20:08 | | | 1| 6| 6| 3| 3| 2| 6| 6| 6| |RO | |

Okay, with this expanded data set, a regular expression is probably a better way to approach this.

With the data given in the original problem statement, I would not go that way.

3      

Hey @Obelix,

I'm playing around with the code that you provided in a playground and noticed a slight error (that provided some lessons in troubleshooting 😅)

the code threw errors for all of the meridiems but after some trial and error I realized that the initializer for self.meridiem was causing the errors:

    //Convenience initializer to turn Regex results
    // into an EventTime object
init(hour: String, minute: String, meridiem: String) {
        self.hour     = Int(hour)!
        self.minute   = Int(minute)!
        self.meridiem = Meridiem(rawValue: String)
    }

this line in particular:

        self.meridiem = Meridiem(rawValue: String)

replaced "String" with meridiem and it ran perfectly!

        self.meridiem = Meridiem(rawValue: meridiem)

Thank you again for the help and direction! I'll be having some fun with it and regex moving forward with my app!

3      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.