extension String { /// Replaces format specifiers (with index) with given arguments. /// (Only works if given arguments are Strings and to replace Strings) /// /// - Parameter arguments: List of String replacements. /// - Returns: This String replaced with given Strings (unless arguments are not a String array) func replaceFormatSpecifiersWithIndex(with arguments: CVarArg...) -> String { guard let args = arguments as? [String] else { return self } // ensure patterns are valid let formatSpecifierPattern = "%[\\d]+\\$@" let digitPattern = "[\\d]+" guard let formatSpecifierRegEx = try? NSRegularExpression(pattern: formatSpecifierPattern, options: .useUnixLineSeparators), let digitRegEx = try? NSRegularExpression(pattern: digitPattern, options: .useUnixLineSeparators) else { return self } // extract range of format specifiers (if any) var range = NSRange(location: 0, length: self.count) var matches = formatSpecifierRegEx.matches(in: self, options: .withoutAnchoringBounds, range: range) let mutableSelf = NSMutableString(string: self) while matches.count > 0 { // extract index from format specifier guard let matchingFormatSpecifierRange = matches.first?.range else { break } let formatSpecifier = mutableSelf.substring(with: matchingFormatSpecifierRange) let digitRange = NSRange(location: 0, length: formatSpecifier.count) let digitMatches = digitRegEx.matches(in: formatSpecifier, options: .withoutAnchoringBounds, range: digitRange) guard let matchingDigitRange = digitMatches.first?.range else { break } let mutableFormatSpecifier = NSMutableString(string: formatSpecifier) let index = NSDecimalNumber(string: mutableFormatSpecifier.substring(with: matchingDigitRange)).intValue - 1 // try to replace string let replacementString = index < args.count ? args[index] : "(null)" mutableSelf.replaceCharacters(in: matchingFormatSpecifierRange, with: replacementString) // overwrite range & matches to reflect newly replaced string range = NSRange(location: 0, length: mutableSelf.description.count) matches = formatSpecifierRegEx.matches(in: mutableSelf.description, options: .withoutAnchoringBounds, range: range) } return mutableSelf.description } /// Replaces format specifiers (without index) with given arguments. /// (Only works if given arguments are Strings and to replace Strings) /// /// - Parameter arguments: List of String replacements. /// - Returns: This String replaced with given Strings (unless arguments are not a String array) func replaceFormatSpecifierWithoutIndex(with arguments: CVarArg...) -> String { guard let args = arguments as? [String] else { return self } // ensure pattern is valid let formatSpecifierPattern = "%@" guard let regEx = try? NSRegularExpression(pattern: formatSpecifierPattern, options: .useUnixLineSeparators) else { return self } // extract range of format specifiers (if any) var range = NSRange(location: 0, length: self.count) var matches = regEx.matches(in: self, options: .withoutAnchoringBounds, range: range) let mutableSelf = NSMutableString(string: self) var index = 0 while matches.count > 0 { // try to replace string guard let matchingFormatSpecifierRange = matches.first?.range else { break } let replacementString = index < args.count ? args[index] : "(null)" mutableSelf.replaceCharacters(in: matchingFormatSpecifierRange, with: replacementString) // overwrite range & matches to reflect newly replaced string range = NSRange(location: 0, length: mutableSelf.description.count) matches = regEx.matches(in: mutableSelf.description, options: .withoutAnchoringBounds, range: range) index += 1 } return mutableSelf.description } }