Parse String into an object in Swift - swift

I have received this response from the server and I am sure there must be a more efficient way to convert it into an object.
I have the following response:
[
id=2997,rapidViewId=62,state=ACTIVE,name=Sprint7,startDate=2018-11-20T10:28:37.256Z,endDate=2018-11-30T10:28:00.000Z,completeDate=<null>,sequence=2992,goal=none
]
How do I convert it nicely into a well formed swift object in the simplest way?
Here is my attempt which gives me just the Sprint Value
if sprintJiraCustomField.count > 0 {
let stringOutput = sprintJiraCustomField.first?.stringValue // convert output to String
let name = stringOutput?.components(separatedBy: "name=") // get name section from string
let nameFieldRaw = name![1].components(separatedBy: ",") // split out to the comma
let nameValue = nameFieldRaw.first!
sprintDetail = nameValue// show name field
}

Not sure what format you want but the below code will produce an array of tuples (key, value) but all values are strings so I guess another conversion is needed afterwards
let items = stringOutput.components(separatedBy: ",").compactMap( {pair -> (String, String) in
let keyValue = pair.components(separatedBy: "=")
return (keyValue[0], keyValue[1])
})

This is a work for reduce:
let keyValueStrings = yourString.components(separatedBy: ",")
let dictionary = keyValueStrings.reduce([String: String]()) {
(var aggregate: [String: String], element: String) -> [String: String] in
let elements = element.componentsSeparatedByString("=")
let key = elements[0]
// replace nil with the value you want to use if there is no value
let value = (elements.count > 1) ? elements[1] : nil
aggregate[key] = value
return aggregate
}
This is a functional approach, but you can achieve the same using a for iteration.
So then you can use Swiftโ€™s basic way of mapping. for example you will have your custom object struct. First, you will add an init method to it. Then map your object like this:
init(with dictionary: [String: Any]?) {
guard let dictionary = dictionary else { return }
attribute = dictionary["attrName"] as? String
}
let customObjec = CustomStruct(dictionary: dictionary)

We already have some suggestion to first split the string at each comma and then split each part at the equals sign. This is rather easy to code and works well, but it is not very efficient as every character has to be checked multiple times. Writing a proper parser using Scanner is just as easy, but will run faster.
Basically the scanner can check if a given string is at the current position or give you the substring up to the next occurrence of a separator.
With that the algorithm would have the following steps:
Create scanner with the input string
Check for the opening bracket, otherwise fail
Scan up to the first =. This is the key
Consume the =
Scan up to the first , or ]. This is the value
Store the key/value pair
If there is a , consume it and continue with step 3
Consume the final ].
Sadly the Scanner API is not very Swift-friendly. With a small extension it is much easier to use:
extension Scanner {
func scanString(_ string: String) -> Bool {
return scanString(string, into: nil)
}
func scanUpTo(_ delimiter: String) -> String? {
var result: NSString? = nil
guard scanUpTo(delimiter, into: &result) else { return nil }
return result as String?
}
func scanUpTo(_ characters: CharacterSet) -> String? {
var result: NSString? = nil
guard scanUpToCharacters(from: characters, into: &result) else { return nil }
return result as String?
}
}
With this we can write the parse function like this:
func parse(_ list: String) -> [String: String]? {
let scanner = Scanner(string: list)
guard scanner.scanString("[") else { return nil }
var result: [String: String] = [:]
let endOfPair: CharacterSet = [",", "]"]
repeat {
guard
let key = scanner.scanUpTo("="),
scanner.scanString("="),
let value = scanner.scanUpTo(endOfPair)
else {
return nil
}
result[key] = value
} while scanner.scanString(",")
guard scanner.scanString("]") else { return nil }
return result
}

Related

Convert list of AppleScript strings to a Swift array

I have a complicated AppleScript that returns a list of strings that I need to access from Swift. I've boiled it down to a simple example and I just can't figure out how to map the AppleScript strings to an array of Swift strings.
let listOfStringsScript = """
set listOfStrings to { "one", "two", "three" }
"""
if let scriptObject = NSAppleScript(source: listOfStringsScript) {
var errorDict: NSDictionary? = nil
let resultDescriptor = scriptObject.executeAndReturnError(&errorDict)
if errorDict == nil {
// TODO: convert the resultDescriptor (NSAppleEventDescriptor) into an array of strings
print(resultDescriptor)
// OUTPUT: <NSAppleEventDescriptor: [ 'utxt'("one"), 'utxt'("two"), 'utxt'("three") ]>
}
}
Answer with help from #Alexander and #MartinR:
extension NSAppleEventDescriptor {
func toStringArray() -> [String] {
guard let listDescriptor = self.coerce(toDescriptorType: typeAEList) else {
return []
}
return (0..<listDescriptor.numberOfItems)
.compactMap { listDescriptor.atIndex($0 + 1)?.stringValue }
}
}
...
let resultDescriptor = scriptObject.executeAndReturnError(&errorDict)
let subjectLines = resultDescriptor.toStringArray()
An alternative is to gather the Apple Script result as lines of text separated by line breaks and then parse the string in Swift.
So break up the Apple Script result using
set AppleScript's text item delimiters to linefeed
Then simply parse
let selectedItems = scriptExecuted.stringValue!
let selectedItemsFiltered = selectedItems.components(separatedBy: .newlines)
.components returns a string array

Is it possible to write a Swift function that replaces only part of an extended grapheme cluster like ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง?

I want to write a function that could be used like this:
let ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ = "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง".replacingFirstOccurrence(of: "๐Ÿ‘ง", with: "๐Ÿ‘ฆ")
Given how odd both this string and Swift's String library are, is this possible in Swift?
Based on the insights gained at Why are emoji characters like ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ treated so strangely in Swift strings?, a sensible approach might be to replace Unicode scalars:
extension String {
func replacingFirstOccurrence(of target: UnicodeScalar, with replacement: UnicodeScalar) -> String {
let uc = self.unicodeScalars
guard let idx = uc.index(of: target) else { return self }
let prefix = uc[uc.startIndex..<idx]
let suffix = uc[uc.index(after: idx) ..< uc.endIndex]
return "\(prefix)\(replacement)\(suffix)"
}
}
Example:
let family1 = "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ"
print(family1.characters.map { Array(String($0).unicodeScalars) })
// [["\u{0001F469}", "\u{200D}"], ["\u{0001F469}", "\u{200D}"], ["\u{0001F467}", "\u{200D}"], ["\u{0001F466}"]]
let family2 = family1.replacingFirstOccurrence(of: "๐Ÿ‘ง", with: "๐Ÿ‘ฆ")
print(family2) // ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ
print(family2.characters.map { Array(String($0).unicodeScalars) })
// [["\u{0001F469}", "\u{200D}"], ["\u{0001F469}", "\u{200D}"], ["\u{0001F466}", "\u{200D}"], ["\u{0001F466}"]]
And here is a possible version which locates and replaces the Unicode scalars of an arbitrary string:
extension String {
func replacingFirstOccurrence(of target: String, with replacement: String) -> String {
let uc = self.unicodeScalars
let tuc = target.unicodeScalars
// Target empty or too long:
if tuc.count == 0 || tuc.count > uc.count {
return self
}
// Current search position:
var pos = uc.startIndex
// Last possible position of `tuc` within `uc`:
let end = uc.index(uc.endIndex, offsetBy: tuc.count - 1)
// Locate first Unicode scalar
while let from = uc[pos..<end].index(of: tuc.first!) {
// Compare all Unicode scalars:
let to = uc.index(from, offsetBy: tuc.count)
if !zip(uc[from..<to], tuc).contains(where: { $0 != $1 }) {
let prefix = uc[uc.startIndex..<from]
let suffix = uc[to ..< uc.endIndex]
return "\(prefix)\(replacement)\(suffix)"
}
// Next search position:
uc.formIndex(after: &pos)
}
// Target not found.
return self
}
}
Using the range(of:options:range:locale:) the solution became quite concise:
extension String {
func replaceFirstOccurrence(of searchString: String, with replacementString: String) -> String {
guard let range = self.range(of: searchString, options: .literal) else { return self }
return self.replacingCharacters(in: range, with: replacementString)
}
}
This works by first finding the range of searchString within the instance, and if a range is found the range is replaced with replacementString. Otherwise the instance just returns itself. And, since the range(of:) method returns as soon as it finds a match, the returned range is guaranteed to be the first occurrence.
"221".replaceFirstOccurrence(of: "2", with: "3") // 321
"๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ".replaceFirstOccurrence(of: "\u{1f469}", with: "\u{1f468}") // ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ
*To clarify, the last test case converts woman-woman-girl-boy to man-woman-girl-boy.

how to write function to evaluate if String is only characters?

I've seen other answers to this question, but I'm just trying to do it differently. Yet whatever I do I can't make my types match.
func ContainsOnlyAlphabets(_ word : String) -> Bool{
let letters = CharacterSet.letters // Set<Character>
let trimmed = word.trimmingCharacters(in: .whitespaces)
let characterViewArray = Array(trimmed.characters) // Array<characterView>
let characterArray = characterViewArray.map{Character($0)} // Error: Can't create Chars
let wordCharactersSet = Set(characterArray) // Set<Character>
let intersection = wordCharactersSet.intersection(letters)
return intersection.count == characterArray.count
}
I had to do all the Set,Char,String,Array conversions but still couldn't get it right :(.
cannot invoke initializer for type 'Character' with an argument list
of type '((String.CharacterView._Element))'
Your code
let characterViewArray = Array(trimmed.characters)
already creates a Array<Character>, so you could simple skip the
next line and create a Set<Character> with
let wordCharactersSet = Set(characterViewArray)
But that does not really help, because Set<Character> and
CharacterSet are different types, so that
let intersection = wordCharactersSet.intersection(letters)
does not compile. Possible alternatives are
return trimmed.rangeOfCharacter(from: letters.inverted) == nil
or
return CharacterSet(charactersIn: trimmed).isSubset(of: letters)
If your intention is to allow both letters and whitespace characters
then it could look like this:
func containsOnlyLettersAndWhitespace(_ word : String) -> Bool{
var allowedSet = CharacterSet.letters
allowedSet.formUnion(CharacterSet.whitespaces)
return word.rangeOfCharacter(from: allowedSet.inverted) == nil
// Alternatively:
return CharacterSet(charactersIn: word).isSubset(of: allowedSet.inverted)
}
As MartinR noted, CharacterSet is not equivalent to Set<Character>
The closest I could get to your original solution was to create a CharacterSet from the trimmed string and apply some of your original algorithm to that:
func ContainsOnlyAlphabets(_ word : String) -> Bool{
let letters = CharacterSet.letters
let trimmed = word.trimmingCharacters(in: .whitespaces)
let wordCharacterSet = CharacterSet(charactersIn:trimmed)
let intersection = wordCharacterSet.intersection(letters)
return intersection == wordCharacterSet
}
Keeping it strictly in the realm of CharacterSet and operations on that, you could also use:
func ContainsOnlyAlphabets(_ word : String) -> Bool{
return CharacterSet.letters.isSuperset(of:
CharacterSet(charactersIn: word.trimmingCharacters(in: .whitespaces))
)
}
That said, I think I'd still go with Martin's solution:
func ContainsOnlyAlphabets(_ word : String) -> Bool{
return word
.trimmingCharacters(in: .whitespaces)
.rangeOfCharacter(from: CharacterSet.letters.inverted) == nil
}
as being more intuitive.

Path extractions swift 3.0

I have a file path ...
/acme101/acmeX100/acmeX100.008.png
I can use this to get the extension .png in this case
let leftSide = (lhs.fnName as NSString).pathExtension
And this to get the filename acmeX100
let leftSide = (lhs.fnName as NSString).lastPathComponent
But I want the bit in the middle... the 008 in this case?
Is there a nice one liner?
Assuming the filepath takes that general form then this is (almost) a one-liner (I like to play it safe):
var filePath = "/acme101/acmeX100/acmeX100.008.png"
func extractComponentBetweenDots(inputString: String) -> String? {
guard inputString.components(separatedBy: ".").count > 2 else { print("Incorrect format") ; return nil } // Otherwise not in the correct format, you caa add other tests
return inputString.components(separatedBy: ".")[inputString.components(separatedBy: ".").count - 2]
}
Use as follows:
if let extractedString : String = extractComponentBetweenDots(inputString: filePath) {
print(extractedString)
}
I wanted to make an example using the same technique as in your question - despite the fact that the downcasting to NSString makes the whole thing rather ugly, it works efficiently. This is in Swift 3 but it would be easy to port it back to Swift 2 if needed.
func getComponents(from str: String) -> (name: String, middle: String, ext: String) {
let compo = (str as NSString).lastPathComponent as NSString
let ext = compo.pathExtension
let temp = compo.deletingPathExtension as NSString
let middle = temp.pathExtension
let file = temp.deletingPathExtension
return (name: file, middle: middle, ext: ext)
}
let result = getComponents(from: "/acme101/acmeX100/acmeX100.008.png")
print(result.name) // "acmeX100"
print(result.middle) // "008"
print(result.ext) // "png"
If you only need the middle part:
func pluck(str: String) -> String {
return (((str as NSString).lastPathComponent as NSString).deletingPathExtension as NSString).pathExtension
}
pluck(str: "/acme101/acmeX100/acmeX100.008.png") // "008"
Bon,
Sparky thanks for your answer. I ended up with this .. which is the same and yet different.
func pluck(str:String) -> String {
if !str.isEmpty {
let bitZero = str.characters.split{$0 == "."}.map(String.init)
if (bitZero.count > 2) {
let bitFocus = bitZero[1]
print("bitFocus \(bitFocus)")
return(bitFocus)
}
}
return("")
}

Swift String to Array

I have a string in Swift that looks like this:
["174580798","151240033","69753978","122754394","72373738","183135789","178841809","84104360","122823486","184553211","182415131","70707972"]
I need to convert it into an NSArray.
I've looked at other methods on SO but it is breaking each character into a separate array element, as opposed to breaking on the comma. See: Convert Swift string to array
I've tried to use the map() function, I've also tried various types of casting but nothing seems to come close.
Thanks in advance.
It's probably a JSON string so you can try this
let string = "[\"174580798\",\"151240033\",\"69753978\",\"122754394\",\"72373738\",\"183135789\",\"178841809\",\"84104360\",\"122823486\",\"184553211\",\"182415131\",\"70707972\"]"
let data = string.dataUsingEncoding(NSUTF8StringEncoding)!
let jsonArray = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(), error: nil) as! [String]
as the type [String] is distinct you can cast it forced
Swift 3+:
let data = Data(string.utf8)
let jsonArray = try! JSONSerialization.jsonObject(with: data) as! [String]
The other two answers are working, although SwiftStudiers isn't the best regarding performance. vadian is right that your string most likely is JSON. Here I present another method which doesn't involve JSON parsing and one which is very fast:
import Foundation
let myString = "[\"174580798\",\"151240033\",\"69753978\",\"122754394\",\"72373738\",\"183135789\",\"178841809\",\"84104360\",\"122823486\",\"184553211\",\"182415131\",\"70707972\"]"
func toArray(var string: String) -> [String] {
string.removeRange(string.startIndex ..< advance(string.startIndex, 2)) // Remove first 2 chars
string.removeRange(advance(string.endIndex, -2) ..< string.endIndex) // Remote last 2 chars
return string.componentsSeparatedByString("\",\"")
}
toArray(myString) // ["174580798", "151240033", "69753978", ...
You probably want the numbers though, you can do this in Swift 2.0:
toArray(myString).flatMap{ Int($0) } // [174'580'798, 151'240'033, 69'753'978, ...
which returns an array of Ints
EDIT: For the ones loving immutability and functional programming, have this solution:
func toArray(string: String) -> [String] {
return string[advance(string.startIndex, 2) ..< advance(string.endIndex, -2)]
.componentsSeparatedByString("\",\"")
}
or this:
func toArray(string: String) -> [Int] {
return string[advance(string.startIndex, 2) ..< advance(string.endIndex, -2)]
.componentsSeparatedByString("\",\"")
.flatMap{ Int($0) }
}
Try this. I've just added my function which deletes any symbols from string except numbers. It helps to delete " and [] in your case
var myString = "[\"174580798\",\"151240033\",\"69753978\",\"122754394\",\"72373738\",\"183135789\",\"178841809\",\"84104360\",\"122823486\",\"184553211\",\"182415131\",\"70707972\"]"
var s=myString.componentsSeparatedByString("\",\"")
var someArray: [String] = []
for i in s {
someArray.append(deleteAllExceptNumbers(i))
}
println(someArray[0]);
func deleteAllExceptNumbers(str:String) -> String {
var rez=""
let digits = NSCharacterSet.decimalDigitCharacterSet()
for tempChar in str.unicodeScalars {
if digits.longCharacterIsMember(tempChar.value) {
rez += tempChar.description
}
}
return rez.stringByReplacingOccurrencesOfString("\u{22}", withString: "")
}
Swift 1.2:
If as has been suggested you are wanting to return an array of Int you can get to that from myString with this single concise line:
var myArrayOfInt2 = myString.componentsSeparatedByString("\"").map{$0.toInt()}.filter{$0 != nil}.map{$0!}
In Swift 2 (Xcode 7.0 beta 5):
var myArrayOfInt = myString.componentsSeparatedByString("\"").map{Int($0)}.filter{$0 != nil}.map{$0!}
This works because the cast returns an optional which will be nil where the cast fails - e.g. with [, ] and ,. There seems therefore to be no need for other code to remove these characters.
EDIT: And as Kametrixom has commented below - this can be further simplified in Swift 2 using .flatMap as follows:
var myArrayOfInt = myString.componentsSeparatedByString("\"").flatMap{ Int($0) }
Also - and separately:
With reference to Vadian's excellent answer. In Swift 2 this will become:
// ...
do {
let jsonArray = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as! [String]
} catch {
_ = error // or do something with the error
}