Path extractions swift 3.0 - swift

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("")
}

Related

Parse String into an object in 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
}

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.

Remove all non-numeric characters from a string in swift

I have the need to parse some unknown data which should just be a numeric value, but may contain whitespace or other non-alphanumeric characters.
Is there a new way of doing this in Swift? All I can find online seems to be the old C way of doing things.
I am looking at stringByTrimmingCharactersInSet - as I am sure my inputs will only have whitespace/special characters at the start or end of the string. Are there any built in character sets I can use for this? Or do I need to create my own?
I was hoping there would be something like stringFromCharactersInSet() which would allow me to specify only valid characters to keep
I was hoping there would be something like stringFromCharactersInSet() which would allow me to specify only valid characters to keep.
You can either use trimmingCharacters with the inverted character set to remove characters from the start or the end of the string. In Swift 3 and later:
let result = string.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.").inverted)
Or, if you want to remove non-numeric characters anywhere in the string (not just the start or end), you can filter the characters, e.g. in Swift 4.2.1:
let result = string.filter("0123456789.".contains)
Or, if you want to remove characters from a CharacterSet from anywhere in the string, use:
let result = String(string.unicodeScalars.filter(CharacterSet.whitespaces.inverted.contains))
Or, if you want to only match valid strings of a certain format (e.g. ####.##), you could use regular expression. For example:
if let range = string.range(of: #"\d+(\.\d*)?"#, options: .regularExpression) {
let result = string[range] // or `String(string[range])` if you need `String`
}
The behavior of these different approaches differ slightly so it just depends on precisely what you're trying to do. Include or exclude the decimal point if you want decimal numbers, or just integers. There are lots of ways to accomplish this.
For older, Swift 2 syntax, see previous revision of this answer.
let result = string.stringByReplacingOccurrencesOfString("[^0-9]", withString: "", options: NSStringCompareOptions.RegularExpressionSearch, range:nil).stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
Swift 3
let result = string.replacingOccurrences( of:"[^0-9]", with: "", options: .regularExpression)
You can upvote this answer.
I prefer this solution, because I like extensions, and it seems a bit cleaner to me. Solution reproduced here:
extension String {
var digits: String {
return components(separatedBy: CharacterSet.decimalDigits.inverted)
.joined()
}
}
You can filter the UnicodeScalarView of the string using the pattern matching operator for ranges, pass a UnicodeScalar ClosedRange from 0 to 9 and initialise a new String with the resulting UnicodeScalarView:
extension String {
private static var digits = UnicodeScalar("0")..."9"
var digits: String {
return String(unicodeScalars.filter(String.digits.contains))
}
}
"abc12345".digits // "12345"
edit/update:
Swift 4.2
extension RangeReplaceableCollection where Self: StringProtocol {
var digits: Self {
return filter(("0"..."9").contains)
}
}
or as a mutating method
extension RangeReplaceableCollection where Self: StringProtocol {
mutating func removeAllNonNumeric() {
removeAll { !("0"..."9" ~= $0) }
}
}
Swift 5.2 โ€ข Xcode 11.4 or later
In Swift5 we can use a new Character property called isWholeNumber:
extension RangeReplaceableCollection where Self: StringProtocol {
var digits: Self { filter(\.isWholeNumber) }
}
extension RangeReplaceableCollection where Self: StringProtocol {
mutating func removeAllNonNumeric() {
removeAll { !$0.isWholeNumber }
}
}
To allow a period as well we can extend Character and create a computed property:
extension Character {
var isDecimalOrPeriod: Bool { "0"..."9" ~= self || self == "." }
}
extension RangeReplaceableCollection where Self: StringProtocol {
var digitsAndPeriods: Self { filter(\.isDecimalOrPeriod) }
}
Playground testing:
"abc12345".digits // "12345"
var str = "123abc0"
str.removeAllNonNumeric()
print(str) //"1230"
"Testing0123456789.".digitsAndPeriods // "0123456789."
Swift 4
I found a decent way to get only alpha numeric characters set from a string.
For instance:-
func getAlphaNumericValue() {
var yourString = "123456789!##$%^&*()AnyThingYouWant"
let unsafeChars = CharacterSet.alphanumerics.inverted // Remove the .inverted to get the opposite result.
let cleanChars = yourString.components(separatedBy: unsafeChars).joined(separator: "")
print(cleanChars) // 123456789AnyThingYouWant
}
A solution using the filter function and rangeOfCharacterFromSet
let string = "sld [f]34รฉ7*หœยต"
let alphaNumericCharacterSet = NSCharacterSet.alphanumericCharacterSet()
let filteredCharacters = string.characters.filter {
return String($0).rangeOfCharacterFromSet(alphaNumericCharacterSet) != nil
}
let filteredString = String(filteredCharacters) // -> sldf34รฉ7ยต
To filter for only numeric characters use
let string = "sld [f]34รฉ7*หœยต"
let numericSet = "0123456789"
let filteredCharacters = string.characters.filter {
return numericSet.containsString(String($0))
}
let filteredString = String(filteredCharacters) // -> 347
or
let numericSet : [Character] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
let filteredCharacters = string.characters.filter {
return numericSet.contains($0)
}
let filteredString = String(filteredCharacters) // -> 347
Swift 4
But without extensions or componentsSeparatedByCharactersInSet which doesn't read as well.
let allowedCharSet = NSCharacterSet.letters.union(.whitespaces)
let filteredText = String(sourceText.unicodeScalars.filter(allowedCharSet.contains))
let string = "+1*(234) fds567#-8/90-"
let onlyNumbers = string.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
print(onlyNumbers) // "1234567890"
or
extension String {
func removeNonNumeric() -> String {
return self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
}
}
let onlyNumbers = "+1*(234) fds567#-8/90-".removeNonNumeric()
print(onlyNumbers)// "1234567890"
Swift 3, filters all except numbers
let myString = "dasdf3453453fsdf23455sf.2234"
let result = String(myString.characters.filter { String($0).rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789")) != nil })
print(result)
Swift 4.2
let numericString = string.filter { (char) -> Bool in
return char.isNumber
}
You can do something like this...
let string = "[,myString1. \"" // string : [,myString1. "
let characterSet = NSCharacterSet(charactersInString: "[,. \"")
let finalString = (string.componentsSeparatedByCharactersInSet(characterSet) as NSArray).componentsJoinedByString("")
print(finalString)
//finalString will be "myString1"
The issue with Rob's first solution is stringByTrimmingCharactersInSet only filters the ends of the string rather than throughout, as stated in Apple's documentation:
Returns a new string made by removing from both ends of the receiver characters contained in a given character set.
Instead use componentsSeparatedByCharactersInSet to first isolate all non-occurrences of the character set into arrays and subsequently join them with an empty string separator:
"$$1234%^56()78*9ยฃยฃ".componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: "0123456789").invertedSet)).joinWithSeparator("")
Which returns 123456789
Swift 3
extension String {
var keepNumericsOnly: String {
return self.components(separatedBy: CharacterSet(charactersIn: "0123456789").inverted).joined(separator: "")
}
}
Swift 4.0 version
extension String {
var numbers: String {
return String(describing: filter { String($0).rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789")) != nil })
}
}
Swift 4
String.swift
import Foundation
extension String {
func removeCharacters(from forbiddenChars: CharacterSet) -> String {
let passed = self.unicodeScalars.filter { !forbiddenChars.contains($0) }
return String(String.UnicodeScalarView(passed))
}
func removeCharacters(from: String) -> String {
return removeCharacters(from: CharacterSet(charactersIn: from))
}
}
ViewController.swift
let character = "1Vi234s56a78l9"
let alphaNumericSet = character.removeCharacters(from: CharacterSet.decimalDigits.inverted)
print(alphaNumericSet) // will print: 123456789
let alphaNumericCharacterSet = character.removeCharacters(from: "0123456789")
print("no digits",alphaNumericCharacterSet) // will print: Vishal
Swift 4.2
let digitChars = yourString.components(separatedBy:
CharacterSet.decimalDigits.inverted).joined(separator: "")
Swift 3 Version
extension String
{
func trimmingCharactersNot(in charSet: CharacterSet) -> String
{
var s:String = ""
for unicodeScalar in self.unicodeScalars
{
if charSet.contains(unicodeScalar)
{
s.append(String(unicodeScalar))
}
}
return s
}
}

Use filter() on a string in Swift 2

I'm not sure if the API in swift 2 changed, but I can't get filter to work on a string in Swift 2. The following should change "abc123$$$ ff" into "abcff".
// Removes all special characters and whitespaces
func compressString(aString: String) -> String{
let charSet = NSCharacterSet.letterCharacterSet()
// The following don't work:
// return aString.filter{charSet.contains($0)}
// return String(filter(aString).{charSet.contains($0)})
}
If you want to use filter, you need to run it on the characters view:
// Removes all special characters and whitespaces
func compressString(aString: String) -> String{
let charSet: [Character] = ["$", " ", "1", "2", "3"]
// The following don't work:
return String(aString.characters.filter { !charSet.contains($0) })
}
let before = "abc123$$$ ff"
let after = compressString(before) // "abcff"
func compressString(aString: String) -> String {
let letterSet = NSCharacterSet.letterCharacterSet()
return String(aString.characters.filter{letterSet.characterIsMember(String($0).utf16.first!)})
}
let str = "abc123$$$ ff"
compressString(str) // abcff
You can also create an extension to simplify your code:
extension String {
var lettersOnly: String {
return String(characters.filter{NSCharacterSet.letterCharacterSet().characterIsMember(String($0).utf16.first!)})
}
}
// Removes all special characters and whitespaces
let str = "abc123$$$ ff"
let letters = str.lettersOnly // "abcff"