Swift: How to cut off text from a longer text if initator and terminator is provided as regex? - swift

How to get from this: "{\n \"DNAHeader\": {},\n \"ItemsSaleable\": []\n}\n"
this: "\"DNAHeader\":{},\"ItemsSaleable\":[]"
I have for initiator this regex:
"<OWSP>{<OWSP>"
for terminator this:
"<OWSP>}<OWSP>"
where <OWSP> is optional white space, the same as in Swift regex \s* is.
I convert them to the Swift equivalent:
if let group = groupOrItem as? Group,
let initiator = group.typeSyntax?.initiator?.literal.literalValue?.replacingOccurrences(of: "<OWSP>", with: "\\s*"),
let terminator = group.typeSyntax?.terminator?.literal.literalValue?.replacingOccurrences(of: "<OWSP>", with: "\\s*")
{
let captureString = "(.*?)"
let regexString = initiator + captureString + terminator
let regexPattern = "#" + regexString + "#"
Then regex pattern looks like this:
(lldb) po regexString
"\\s*{\\s*(.*?)\\s*}\\s*"
Question, how to apply it, how to cut off meaningful inner text? I tried this,
var childText = text.replacingOccurrences(of: regexPattern, with: "$1", options: .regularExpression).filter { !$0.isWhitespace }
but does not remove the initiator / terminator texts, like the { and } parts from here:
(lldb) po text
"{\n \"DNAHeader\": {},\n \"ItemsSaleable\": []\n}\n"
(lldb) po childText
"{\"DNAHeader\":{},\"ItemsSaleable\":[]}"

As said in comments, you currently have JSON (but you say to not focus on it, but...), which make the Regex construction quite hard.
As I suspect a Stream, I create fake tests values for that, but it's not necessary the case:
let startSeparator = "<OWS>{<OWS>"
let endSeparator = "<OWS>}<OWS>"
//Fake structure
struct Object: Codable {
let id: Int
let NDAHeader: Header
let ItemsSaleable: [Saleable]
}
struct Header: Codable {}
struct Saleable: Codable {}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let str0 = embedStr(codable: Object(id: 0, NDAHeader: Header(), ItemsSaleable: []), with: encoder)
let str1 = embedStr(codable: Object(id: 1, NDAHeader: Header(), ItemsSaleable: []), with: encoder)
let str2 = embedStr(codable: Object(id: 2, NDAHeader: Header(), ItemsSaleable: []), with: encoder)
let str3 = embedStr(codable: Object(id: 3, NDAHeader: Header(), ItemsSaleable: []), with: encoder)
//Replace starting `{` & closing `}` of JSON with surroundnig <OWS>
func embedStr(codable: Codable, with encoder: JSONEncoder) -> String {
let jsonData = try! encoder.encode(codable)
var value = String(data: jsonData, encoding: .utf8)!
value = startSeparator + String(value.dropFirst())
value = String(value.dropLast()) + endSeparator
return value
}
//Create a fake stream, by joining multiple JSON values, and "cut it"
func concate(strs: [String], dropStart: Int, dropEnd: Int) -> String {
var value = strs.joined()
value = String(value.dropFirst(dropStart))
value = String(value.dropLast(dropEnd))
return value
}
//Fake Streams
let concate0 = concate(strs: [str0], dropStart: 0, dropEnd: 0)
let concate1 = concate(strs: [str0, str1, str2], dropStart: 13, dropEnd: 13)
let concate2 = concate(strs: [str0, str1, str2, str3], dropStart: 20, dropEnd: 13)
The "extract/find" code:
//Here, if it's a stream, you could return the rest of `value`, because it might be the start of a message, and to concatenate with the next part of the stream
//Side note, if it's a `Data`, `range(of:range:)` can be called on `Data`, avoiding you a strinigification if possible (like going back to JSON to remove the pretty printed format)
func analyze(str: String, found: ((String) -> Void)) {
var value = str
var start = value.range(of: startSeparator)
//Better coding might be applied, it's more a proof of concept, but you should be able to grasp the logic:
// Search for START to next END, return that captured part with closure `found`
// Keep searching for the rest of the string.
guard start != nil else { return }
var end = value.range(of: endSeparator, range: start!.upperBound..<value.endIndex)
while (start != nil && end != nil) {
let sub = value[start!.upperBound..<end!.lowerBound]
found("{" + String(sub) + "}") //Here is hard encoded the part surrounded by <OWS> tag
value = String(value[end!.upperBound...])
start = value.range(of: startSeparator)
if start != nil {
end = value.range(of: endSeparator, range: start!.upperBound..<value.endIndex)
} else {
end = nil
}
}
}
To test:
func test(str: String) {
print("In \(str.debugDescription)")
analyze(str: str) { match in
print("Found \(match.debugDescription)")
//The next part isn't beautiful, but it's one of the safest way to get rid of spaces/\n which are part of the pretty printed
let withouthPrettyPrintedData = try! (JSONSerialization.data(withJSONObject: try! JSONSerialization.jsonObject(with: Data(match.utf8))))
print("Cleaned: \(String(data: withouthPrettyPrintedData, encoding: .utf8)!.debugDescription)")
}
print("")
}
//Test the fake streams
[concate0, concate1, concate2].forEach {
test(str: $0)
}
I used debugDescription in order to see in console the "\n".
Output:
$>In "<OWS>{<OWS>\n \"id\" : 0,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n }\n<OWS>}<OWS>"
$>Found "{\n \"id\" : 0,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n }\n}"
$>Cleaned: "{\"id\":0,\"ItemsSaleable\":[],\"NDAHeader\":{}}"
$>In " \"id\" : 0,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n }\n<OWS>}<OWS><OWS>{<OWS>\n \"id\" : 1,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n }\n<OWS>}<OWS><OWS>{<OWS>\n \"id\" : 2,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n "
$>Found "{\n \"id\" : 1,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n }\n}"
$>Cleaned: "{\"id\":1,\"ItemsSaleable\":[],\"NDAHeader\":{}}"
$>In " 0,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n }\n<OWS>}<OWS><OWS>{<OWS>\n \"id\" : 1,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n }\n<OWS>}<OWS><OWS>{<OWS>\n \"id\" : 2,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n }\n<OWS>}<OWS><OWS>{<OWS>\n \"id\" : 3,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n "
$>Found "{\n \"id\" : 1,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n }\n}"
$>Cleaned: "{\"id\":1,\"ItemsSaleable\":[],\"NDAHeader\":{}}"
$>Found "{\n \"id\" : 2,\n \"ItemsSaleable\" : [\n\n ],\n \"NDAHeader\" : {\n\n }\n}"
$>Cleaned: "{\"id\":2,\"ItemsSaleable\":[],\"NDAHeader\":{}}"

If you can use RegexBuilder, available from iOS 16 and macOS 13, the following code works (see function extracted()):
import RegexBuilder
import SwiftUI
struct MyView: View {
let text = "{\n \"DNAHeader\": {},\n \"ItemsSaleable\": []\n}\n"
var body: some View {
VStack {
Text(text)
Text(extracted(text) ?? "Not found")
}
}
private func extracted(_ text: String) -> String? {
let initiator = Regex {
ZeroOrMore(.horizontalWhitespace)
"{"
ZeroOrMore(.horizontalWhitespace)
}
let terminator = Regex {
ZeroOrMore(.horizontalWhitespace)
"}"
ZeroOrMore(.horizontalWhitespace)
}
// Read the text and capture only what's in between the
// initiator and terminator
let searchJSON = Regex {
initiator
Capture { OneOrMore(.any) } // The real contents
terminator
}
// Extract the whole string and the extracted string
if let match = text.firstMatch(of: searchJSON) {
let (wholeMatch, extractedString) = match.output
print(wholeMatch)
// Replace whitespaces and line feeds before returning
return String(extractedString
.replacing("\n", with: "")
.replacing(" ", with: ""))
} else {
return nil
}
}
}

Related

swift string separation but include the

I need to separate a string to a array of substring but need to include ",.?!" as the substring in Swift.
var sentence = "What is your name?" into
var words = ["What", "is", "your", "name", "?"]
I know I can use this to separate the white space, but I need the ".,?!" to be separated into a word in the words array. How can I do that?
var words = sentence.components(separatedBy: " ")
I only get ["What", "is", "your", "name?"]
I need to separate the ? at the end word, and make words array like this:
var words = ["What", "is", "your", "name", "?"]
You can enumerate your substrings in range using .byWords options, append the substring to your words array, get the substring range upperBound and the enclosed range upperBound, remove the white spaces on the resulting substring and append it to the words array:
import Foundation
let sentence = "What is your name?"
var words: [String] = []
sentence.enumerateSubstrings(in: sentence.startIndex..., options: .byWords) { substring, range, enclosedRange, _ in
words.append(substring!)
let start = range.upperBound
let end = enclosedRange.upperBound
words += sentence[start..<end]
.split{$0.isWhitespace}
.map(String.init)
}
print(words) // "["What", "is", "your", "name", "?"]\n"
You can also use a regular expression to replace the punctuation by the same punctuation preceded by a space before splitting your words by whitespaces:
let sentence = "What is your name?"
let words = sentence
.replacingOccurrences(of: "[.,;:?!]",
with: " $0",
options: .regularExpression)
.split{$0.isWhitespace}
print(words) // "["What", "is", "your", "name", "?"]\n"
Swift native approach:
var sentence = "What is your name?"
for index in sentence
.indices
.filter({ sentence[$0].isPunctuation })
.reversed() {
sentence.insert(" ", at: index)
}
let words = sentence.split { $0.isWhitespace }
words.forEach { print($0) }
This will print:
What
is
your
name
?
This function will split on whitespace and also include each punctuation character as a separate string. Apostrophes are treated as part of a word, so "can't" and "it's" are kept together as a single string. This function will also handle double spaces and tabs.
func splitSentence(sentence: String) -> [String] {
var result : [String] = []
var word = ""
let si = sentence.startIndex
for i in 0..<sentence.count {
let c = sentence[sentence.index(si, offsetBy: i)]
if c.isWhitespace {
if word.count > 0 {
result.append(word)
word = ""
}
} else if (c.isLetter || (String(c) == "'")) {
word = word + String(c)
} else {
if word.count > 0 {
result.append(word)
word = ""
}
result.append(String(c))
}
}
if word.count > 0 {
result.append(word)
}
return result
}
Here is some testing code:
func test(_ sentence: String, _ answer: [String]) {
print("--------------------------------")
print("sentence=" + sentence)
let result : [String] = splitSentence(sentence: sentence)
for s in result {
print("s={" + s + "}")
}
if answer.count != result.count {
print("#### Answer count mismatch")
}
for i in 0..<answer.count {
if answer[i] != result[i] {
print("### Mismatch: {" + answer[i] + "} != {" + result[i] + "}")
}
}
}
func runTests() {
test("", [])
test(" ", [])
test(" ", [])
test(" a", ["a"])
test("a ", ["a"])
test(" a", ["a"])
test(" a ", ["a"])
test("a ", ["a"])
test("aa", ["aa"])
test("a a", ["a", "a"])
test("?", ["?"])
test("a?", ["a", "?"])
test("???", ["?", "?", "?"])
test("What is your name?", [ "What", "is", "your", "name", "?" ])
test("What is your name? ", [ "What", "is", "your", "name", "?" ])
test("La niña es linda.", [ "La", "niña", "es", "linda", "."])
test("ñññ ñ ññ ñ", [ "ñññ", "ñ", "ññ", "ñ" ])
test("It's the 'best'.", [ "It's", "the", "'best'", "." ])
test("¿Cómo te llamas?", [ "¿", "Cómo", "te", "llamas", "?" ])
test("你好吗?", [ "你好吗", "?" ])
}
XCTAssertEqual(
"¿What is your name? My name is 🐱, and I am a cat!"
.split(separator: " ")
.flatMap { $0.split(includingSeparators: \.isPunctuation) }
.map(Array.init)
.map { String($0) },
[ "¿", "What", "is", "your", "name", "?",
"My", "name", "is", "🐱", ",", "and", "I", "am", "a", "cat", "!"
]
)
public enum Spliteration<Element> {
case separator(Element)
case subSequence([Element])
}
public extension Array {
init(_ spliteration: Spliteration<Element>) {
switch spliteration {
case .separator(let separator):
self = [separator]
case .subSequence(let array):
self = array
}
}
}
public extension Sequence {
/// The first element of the sequence.
/// - Note: `nil` if the sequence is empty.
var first: Element? {
var iterator = makeIterator()
return iterator.next()
}
func split(includingSeparators getIsSeparator: #escaping (Element) -> Bool)
-> AnySequence< Spliteration<Element> > {
var separatorFromPrefixIteration: Element?
func process(next: Element?) -> Void {
separatorFromPrefixIteration =
next.map(getIsSeparator) == true
? next
: nil
}
process(next: first)
let prefixIterator = AnyIterator(
dropFirst(
separatorFromPrefixIteration == nil
? 0
: 1
),
processNext: process
)
return .init {
if let separator = separatorFromPrefixIteration {
separatorFromPrefixIteration = nil
return .separator(separator)
}
return Optional(
prefixIterator.prefix { !getIsSeparator($0) },
nilWhen: \.isEmpty
).map(Spliteration.subSequence)
}
}
}
public extension AnyIterator {
/// Use when `AnyIterator` is required / `UnfoldSequence` can't be used.
init<State>(
state: State,
_ getNext: #escaping (inout State) -> Element?
) {
var state = state
self.init { getNext(&state) }
}
/// Process iterations with a closure.
/// - Parameters:
/// - processNext: Executes with every iteration.
init<Sequence: Swift.Sequence>(
_ sequence: Sequence,
processNext: #escaping (Element?) -> Void
) where Sequence.Element == Element {
self.init( state: sequence.makeIterator() ) { iterator -> Element? in
let next = iterator.next()
processNext(next)
return next
}
}
}
public extension AnySequence {
/// Use when `AnySequence` is required / `AnyIterator` can't be used.
/// - Parameter getNext: Executed as the `next` method of this sequence's iterator.
init(_ getNext: #escaping () -> Element?) {
self.init( Iterator(getNext) )
}
}
public extension Optional {
/// Wraps a value in an optional, based on a condition.
/// - Parameters:
/// - wrapped: A non-optional value.
/// - getIsNil: The condition that will result in `nil`.
init(
_ wrapped: Wrapped,
nilWhen getIsNil: (Wrapped) throws -> Bool
) rethrows {
self = try getIsNil(wrapped) ? nil : wrapped
}
}
Not answering using swift, but I believe the algorithm can be emulated with any language.
Done the implementation using Java. The code uses standard Java libraries and not external ones.
private void setSpecialCharsAsLastArrayItem() {
String name = "?what is your name?";
String regexCompilation = "[$&+,:;=?##|]";
Pattern regex = Pattern.compile(regexCompilation);
Matcher matcher = regex.matcher(name);
StringBuilder regexStr = new StringBuilder();
while (matcher.find()) {
regexStr.append(matcher.group());
}
String stringOfSpecialChars = regexStr.toString();
String stringWithoutSpecialChars = name.replaceAll(regexCompilation, "");
String finalString = stringWithoutSpecialChars + " "+stringOfSpecialChars;
String[] splitString = finalString.split(" ");
System.out.println(Arrays.toString(splitString));
}
will print [what, is, your, name, ??]
Here is the solution (Swift 5):
let sentence = "What is your name?".replacingOccurrences(of: "?", with: " ?")
let words = sentence.split(separator: " ")
print(words)
Output:
["What", "is", "your", "name", "?"]

swift: working with string

We have a simple string:
let str = "\"abc\", \"def\",\"ghi\" , 123.4, 567, \"qwe,rty\""
If we do this:
let parsedCSV = str
.components(separatedBy: .newlines)
.filter { !$0.isEmpty }
.map { $0.components(separatedBy: ",") }
.map { $0.map { $0.trimmingCharacters(in: .whitespaces) } }
print(parsedCSV)
we get this:
[["\"abc\"", "\"def\"", "\"ghi\"", "123.4", "567", "\"qwe", "rty\""]]
Is there a simple solution (using functional programming) not to split the last element \"qwe,rty\", because we know that it's one whole thing?
Well this is a hack, it works for this case.... not very simple solution for complex issue ...
let str = "\"abc\", \"def\",\"ghi\" , 123.4, 567, \"qwe,rty\""
let parsedCSV = str
.components(separatedBy: .newlines)
.filter { !$0.isEmpty }
.map { $0.components(separatedBy: ",") }
.map { $0.map { $0.trimmingCharacters(in: .whitespaces) } }.reduce([]) { (result, items) -> [String] in
var goodItems = items.filter{ $0.components(separatedBy: "\"").count == 3 || $0.components(separatedBy: "\"").count == 1}
let arr = items.filter{ $0.components(separatedBy: "\"").count == 2}
var join:[String] = []
for x in 0..<arr.count {
let j = x + 1
if j < arr.count {
join = [arr[x] + "," + arr[j]]
}
}
goodItems.append(contentsOf: join)
return goodItems
}
print(parsedCSV)
print out
["\"abc\"", "\"def\"", "\"ghi\"", "123.4", "567", "\"qwe,rty\""]
I've done that.
let str = "_, * ,, \"abc\", 000, def, ghi , 123.4,, 567, \"qwe,rty,eur\", jkl"
let separator = ","
let parsedCSV = str
.components(separatedBy: .newlines)
.filter { !$0.isEmpty }
.map { $0.components(separatedBy: separator).map { $0.trimmingCharacters(in: .whitespaces) } }
.reduce([]) { (result, items) -> [String] in
var result: [String] = []
for item in items {
guard let last = result.last, last.components(separatedBy: "\"").count % 2 == 0 else {
result.append(item)
continue
}
result.removeLast()
let lastModified = record + separator + item
result.append(lastModified)
}
return result
}.map { $0.trimmingCharacters(in: CharacterSet(charactersIn: "\"")) }
print(parsedCSV)
["_", "*", "", "abc", "000", "def", "ghi", "123.4", "", "567",
"qwe,rty,eur", "jkl"]
If you do not mind using Regular Expression, you can write something like this:
let str = "_, * ,, \"abc\", 000, def, ghi , 123.4,, 567, \"qwe,rty,eur\", jkl"
let pattern = "((?:[^\",]|\"[^\"]*\")*)(?:,|$)"
let regex = try! NSRegularExpression(pattern: pattern)
let parsedCSV = regex.matches(in: str, options: .anchored, range: NSRange(0..<str.utf16.count))
.map {$0.range(at: 1)}
.filter {$0.location != NSNotFound && $0.location < str.utf16.count}
.map {String(str[Range<String.Index>($0, in: str)!]).trimmingCharacters(in: .whitespaces)}
.map {$0.trimmingCharacters(in: CharacterSet(charactersIn: "\""))}
print(parsedCSV) //->["_", "*", "", "abc", "000", "def", "ghi", "123.4", "", "567", "qwe,rty,eur", "jkl"]

How to split string into Int:String Dictionary

So I'm trying to split a string that would look like this:
let Ingredients = "1:egg,4:cheese,2:flour,50:sugar"
and I'm attempting to get a dictionary output like this
var decipheredIngredients : [Int:String] = [
1 : "egg",
4 : "cheese",
2 : "flour",
50 : "sugar"
]
Here is the code that I am attempting this with
func decipherIngredients(input: String) -> [String:Int]{
let splitStringArray = input.split(separator: ",")
var decipheredIngredients : [String:Int] = [:]
for _ in splitStringArray {
decipheredIngredients.append(splitStringArray.split(separator: ":"))
}
return decipheredIngredients
}
When I try this I get an error saying I can't append to the dictionary. I've tried other methods like this:
func decipherIngredients(input: String) -> [String.SubSequence]{
let splitStringArray = input.split(separator: ",")
return splitStringArray
}
let newThing = decipherIngredients(input: "1:egg,4:cheese,2:flour,50:sugar").split(separator: ":")
print(newThing)
but I get this as the output of the function
[ArraySlice(["1:egg", "4:cheese", "2:flour", "50:sugar"])]
An alternative approach using Swift 4 and functional programming:
let ingredients = "1:egg,4:cheese,2:flour,50:sugar"
let decipheredIngredients = ingredients.split(separator: ",").reduce(into: [Int: String]()) {
let ingredient = $1.split(separator: ":")
if let first = ingredient.first, let key = Int(first), let value = ingredient.last {
$0[key] = String(value)
}
}
print(decipheredIngredients)
Swift 3
try this, assuming you want dictionary keys of type Int and values of type String
func decipherIngredients(_ input: String) -> [Int:String] {
var decipheredIngredients : [Int:String] = [:]
let keyValueArray = input.components(separatedBy: ",")
for keyValue in keyValueArray {
let components = keyValue.components(separatedBy: ":")
decipheredIngredients[Int(components[0])!] = components[1]
}
return decipheredIngredients
}

How can I nest debug output in Swift?

Suppose I have an object like:
class MyClass
{
let a_number : Int?
let a_string : String?
let an_array_of_strings : Array<String>?
let an_array_of_objs : Array<Any>?
}
How could I make it so that when I print this object to console, z is indented like so:
MyClass
a_number = 4
a_string = "hello"
an_array_of_strings = ["str1",
"str2",
"str3"]
an_array_of_objs = [MyClass
a_number = 5
a_string = "world"
an_array_of_strings = nil
an_array_of_objs = nil]
I'd do that with a recursive function with an accumulator parameter for the indentation. It defaults to no indentation and is increased by the first column's width on each recursive call:
func describe<T>(_ x: T, indent: String = "") -> String
{
let mirror = Mirror(reflecting: x)
guard !mirror.children.isEmpty else { return x is String ? "\"\(x)\"" : "\(x)" }
switch mirror.displayStyle! {
case .tuple:
let descriptions = mirror.children.map { describe(unwrap($0.value), indent: indent) }
return "(" + descriptions.joined(separator: ",\n\(indent)") + ")"
case .collection:
let descriptions = mirror.children.map { describe(unwrap($0.value), indent: indent) }
return "[" + descriptions.joined(separator: ",\n\(indent)") + "]"
case .dictionary:
let descriptions = mirror.children.map { (child: Mirror.Child) -> String in
let entryMirrors = Array(Mirror(reflecting: unwrap(child.value)).children)
return describe(unwrap(entryMirrors[0].value), indent: indent) + ": "
+ describe(unwrap(entryMirrors[1].value))
}
return "[" + descriptions.joined(separator: ",\n\(indent)") + "]"
case .set:
let descriptions = mirror.children.map { describe(unwrap($0.value), indent: indent) }
return "Set(" + descriptions.joined(separator: ",\n\(indent)") + ")"
default:
let childrenWithLabel = mirror.children.filter { $0.label != nil }
let separator = " = "
let firstColumnWidth = (childrenWithLabel.map { Int($0.label!.characters.count) }.max() ?? 0)
+ separator.characters.count
let subindent = indent + String(repeating: " ", count: firstColumnWidth)
let lines = childrenWithLabel.map {
indent
+ ($0.label! + separator).padding(toLength: firstColumnWidth, withPad: " ", startingAt: 0)
+ describe(unwrap($0.value), indent: subindent)
}
return (["\(mirror.subjectType)"] + lines).joined(separator: "\n")
}
}
This function uses the unwrap(_:) function from my answer to another question
func unwrap<T>(_ any: T) -> Any
{
let mirror = Mirror(reflecting: any)
guard mirror.displayStyle == .optional, let first = mirror.children.first else {
return any
}
return first.value
}
When using describe(_:) like this (I made MyClass a struct so I can use the memberwise initializer):
struct MyClass: CustomStringConvertible
{
let a_number : Int?
let a_string : String?
let an_array_of_strings : Array<String>?
let an_array_of_objs : Array<Any>?
var description: String { return describe(self) }
}
print(MyClass(a_number: 4, a_string: "hello",
an_array_of_strings: ["str1", "str2", "str3"],
an_array_of_objs: [
MyClass(a_number: 5, a_string: "world",
an_array_of_strings: nil, an_array_of_objs: nil)]))
then the output is
MyClass
a_number = 4
a_string = "hello"
an_array_of_strings = ["str1",
"str2",
"str3"]
an_array_of_objs = [MyClass
a_number = 5
a_string = "world"
an_array_of_strings = nil
an_array_of_objs = nil]
Please note that this is only tested with your specific example and some simple additions. I am also not happy about the forced unwrap of mirror.displayStyle but in my shallow testing this only ever happened when mirror.children is empty, which is covered by the preceding guard. If anybody has investigated this more closely, I'd love a comment. I haven't found anything in the documentation of Mirror.
And just like in my answer to your related question I mixed up where the = is supposed to be. Just the other way round this time, duh! :)

Proper way to concatenate optional swift strings?

struct Person {
var firstName: String?
var lastName: String?
}
Now I want to construct a fullName string that contains either just their first or last name (if that's all that is available), or if we have both, their first and last name with a space in the middle.
var fullName: String?
if let first = person.firstName {
fullName = first
}
if let last = person.lastName {
if fullName == nil {
fullName = last
} else {
fullName += " " + last
}
}
or
var fullName = ""
if let first = person.firstName {
fullName = first
}
if let last = person.lastName {
fullName += fullName.count > 0 ? (" " + last) : last
}
Are we just supposed to nest if let's? Nil coalescing seems appropriate but I can't think of how to apply it in this scenario. I can't help but feeling like I'm doing optional string concatenation in an overly complicated way.
compactMap would work well here, combined with .joined(separator:):
let f: String? = "jo"
let l: String? = "smith"
[f,l] // "jo smith"
.compactMap { $0 }
.joined(separator: " ")
It doesn't put the space between if one is nil:
let n: String? = nil
[f,n] // "jo"
.compactMap { $0 }
.joined(separator: " ")
Somewhere, I believe in the swift book, I ran into this pattern, from when before you could have multiple lets in a single if:
class User {
var lastName : String?
var firstName : String?
var fullName : String {
switch (firstName, lastName) {
case (.Some, .Some):
return firstName! + " " + lastName!
case (.None, .Some):
return lastName!
case (.Some, .None):
return firstName!
default:
return ""
}
}
init(lastName:String?, firstName:String?) {
self.lastName = lastName
self.firstName = firstName
}
}
User(lastName: nil, firstName: "first").fullName // -> "first"
User(lastName: "last", firstName: nil).fullName // -> "last"
User(lastName: nil, firstName: nil).fullName // -> ""
User(lastName: "last", firstName: "first").fullName // -> "first last"
An even briefer solution, given swift 3.0:
var fullName : String {
return [ firstName, lastName ].flatMap({$0}).joined(separator:" ")
}
Sometimes simple is best:
let first = p.first ?? ""
let last = p.last ?? ""
let both = !first.isEmpty && !last.isEmpty
let full = first + (both ? " " : "") + last
This works if there is no first or last, if there is a first but no last, if there is a last but no first, and if there are both a first and a last. I can't think of any other cases.
Here's an idiomatic incorporation of that idea into a calculated variable; as an extra benefit, I've allowed full to be nil just in case both the other names are nil:
struct Person {
var first : String?
var last : String?
var full : String? {
if first == nil && last == nil { return nil }
let fi = p.first ?? ""
let la = p.last ?? ""
let both = !fi.isEmpty && !la.isEmpty
return fi + (both ? " " : "") + la
}
}
Here is an alternative method:
let name =
(person.first != nil && person.last != nil) ?
person.first! + " " + person.last! :
person.first ?? person.last!
For those who are want to check nil and "" value as well you can do something like this:
var a: String? = nil
let b = "first value"
let c: String? = nil
let d = ""
let e = "second value"
var result = [a,b,c,d,e].compactMap{ $0 }.filter { $0 != "" }.joined(separator:", ")
print(result)
//first value, second value
I like oisdk's approach but I didn't like the empty string if both were nil. I would rather have nil.
func add(a a: String?, b: String?, separator: String = " ") -> String? {
let results = [a, b].flatMap {$0}
guard results.count > 0 else { return nil }
return results.joinWithSeparator(separator)
}
What oisdk answered was great, but I needed something very specific along the lines of the OP's original question.
Writing for Swift 4.x, I created this extension which works well when populating other strings, such as text labels. I have also updated it to include a function for handling an array if needed.
extension String {
static func combine(first: String?, second: String?) -> String {
return [first, second].compactMap{ $0 }.joined(separator: " ")
}
static func combine(strings: [String?]) -> String {
return strings.compactMap { $0 }.joined(separator: " ")
}
}
An example of this populating a text label with two optional strings:
print(String.combine(first: "First", second: "Last")) // "First Last"
print(String.combine(first: "First", second: nil)) // "First"
print(String.combine(first: nil, second: "Last")) // "Last"
If you have an array of optional strings, you can call the array function:
print(String.combine(strings: ["I", "Have", nil, "A", "String", nil, "Here"]))
// "I Have A String Here"
for swift 4
let name: String? = "Foo"
let surname: String? = "Bar"
let fullname = (name ?? "") + " " + (surname ?? "")
print(fullname)
// Foo Bar
func getSingleValue(_ value: String?..., seperator: String = " ") -> String? {
return value.reduce("") {
($0) + seperator + ($1 ?? "")
}.trimmingCharacters(in: CharacterSet(charactersIn: seperator) )
}
It's too bad that there isn't more support for operators on the Optional enum, but I overloaded the standard concatenation operator (+) like this:
func +<T: StringProtocol>(lhs: Optional<T>, rhs: Optional<T>) -> String {
return [lhs, rhs].compactMap({ $0 }).joined()
}
Then you can use it like this:
let first: String? = "first"
let last: String? = nil
first + first // "firstfirst"
first + last // "first"
last + first // "first"
last + last // ""
Add-On:
Consider you have Struct:
struct Person {
var firstName: String?
var lastName: String?
}
you can use the CustomStringConvertible
extension Person: CustomStringConvertible {
var description: String {
[firstName, lastName].compactMap { $0 }.joined(separator: " ")
}
}
let person1 = Person(firstName "Jeba", lastName: "Moses")
print(person1) // Prints "Jeba Moses"
let person2 = Person(firstName: "Ebenrick", lastName: nil)
print(person2) // Prints "Ebenrick"